存檔、讀檔和回滾 link

Ren’Py支持保存遊戲狀態、載入遊戲狀態和回滾到之前的某個遊戲狀態。儘管實現的方式明顯不同,回滾(rollback)可以認為每一條能與用戶互動的語句開始時都保存了遊戲狀態,當用戶進行回滾時載入之前保存的狀態。

Note

我們通常需要保證不同release版本存檔的相容性,但相容性並不能得到絕對保證。如果能帶來巨大的遊戲表現提升,我們也可以打破存檔相容性的要求。

什麼內容會被存檔 link

Ren’Py會保存遊戲狀態。保存的內容包括內部狀態和Python的狀態。

內部狀態由幾個部分組成:Ren’Py在遊戲啟動後就改變的所有內容,以及下列內容:

  • 當前語句和所有主控流程可能返回到的語句。

  • 正在顯示的圖像和可視組件。

  • 正在顯示的界面,以及界面內所有的變數值。

  • Ren’Py正在播放的音樂。

  • NVL模式文本塊(block)列表。

Python狀態包括從遊戲啟動後儲存區變化過的所有變數,以及跟那些變數有關的所有對象。注意,只有變數相關才行——改變對象內的欄位(field)並不會觸發對象狀態被存檔。

使用 default語句 定義的變數總是會存檔。

在下例中:

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

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

只有 bc 會被存檔。 a 不會被存檔,因為它從遊戲啟動後就沒有變動。 o 不會被存檔因為它也沒有變動——這裡的變動是指引用對象發生變化,而不是對象成員變數的值的變化。

什麼內容不會被存檔 link

遊戲開始後沒有改變過的Python變數不會存檔。 這可能是個重大的問題,前提是某個存檔的變數引用了相同的對象。(對象的別名(alias)。)在這個例子中:

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

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

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

ba 的別名。存檔和載入可能打斷這個別名關係,導致 ab 引用不同的對象。因為這個問題讓人十分頭大,所以最好的辦法就是避免在存檔和不存檔的變數間建立別名關係。(很少直接遇到這種情況,往往發生在不存檔的變數和存檔的欄位(field)別名上。)

還有幾種其他類型的狀態不存檔。

control flow path

Ren’Py值存檔當前語句,以及該語句需要返回的對應語句。Ren’Py不記得如何抵達當前語句。關鍵是,被添加到遊戲的語句(包括變數聲明)不會運行。

mappings of image names to displayables

因為這個映射關係不會存檔,遊戲載入後圖像可能變成了一個新的圖像。隨著遊戲的進度,允許某個圖像變為使用一個新的文件。

configuration variables, styles, and style properties

配置項變數和樣式不會作為遊戲的一部分存檔。所以它們應該只在初始化語句塊(init block)中改變,遊戲啟動後就不再改變。

Ren’Py存檔在哪裡 link

存檔發生在外沿(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能存檔什麼內容 link

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)對象。

Ren’Py不能存檔什麼內容 link

還有幾種無法pickle的數據類型:

  • 渲染(render)對象

  • 疊代器(iterator)對象。

  • 生成器(generator)對象。

  • 協程任務和future執行緒,比如使用 asyncawait 創建的對象。

  • 類文件(file-like)對象。

  • 網路socket埠,及依附於埠的對象。

  • 內部函數和lambda。

以下是一個不完整的清單。

無法使用pickle處理的對象依然可以使用,只是無法在Ren’Py儲存而已, 但可以在儲存命名空間的某些用法中儲存(比如初始化變數值,儲存空間內的函數,或 python hide 語句塊)。

例如,像這樣使用一個文件對象:

$ monika_file = open(config.gamedir + "/monika.chr", "w")
$ monika_file.write("不要刪除。\r\n")
$ monika_file.close()

是不能正常生效的,因為 f 會在3條Python語句中儲存。 需要放在 python hide 語句塊中才可以:

python hide:

    monika_file = open(config.gamedir + "/monika.chr", "w")
    monika_file.write("不要刪除。\r\n")
    monika_file.close()

(當然,使用Python中的 with 語句更簡潔):

python hide:

    with open(config.gamedir + "/monika.chr", "w") as monika_file:
        monika_file.write("不要刪除。\r\n")

使用 asyncawaitasyncio 開啟的協程類似,這樣處理:

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

反而可以正常運行。

存檔函數和變數 link

有一個變數用於高級存檔系統:save_name

這是一個字串,每次存檔時都會儲存。它可以用作存檔名稱,幫助用戶區分不同存檔。

更多存檔文件的訂製化可以使用Json數據系統,詳見 config.save_json_callbacks

界面行為 中定義了一些高級別的存檔行為和函數。除此之外,還有一些低級別的存檔和載入行為。

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

列出存檔的遊戲。每一個存檔的遊戲返回的一個元組中包含:

  • 存檔的檔案名。

  • 傳入的extra_info。

  • 一個可視組件,存檔的截圖。

  • 遊戲時間戳,UNIX時代開始計算的秒數。

regexp

在列表中過濾檔案名的正則表達式。

fast

若為True,返回檔案名而不是元組。

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

將遊戲狀態存檔至某個存檔槽。

filename

一個表示存檔槽名稱的字串。 這是個變數名,不要求與存檔檔案名嚴格對應。

extra_info

會保存在存檔文件中的一個額外字串。通常就是 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

執行截圖。截圖圖像會被作為存檔的一部分保存。

刪除指定名稱的存檔。

讀取存檔後保持數據 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)時,保持數據。

回滾 link

回滾(rollback)允許用戶將遊戲恢復到之前的狀態,類似流行應用程式中的“撤銷/重做”系統。在回滾事件中,系統需要重點維護可視化和遊戲變數,所以在創作遊戲時有幾點需要考慮。

什麼數據會回滾 link

回滾操作的作用範圍包括,初始化階段之後還可以改變的變數,以及通過那些變數訪問的可恢復的對象。 粗略來說,就是在Ren’Py腳本中定義並創建的類的實例,比如列表、字典和集合。 在Python和Ren’Py中內部創建的數據通常都是不可恢復的。

進一步來看,在Ren’Py腳本運行時,腳本內部的Python儲存區中對象,包括列表、字典和集合類型都會替換為可恢復的類型。 從以上類派生的類型也是可恢復的。renpy.Displayable 派生的類也是可恢復的類。

為了使可恢復的對象使用起來更便利,Ren’Py會對腳本中找到的Python語句做如下修改:

  • 原生的列表、字典和集合會自動轉為可恢復的等效對象。

  • 包含在其他語句中的列表、字典和集合也會自動轉為可恢復的等效對象。

  • 其他Python語法中,類似解包之類的操作,會創建列表、字典和集合的部分也會轉為可恢復的等效對象。 但是,函數和方法中帶兩個星號的入參(即根據額外關鍵字入參創建字典)並不會轉為可恢復的對象。

  • 不顯示從其他任意類型派生的類,會自動從可恢復對象的類型派生。

除此之外:

  • 可恢復類型的方法和操作中產生的列表、字典和集合類型會修改可恢復對象。

  • 內建函數如果返回列表、字典和集合的,都會返回可恢復的等效對象。

直接調用Python代碼一般都不會生成可恢復對象。 某些情況下,獲得的對象可能不會參與回滾:

  • 調用內建類型的方法,比如 str.split 方法。

  • 使用導入的Python模組創建的對象,返回給Ren’Py。 (例如,collections.defaultdict的實例不參與回滾。)

  • Ren’Py的API返回的對象,除非文件另有說明。

如果以上數據需要參與回滾,需要對其進行轉換。例如:

# Ren'Py中的Python代碼中調用list函數
# 可以將不可恢復列錶轉為可恢復列表
$ attrs = list(renpy.get_attributes("eileen"))

支持回滾和前向滾動 link

大多數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)。一旦調用這個函數,當前語句就不該再出現互動行為。

data

當遊戲回滾時,這個數據通過 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)。

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) link

回滾會跳過遊戲中已經掛起回滾的章節。

flag

flag 為True時,回滾掛起。當 flag 為False時,回滾恢復。

阻塞回滾 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。

固定回滾 link

固定回滾提供了一種介於完全無限制回滾和完全阻塞回滾之間的中間選項。回滾是允許的,但用戶無法修改之前做出的選擇。固定回滾使用 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語句穿透這個顯示並修改數據,這樣會導致不需要的結果。 特別注意,call screen 不能與固定回滾共用。 這最終取決於遊戲設計者是否在某些有問題的地方阻塞回滾來處理問題。

內部用戶的菜單互動選項, renpy.input()renpy.imagemap() 被設計為完全支持fix_rollback。

樣式化固定回滾 link

因為fix_rollback改變了菜單和imagemap的功能,建議考慮應對這種情況。理解菜單按鈕的組件狀態如何改變很重要。通過 config.fix_rollback_without_choice() 選項,可以更改兩種模式。

默認配置會將選過的選項設定為“selected”,進而啟用樣式所有帶“selected_”前綴的樣式特性。所有其他按鈕會被設置為不可用,並使用前綴為“insensitive_”前綴的特性顯示。這樣的最終效果就是菜單僅有一個可選的選項。

config.fix_rollback_without_choice() 項被設為False時,所有按鈕都會設置為不可用。之前選過的那項會使用“selected_insensitive_”前綴的風格特性,而其他按鈕會使用前綴為“insensitive_”前綴的特性。

固定回滾和自訂界面 link

當使用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]!"

回滾阻塞和固定函數 link

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 參數。)

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) link

一個菜單選項行為(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() 修改。

不回滾 link

class NoRollback link

從這個類繼承的類的實例,在回滾操作中不執行回滾。一個NoRollback類實例的所有相關對象,僅在它們有其他可抵達路徑的情況下才不回滾。

class SlottedNoRollback link

從這個類繼承的類的實例,在回滾操作中不執行回滾。此類與 NoRollback 的區別是,沒有一個關聯字典,可以使用 __slots__ 降低記憶體消耗。

NoRollback類對象實例在回滾後的變化,就像其是透過非回滾的其他方式抵達回滾目標點。

舉例:

init python:

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

label start:
    $ o = MyClass()

    "歡迎!"

    $ o.value += 1

    "o.value的值是 [o.value] 。你每次回滾並點到這裡都會增加它的值。"

支持回滾的類 link

下列的幾個類用於支持遊戲中回滾。在某些場景下可能會用到。

class MultiRevertable link

MultiRevertable是一種可恢復對象的最小可繼承抽象類。可以繼承MultiRevertable並實現自己需要的可恢復對象。

舉例:

class MyDict(MultiRevertable, dict, object):
    pass

這個樣例會創建一個類,在回滾時恢復dict的內容和object的對象欄位。

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

這是一個可恢復版的collections.defaultdict。該類會接受一個工廠(factory)函數。 如果接入的“鍵”不存在,則會將“鍵”作為入參並調用 default_factory 函數,將結果返回。

如果該類的對象中存在 default_factory 屬性,則不會參與回滾,即回滾不會改變該對象。