存檔、讀檔和回滾 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)對象。

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

  • 渲染(render)對象
  • 疊代器(iterator)對象。
  • 類文件(file-like)對象。
  • 內部函數和lambda。

默認情況下,Ren’Py使用cPickle模組保存遊戲。將配置項 config.use_cpickle 的值改為False,可以讓Ren’Py使用pickle模組。 默認配置速度較慢,但是在Python 2.x環境下比保存報錯要好。 注意這個設置對Python 3沒有效果。

保存函數和變數 link

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

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

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

  • 保存的檔案名。
  • 傳入的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語句自動支持回滾和前向滾動。如果直接調用 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語句穿透這個顯示並修改數據,這樣會導致不需要的結果。這取決於遊戲設計者是否在某些有問題的地方阻攔回滾或者寫了額外的Python語句處理問題。

內部用戶的菜單互動選項, 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類實例的所有相關對象,僅在它們有其他可抵達路徑的情況下才不回滾。

舉例:

init python:

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

label start:
    $ o = MyClass()

    "歡迎!"

    $ o.value += 1

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