Сохранение, загрузка и откат (Saving, Loading, and Rollback)

Материал из Ren'Py Wiki
Перейти к навигации Перейти к поиску

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



Заметка: Обычно мы пытаемся сохранить совместимость между версиями, но в данном случает она не гарантируется. Мы можем нарушить сохранение совместимости, если это даст достаточно большое преимущество.


Что сохраняется (What is Saved)[править | править код]

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

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

  • Текущий оператор и все операторы, к которым можно вернуться.
  • Изображения и отображаемые объекты, которые отображаются.
  • Отображаемые экраны и значения переменных на этих экранах.
  • Музыка, воспроизводимая Ren'Py.
  • Список текстовых блоков в NVL-режиме.

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

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

define a = 1
define o = object()

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

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

Что не сохраняется (What isn't Saved)[править | править код]

Переменные в 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 сохраняет только текущий оператор и оператор, к которому он должен вернуться. Он не помнит, как он туда попал. Важно отметить, что операторы (включая присвоения переменных), добавленные в игру, не будут выполняться.
Сопоставление имён изображений в отображаемых объектах
Поскольку это сопоставление не сохраняется, изображение может измениться на новое изображение, когда игра снова загрузится. Это позволяет изображению переходить в новый файл по мере развития игры.
Переменные конфигурации, стили и свойства стиля
Переменные конфигурации и стили не сохраняются как часть игры. Поэтому их следует менять только в блоках init и оставлять в покое после начала игры.

Где Ren'Py сохраняет (Where Ren'Py Saves)[править | править код]

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

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

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

python:

     i = 0

     while i < 10:

          i += 1

          narrator("Счёт сейчас [i].")

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

$ i = 0

while i < 10:

     $ i += 1

     "Счёт сейчас [i]."

Что Ren'Py может сохранить (What Ren'Py can Save)[править | править код]

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

  • Основные типы, такие как True, False, None, int, str, float, complex, str и объекты Unicode.
  • Составные типы, такие как списки, кортежи, наборы и словари.
  • Определяемые разработчиком объекты, классы, функции, методы и связанные методы. Для успешного выполнения этих функций они должны оставаться доступными под своими первоначальными именами.
  • Объекты Character, Displayable, Transform и Transition.


Существуют определенные виды, которые нельзя законсервировать:

  • Объекты рендеринга.
  • Интегрируемые объекты.
  • Файло-подобные объекты.
  • Внутренние и анонимные функции.


По умолчанию Ren'Py использует модуль cPickle для сохранения игры. Установка config.use_cpickle заставит Ren'Py использовать вместо этого модуль pickle. Это замедляет игру, но лучше сообщает об ошибках сохранения.

Сохранение функций и переменных (Save Functions and Variables)[править | править код]

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

save_name = ...

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


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

renpy.can_load(filename, test=False)

Возвращает значение True, если filename существует как слот для сохранения, иначе False.


renpy.copy_save(old, new)

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


renpy.list_saved_games(regexp='.', fast=False)

Списки сохраненных игр. Для каждой сохраненной игры вычисляется кортеж, содержащий:
  • Имя файла сохранения.
  • Extra_info, который был передан.
  • Отображаемый объект, который при отображении показывает скриншот, который использовался при сохранении игры.
  • Время, когда игра была остановлена, в секундах с эпохи UNIX.
regexp
Регулярное выражение, сопоставляемое с началом имени файла для фильтрации списка.
fast
Если fast имеет значение True, вместо кортежа вычисляется имя файла.


renpy.list_slots(regexp=None)

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


renpy.load(filename)

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


renpy.newest_slot(regexp=None)

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


renpy.rename_save(old, new)

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


renpy.save(filename, extra_info=)

Сохраняет состояние игры в слот сохранения.
filename
Строка, задающая имя слота сохранения. Несмотря на имя переменной, она соответствует только именам файлов. Обычно это значение save_name.
extra_info
Дополнительная строка, которая должна быть сохранена в файле сохранения.
renpy.take_screenshot() должен быть вызван до этой функции.


renpy.slot_json(slotname)

Возвращает информацию из json для slotname или значение None, если слот пуст.


renpy.slot_mtime(slotname)

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


renpy.slot_screenshot(slotname)

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


renpy.take_screenshot(scale=None, background=False)

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


renpy.unlink_save(filename)

Удаляет слот сохранения с указанным именем.

Сохранение данных после загрузки (Retaining Data After Load)[править | править код]

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

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

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

Например:

creen 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()

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

Откат (Rollback)[править | править код]

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

Поддержка отката и прокрутки вперёд (Supporting Rollback and Roll Forward)[править | править код]

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

# Принимает значение None, если мы не откатимся назад, или же значение, которое
# было передано в контрольную точку в прошлый раз, если мы продвигаемся вперёд.
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)

Делает текущий оператор контрольной точкой, к которой пользователь может выполнить откат. После вызова этой функции в текущем операторе больше не должно быть взаимодействия с пользователем.
Это также очистит текущий скриншот, используемый сохраненными играми.
data
При откате игры эти данные возвращаются renpy.roll_forward_info().


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, откат будет происходить при любых обстоятельствах. Иначе откат произойдет только в том случае, если он включён в хранилище, контекст и конфигурацию.
checkpoints
Ren'Py выполнит откат через множество вызовов renpy.checkpoint. Он будет откатываться назад, насколько это возможно, при условии соблюдения этого условия.
defer
Если принимает значение True, вызов будет отложен до тех пор, пока управление не вернётся в основной контекст.
greedy
Если принимает значение True, откат завершится сразу после предыдущей контрольной точки. Если принимает значение False, откат завершится непосредственно перед текущей контрольной точкой.
label
Если принимает значение не None, метка, которая вызывается, когда откат завершается.
abnormal
Если принимает значение True, скрипт по умолчанию, выполняемый после перехода, запускается в нештатном режиме, который пропускает переходы, которые в ином случае произошли бы. Нештатный режим заканчивается, когда начинается взаимодействие.


renpy.suspend_rollback(flag)

Откат пропустит разделы игры, где откат был приостановлен.
flag
Когда flag принимает значение True, откат приостанавливается. При значении False откат возобновляется.

Блокировка отката (Blocking Rollback)[править | править код]

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


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

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

label final_answer:
    "Это твой окончательный ответ?"

menu:
    "Да":
        jump no_return
    "Нет":
        "У нас есть способы заставить тебя говорить."
        "Тебе следует это учесть."
        "Я спрошу тебя ещё раз..."
        jump final_answer

label no_return:
    $ renpy.block_rollback()

    "Так и быть. Теперь пути назад нет."

При достижении метки no_return, Ren'Py не позволит совершить откат к оператору menu.

Фиксация отката (Fixing Rollback)[править | править код]

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

label final_answer:
    "Это твой окончательный ответ?"
menu:
    "Да":
        jump no_return
    "Нет":
        "У нас есть способы заставить тебя говорить."
        "Тебе следует это учесть."
        "Я спрошу тебя ещё раз..."
        jump final_answer

label no_return:
    $ renpy.fix_rollback()

    "Так и быть. Теперь пути назад нет."

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

При разработке игры с fix_rollback следует учесть некоторые особенности. Ren'Py автоматически позаботится о блокировке любых данных, которые передаются в checkpoint(). Тем не менее, из-за универсальной природы Ren'Py, можно написать код на Python, который обойдёт это и изменит таким образом многое, что приведёт к непредсказуемым результатам. Разработчик игры должен заблокировать откат в проблемных местах или написать дополнительный код на Python для решения этой проблемы.

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

Стилевое оформление фиксированного отката (Styling Fixed Rollback)[править | править код]

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

Опция по умолчанию установит выбранную опцию в «selected», тем самым активируя свойства стиля с префиксом «selected_». Все остальные кнопки будут нечувствительны к нажатию и отображаться с помощью свойства с префиксом «insensitive_». Практически это оставляет меню с одним выбором.

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

Фиксированный откат и пользовательские экраны (Fixed Rollback and Custom Screens)[править | править код]

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

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

Например:

screen demo_imagemap:
    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 uiChoiceJump("home", "go_home", block_all=False)

Например:

python:
    roll_forward = renpy.roll_forward_info()
    if roll_forward not in ("Rock", "Paper", "Scissors"):
        roll_forward = None

    ui.hbox()
    ui.imagebutton("rock.png", "rock_hover.png", selected_insensitive="rock_hover.png", clicked=ui.ChoiceReturn("rock", "Камень", block_all=True))
    ui.imagebutton("paper.png", "paper_hover.png", selected_insensitive="paper_hover.png", clicked=ui.ChoiceReturn("paper", "Бумага", block_all=True))
    ui.imagebutton("scissors.png", "scissors_hover.png", selected_insensitive="scissors_hover.png", clicked=ui.ChoiceReturn("scissors", "Ножницы", block_all=True))
    ui.close()

    if renpy.in_fixed_rollback():
        ui.saybehavior()

    choice = ui.interact(roll_forward=roll_forward)
    renpy.checkpoint(choice)

$ renpy.fix_rollback()
m "[choice]!"

Функции блокировки и фиксации отката (Rollback-blocking and -fixing Functions)[править | править код]

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, одновременно управляя состоянием кнопки в соответствии с фиксированным откатом. Описание поведения смотри в разделе block_all.
label
Текст метки кнопки. Для кнопок изображений и горячих точек это может быть что угодно. Эта метка используется в качестве уникального идентификатора параметров на текущем экране. Вместе с location используется для хранения информации о том, был ли выбран этот параметр.
value
Место, к которому можно перейти.
location
Уникальный идентификатор местоположения для текущего экрана выбора.
block_all
Если принимает значение False, выбранная роль присваивается кнопке, если она выбрана, если кнопка не выбрана, она нечувствительна к нажатию.
Если принимает значение True, кнопка всегда нечувствительна к нажатию при фиксированном откате.
Если принимает значение None, значение берётся из переменной config.fix_rollback_without_choice.
Когда значение True задаётся для всех элементов на экране, кнопка станет неактивной (перемотка вперёд будет работать). Это можно изменить, вызвав ui.saybehavior() перед вызовом ui.interact().


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

Действие выбора меню, которое возвращает value, одновременно управляя состоянием кнопки в соответствии с фиксированным откатом. Описание поведения смотри в разделе block_all.
label
Текст метки кнопки. Для кнопок изображений и горячих точек это может быть что угодно. Эта метка используется в качестве уникального идентификатора параметров на текущем экране. Вместе с location используется для хранения информации о том, был ли выбран этот параметр.
value
Значение возвращается при выборе.
location
Уникальный идентификатор местоположения для текущего экрана выбора.
block_all
Если принимает значение False, выбранная роль присваивается кнопке, если она выбрана, если кнопка не выбрана, она нечувствительна к нажатию.
Если принимает значение True, кнопка всегда нечувствительна к нажатию при фиксированном откате.
Если принимает значение None, значение берётся из переменной config.fix_rollback_without_choice.
Когда значение True задаётся для всех элементов на экране, кнопка станет неактивной (перемотка вперёд будет работать). Это можно изменить, вызвав ui.saybehavior() перед вызовом ui.interact().

Класс NoRollback (NoRollback)[править | править код]

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


Например:

init python:

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

label start:
    $ o = MyClass()

    "Добро пожаловать!"

    $ o.value += 1

    "o.value - это [o.value]. Значение увеличится при каждом откате, а затем нажмите вперёд."

Источник[править | править код]

Статья "Saving, Loading, and Rollback" на английском языке.


На главную страницу Официальная документация движка Вернуться к началу статьи