Сохранение, загрузка и откат

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

Примечание

Хотя мы обычно стараемся поддерживать совместимость сохранений между релизами, эта совместимость не гарантируется. Мы можем нарушить совместимость сохранений, если это принесет достаточно большую пользу.

Что сохраняется

Ren'Py пытается сохранить состояние игры. Это включает в себя как внутреннее состояние, так и состояние Python.

Внутреннее состояние состоит из всех аспектов Ren'Py, которые предназначены для изменения после начала игры, и включает в себя:

  • Текущую инструкцию и все инструкции, к которым можно вернуться.

  • Отображаемые изображения и displayables (отображаемые объекты).

  • Отображаемые экраны и значения переменных на этих экранах.

  • Музыку, которую воспроизводит Ren'Py.

  • Список текстовых блоков в режиме NVL.

Состояние Python состоит из переменных в хранилище (store), которые изменились с начала игры, и всех объектов, доступных через эти переменные. Обратите внимание, что важно именно изменение переменных – изменение полей в объектах не приведёт к сохранению этих объектов.

Переменные, установленные с помощью инструкции default, всегда будут сохраняться.

В этом примере:

define a = 1
define o = object()
default c = 17

label start:
    $ b = 1
    $ o.value = 42

будут сохранены только b и c. A не будет сохранена, так как она не изменяется после начала игры. O не сохраняется, потому что она не изменяется – объект, на который она ссылается, изменяется, но сама переменная нет.

Что не сохраняется

Переменные Python, которые не изменяются после начала игры, не будут сохраняться. Это может стать серьезной проблемой, если несохраняемая переменная и сохраняемая переменная ссылаются на один и тот же объект (создание псевдонима/алиасинг). В этом примере:

init python:
    a = object()
    a.f = 1

label start:
    $ b = a
    $ b.f = 2

    "a.f=[a.f] b.f=[b.f]"

a и b являются псевдонимами. Сохранение и загрузка могут нарушить этот псевдоним, в результате чего a и b будут ссылаться на разные объекты. Поскольку это может быть очень запутанно, лучше избегать создания псевдонимов для сохраняемых и несохраняемых переменных. (С этим редко сталкиваются напрямую, но это может произойти при создании псевдонима для несохраняемой переменной и сохраняемого поля).

Есть несколько других видов состояний, которые не сохраняются:

путь потока управления

Ren'Py сохраняет только текущую инструкцию и инструкцию, к которой нужно вернуться. Он не помнит, как он туда попал. Важно отметить, что инструкции (включая присваивания переменных), которые добавляются в игру, не будут выполнены.

сопоставления имен изображений с displayables (отображаемыми объектами)

Поскольку это сопоставление не сохраняется, изображение может измениться на новое при повторной загрузке игры. Это позволяет изменять файл изображения по мере развития игры.

переменные конфигурации, стили и свойства стилей

Переменные конфигурации и стили не сохраняются как часть игры. Поэтому их следует изменять только в блоках init и не трогать после начала игры.

Где Ren'Py сохраняет

Сохранения происходят в начале инструкции Ren'Py во внешнем контексте взаимодействия.

Здесь важно отметить, что сохранение происходит в начале инструкции. Если загрузка или откат происходят в середине инструкции, которая взаимодействует с пользователем несколько раз, состояние будет тем, которое было активным на момент начала выполнения этой инструкции.

Это может быть проблемой в инструкциях, определенных на Python. В:

python:

    i = 0

    while i < 10:

        i += 1

        narrator("The count is now [i].")

если пользователь сохранится и загрузится в середине цикла, он начнется заново. Использование скрипта Ren'Py – а не Python – для создания цикла позволяет избежать этой проблемы.:

$ i = 0

while i < 10:

    $ i += 1

    "The count is now [i]."

Что Ren'Py может сохранить

Ren'Py использует систему pickle из Python для сохранения состояния игры. Этот модуль может сохранять:

  • Базовые типы, такие как True, False, None, int, str, float, complex, str и объекты Unicode.

  • Составные типы, такие как списки (lists), кортежи (tuples), множества (sets) и словари (dicts).

  • Объекты, классы, функции, методы и связанные методы, определенные разработчиком. Чтобы сериализация этих функций прошла успешно, они должны оставаться доступными под своими первоначальными именами.

  • Объекты Character, Displayable, Transform и Transition.

Что Ren'Py не может сохранить

Существуют определенные типы, которые не могут быть сериализованы (pickled):

  • Объекты Render.

  • Объекты-итераторы.

  • Объекты-генераторы.

  • Задачи и future-объекты корутин, подобные тем, что создаются с помощью async и await.

  • Файлоподобные объекты.

  • Сетевые сокеты и объекты, которые их инкапсулируют.

  • Вложенные функции и лямбда-функции.

Этот список может быть неполным.

Объекты, которые нельзя сериализовать, все равно можно использовать, при условии, что они используются в пространствах имен, которые не сохраняются Ren'Py (например, переменные в блоке init, пространства имен внутри функций или блоки python hide).

Например, использование файлового объекта так:

$ monika_file = open(config.gamedir + "/monika.chr", "w")
$ monika_file.write("Do not delete.\r\n")
$ monika_file.close()

Не сработает, так как f может быть сохранена между любыми из трех инструкций Python. Помещение этого кода в блок python hide сработает:

python hide:

    monika_file = open(config.gamedir + "/monika.chr", "w")
    monika_file.write("Do not delete.\r\n")
    monika_file.close()

(Конечно, использование инструкции with из Python было бы чище.)

python hide:

    with open(config.gamedir + "/monika.chr", "w") as monika_file:
        monika_file.write("Do not delete.\r\n")

Корутины, созданные с помощью async, await или asyncio, ведут себя аналогично. Если у вас есть:

init python:

    import asyncio

    async def sleep_func():
        await asyncio.sleep(1)
        await asyncio.sleep(1)

тогда:

$ sleep_task = sleep_func()
$ asyncio.run(sleep_task)

вызовет проблемы, поскольку sleep_task не может быть сохранен. Но если результат не присваивать переменной:

$ asyncio.run(sleep_func())

сработает без проблем.

Структура файла сохранения

Файлы сохранения Ren'Py (например, 1-1.save) представляют собой сжатые архивы, содержащие:

  • Состояние игры: Сериализованные объекты Python, представляющие состояние игры.

  • Метаданные: Словарь с полями, описывающими сохранение. Доступные поля зависят от метода доступа:

    • renpy.slot_json(): Возвращает словарь с _save_name, _renpy_version, _version, _game_runtime, _ctime и любыми пользовательскими полями, добавленными через config.save_json_callbacks.

    • FileJson(): Возвращает тот же словарь метаданных, что и renpy.slot_json(), когда параметр key равен None (по умолчанию), включая пользовательские поля. Когда указан key (например, "_save_name"), возвращает значение этого поля.

    • renpy.list_saved_games(): Возвращает _save_name (как extra_info), mtime (как time) и _screenshot (как screenshot) в виде кортежа, но никаких других полей метаданных.

    Стандартные поля метаданных включают:

    • _save_name: Строка, установленная аргументом extra_info в renpy.save(), обычно это глобальная переменная save_name (например, "Глава 1"). Если пустая, то пустая строка.

    • _renpy_version: Версия Ren'Py, использованная для создания сохранения, в виде списка целых чисел (например, [8, 3, 7, 25031702] для Ren'Py 8.3.7.25031702).

    • _version: Версия игры, установленная config.version на момент сохранения (например, "1.0").

    • _game_runtime: Количество секунд, в течение которых игра ожидала ввода от пользователя, как возвращается renpy.get_game_runtime() на момент сохранения.

    • _ctime: Временная метка UNIX (секунды с начала эпохи), когда было создано сохранение.

    • _screenshot: Отображаемый объект (displayable) для скриншота сохранения, доступный через renpy.list_saved_games() или FileScreenshot().

Метаданные хранятся в формате JSON, поэтому они ограничены значениями, которые может представлять JSON, такими как строки, числа, списки и словари. При использовании _save_name (через save_name или extra_info) специальные символы, такие как [] или {}, могут вызывать проблемы, если они интерпретируются напрямую парсером текста Ren'Py в интерфейсе сохранения/загрузки, что может привести к ошибкам или неожиданному отображению. Используйте config.save_json_callbacks для безопасного хранения сложных метаданных или используйте substitute False при их отображении как Text.

Сохранения с трассировкой

По возможности, Ren'Py создает сохранения при возникновении неперехваченного исключения, содержащие как состояние игры, так и трассировку. Эти сохранения хранятся в слотах с именами от _tracesave-1 до _tracesave-10. В этих файлах трассировка хранится в метаданных файла и может быть получена с помощью:

traceback = renpy.slot_json("_tracesave-1")["_traceback"]

Также возможно использовать renpy.get_save_data(), чтобы получить переменные, которые игра использовала на момент создания сохранения с трассировкой.

Новая функция renpy.get_save_data() позволяет получить данные для определенного слота сохранения, не загружая его. Это можно использовать с сохранением с трассировкой для получения данных игры, не загружаясь в состояние ошибки.

Функции и переменные сохранения

Существует одна переменная, которая используется высокоуровневой системой сохранения: save_name.

Это строка, которая хранится с каждым сохранением. Ее можно использовать, чтобы дать имя сохранению, чтобы помочь пользователям различать их.

Более детальная настройка данных для каждого сохранения может быть выполнена с помощью системы дополнительных данных Json, см. config.save_json_callbacks.

Существует ряд высокоуровневых действий и функций сохранения, определенных в экранных действиях. Кроме того, существуют следующие низкоуровневые действия сохранения и загрузки.

renpy.can_load(filename, test=False)

Возвращает true, если filename (например, '3-2' для страницы 3, слота 2) существует как слот сохранения, и false в противном случае.

renpy.copy_save(old, new)

Копирует сохранение из old в new. (Ничего не делает, если old не существует.)

renpy.get_save_data(filename)

Эта функция загружает данные игры из слота сохранения filename (например, '3-2' для страницы 3, слота 2), фактически не изменяя состояние игры. Она возвращает словарь, содержащий имена переменных относительно хранилища по умолчанию, сопоставленные со значениями этих переменных на момент сохранения.

renpy.list_slots(regexp=None)

Возвращает список непустых слотов сохранения. Если regexp задан, возвращаются только слоты, начинающиеся с regexp. Слоты сортируются в строковом порядке.

Список полезных регулярных выражений см. в list_saved_games().

renpy.load(filename)

Загружает состояние игры из слота сохранения filename (например, '3-2' для страницы 3, слота 2). Если файл загружен успешно, эта функция никогда не возвращает управление.

renpy.newest_slot(regexp=None)

Возвращает имя самого нового слота сохранения (слота с самым последним временем изменения) или None, если нет (соответствующих) сохранений.

Если regexp задан, возвращаются только слоты, начинающиеся с regexp.

Список полезных регулярных выражений см. в list_saved_games().

renpy.rename_save(old, new)

Переименовывает сохранение из old в new. (Ничего не делает, если old не существует.)

renpy.save(filename, extra_info='', *, extra_json=None)

Сохраняет игру в слот, определённый filename.

filename

Строка, задающая имя слота сохранения. Несмотря на название переменной, это лишь косвенно соответствует именам файлов (например, '3-2' для страницы 3, слота 2).

extra_info

Строка, хранящаяся как поле _save_name в метаданных файла сохранения. Её можно получить через renpy.slot_json() (используя ключ "_savename"), FileJson() (с key=None или key="_save_name") или как поле extra_info в renpy.list_saved_games(). Обычно используется с глобальной переменной save_name (например, "Глава 1"). Если строка пуста, _save_name будет пустой строкой.

extra_json

Словарь с дополнительными метаданными для сохранения в файле. Он объединяется с метаданными по умолчанию и метаданными из config.save_json_callbacks.

Пример::

$ save_name = "Глава 1" $ renpy.save("1-1", extra_info=save_name)

Для хранения дополнительных или сложных метаданных используйте config.save_json_callbacks для добавления пользовательских полей в словарь метаданных.

Функцию renpy.take_screenshot() следует вызывать до этой функции.

renpy.slot_json(slotname)

Возвращает словарь, содержащий метаданные файла сохранения в slotname, включая _save_name, _renpy_version, _version, _game_runtime, _ctime и любые пользовательские поля, добавленные через config.save_json_callbacks на момент сохранения. Если слот пуст, возвращается None. Для действий на экране сохранения/загрузки FileJson() предоставляет подмножество этих полей (исключая пользовательские поля).

Пример:

def show_game_runtime(slot):
    metadata = renpy.slot_json(slot)
    name = metadata.get('_save_name', '')
    runtime = metadata.get('_game_runtime', 0)
    hours = int(runtime // 3600)
    minutes = int((runtime % 3600) // 60)
    seconds = int(runtime % 60)
    runtime_formatted = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
    renpy.notify("Имя сохранения: [name], Время игры: [runtime_formatted]")
renpy.slot_mtime(slotname)

Возвращает время изменения для slot или None, если слот пуст.

renpy.slot_screenshot(slotname)

Возвращает displayable, который можно использовать как скриншот для slotname, или None, если слот пуст.

renpy.take_screenshot()

Вызывает создание скриншота. Этот скриншот будет сохранён как часть сохранённой игры.

Удаляет слот сохранения с указанным именем (например, '3-2' для страницы 3, слота 2).

Сохранение данных после загрузки

Когда игра загружается, ее состояние сбрасывается (с использованием системы отката, описанной ниже) до состояния, в котором она находилась на момент начала выполнения текущей инструкции.

В некоторых случаях это может быть нежелательно. Например, когда экран позволяет редактировать значение, мы можем захотеть сохранить это значение при загрузке игры. Когда вызывается renpy.retain_after_load, данные не будут отменены при сохранении и загрузке игры до конца следующего взаимодействия с контрольной точкой.

Обратите внимание, что хотя данные не изменяются, управление сбрасывается на начало текущей инструкции. Эта инструкция выполнится снова, но уже с новыми данными, доступными в ее начале.

Например:

screen edit_value:
    hbox:
        text "[value]"
        textbutton "+" action SetVariable("value", value + 1)
        textbutton "-" action SetVariable("value", value - 1)
        textbutton "+" action Return(True)

label start:
    $ value = 0
    $ renpy.retain_after_load()
    call screen edit_value
renpy.retain_after_load()

Заставляет данные, измененные между текущим оператором и оператором, содержащим следующую контрольную точку, сохраняться при загрузке.

Откат

Откат позволяет пользователю вернуть игру в более раннее состояние, во многом подобно системам отмены/повтора действий, которые доступны в большинстве современных приложений. Хотя система заботится о поддержании визуальных эффектов и игровых переменных во время событий отката, при создании игры следует учитывать несколько моментов.

Какие данные откатываются?

Откат влияет на переменные, которые были изменены после фазы init, и на объекты откатываемых типов, доступные через эти переменные. Вкратце, списки, словари и множества, созданные в скрипте Ren'Py, являются откатываемыми, так же как и экземпляры классов, определенных в скриптах Ren'Py. Данные, созданные внутри Python или внутри Ren'Py, обычно не являются откатываемыми.

Более подробно, внутри хранилищ (stores), в которых выполняется Python-код, встроенный в скрипты Ren'Py, типы object, list, dict и set были заменены на эквивалентные откатываемые типы. Объекты, наследующие от этих типов, также являются откатываемыми. Тип renpy.Displayable наследует от откатываемого типа object.

Чтобы сделать использование откатываемых объектов более удобным, Ren'Py изменяет Python, находящийся внутри файлов скриптов Ren'Py, следующим образом.

  • Литеральные списки, словари и множества автоматически преобразуются в их откатываемые эквиваленты.

  • Списковые, словарные и множественные включения (comprehensions) также автоматически преобразуются в их откатываемые эквиваленты.

  • Другой синтаксис Python, такой как расширенная распаковка, который может создавать списки, словари или множества, преобразует результат в откатываемый эквивалент. Однако из соображений производительности параметры с двумя звездочками (double-starred parameters) в функциях и методах (которые создают словари дополнительных именованных аргументов) не преобразуются в откатываемые объекты.

  • Классы, которые не наследуют от каких-либо других типов, автоматически наследуют от откатываемого объекта.

Кроме того:

  • Методы и операторы откатываемых типов были изменены так, чтобы возвращать откатываемые объекты при создании списка, словаря или множества.

  • Встроенные функции, возвращающие списки, словари и множества, возвращают их откатываемые эквиваленты.

Вызов кода Python, как правило, не создает откатываемый объект. Некоторые случаи, когда вы получите объект, который не сможет участвовать в откате:

  • Вызов методов встроенных типов, например, метода str.split.

  • Когда объект создается в импортированном модуле Python, а затем возвращается в Ren'Py. (Например, экземпляр collections.defaultdict не будет участвовать в откате).

  • Объекты, возвращаемые API Ren'Py, если в документации не указано иное.

Если такие данные должны участвовать в откате, имеет смысл преобразовать их в тип, который это делает. Например:

# Вызов list внутри Python-в-Ren'Py преобразует неоткатываемый список
# в откатываемый.
$ attrs = list(renpy.get_attributes("eileen"))

Поддержка отката и наката

Большинство инструкций Ren'Py автоматически поддерживают откат (rollback) и накат (roll forward). Если вы вызываете ui.interact() напрямую, вам потребуется самостоятельно добавить поддержку отката и наката. Это можно сделать, используя следующую структуру:

# Это None, если мы не откатываемся, или значение, которое было
# передано в checkpoint в прошлый раз, если мы накатываемся вперед.
roll_forward = renpy.roll_forward_info()

# Настройте экран здесь...

# Взаимодействие с пользователем.
rv = ui.interact(roll_forward=roll_forward)

# Сохраните результат взаимодействия.
renpy.checkpoint(rv)

Важно, чтобы ваша игра не взаимодействовала с пользователем после вызова renpy.checkpoint. (Если это произойдет, пользователь может потерять возможность отката).

renpy.can_rollback()

Возвращает true, если мы можем выполнить откат.

renpy.checkpoint(data=None, *, hard=True)

Делает текущий оператор контрольной точкой, к которой пользователь может откатиться. После вызова этой функции не должно быть больше взаимодействий с пользователем в текущем операторе.

Это также очистит текущий скриншот, используемый в сохранениях.

data

Эти данные возвращаются функцией renpy.roll_forward_info() во время отката игры.

hard

Если true, это жесткая контрольная точка, на которой откат остановится. Если false, это мягкая контрольная точка, которая не остановит откат.

renpy.get_identifier_checkpoints(identifier)

Получив rollback_identifier из объекта HistoryEntry, возвращает количество контрольных точек, которые нужно передать в renpy.rollback(), чтобы достичь этого идентификатора. Возвращает None, если идентификатор отсутствует в истории отката.

renpy.in_rollback()

Возвращает true, если игра была откачена.

renpy.roll_forward_info()

Во время отката возвращает данные, которые были переданы в renpy.checkpoint() при последнем выполнении этого оператора. Вне отката возвращает None.

renpy.rollback(force=False, checkpoints=1, defer=False, greedy=True, label=None, abnormal=True)

Откатывает состояние игры до последней контрольной точки.

force

Если true, откат произойдет в любых обстоятельствах. В противном случае, откат произойдет только если он включен в store, context и config.

checkpoints

Ren'Py откатится через указанное количество вызовов renpy.checkpoint. Он откатится настолько далеко, насколько это возможно, с учетом этого условия.

defer

Если true, вызов будет отложен до тех пор, пока управление не вернется в основной контекст.

greedy

Если true, откат завершится сразу после предыдущей контрольной точки. Если false, откат завершится прямо перед текущей контрольной точкой.

label

Если не None, это метка, которая вызывается по завершении отката.

abnormal

Если true (по умолчанию), скрипт, выполняемый после перехода, запускается в аномальном режиме, который пропускает переходы, которые могли бы произойти в ином случае. Аномальный режим заканчивается, когда начинается взаимодействие.

renpy.suspend_rollback(flag)

Откат будет пропускать участки игры, где он был приостановлен.

flag:

Когда flag равен true, откат приостанавливается. Когда false, откат возобновляется.

Блокировка отката

Предупреждение

Блокировка отката — недружественное по отношению к пользователю действие. Если пользователь по ошибке выберет не тот вариант, он не сможет исправить свою ошибку. Поскольку откат эквивалентен сохранению и загрузке, ваши пользователи будут вынуждены сохраняться чаще, что нарушает погружение в игру.

Можно частично или полностью отключить откат. Если откат вообще не нужен, его можно просто выключить с помощью опции config.rollback_enabled.

Чаще встречается частичная блокировка отката. Этого можно достичь с помощью функции renpy.block_rollback(). При вызове она указывает Ren'Py не откатываться дальше этой точки. Например:

label final_answer:
    "Is that your final answer?"

menu:
    "Yes":
        jump no_return
    "No":
        "We have ways of making you talk."
        "You should contemplate them."
        "I'll ask you one more time..."
        jump final_answer

label no_return:
    $ renpy.block_rollback()

    "So be it. There's no turning back now."

Когда игра достигнет метки no_return, Ren'Py не позволит откатиться назад к меню.

Фиксация отката

Фиксация отката представляет собой промежуточный вариант между неограниченным откатом и его полной блокировкой. Откат разрешен, но пользователю не позволяется изменять свои решения. Фиксация отката выполняется с помощью функции renpy.fix_rollback(), как показано в следующем примере:

label final_answer:
    "Is that your final answer?"
menu:
    "Yes":
        jump no_return
    "No":
        "We have ways of making you talk."
        "You should contemplate them."
        "I'll ask you one more time..."
        jump final_answer

label no_return:
    $ renpy.fix_rollback()

    "So be it. There's no turning back now."

Теперь, после вызова функции fix_rollback, пользователь все еще сможет откатиться к меню. Однако сделать другой выбор будет невозможно.

При разработке игры с использованием fix_rollback следует учитывать некоторые нюансы. Ren'Py автоматически позаботится о блокировке любых данных, переданных в checkpoint(). Однако, из-за общей природы Ren'Py, можно написать скрипты, которые обходят это и изменяют данные способами, которые могут привести к непредсказуемым результатам. В частности, call screen плохо работает с фиксированным откатом. Разработчик должен сам блокировать откат в проблемных местах.

Внутренние опции взаимодействия с пользователем для меню, renpy.input() и renpy.imagemap(), разработаны так, чтобы полностью работать с fix_rollback.

Стилизация фиксированного отката

Поскольку fix_rollback изменяет функциональность меню и карт изображений (imagemaps), рекомендуется отразить это во внешнем виде. Для этого важно понимать, как изменяются состояния виджетов кнопок меню. Есть два режима, которые можно выбрать с помощью опции config.fix_rollback_without_choice.

Опция по умолчанию установит выбранный вариант в состояние "selected" (выбран), тем самым активируя свойства стиля с префиксом "selected_". Все остальные кнопки станут неактивными (insensitive) и будут отображаться с использованием свойств с префиксом "insensitive_". Фактически это оставляет в меню только один доступный для выбора вариант.

Когда опция config.fix_rollback_without_choice установлена в False, все кнопки становятся неактивными. Это означает, что выбранный вариант будет использовать префикс "selected_insensitive_" для свойств стиля, в то время как другие кнопки будут использовать свойства с префиксом "insensitive_".

Фиксированный откат и пользовательские экраны

Для упрощения создания пользовательских экранов предоставляются два действия, которые помогают в наиболее распространенных случаях. Действие ui.ChoiceReturn() возвращает значение при нажатии на кнопку, к которой оно привязано. Действие ui.ChoiceJump() можно использовать для перехода к метке скрипта. Однако это действие работает правильно только тогда, когда экран вызывается через инструкцию call screen.

Примеры:

screen demo_imagemap():
    roll_forward True

    imagemap:
        ground "imagemap_ground.jpg"
        hover "imagemap_hover.jpg"
        selected_idle "imagemap_selected_idle.jpg"
        selected_hover "imagemap_hover.jpg"

        hotspot (8, 200, 78, 78) action ui.ChoiceJump("swimming", "go_swimming", block_all=False)
        hotspot (204, 50, 78, 78) action ui.ChoiceJump("science", "go_science_club", block_all=False)
        hotspot (452, 79, 78, 78) action ui.ChoiceJump("art", "go_art_lessons", block_all=False)
        hotspot (602, 316, 78, 78) action ui.ChoiceJump("home", "go_home", block_all=False)
screen rps():
    roll_forward True

    hbox:
        imagebutton:
            idle "rock.png"
            hover "rock_hover.png"
            selected_insensitive "rock_hover.png"
            action ui.ChoiceReturn("rock", "Rock", block_all=True)
        imagebutton:
            idle "paper.png"
            hover "paper_hover.png"
            selected_insensitive "paper_hover.png"
            action ui.ChoiceReturn("paper", "Paper", block_all=True)
        imagebutton:
            idle "scissors.png"
            hover "scissors_hover.png"
            selected_insensitive "scissors_hover.png"
            action ui.ChoiceReturn("scissors", "Scissors", block_all=True)

        if renpy.in_fixed_rollback():
            textbutton "Advance":
                action Return(renpy.roll_forward_info())
                # требуется из-за block_all=True во всех остальных кнопках

label dough:
    call screen rps
    $ chosen = _return
    $ renpy.fix_rollback()
    m "[chosen]!"

При написании собственных подпрограмм на Python, которые должны корректно работать с системой fix_rollback, нужно знать несколько простых вещей. Во-первых, функция renpy.in_fixed_rollback() может быть использована для определения, находится ли игра в данный момент в состоянии фиксированного отката. Во-вторых, в состоянии фиксированного отката ui.interact() всегда будет возвращать предоставленные данные roll_forward, независимо от того, какое действие было выполнено. Это фактически означает, что при использовании функций ui.interact()/renpy.checkpoint() большая часть работы уже сделана.

Функции блокировки и фиксации отката

renpy.block_rollback()

Запрещает игре откатываться к моменту до текущей инструкции.

renpy.fix_rollback()

Запрещает пользователю изменять решения, принятые до текущей инструкции.

renpy.in_fixed_rollback()

Возвращает true, если в данный момент происходит откат, и текущий контекст находится до выполненной инструкции renpy.fix_rollback().

ui.ChoiceJump(label, value, location=None, block_all=None, sensitive=True, args=None, kwargs=None)

Действие выбора в меню, которое возвращает value, управляя состоянием кнопки в соответствии с фиксированным откатом (fixed rollback). (См. block_all для описания поведения.)

label

Текст метки кнопки. Для imagebuttons и hotspots это может быть что угодно. Эта метка используется как уникальный идентификатор вариантов в пределах текущего экрана. Вместе с location она используется для хранения информации о том, был ли выбран этот вариант.

value

Метка (location), к которой нужно перейти.

location

Уникальный идентификатор местоположения для текущего экрана выбора.

block_all

Если false, кнопке присваивается роль selected, если это был выбранный вариант, и insensitive, если он не был выбран.

Если true, кнопка всегда неактивна (insensitive) во время фиксированного отката.

Если None, значение берется из переменной config.fix_rollback_without_choice.

Когда true указано для всех элементов на экране, он становится некликабельным (перемотка вперед все равно будет работать).

ui.ChoiceReturn(label, value, location=None, block_all=None, sensitive=True, args=None, kwargs=None)

Действие выбора в меню, которое возвращает value, управляя состоянием кнопки в соответствии с фиксированным откатом (fixed rollback). (См. block_all для описания поведения.)

label

Текст метки кнопки. Для imagebuttons и hotspots это может быть что угодно. Эта метка используется как уникальный идентификатор вариантов в пределах текущего экрана. Вместе с location она используется для хранения информации о том, был ли выбран этот вариант.

value

Значение, которое возвращается при выборе этого варианта.

location

Уникальный идентификатор местоположения для текущего экрана выбора.

block_all

Если false, кнопке присваивается роль selected, если это был выбранный вариант, и insensitive, если он не был выбран.

Если true, кнопка всегда неактивна (insensitive) во время фиксированного отката.

Если None, значение берется из переменной config.fix_rollback_without_choice.

Когда true указано для всех элементов на экране, он становится некликабельным (перемотка вперед все равно будет работать).

NoRollback

class NoRollback

Экземпляры этого класса и классов, наследующих от него, не участвуют в откате (rollback). Объекты, достижимые через экземпляр класса NoRollback, участвуют в откате только в том случае, если они достижимы по другим путям.

class SlottedNoRollback

Экземпляры классов, наследующих от этого класса, не участвуют в откате. Разница между этим классом и NoRollback заключается в том, что у этого класса нет связанного словаря, поэтому его можно использовать с __slots__ для уменьшения потребления памяти.

Объекты, достижимые через экземпляр класса NoRollback, участвуют в откате только в том случае, если они достижимы по другим путям.

Например:

init python:

    class MyClass(NoRollback):
        def __init__(self):
            self.value = 0

label start:
    $ o = MyClass()

    "Welcome!"

    $ o.value += 1

    "o.value равно [o.value]. Оно будет увеличиваться каждый раз, когда вы откатываетесь, а затем проходите вперед."

Классы, поддерживающие откат

Следующие классы существуют для поддержки использования отката в вашей игре. Они могут быть полезны в некоторых обстоятельствах.

class MultiRevertable

MultiRevertable — это примесный класс (mixin), который позволяет объекту наследовать более чем один вид откатываемых объектов. Чтобы использовать его, наследуйте сначала от MultiRevertable, а затем от откатываемых классов, от которых вы хотите наследовать.

Например:

class MyDict(MultiRevertable, dict, object):
    pass

создаст класс, который будет откатывать как содержимое своего словаря (dict), так и поля объекта (object).

class defaultdict(default_factory, /, *args, **kwargs)

Это откатываемая версия collections.defaultdict. Она принимает фабричную функцию. Если происходит обращение к ключу, которого не существует, функция default_factory вызывается с ключом в качестве аргумента, и результат возвращается.

Хотя атрибут default_factory присутствует в этом объекте, он не участвует в откате, и поэтому не должен изменяться.