RenPy中文空间

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 118|回复: 0

存档、读档和回滚(新)

 关闭 [复制链接]
发表于 2018-4-26 18:01:33 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
存档、读档和回滚(翻译:被诅咒的章鱼)

Ren’Py支持保存游戏状态、载入游戏状态和回滚到之前的某个游戏状态。尽管实现的方式明显不同,回滚(rollback)可以认为每一条能与用户互动的语句开始时都保存了游戏状态,当用户进行回滚时加载之前保存的状态。

注意:
我们通常需要保证不同release版本存档的兼容性,但兼容性并不能得到绝对保证。如果能带来巨大的游戏表现提升,我们也可以打破存档兼容性的要求。


保存了什么
Ren’Py会保存游戏状态。保存的内容包括内部状态和 Python的状态。

内部状态由几个部分组成:Ren’Py在游戏启动后就改变的所有内容,以及下列内容:

  • 当前语句和所有主控流程可能返回到的语句。
  • 正在显示的图像和可视组件。
  • 正在显示的界面,以及界面内所有的变量值。
  • Ren’Py正在播放的音乐。
  • NVL模式文本块(block)列表。

Python状态包括从游戏启动后存储区变化过的所有变量,以及跟那些变量有关的所有对象。注意,只有变量相关才行——改变对象内的域(field)并不会触发对象状态被保存。

在下例中:
[RenPy] 纯文本查看 复制代码
define a = 1
define o = object()

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

只有 b 会被保存。 a 不会被保存,因为它从游戏启动后就没有变动。 o 不会被保存因为它也没有变动——这里的变动是指引用对象的变化,而不是对象中变量值的变化。

不保存什么
在游戏开始之前定义,之后一直没有改变过的Python变量不会被保存。这可能是个重大的问题,前提是某个保存的变量引用了相同的对象。(对象的别名(alias)。)在这个例子中:
[RenPy] 纯文本查看 复制代码
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)别名上。)

还有几种其他类型的状态不保存。

control flow path
    Ren’Py值保存当前语句,以及该语句需要返回的对应语句。Ren’Py不记得如何抵达当前语句。关键是,被添加到游戏的语句(包括变量声明)不会运行。

mappings of image names to displayables
    因为这个映射关系不会保存,游戏加载后图像可能变成了一个新的图像。随着游戏的进度,允许某个图像变为使用一个新的文件。

configuration variables, styles, and style properties

    配置项变量和样式不会作为游戏的一部分保存。所以它们应该只在初始化语句块(init block)中改变,游戏启动后就不再改变。

Ren’Py保存在哪里
保存发生在外沿(outermost)互动上下文(context)中Ren’Py语句的开头。

这里关注的重点是,保存发生在语句的 开头 。如果加载或回滚发生在某个语句中间,而且那个语句有多次互动,所有状态都会重置为语句开始的状态。

在使用Python定义的语句中,这可能会导致问题。在下面的语句:
[RenPy] 纯文本查看 复制代码
python:

     i = 0

     while i < 10:

          i += 1

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

如果用户在中间保存和加载,循环会从头开始。使用Ren’Py脚本——而不是直接用Python语句——的循环就能避免这个问题:
[RenPy] 纯文本查看 复制代码
$ i = 0

while i < 10:

     $ i += 1

     "The count is now [i]."

Ren’Py能保存什么
Ren’Py使用Python的pickle系统保存游戏状态。这个模块可以保存:

  • 基本数据类型,比如True、False、None、整型(int)、字符型(str)、浮点型(float)、复杂字符(complex str)和unicode对象。
  • 复合类型,比如列表(list)、元组(tuple)、集合(set)和字典(dict)。
  • 创作者定义的对象(object)、类(class)、函数(function)、方法(methed)和绑定方法(bound method)。成功pickle后,它们可以使用原来的名称维持功能。
  • 角色(character)、可视组件(displayable)、变换(transform)和转场(transition)对象。

还有几种无法pickle的数据类型:

  • 渲染(render)对象。
  • 迭代器(iterator)对象。
  • 类文件(file-like)对象。
  • 内部函数和lambda。

默认情况下,Ren’Py使用cPickle模块保存游戏。配置 config.use_cpickle() 的值可以让 Ren’Py使用 pickle模块。默认配置速度较慢,但是比保存报错要好。

保存函数和变量
有一个变量用于高级保存系统:

save_name
= ...
    这是一个字符串,每次保存时都会存储。它可以用作存档名称,帮助用户区分不同存档。

界面动作 中定义了一些高级别的保存动作和函数。除此之外,还有一些低级别的保存和加载动作。

renpy.can_load(filename, test=False)
    如果 filename 作为存档槽已存在则返回True,否则返回False。

renpy.copy_save(old, new)
    将存档 old 复制到存档 new 。(如果 old 不存在则不做任何事。)

renpy.list_saved_games(regexp='.', fast=False)
    列出保存的游戏。每一个保存的游戏返回的一个元组中包含:

  •     保存的文件名。
  •     传入的extra_info。
  •     一个可视组件,保存的截屏。
  •     游戏时间戳,UNIX时代开始计算的秒数。

    regexp
        在列表中过滤文件名的正则表达式。

    fast
        若为True,返回文件名而不是元组。

renpy.list_slots(regexp=None)
    返回一个非空存档槽的列表。如果 regexp 存在,只返回开头为 regexp 的槽位。列表内的槽位按照字符串排序(string-order)。

renpy.load(filename)
    从存档槽 filename 加载游戏状态。如果文件加载成功,这个函数不会返回。

renpy.newest_slot(regexp=None)
    返回最新(具有最近修改时间)存档槽的名称,如果没有(匹配的)存档则返回None。

    如果 regexp 存在,只返回开头为 regexp 的槽位。

renpy.rename_save(old, new)
    将某个名为 old 的存档重命名为 new 。(如果 old 不存在则不做任何事。)

renpy.save(filename, extra_info='')
    将游戏状态保存至某个存档槽。

    filename
        一个表示存档槽名称的字符串。尽管名称是变量,但这只对应于文件名。

    extra_info
        会保存在存档文件中的一个额外字符串。通常就是 save_name 的值。

    renpy.take_screenshot() 应该在这个函数之前被调用。

renpy.slot_json(slotname)
    返回 slotname 的 json信息,如果对应的槽位为空,则返回 None。

renpy.slot_mtime(slotname)
    返回 slotname 的修改时间,如果对应的槽位为空,则返回 None。

renpy.slot_screenshot(slotname)
    返回 slotname 使用的截屏,如果对应的槽位为空,则返回 None。

renpy.take_screenshot(scale=None, background=False)
    执行截屏。截屏图像会被作为存档的一部分保存。

renpy.unlink_save(filename)
    删除指定名称的存档。

读取存档后保持数据
当游戏加载后,游戏状态会被重置(使用下面会提到的回滚系统)为当前语句开始执行的状态。

在某些情况下,这是不希望发生的。例如,当某个界面允许编辑某个值时,我们可能想要游戏加载后维持那个值。调用 renpy.retain_after_load() 后,当游戏在下一个带检查点(checkpoint)的交互结束前,进行保存和加载动作都会保持不变。

注意,当数据没有被改变,主控流程会被重置为当前语句的开头。这条语句将再次执行,语句开头则使用新的数据。

举例:
[RenPy] 纯文本查看 复制代码
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()
    在当前语句和包含下一个检查点(checkpoint)的语句之间发生加载(load)时,保持数据。

回滚
回滚(rollback)允许用户将游戏恢复到之前的状态,类似流行应用程序中的“撤销/重做”系统。在回滚事件中,系统需要重点维护可视化和游戏变量,所以在创作游戏时有几点需要考虑。

支持回滚和前向滚动
大多数Ren’Py语句自动支持回滚和前向滚动。如果直接调用 ui.interact() ,就需要自行添加对回滚和前向滚动的支持。可以使用下列结构实现:
[RenPy] 纯文本查看 复制代码
# 非回滚状态这项是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)
    在当前语句设置一个能让用户回滚的检查点(checkpoint)。一旦调用这个函数,当前语句就不该再出现互动行为。

    data
        当游戏回滚时,这个数据通过 renpy.roll_forward_info() 返回。

renpy.get_identifier_checkpoints(identifier)
    从 HistoryEntry对象中寻找 rollback_identifier,返回需要的检查点(checkpoint)数量,并传入 renpy.rollback() 以到达目标标识符(identifier)。如果标识符不在回滚历史中,返回 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)
    将游戏状态回滚至最后一个检查点(checkpoint)。

    force
        若为True,所有情况下都可以回滚。否则,在存储区、上下文(context)和配置(config)中启用时才能进行回滚。

    checkpoints
        通过 renpy.checkpoint 回滚的目标检查点(checkpoint)。这种情况下,会尽可能快地回滚。

    defer
        若为 True,调用会推迟到主控流程回到主语境(context)。

    greedy
        若为 True,回滚会在前一个检查点(checkpoint)后面结束。若为False,回滚会在当前检查点前结束。

    label
        若不是 None,当回滚完成后,调用的脚本标签(label)。

    abnormal
        若为 True,也是默认值,异常(abnormal)模式下的转场(transition)会被跳过,否则显示转场。当某个互动行为开始时,异常(abnormal)模式结束。

renpy.suspend_rollback(flag)
    回滚会跳过游戏中已经挂起回滚的章节。

    flag
        当 flag 为 True时,回滚挂起。当 flag 为 False时,回滚恢复。

阻拦回滚

警告:
阻拦回滚是一个对用户不友好的事情。如果一个用户错误点击了不希望进入的分支选项,ta就不能修正自己的错误。由于回滚等效于存档和读档,用户就会被强迫频繁地存档,破坏游戏体验。


部分或者完全禁用回滚是可能的。如果根本不想要回滚,可以使用 config.rollback_enabled() 函数关闭选项。

更通用的做法是分段阻拦回滚。这可以通过 renpy.block_rollback() 函数实现。当调用该函数时,Ren’Py的回滚会在某个点上停止。举例:
[RenPy] 纯文本查看 复制代码
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."

当到达脚本标签(label)no_return时,Ren’Py就停止回滚,不会进一步回滚到标签menu。

混合回滚
混合回滚提供了一种介于完全无限制回滚和完全阻拦回滚之间的中间选项。回滚是允许的,但用户无法修改之前做出的选择。混合修改使用 renpy.fix_rollback() 函数实现,下面是样例:
[RenPy] 纯文本查看 复制代码
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 函数后,用户依然可以回滚到标签 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 语句被调用时,这个动作才能正常工作。

举例:
[RenPy] 纯文本查看 复制代码
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)

举例:
[RenPy] 纯文本查看 复制代码
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()
    防止回滚到当前语句之前的脚本。

renpy.fix_rollback()
    防止用于更改在当前语句之前做出的选项决定。

renpy.in_fixed_rollback()
    如果正在发生回滚的当前上下文(context)后面有一个执行过的renpy.fix_rollback()语句,就返回True。

ui.ChoiceJump(label, value, location=None, block_all=None)
    一个菜单选项动作(action),返回值为 value 。同时管理按钮在混合回滚模式下的状态。(详见对应的 block_all 参数。)

    label
        按钮的文本标签(label)。对imagebutton和hotspot来说可以是任何类型。这个标签用作当前界面内选项的唯一标识符。这个标识符与 location 一起存储,用于记录该选项是否可以被选择。

    value
        跳转的位置。

    location
        当前选项界面的唯一位置标识符。

    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)
    一个菜单选项动作(action),返回值为 value 。同时管理按钮在混合回滚模式下的状态。(详见对应的 block_all 参数。)

    label
        按钮的文本标签(label)。对imagebutton和hotspot来说可以是任何类型。这个标签用作当前界面内选项的唯一标识符。这个标识符与 location 一起存储,用于记录该选项是否可以被选择。

    value
        选择某个选项后返回的位置。

    location
        当前选项界面的唯一位置标识符。

    block_all
        若为 False,被选中选项的按钮会赋予“selected”角色,未选中的选项按钮会置为不可用。

        若为 True,混合回滚时按钮总是不可用。

        若为 None,该值使用 config.fix_rollback_without_choice() 配置项。

        当某个界面内所有选项都被赋值为 True 时,选项菜单变成点击无效状态(回滚依然有效)。这可以通过在 ui.interact() 之前调用 ui.saybehavior() 修改。

不回滚

class NoRollback
    从这个类继承的类的实例,在回滚操作中不执行回滚。一个 NoRollback 类实例的所有相关对象,仅在它们有其他可抵达路径的情况下才不回滚。

举例:
[RenPy] 纯文本查看 复制代码
init python:

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

label start:
    $ o = MyClass()

    "Welcome!"

    $ o.value += 1

    "o.value is [o.value]. It will increase each time you rolllback and then click ahead."



您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|RenPy中文空间 ( 苏ICP备17067825号 | 苏公网安备 32092302000068号 )

GMT+8, 2018-10-24 11:45 , Processed in 0.033796 second(s), 21 queries , File On.

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表