Сохранение, загрузка и откат (Saving, Loading, and Rollback)
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" на английском языке.
На главную страницу | Официальная документация движка | Вернуться к началу статьи |