Ren’Py支持保存游戏状态、载入游戏状态和回滚到之前的某个游戏状态。尽管实现的方式明显不同,回滚(rollback)可以认为每一条能与用户互动的语句开始时都保存了游戏状态,当用户进行回滚时加载之前保存的状态。
Note
我们通常需要保证不同release版本存档的兼容性,但兼容性并不能得到绝对保证。如果能带来巨大的游戏表现提升,我们也可以打破存档兼容性的要求。
Ren’Py会保存游戏状态。保存的内容包括内部状态和Python的状态。
内部状态由几个部分组成:Ren’Py在游戏启动后就改变的所有内容,以及下列内容:
Python状态包括从游戏启动后存储区变化过的所有变量,以及跟那些变量有关的所有对象。注意,只有变量相关才行——改变对象内的字段(field)并不会触发对象状态被保存。
使用 default语句 定义的变量总是会保存。
在下例中:
define a = 1
define o = object()
default c = 17
label start:
$ b = 1
$ o.value = 42
只有 b 和 c 会被保存。 a 不会被保存,因为它从游戏启动后就没有变动。 o 不会被保存因为它也没有变动——这里的变动是指引用对象发生变化,而不是对象成员变量的值的变化。
游戏开始后没有改变过的Python变量不会保存。 这可能是个重大的问题,前提是某个保存的变量引用了相同的对象。(对象的别名(alias)。)在这个例子中:
init python:
a = object()
a.f = 1
label start:
$ b = a
$ b.f = 2
"a.f=[a.f] b.f=[b.f]"
b 是 a 的别名。保存和加载可能打断这个别名关系,导致 a 和 b 引用不同的对象。因为这个问题让人十分头大,所以最好的办法就是避免在保存和不保存的变量间建立别名关系。(很少直接遇到这种情况,往往发生在不保存的变量和保存的字段(field)别名上。)
还有几种其他类型的状态不保存。
保存发生在外沿(outermost)互动上下文(context)中,Ren’Py语句的开头。
这里关注的重点是,保存发生在语句的 开头 。如果加载或回滚发生在某个语句中间,而且那个语句有多次互动,所有状态都会重置为语句开始的状态。
在使用Python定义的语句中,这可能会导致问题。在下面的语句:
python:
i = 0
while i < 10:
i += 1
narrator("现在的计数是 [i] 。")
如果用户在中间保存和加载,循环会从头开始。使用Ren’Py脚本——而不是直接用Python语句——的循环就能避免这个问题:
$ i = 0
while i < 10:
$ i += 1
"The count is now [i]."
Ren’Py使用Python的pickle系统保存游戏状态。这个模块可以保存:
还有几种无法pickle的数据类型:
默认情况下,Ren’Py使用cPickle模块保存游戏。将配置项 config.use_cpickle 的值改为False,可以让Ren’Py使用pickle模块。
默认配置速度较慢,但是在Python 2.x环境下比保存报错要好。
注意这个设置对Python 3没有效果。
有一个变量用于高级保存系统:
save_name = … link这是一个字符串,每次保存时都会存储。它可以用作存档名称,帮助用户区分不同存档。
在 界面行为 中定义了一些高级别的保存行为和函数。除此之外,还有一些低级别的保存和加载行为。
renpy.can_load(filename, test=False) link如果 filename 作为存档槽已存在则返回True,否则返回False。
renpy.copy_save(old, new) link将存档 old 复制到存档 new 。(如果 old 不存在则不做任何事。)
renpy.list_saved_games(regexp='.', fast=False) link列出保存的游戏。每一个保存的游戏返回的一个元组中包含:
renpy.list_slots(regexp=None) link返回一个非空存档槽的列表。如果 regexp 存在,只返回开头为 regexp 的槽位。列表内的槽位按照字符串排序(string-order)。
renpy.load(filename) link从存档槽 filename 加载游戏状态。如果文件加载成功,这个函数不会返回。
renpy.newest_slot(regexp=None) link返回最新(具有最近修改时间)存档槽的名称,如果没有(匹配的)存档则返回None。
如果 regexp 存在,只返回开头为 regexp 的槽位。
renpy.rename_save(old, new) link将某个名为 old 的存档重命名为 new 。(如果 old 不存在则不做任何事。)
renpy.save(filename, extra_info='') link将游戏状态保存至某个存档槽。
save_name() 的值。renpy.take_screenshot() 应该在这个函数之前被调用。
renpy.slot_json(slotname) link返回 slotname 的json信息,如果对应的槽位为空则返回None。
renpy.slot_mtime(slotname) link返回 slotname 的修改时间,如果对应的槽位为空则返回None。
renpy.slot_screenshot(slotname) link返回 slotname 使用的截屏,如果对应的槽位为空则返回None。
renpy.take_screenshot(scale=None, background=False) link执行截屏。截屏图像会被作为存档的一部分保存。
renpy.unlink_save(filename) link删除指定名称的存档。
当游戏加载后,游戏状态会被重置(使用下面会提到的回滚系统)为当前语句开始执行的状态。
在某些情况下,这是不希望发生的。例如,当某个界面允许编辑某个值时,我们可能想要游戏加载后维持那个值。调用 renpy.retain_after_load() 后,当游戏在下一个带检查点(checkpoint)的交互结束前,进行保存和加载行为都会保持不变。
注意,当数据没有被改变,主控流程会被重置为当前语句的开头。这条语句将再次执行,语句开头则使用新的数据。
举例:
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() link在当前语句和包含下一个检查点(checkpoint)的语句之间发生加载(load)时,保持数据。
回滚(rollback)允许用户将游戏恢复到之前的状态,类似流行应用程序中的“撤销/重做”系统。在回滚事件中,系统需要重点维护可视化和游戏变量,所以在创作游戏时有几点需要考虑。
大多数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() link如果可以回滚则返回True。
renpy.checkpoint(data=None) link在当前语句设置一个能让用户回滚的检查点(checkpoint)。一旦调用这个函数,当前语句就不该再出现互动行为。
renpy.roll_forward_info() 返回。renpy.get_identifier_checkpoints(identifier) link从HistoryEntry对象中寻找rollback_identifier,返回需要的检查点(checkpoint)数量,并传入 renpy.rollback() 以到达目标标识符(identifier)。如果标识符不在回滚历史中,返回None。
renpy.in_rollback() link游戏回滚过则返回True。
renpy.roll_forward_info() link在回滚中,返回这条语句最后一次执行时返回并应用于 renpy.checkpoint() 的数据。如果超出滚回范围,则返回None。
renpy.rollback(force=False, checkpoints=1, defer=False, greedy=True, label=None, abnormal=True) link将游戏状态回滚至最后一个检查点(checkpoint)。
renpy.suspend_rollback(flag) link回滚会跳过游戏中已经挂起回滚的章节。
Warning
阻拦回滚是一个对用户不友好的事情。如果一个用户错误点击了不希望进入的分支选项,ta就不能修正自己的错误。由于回滚等效于存档和读档,用户就会被强迫频繁地存档,破坏游戏体验。
部分或者完全禁用回滚是可能的。如果根本不想要回滚,可以使用 config.rollback_enabled 函数关闭选项。
更通用的做法是分段阻拦回滚。这可以通过 renpy.block_rollback() 函数实现。当调用该函数时,Ren’Py的回滚会在某个点上停止。举例:
label final_answer:
"这就是你的最终答案吗?"
menu:
"是":
jump no_return
"不":
"我们有办法让你开口。"
"你还是好好想考虑下吧。"
"我再问你一次……"
jump final_answer
label no_return:
$ renpy.block_rollback()
"然后到了这里。现在不能回头了。"
当到达脚本标签(label)no_return时,Ren’Py就停止回滚,不会进一步回滚到标签menu。
混合回滚提供了一种介于完全无限制回滚和完全阻拦回滚之间的中间选项。回滚是允许的,但用户无法修改之前做出的选择。混合修改使用 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。
因为fix_rollback改变了菜单和imagemap的功能,建议考虑应对这种情况。理解菜单按钮的组件状态如何改变很重要。通过 config.fix_rollback_without_choice() 选项,可以更改两种模式。
默认配置会将选过的选项设置为“selected”,进而激活样式所有带“selected_”前缀的样式特性。所有其他按钮会被设置为不可用,并使用前缀为“insensitive_”前缀的特性显示。这样的最终效果就是菜单仅有一个可选的选项。
当 config.fix_rollback_without_choice() 项被设为False时,所有按钮都会设置为不可用。之前选过的那项会使用“selected_insensitive_”前缀的风格特性,而其他按钮会使用前缀为“insensitive_”前缀的特性。
当使用fix_rollback系统编写定制Python路由,使游戏流程更舒服时,有几个简单的要点。首先是 renpy.in_fixed_rollback() 函数可以用作决定游戏当前是否处于混合回滚状态。其次,当处于混合回滚状态时, ui.interact() 函数总会返回使用的roll_forward数据,而不考虑行为是否执行。这表示,当 ui.interact()/renpy.checkpoint() 函数被使用时,大多数工作都已经完成了。
为了简化定制界面的创建,Ren’Py提供了两个最常用的行为(action)。当按钮检测到被按下时, ui.ChoiceReturn() 行为会返回。 ui.ChoiceJump() 行为可以用于跳转到某个脚本标签(label)。当界面通过一个 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 ui.ChoiceJump("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", "Rock", block_all=True))
ui.imagebutton("paper.png", "paper_hover.png", selected_insensitive="paper_hover.png", clicked=ui.ChoiceReturn("paper", "Paper", block_all=True))
ui.imagebutton("scissors.png", "scissors_hover.png", selected_insensitive="scissors_hover.png", clicked=ui.ChoiceReturn("scissors", "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]!"
renpy.block_rollback() link防止回滚到当前语句之前的脚本。
renpy.fix_rollback() link防止用于更改在当前语句之前做出的选项决定。
renpy.in_fixed_rollback() link如果正在发生回滚的当前上下文(context)后面有一个执行过的renpy.fix_rollback()语句,就返回True。
ui.ChoiceJump(label, value, location=None, block_all=None) link一个菜单选项行为(action),返回值为 value 。同时管理按钮在混合回滚模式下的状态。(详见对应的 block_all 参数。)
若为False,被选中选项的按钮会赋予“selected”角色,未选中的选项按钮会置为不可用。
若为True,混合回滚时按钮总是不可用。
若为None,该值使用 config.fix_rollback_without_choice() 配置项。
当某个界面内所有选项都被赋值为True时,选项菜单变成点击无效状态(回滚依然有效)。这可以通过在 ui.interact() 之前调用 ui.saybehavior() 修改。
ui.ChoiceReturn(label, value, location=None, block_all=None) link一个菜单选项行为(action),返回值为 value 。同时管理按钮在混合回滚模式下的状态。(详见对应的 block_all 参数。)
若为False,被选中选项的按钮会赋予“selected”角色,未选中的选项按钮会置为不可用。
若为True,混合回滚时按钮总是不可用。
若为None,该值使用 config.fix_rollback_without_choice() 配置项。
当某个界面内所有选项都被赋值为True时,选项菜单变成点击无效状态(回滚依然有效)。这可以通过在 ui.interact() 之前调用 ui.saybehavior() 修改。