Everlasting Summer

Everlasting Summer

32 ratings
MoveDissolve или Как двигать и менять спрайт одновременно
By eclipse
В этом руководстве мы напишем переход с помощью CDD, который позволит двигать (подобно переходам move и ease), а также рассеивать (dissove, dspr) изображения.
   
Award
Favorite
Favorited
Unfavorite
Введение
И снова здравствуйте, коллеги
Прошло не так много времени с написания руководства по фонарику, и вот мы снова здесь.
На этот раз поговорим о том, как сделать собственный переход на основе CDD, который позволит Вам перемешать любые изображения и рассеивать их параллельно.

Уверен, многие при написании модов сталкивались с тем, что в оригинальном Ren'Py нет возможности совместить переходы MoveTransition и Dissolve, поэтому если нужно изменить спрайт и передвинуть, применять эти переходы приходится последовательно, а не параллельно. Решение проблемы лежит в области CDD, о которых я более подробно рассказывал в руководстве по фонарику, так что на том, что уже было рассказано, я не буду концентрировать Ваше внимание. Для понимая данного руководства предлагаю вам ознакомиться с разделами "И сразу в омут с головой", "Продолжаем копаться в движке" и "А как рендерить?" из упомянутого выше руководства.


ВНИМАНИЕ
Для понимания описанного в данном руководстве кода необходимо знать объектно-ориентированное программирование на Python, а также желательно понимать производные функций. Если вы не знакомы с этим, советую не углубляться в объяснение кода. Пример использования этого перехода будет расположен в разделе "Менять и двигать. Менгать? Двинять?".

Итак, приступаем.
Переходы в Ren'Py
Transitions
Transitions, или переходы - это такие renpy.Displayable, которые позволяют с помощью объекта рендера изменять старое изображение (old_widget) на новое (new_widget), применяя к ним определённые операции.

Их главное отличие от обычных Displayble и CDD в том, что при инициализации объекта мы НЕ передаём ему изображения, с которыми будем работать. Например, в случае с фонариком, изображения child, mask и curtain мы устанавливали при инициализации объекта, т.е. создавали, так сказать, уже укомплектованный виджет, который потом отрисовывали.

В случае же с переходами, при инициализации объекта, мы передаём в него настройки, например, длительность перехода или качество проводимой операции. Если это какой-нибудь AlphaDissolve, то при инициализации этого перехода мы передаём ему ещё ATL-анимацию с альфа каналом, на прозрачных пикселях которой потом отрисовывается старое изображение, а на непрозрачных - новое. Т.е. мы именно настраиваем то, как переход будет работать, но изображения в него уже передаются самим движком при вызове перехода оператором with:
show bg white with None $ renpy.pause(1.0) show bg black with Dissolve(0.5) # здесь переходу в качестве old_widget передаётся "bg white" # а в качестве new_widget - "bg black"

А чтобы осуществить подобную систему с "настройкой" на практике, обратимся к renpy.curry.
Косплеим Саб-Зиро
renpy.curry.Curry
А вот щас сложно. Приготовьтесь.

Класс renpy.curry.Curry - это специальный инструмент, который позволяет сохранить определённую функцию (или конструктор с инициализатором, в общем, любой вызываемый элемент, именуемый callable) вместе с определёнными аргументами.

Это позволяет как бы "заморозить" вызов функции с определёнными аргументами. Т.е. мы сохраняем эту функцию и её аргументы в отдельном участке памяти, к которому потом через переменную можем получить доступ и вызвать именно эту функцию именно с теми аргументами, которые мы уже передали, а также добавить к ним новые при необходимости.

Давайте разберёмся на примерах.

def sum(a, b): return a + b
Вот простенькая функция, которая складывает два числа. Мы можем её сохранить с помощью renpy.curry.Curry вместе с аргументами, которые мы хотим в неё передать. А потом, если мы вызовем как функцию тот объект, что хранит данные о нашей функции и её аргументах, а также передадим в него недостающий аргумент, то получим ожидаемый результат:
sum_curried = renpy.curry.Curry(sum, a=3) sum_curried(b=6) # -*- output: 9 -*-

На первый взгляд, кажется абсолютно бесполезным инструментом, но вспомним о наших переходах. О чём мы там говорили? Нам нужно сначала настроить объект, а потом уже передать в него изображения, с которыми он будет работать. Не замечаете ли Вы, что renpy.curry.Curry - идеальное решение в данной ситуации?

Именно так оно и есть: renpy.curry.Curry создавался именно с целью появления возможности настраивать определённые инструменты перед их использованием "в поле".

renpy.curry
Но на самом деле мы будет использовать не чистый renpy.curry.Curry, а функцию
renpy.curry(fn)
Она возвращает объект renpy.curry.Curry, который, внимание, принимает как callable (т.е. функцию, которую будет потом вызывать) конструктор renpy.curry.Curry, а как аргумент конструктора - функцию fn.

Сложно? Потерпите.

Зачем это нужно? Чтобы не создавать всякий раз вручную объект renpy.curry.Curry для каждого инструмента, который вы хотите заморозить. Он будет создаваться автоматически. Поясню на примере.

Есть у нас условный переход Dissolve, который позволяет с помощью рассеивания менять old_widget на new_widget. Эти поля принимаются в аргументы инициализатора. Также как аргумент пусть принимается время delay, за которое переход завершится. Таким образом у нас есть вот такой вот переход:
Dissolve(dely, old_widget, new_widget)
После этого мы используем на нём функцию renpy.curry и записываем результат в ту же ячейку памяти, в которой раньше хранилась ссылка на конструктор этого перехода:
Dissolve = renpy.curry(Dissolve)
Теперь в переменной Dissolve хранится не ссылка на оригинальный конструктор класса Dissolve, а на объект renpy.curry.Curry, который хранит в себе ссылку на конструктор класса renpy.curry.Curry как callable и ссылку на конструктор Dissolve как аргумент.
А теперь давайте вызовем получившуюся шушеру с аргументом delay равным, например, половине секунды, 0.5:
dissolve = Dissolve(delay=0.5)
В итоге в переменной dissolve у нас лежит объект renpy.curry.Curry, у которого callable хранит в себе ссылку на конструктор Dissolve, а в качестве аргумента хранит delay со значением, равным 0.5. Это означает, что при вызове dissolve мы инициируем создание объекта Dissolve; аргумент delay уже установлен, а old_widget и new_widget передадутся автоматически при вызове перехода с оператором with.

Таким образом, с помощью renpy.curry мы можем создать "задержку" между настройкой и исполнением определённого кода.
Хитрости
renpy.display.movetransition.MoveInterpolate
Мы научились настраивать наш переход, можно приступать к его написанию. Но перед этим давайте разберёмся с некоторыми интересными моментами, которые упростят нам жизнь.

MoveInterpolate - это оригинальный класс, объявленный в движке Ren'Py, на основе которого создаются переходы семейства move и ease.

В основе его работы лежит использование ещё одного метода класса renpy.Displayable, который мы не разбирали в прошлый раз, а именно:
renpy.Displayable.get_placement(self)

renpy.Displayable.get_placement
Этот метод должен возвращать кортеж значений позиционирования данного отображаемого элемента. Простыми словами, все значения, нужные движку, чтобы понять, как расположить этот отображаемый элемент на экране. Кортеж состоит из:
  • xpos
    координата по оси абсцисс экрана, значение от нуля до ширины экрана (в абсолютном виде; в относительном: от нуля до единицы), в которую поставится якорь изображения
  • ypos
    координата по оси ординат экрана, значение от нуля до высоты экрана (также может быть и относительным), в которую поставится якорь изображения
  • xanchor
    координата якоря по оси абсцисс изображения, значение от нуля до ширины изображения (может быть относительным)
  • yanchor
    координата якоря по оси ординат изображения, значение от нуля до высоты изображения (может быть относительным)
  • xoffset
    значение, показывающее, насколько изображение смещено относительно установленных координат по оси абсцисс (может быть и абсолютным, и относительным)
  • yoffset
    значение, показывающее, насколько изображение смещено относительно установленных координат по оси ординат (абсолютное или относительное)
  • subpixel
    используются ли субпиксели при позиционировании, значение типа bool

Так как в классе MoveInterpolate уже описан рабочий метод get_placement(self), который позволяет передвигать картинки, то зачем нам писать свой? "Хитрость", а на самом деле просто логика объектно-ориентированного программирования, которую я хочу применить, заключается в том, чтобы наследовать наш CDD от MoveInterpolate, оставить без изменений get_placement(self), но переопределить метод render(self, width, height, st, at) на работу с операцией renpy.display.render.DISSOLVE.
Инициализатор
Аргументики
Мы уже умеем работать с renpy.Displayable и renpy.Render (см. руководство по фонарику), неизвестный метод get_placement(self) мы уже посмотрели, а также разобрались в работе renpy.curry(fn), следовательно можно переходить к написанию перехода.

Итак, давайте рассмотрим, какие аргументы нужны нашему переходу для работы.
  • Нам нужно знать, какое время будет занимать наш переход, причём длительность движения и длительность рассеивания могут не совпадать, поэтому нам нужны два аргумента: motion_delay (длительность движения) и dissolve_delay (длительность рассеивания);

  • Старое и новое изображения, old_widget и new_widget - обязательная часть любого перехода. Конечно же они нам нужны. Примечание: они уже передаются в виде объектов класса renpy.Displayable самим движком, поэтому приводить их не нужно;

  • Так как длительность движения и длительность рассеивания могут не совпадать, добавим специальный аргумент use_old. Давайте предположим, что motion_delay больше, чем dissolve_delay, при этом представим их на единой временной оси. Тогда use_old будет показывать, в какой момент запустится операция dissolve. Если use_old == False, то операция запустится в самом начале перехода, а если use_old == True, то она запустится в такое время, чтобы конец рассеивания совпал с концом движения:





    Аналогично для того случая, когда dissolve_delay больше, чем motion_delay, только dissolve_delay и motion_delay поменяются местами на картинке.
    Если motion_delay и dissolve_delay равны друг другу, аргумент use_old не играет роли, ведь и конец и начало обоих переходов совпадают;

  • Также нам нужны функции интерполяции времени motion_time_warp и dissolve_time_warp. Эти функции нужны для изменения скорости течения времени. Такие функции принимают и возвращают значение в диапазоне от нуля до единицы. Обычное течение времени представляет собой прямую y = x:



    Производная этой функции по x равна единице, что по-русски означает: скорость изменения значений функции (равно скорость течения времени) постоянна.
    Но функции интерполяции времени позволяют изменить скорость течения времени, что можно использовать для разных визуальных эффектов. Переходы ease и move создаются на основе одного и того же кода, но с разной интерполяцией времени: у move это linear, т.е. y = x, а у ease: y = 0.5 - cos(pi * x) / 2, график которой выглядит так:



    Если посмотреть производную этой функции, то она равна y' = pi / 2 * sin(pi * x), а её график выглядит вот так:



    Так как производная показывает скорость изменения значений функции (равно скорость течения времени) можно заметить, что она изменятся по синусоидальному закону: от нуля до 0.5 она плавно увеличивается, а от 0.5 до единицы плавно уменьшается.

__init__ - это не конструктор!
Вот мы разобрались со всеми аргументами нашего перехода, так что можно начать писать инициализатор.

Создадим наш класс CDD, который я назвал GreenLightMoveDissolveInterpolate (повторюсь, так я назвал его лишь потому, что разрабатывал его для "Зелёного Света", Вы можете называть как хотите, но главное, чтобы идентификатор был максимально уникальным), который наследует MoveInterpolate (не забудьте импортировать его), а также принимает описанные выше аргументы. Также пусть аргумент use_old имеет стандартное значение False. Также мы должны передать все аргументы, которые нужны для работы MoveInterpolate, в его инициализатор:
init 0 python: import __builtin__ as builtin from renpy.display.movetransition import MoveInterpolate class GreenLightMoveDissolveInterpolate(MoveInterpolate): def __init__(self, motion_delay, dissolve_delay, old_widget, new_widget, use_old=False, motion_time_warp=None, dissolve_time_warp=None ): super(GreenLightMoveDissolveInterpolate, self).__init__( motion_delay, old_widget, new_widget, False, motion_time_warp )
Его аргументы аналогичны нашим, только у нас они "раздваиваются" на motion и dissolve, из-за того, что мы выполняем обе операции, так что просто передаём ему все аргументы с префиксом motion_.

Теперь давайте сохраним все нужные для работы для работы с операцией DISSOLVE аргументы в поля объекта. Так как за движение отвечает родительский класс MoveInterploate, нам не нужно сохранять аргументы с префиксом motion_: они уже сохранены в полях этого же объекта, ведь при инициализации объекта дочернего класса инициализируются объекты всех родительских классов:
init 0 python: import __builtin__ as builtin from renpy.display.movetransition import MoveInterpolate class GreenLightMoveDissolveInterpolate(MoveInterpolate): def __init__(self, motion_delay, dissolve_delay, old_widget, new_widget, use_old=False, motion_time_warp=None, dissolve_time_warp=None ): super(GreenLightMoveDissolveInterpolate, self).__init__( motion_delay, old_widget, new_widget, False, motion_time_warp ) self.total_delay = builtin.max(motion_delay, dissolve_delay) self.dissolve_delay = dissolve_delay self.old_widget = old_widget self.new_widget = new_widget self.motion_pause_delay = 0.0 self.dissolve_pause_delay = 0.0 if use_old: self.motion_pause_delay = self.total_delay - motion_delay self.dissolve_pause_delay = self.total_delay - dissolve_delay self.dissolve_time_warp = dissolve_time_warp self.events = True

Поле total_delay - это общая продолжительность всего перехода. Т.к. длительность рассеивания и движения могут не быть равными, то общая длительность перехода - это максимум из этих двух величин, ведь они применяются параллельно, а не последовательно.

Поля motion_pause_delay и dissolve_pause_delay - это задержки начала применения операций, которые мы должны рассчитать для правильной работы логики, которую мы описали в пункте аргумента use_old. Если motion_delay больше dissolve_delay, то total_delay будет равен motion_delay, следовательно motion_pause_delay будет равен нулю, никакой задержки движения, а вот задержка применения операции dissolve будет равна motion_delay - dissolve_delay. Аналогично для случая dissolve_delay > motion_delay. Если motion_delay равен dissolve_delay, то обе задержки будут равны нулю.
Рендер
Ну чё пацаны, рисуем аниме?
Инициализатор мы написали, теперь можно приступать к написанию метода render.

Сперва добавим такую строчку:
def render(self, width, height, st, at): if renpy.game.less_updates: return renpy.dispaly.transition.null_render(self, width, height, st, at)
Эта строчка нужна для оптимизации. Если в движке установлен флаг на уменьшение использования операций рендера, то просто возвращаем нулевой рендер, который не применяет никаких операций, а заменят одну картинку на другую моментально.

Теперь вспоминаем поле renpy.Render.operation_complete. Оно показывает, какая часть операции уже завершена. Для того, чтобы рассчитать эту часть, давайте разработаем формулу. Итак, у нас есть аргумент st, который показывает, сколько времени прошло с появления нашего CDD (ну в данном случае перехода) на экране. Также у нас есть поле dissolve_delay, которое показывает длительность нашей операции рассеивания. Если мы поделим первое на второе, как раз и получим "часть" операции, которая уже должна быть завершена. Но также у нас есть задержка dissolve_pause_delay, которую мы должны вычесть из st, ведь таким образом мы как бы "сдвинем" начало операции DISSOLVE во времени немного вправо, что нам и нужно (вспоминаем картинку с осью времени). Так что окончательная формула будет выглядеть так: (st - dissolve_pause_delay) / dissolve_delay. Давайте обернём её в код:
def render(self, width, height, st, at): if renpy.game.less_updates: return renpy.dispaly.transition.null_render(self, width, height, st, at) operation_complete = builtin.min(1.0, builtin.max(0.0, st - self.dissolve_pause_delay) / self.dissolve_delay)
min здесь используется, чтобы удержать значение operation_complete в диапазоне от нуля до единицы; а max, чтобы пока мы не дойдём до отрезка времени, равного dissolve_pause_delay, значение operation_complete оставалось равным нулю (т.е. операция как бы ещё не началась).

Далее, если у нас есть функция интерполяции времени для рассеивания, то применяем её:
def render(self, width, height, st, at): if renpy.game.less_updates: return renpy.dispaly.transition.null_render(self, width, height, st, at) operation_complete = builtin.min(1.0, builtin.max(0.0, st - self.dissolve_pause_delay) / self.dissolve_delay) if self.dissolve_time_warp is not None: operation_complete = self.dissolve_time_warp(operation_complete)

Далее создаём объекты рендера наших old_widget и new_widget, а также общий объект ренедера, и применяем к ним операцию DISSOLVE:
def render(self, width, height, st, at): if renpy.game.less_updates: return renpy.dispaly.transition.null_render(self, width, height, st, at) operation_complete = builtin.min(1.0, builtin.max(0.0, st - self.dissolve_pause_delay) / self.dissolve_delay) if self.dissolve_time_warp is not None: operation_complete = self.dissolve_time_warp(operation_complete) old_render = renpy.render(self.old_widget, width, height, st, at) new_render = renpy.render(self.new_widget, width, height, st, at) rv = renpy.Render(width, height, opaque=False) rv.operation = renpy.display.render.DISSOLVE rv.operation_alpha = True rv.operation_complete = operation_complete

Подумайте о родителях
Важное уточнение: для правильной работы родительского класса MoveInterpolate мы должны записать в поля объекта вот эти значения:
  • screen_width, screen_height
    ширина и высота экрана
  • child_width, child_height
    ширина и высота нового изображения
  • st
    время, прошедшее с первого рендера нашего перехода

Но давайте не будем забывать, что у нас также есть параметр motion_pause_delay. Аналогично dissolve_pause_delay, мы можем вычесть его из st и "передать" это значение в MoveInterpolate, а код внутри родительского класса уже выполнит всю остальную работу по поиску завершённой части перехода:
def render(self, width, height, st, at): if renpy.game.less_updates: return renpy.dispaly.transition.null_render(self, width, height, st, at) operation_complete = builtin.min(1.0, builtin.max(0.0, st - self.dissolve_pause_delay) / self.dissolve_delay) if self.dissolve_time_warp is not None: operation_complete = self.dissolve_time_warp(operation_complete) old_render = renpy.render(self.old_widget, width, height, st, at) new_render = renpy.render(self.new_widget, width, height, st, at) rv = renpy.Render(width, height, opaque=False) rv.operation = renpy.display.render.DISSOLVE rv.operation_alpha = True rv.operation_complete = operation_complete self.screen_width, self.screen_height = width, height self.child_width, self.child_height = new_render.get_size() self.st = builtin.max(0.0, st - self.motion_pause_delay)

Вот и всё. Теперь осталось только добавить шейдеры, параметры униформы и отрисовать старое и новое изображение на общем объекте рендера:
def render(self, width, height, st, at): if renpy.game.less_updates: return renpy.dispaly.transition.null_render(self, width, height, st, at) operation_complete = builtin.min(1.0, builtin.max(0.0, st - self.dissolve_pause_delay) / self.dissolve_delay) if self.dissolve_time_warp is not None: operation_complete = self.dissolve_time_warp(operation_complete) old_render = renpy.render(self.old_widget, width, height, st, at) new_render = renpy.render(self.new_widget, width, height, st, at) rv = renpy.Render(width, height, opaque=False) rv.operation = renpy.display.render.DISSOLVE rv.operation_alpha = True rv.operation_complete = operation_complete self.screen_width, self.screen_height = width, height self.child_width, self.child_height = new_render.get_size() self.st = builtin.max(0.0, st - self.motion_pause_delay) if renpy.display.render.models: target = rv.get_size() if new_render.get_size() != target: new_render = new_render.subsurface((0, 0, width, height)) if old_render.get_size() != target: old_render = old_render.subsurface((0, 0, width, height)) rv.mesh = True rv.add_shader("renpy.dissolve") rv.add_uniform("u_renpy_dissolve", operation_complete) rv.add_property("mipmap", renpy.config.mipmap_dissolves if (self.style.mipmap is None) else self.style.mipmap) rv.blit(old_render, (0, 0), focus=False, main=False) rv.blit(new_render, (0, 0), focus=True, main=True) if st < self.total_delay: renpy.redraw(self, 0) return rv

Выражение st < self.total_delay будет истинным в том случае, если прошло меньше времени, чем длится переход. Значит, переход ещё не завершён, поэтому перерисовываем переход.
Ивенты и гости
Необязательная шушера
Как вы помните, методы event и visit переопределять необязательно. Но мы программисты усердные и честные, поэтому напишем их. Тем более, что код будет небольшим.

Перерисовка перехода у нас происходит прямо внутри рендера, потому что она никак не зависит ни от каких событий. Поэтому в методе event просто передадим событие в новое изображение.
def event(self, ev, x, y, st): if self.events or ev.type == pygame.TIMEEVENT: return self.new_widget.event(ev, x, y, st) return None

А в методе visit вернём старое и новое изображения:
def visit(self): return [self.old_widget, self.new_widget]
Не всё так просто
renpy.display.layout.MultiBox
Казалось бы, вот мы написали свой переход, теперь можно замораживать его с помощью renpy.curry - и в бой! Но нет, не всё так просто.

В движке Ren'Py существует такой инструмент как MultiBox, который позволяет создавать списки отображаемых элементов для работы с ними. Учитывать это при применении операций рендера необязательно, но вот при работе с движением нам нужно написать код для правильной работы с этим инструментом.

Однако, на самом деле, этот код уже существует. PyTom написал его за нас, спасибо ему большое. Этот код позволяет работать с MoveInterpolate и создавать на его основе переходы, так что мы просто позаимствуем этот код и усовершенствуем: добавим раздвоенные аргументы motion_delay, dissolve_delay, motion_time_warp и dissolve_time_warp; а также везде внутри кода заменим MoveInterpolate на GreenLightMoveDissolveInterpolate:
def GreenLightMoveDissolve( motion_delay, dissolve_delay, old_widget=None, new_widget=None, enter=None, leave=None, old=False, layers=["master"], motion_time_warp=None, dissolve_time_warp=None, enter_time_warp=None, leave_time_warp=None): use_old = old def merge_slide(old, new, merge_slide): if (not isinstance(new, renpy.display.layout.MultiBox) or (new.layers is None and new.layer_name is None)): if old is new: return new else: return GreenLightMoveDissolveInterpolate(motion_delay, dissolve_delay, old, new, use_old, motion_time_warp, dissolve_time_warp) if new.layers: rv = renpy.display.layout.MultiBox(layout='fixed') rv.layers = dict() for layer in renpy.config.layers: f = new.layers[layer] if (isinstance(f, renpy.display.layout.MultiBox) and layer in layers and f.scene_list is not None): f = merge_slide(old.layers[layer], new.layers[layer], merge_slide) rv.layers[layer] = f rv.add(f) return rv def wrap(sle): return renpy.display.layout.AdjustTimes(sle.displayable, sle.show_time, sle.animation_time) def tag(sle): return sle.tag or sle.displayable def merge(sle, d): rv = sle.copy() rv.show_time = 0 rv.displayable = d return rv def entering(sle): if not enter: return new_d = wrap(new_sle) move = GreenLightMoveDissolveInterpolate(motion_delay, dissolve_delay, enter(new_d), new_d, False, enter_time_warp, dissolve_time_warp) rv_sl.append(merge(new_sle, move)) def leaving(sle): if not leave: return old_d = wrap(sle) move = GreenLightMoveDissolveInterpolate(motion_delay, dissolve_delay, old_d, leave(old_d), True, leave_time_warp, dissolve_time_warp) move = renpy.display.layout.IgnoresEvents(move) rv_sl.append(merge(old_sle, move)) def moving(old_sle, new_sle): if old_sle.displayable is new_sle.displayable: rv_sl.append(new_sle) return old_d = wrap(old_sle) new_d = wrap(new_sle) move = GreenLightMoveDissolveInterpolate(motion_delay, dissolve_delay, old_d, new_d, use_old, motion_time_warp, dissolve_time_warp) rv_sl.append(merge(new_sle, move)) old_sl = old.scene_list[:] new_sl = new.scene_list[:] rv_sl = list() old_map = dict((tag(i), i) for i in old_sl if i is not None) new_tags = set(tag(i) for i in new_sl if i is not None) rv_tags = set() while old_sl or new_sl: if old_sl: old_sle = old_sl[0] old_tag = tag(old_sle) if old_tag in rv_tags: old_sl.pop(0) continue if old_tag not in new_tags: leaving(old_sle) rv_tags.add(old_tag) new_sle = new_sl.pop(0) new_tag = tag(new_sle) if new_tag in old_map: old_sle = old_map[new_tag] moving(old_sle, new_sle) rv_tags.add(new_tag) continue else: entering(new_sle) rv_tags.add(new_tag) continue rv_sl.sort(key=lambda a: a.zorder) layer = new.layer_name rv = renpy.display.layout.MultiBox(layout='fixed', focus=layer, **renpy.game.interface.layer_properties[layer]) rv.append_scene_list(rv_sl) return rv rv = merge_slide(old_widget, new_widget, merge_slide) rv.delay = builtin.max(motion_delay, dissolve_delay) return rv

А также в конце добавим нашу заморозку:
GreenLightMoveDissolve = renpy.curry(GreenLightMoveDissolve)
Менять и двигать. Менгать? Двинять?
Созидание
Чтобы создать переход на основе GreenLightMoveDissolveInterploate воспользуемся нашей замороженной функцией GreenLightMoveDissolve.

Например, я хочу сделать переход move с параллельным переходом dspr. Оригинальный переход move имеет длительность 0.5 секунды и не имеет никакой интерполяции времени, а dspr - это тот же Dissolve, только длительность перехода равна 0.25 секунды. Поэтому для того, чтобы их объединить нужно написать:
define gl_move_dspr = GreenLightMoveDissove(0.5, 0.25)
Первый аргумент - длительность перехода move, а второй - длительность перехода dissolve.

Или например, я хочу использовать переход ease. Он мне нравится больше, потому что он такой весь плавный и красивый, и пусть рассеивание там длится 0.5 секунды, а движение - одну секунду. Тогда нам нужно в аргумент motion_time_warp передать определённую внутри самого движка функцию _ease_time_warp, в motion_delay передаём 1.0, а в dissolve_delay - 0.5:
define gl_ease_dissolve = GreenLightMoveDissolve(1.0, 0.5, motion_time_warp=_ease_time_warp)

Применение
Для того, чтобы применить созданный Вами переход просто используйте его как и все остальные переходы, с помощью оператора with:
show sl normal pioneer at cright with dissolve "Славя встала справа от меня." show sl angry pioneer at center with gl_move_dspr

Также не забывайте, что переходы можно применять к нескольким спрайтам одновременно. Например, вот кусок кода из "Зелёного Света" без использования GreenLightMoveDissolve:
me "Эй, ребят, вы чего?" "Спросил я, выйдя с пляжа на дорожку." show dv shy dress gl at cleft show sh upset pioneer at fleft with dissolve "Теперь все замерли и обратили свой взгляд только на меня." "Между нами повисло неловкое молчание." show el smile pioneer at left with dissolve gl_el "А вот и Семён нашёлся…" show dv normal dress gl show sh normal pioneer with dspr show dv normal dress gl at fright show el smile pioneer at center with ease me "Что у вас тут происходит? Вас было слышно аж с другого конца берега."
Как видите, здесь сначала с помощью перехода dspr у Алисы и Шурика изменяются эмоции с shy и upset соответственно на normal, а потом уже с помощью перехода ease Электроник и Алиса переезжают на новую позицию.

Но если использовать GreenLightMoveDissolve, то это можно совместить:
me "Эй, ребят, вы чего?" "Спросил я, выйдя с пляжа на дорожку." show dv shy dress gl at cleft show sh upset pioneer at fleft with dissolve "Теперь все замерли и обратили свой взгляд только на меня." "Между нами повисло неловкое молчание." show el smile pioneer at left with dissolve gl_el "А вот и Семён нашёлся…" show dv normal dress gl at fright show sh normal pioneer show el smile pioneer at center with gl_ease_dspr me "Что у вас тут происходит? Вас было слышно аж с другого конца берега."
Теперь Шурик изменяет свою эмоцию с переходом dspr, но т.к. его координаты мы не меняли, то двигаться он никуда не будет; у Электроника не меняется эмоция, но он двигается в новую позицию с переходом ease; а у Алисы меняется и эмоция (с переходом dspr) и позиция (с переходом ease), причём параллельно.

Удобно? Я считаю, что очень.

Визуально на разницу между этими двумя переходами Вы можете посмотреть в прикреплённом в Введении видео.
Весь код перехода целиком
init 0 python: import __builtin__ as builtin from renpy.display.movetransition import MoveInterpolate class GreenLightMoveDissolveInterpolate(MoveInterpolate): def __init__(self, motion_delay, dissolve_delay, old_widget, new_widget, use_old=False, motion_time_warp=None, dissolve_time_warp=None): super(GreenLightMoveDissolveInterpolate, self).__init__(motion_delay, old_widget, new_widget, False, motion_time_warp) self.total_delay = builtin.max(motion_delay, dissolve_delay) self.dissolve_delay = dissolve_delay self.old_widget = old_widget self.new_widget = new_widget self.motion_pause_delay = 0.0 self.dissolve_pause_delay = 0.0 if use_old: self.motion_pause_delay = self.total_delay - motion_delay self.dissolve_pause_delay = self.total_delay - dissolve_delay self.dissolve_time_warp = dissolve_time_warp self.events = True def render(self, width, height, st, at): if renpy.game.less_updates: return renpy.dispaly.transition.null_render(self, width, height, st, at) operation_complete = builtin.min(1.0, builtin.max(0.0, st - self.dissolve_pause_delay) / self.dissolve_delay) if self.dissolve_time_warp is not None: operation_complete = self.dissolve_time_warp(operation_complete) self.screen_width, self.screen_height = width, height old_render = renpy.render(self.old_widget, width, height, st, at) new_render = renpy.render(self.new_widget, width, height, st, at) self.child_width, self.child_height = new_render.get_size() self.st = builtin.max(0.0, st - self.motion_pause_delay) rv = renpy.Render(width, height, opaque=False) rv.operation = renpy.display.render.DISSOLVE rv.operation_alpha = True rv.operation_complete = operation_complete if renpy.display.render.models: target = rv.get_size() if new_render.get_size() != target: new_render = new_render.subsurface((0, 0, width, height)) if old_render.get_size() != target: old_render = old_render.subsurface((0, 0, width, height)) rv.mesh = True rv.add_shader("renpy.dissolve") rv.add_uniform("u_renpy_dissolve", operation_complete) rv.add_property("mipmap", renpy.config.mipmap_dissolves if (self.style.mipmap is None) else self.style.mipmap) rv.blit(old_render, (0, 0), focus=False, main=False) rv.blit(new_render, (0, 0), focus=True, main=True) if st < self.total_delay: renpy.redraw(self, 0) return rv def event(self, ev, x, y, st): if self.events or ev.type == pygame.TIMEEVENT: return self.new_widget.event(ev, x, y, st) return None def visit(self): return [self.old_widget, self.new_widget] def GreenLightMoveDissolve( motion_delay, dissolve_delay, old_widget=None, new_widget=None, enter=None, leave=None, old=False, layers=["master"], motion_time_warp=None, dissolve_time_warp=None, enter_time_warp=None, leave_time_warp=None): use_old = old def merge_slide(old, new, merge_slide): if (not isinstance(new, renpy.display.layout.MultiBox) or (new.layers is None and new.layer_name is None)): if old is new: return new else: return GreenLightMoveDissolveInterpolate(motion_delay, dissolve_delay, old, new, use_old, motion_time_warp, dissolve_time_warp) if new.layers: rv = renpy.display.layout.MultiBox(layout='fixed') rv.layers = dict() for layer in renpy.config.layers: f = new.layers[layer] if (isinstance(f, renpy.display.layout.MultiBox) and layer in layers and f.scene_list is not None): f = merge_slide(old.layers[layer], new.layers[layer], merge_slide) rv.layers[layer] = f rv.add(f) return rv def wrap(sle): return renpy.display.layout.AdjustTimes(sle.displayable, sle.show_time, sle.animation_time) def tag(sle): return sle.tag or sle.displayable def merge(sle, d): rv = sle.copy() rv.show_time = 0 rv.displayable = d return rv def entering(sle): if not enter: return new_d = wrap(new_sle) move = GreenLightMoveDissolveInterpolate(motion_delay, dissolve_delay, enter(new_d), new_d, False, enter_time_warp, dissolve_time_warp) rv_sl.append(merge(new_sle, move)) def leaving(sle): if not leave: return old_d = wrap(sle) move = GreenLightMoveDissolveInterpolate(motion_delay, dissolve_delay, old_d, leave(old_d), True, leave_time_warp, dissolve_time_warp) move = renpy.display.layout.IgnoresEvents(move) rv_sl.append(merge(old_sle, move)) def moving(old_sle, new_sle): if old_sle.displayable is new_sle.displayable: rv_sl.append(new_sle) return old_d = wrap(old_sle) new_d = wrap(new_sle) move = GreenLightMoveDissolveInterpolate(motion_delay, dissolve_delay, old_d, new_d, use_old, motion_time_warp, dissolve_time_warp) rv_sl.append(merge(new_sle, move)) old_sl = old.scene_list[:] new_sl = new.scene_list[:] rv_sl = list() old_map = dict((tag(i), i) for i in old_sl if i is not None) new_tags = set(tag(i) for i in new_sl if i is not None) rv_tags = set() while old_sl or new_sl: if old_sl: old_sle = old_sl[0] old_tag = tag(old_sle) if old_tag in rv_tags: old_sl.pop(0) continue if old_tag not in new_tags: leaving(old_sle) rv_tags.add(old_tag) old_sl.pop(0) continue new_sle = new_sl.pop(0) new_tag = tag(new_sle) if new_tag in old_map: old_sle = old_map[new_tag] moving(old_sle, new_sle) rv_tags.add(new_tag) continue else: entering(new_sle) rv_tags.add(new_tag) continue rv_sl.sort(key=lambda a: a.zorder) layer = new.layer_name rv = renpy.display.layout.MultiBox(layout='fixed', focus=layer, **renpy.game.interface.layer_properties[layer]) rv.append_scene_list(rv_sl) return rv rv = merge_slide(old_widget, new_widget, merge_slide) rv.delay = builtin.max(motion_delay, dissolve_delay) return rv GreenLightMoveDissolve = renpy.curry(GreenLightMoveDissolve)
До новых встреч
С Вами был кодер мода "Зелёный Свет", alex6712. Увидимся!
9 Comments
eclipse  [author]
Mar 26 @ 12:25pm 
На одном слое в один момент времени может находиться только одна картинка с определённым тегом. Поэтому нельзя вывести (опять же, на одном слое) двух Алис, например. Один спрайт заменит другой, т.к. у них совпадают теги.

Также я давно уже не трогал RenPy, поэтому могу ошибаться, но должна сработать ещё и такая запись:
`renpy.display.core.displayable_by_tag("master", "dv").get_placement().pos`

Т.е. можно вызывать метод самого объекта Displayable для получения данных о его положении.
eclipse  [author]
Mar 26 @ 12:25pm 
Прошу прощения, что не ответил, что-то давно не проверял свои руководства. Да, вы правильно описали принцип.

Ещё добавлю немного: тегом картинки всегда является первое слово в названии изображения. В случае с Алисой - это `dv`, а, например, для Мику это будет `mi` и т.д. Если вы назвали спрайты как-то по иному, то стоит это учитывать (например, если назову картинку `random sprite name`, то тегом будет слово `random`).
Mar 25 @ 11:54am 
Ну я разобрался. Если кому надо, держите.

renpy.get_placement(renpy.display.core.displayable_by_tag("имя_слоя", "тег_картинки")).свойство

Пример:
renpy.get_placement(renpy.display.core.displayable_by_tag("master", "dv")).pos

master - это слой, на котором висят спрайты и фоны с артами.
dv - это тег самой картинки (в данном случае у нас Алиса).

После точки запрашиваем любое свойство из перечня:
pos
xpos
ypos
anchor
xanchor
yanchor
offset
xoffset
yoffset
subpixel
Oct 8, 2025 @ 9:12am 
renpy.get_placement("dv shocked pioneer far") — говорит, unicode не подходит в качестве аргумента.
renpy.get_placement(dv shocked pioneer far) — говорит "expected statement"

Понимаю, что упираюсь в незнание питона. И не выходит найти примеры правильного использования метода, чтобы сделать по аналогии.
Oct 8, 2025 @ 9:12am 
Здесь упомянут метод renpy.Displayable.get_placement()

Подскажите простыми словами как воспользоваться методом renpy.Displayable.get_placement() для получения информации о позиции спрайта.

Вот, скажем, есть у меня на экране спрайт
dv shocked pioneer far

Пусть я даже знаю его позицию, но хочу разобраться, как получить её от движка. Как мне скормить этот спрайт методу renpy.Displayable.get_placement() ну или renpy.get_placement(), так чтобы получить результат?
Jun 15, 2022 @ 12:05am 
Спасибо! Все вышло, очень красивый переход! вот только я ещё одну ошибку заметил, буква пропущена, может из-за этого у кого-то тоже не получалось.
В главе "менгать и двинять" в примере когда даем заморозке определение, буква l англисйкая пропущена в dissolve
eclipse  [author]
Jun 14, 2022 @ 9:51pm 
clown , при копировании кода потерялись символы. Из-за этого код перестал работать. Скопируйте новую версию, и всё должно быть хорошо)
Jun 14, 2022 @ 1:36pm 
Сижу уже с пару часов, вчитываюсь в текст, кручу верчу этот код, но все равно применить на практике не получается, все получаю трейсбеки, непутевый из меня кодер, может можно как-то с тобой связаться, проконсультироваться на счёт этого вопроса?
Jun 13, 2022 @ 11:18am 
Ха-ха. Я ничего не понимаю, но какой же классный контент! Повторюсь — респектище за просвещение масс!