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()
修改。