Ren'Py はゲーム状態のセーブ、ゲーム状態のロード、そして以前のゲーム状態へのロールバックをサポートしています。少し流行とは違いますが、ロールバックはユーザーとのインタラクションを持つ各ステートメントの開始でセーブし、ユーザーがロールバックするとそのセーブをロードするものとして考えられます。
注釈
通常リリース間でのセーブの互換性を保とうとはしますが、この互換性は保証されません。十分大きな利益があればセーブの互換性を破壊する決定をします。
Ren'Py はゲーム状態のセーブを試みます。これは内部の状態と python の状態の両方を含みます。
内部状態は一旦ゲームが開始されてから Ren'Py が変更を試みたすべての要素で構成され、以下を含みます :
現在のステートメントと呼び出し先から戻るすべてのステートメント
表示されている画像と displayable
表示されているスクリーンとそれらのスクリーン内部の変数の値
Ren'Py が再生している曲
NVLモードのテキストブロックのリスト
Python の状態は、store 内の変数のうちゲーム開始時から変更されたもの、及び、それらの変数から到達可能なすべてのオブジェクトから成ります。ここで、変数への変更という点が重要であり、オブジェクト内のフィールドへの変更は保存の対象にならないという点に注意してください。
default statement を使用して設定した変数は常に保存されます。
この例では
define a = 1
define o = object()
default c = 17
label start:
$ b = 1
$ o.value = 42
b と c だけがセーブされます。 A は一旦ゲームが開始されてからは変更されていないのでセーブされません。 O はそれが参照するオブジェクトは変更されましたが、変数自体は変更されていないのでセーブされません。
Python 変数のうち、ゲーム開始時から変更されていないものは保存されません。これは、ある保存される変数が別の保存されないオブジェクトを参照(エイリアス)している場合に問題となりえます。例
init python:
a = object()
a.f = 1
label start:
$ b = a
$ b.f = 2
"a.f=[a.f] b.f=[b.f]"
a と b はエイリアスです。セーブとロードはおそらくこのエイリアスを破壊し、 a と b に違うオブジェクトを参照させます。これはとても面倒なことになり得るので、保存された変数と保存されない変数のエイリアスは避けるべきです( これは直接的には滅多にないことですが、保存されない変数と保存されるフィールドのエイリアスで起こるかもしれません)。
その他いくつかの種類の、セーブされない状態を示します:
Ren'Py がセーブするものは、現在のステートメントと、呼び出し先から戻るために必要なステートメントだけです。どうやってそこに到達したかは記憶しません。重要なことは、(変数の代入のような)コードがゲームに追加された場合、それは実行されないことです。
このマッピングはセーブされないので、画像はゲームをもう一度ロードするときに新しい画像に変わっているかもしれません。これにより画像はゲームの更新とともに新しいファイルに変更できるようになります。
設定変数とスタイルはゲームの一部としてはセーブされません。それ故、それらは init
ブロックでのみ変更され、一旦ゲームが開始されたらそのままであるべきです。
セーブは最も外側のインタラクションコンテキストにある Ren'Py のステートメント開始点で実行されます。
ここで重要なことはセーブはステートメントの 開始点 で実行されることに気をつけることです。複数回実行されるステートメントの内部でロードやロールバックが起きると、ステートメント開始時にアクティブだった状態になります。
このことは python で定義されたステートメントで問題になり得ます。 このようなコードでは
python:
i = 0
while i < 10:
i += 1
narrator("The count is now [i].")
ユーザーが内部でセーブし、ロードすると、ループは最初から始まります。 python ではなく Ren'Py で同一のコードを行えばこの問題は避けられます
$ i = 0
while i < 10:
$ i += 1
"The count is now [i]."
Ren'Py はゲーム状態をセーブするために python pickle システムを使用しています。 このモジュールは以下をセーブできます :
True, False, None, int, str, float, complex, str, unicode オブジェクトのような基本的な型
list, tuple, set, dict のようなコレクション型
ユーザー定義オブジェクト、クラス、関数、メソッド、 bound method 。これらの関数の保存を成功させるためには、それらをオリジナルの名前で利用可能なままにしておく必要があります。
Character, Displayable, Transform, Transition オブジェクト
ある特定の型は pickle できません :
Render オブジェクト
Iterator オブジェクト
ジェネレーターオブジェクト
async
や await
で作成されるようなコルーチンタスクやフューチャーのこと。
File-like オブジェクト
ネットワークソケット、およびそれを含むオブジェクト。
内部関数とラムダ関数
これは完全なリストではないかもしれません。
pickle 化できないオブジェクトも、 Ren'Py が保存しない名前空間 (init 変数、関数内の名前空間、python hide
ブロックなど ) と組み合わせて使用すれば、使用できます。
例えば、次のようにファイルオブジェクトを使用しても
$ monika_file = open(config.gamedir + "/monika.chr", "w")
$ monika_file.write("Do not delete.\r\n")
$ monika_file.close()
f
は 3 つの Python ステートメントのどれかの間に保存される可能性があるため、うまくいきません。これを python hide
ブロックに入れるとうまくいきます
python hide:
monika_file = open(config.gamedir + "/monika.chr", "w")
monika_file.write("Do not delete.\r\n")
monika_file.close()
(もちろん with
ステートメントを使用したほうがすっきりします)
python hide:
with open(config.gamedir + "/monika.chr", "w") as monika_file:
monika_file.write("Do not delete.\r\n")
async
や await
, asyncio
で作られるようなコルーチンは、これに似ています。次のコードを書き
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())
正常に動作します。
高級なセーブシステムによって使用される一つの変数があります。
save_name
= ... linkこれは各セーブに保存される文字列です。セーブに名前を与えて、ユーザーがそれらを区別するのを助けます。
さらなるセーブデートのカスタマイズが Json 補助データシステムにより可能です。 config.save_json_callbacks
を参照してください。
高級なセーブ用のアクションが アクション で説明されています。さらに以下の低級なセーブロード用の関数があります。
renpy.
can_load
(filename, test=False) linkfilename がセーブスロットに存在するときに True を返し、それ以外の時に False を返します。
renpy.
copy_save
(old, new) linkセーブを old から new へコピーします( old が存在しなければ何もしません )。
renpy.
list_saved_games
(regexp=u'.', fast=False) linkセーブをリストアップします。各セーブに対して以下をタプルに含んで返します :
セーブのファイル名
渡された extra_info
ゲームセーブ時に使用されたスクリーンショットを表示する displayable
ゲームが開始されてからの UNIX epoch の秒数での時間
ファイル名の先頭に対してマッチするリストを絞り込むための正規表現
fast が True なら、タプルの代わりにファイル名が返されます。
renpy.
list_slots
(regexp=None) link空でないセーブスロットのリストが返されます。 regexp が存在すれば regexp で始まるスロットのみが返されます。スロットは文字列の順に並べられます。
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 ですが、ファイル名の一部に対応するだけです。
セーブファイルにセーブされる追加の文字列です。通常これは save_name
の値です。
renpy.take_screenshot()
がこの関数の前に呼び出されるべきです。
renpy.
slot_json
(slotname) linkslotname の json 情報を返すか、スロットが空のときは None を返します。
var:config.save_json_callbacks 関数への d
引数と同じように辞書として返されます。より正確には、この辞書にはゲームが保存されたときと同じデータが含まれます。
renpy.
slot_mtime
(slotname) linkslot の更新日時を返すか、スロットが空のときは None を返します。
renpy.
slot_screenshot
(slotname) linkslotname に対するスクリーンショットで利用可能な画面を返すか、スロットが空のときは None を返します。
renpy.
take_screenshot
(scale=None, background=False) linkスクリーンショットを撮ります。このスクリーンショットはセーブの一部として保存されます。
renpy.
unlink_save
(filename) link与えられた名前のセーブスロットを削除します。
ゲームがロードされると、ゲームの状態は現在のステートメントが処理を開始した時の状態まで ( 後述するロールバックシステムを用いて ) リセットされます。
いくつかの場合では、これは不適切かもしれません。例えば値を変更出来るスクリーンがある時は、ゲームのロード後もその値を保持したいかもしれません。 renpy.retain_after_load を呼び出しておくと、次のチェックポイントとなるインタラクションの終了以前にゲームがセーブ・ロードされた場合でもデータは復元されません。
データが変更されなくても、制御は現在のステートメントの開始位置までリセットされることに注意してください。そのステートメントは、ステートメント開始位置における新しいデータで再び実行されます。
例
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現在のステートメントと次のチェックポイントを含むステートメントの間で変更されたデータを、ロード時に保持します。
ロールバックはユーザーがほとんどの現代的なアプリケーションで利用可能なアンドゥーやリドゥーとほとんど同様に、以前の状態にゲームを再現できるようにします。ロールバックの間システムは外見とゲーム変数の維持に注意していますが、ゲーム作成時にはいくつか配慮するべきところがあります。
ロールバックは、初期化後に変更された変数と、その変数から到達可能な revertable 型のオブジェクトに影響します。簡単に言うと、Ren'Pyスクリプトで作成されたリスト、辞書、セットは、Ren'Pyスクリプトで定義されたクラスのインスタンスと同様に、 revertable です。 Python内部や Ren'Py 内部で作成されたデータは、通常、 revertable ではありません。
より詳細には、 Ren'Py スクリプトに埋め込まれた Python が実行される store 内で、 object, list, dict, set 型は revertable である同等の型に置き換えられます。これらの型を継承するオブジェクトもまた revertable です。 renpy.Displayable
型は revertable オブジェクト型を継承しています。
revertable オブジェクトの使用をより便利にするために、 Ren'Py は、 Ren'Py スクリプトファイル内の Python を以下のように変更します。
literal なリスト、辞書、およびセットは自動的に revertable な同等物に変換されます。
リスト、辞書、セットの内包表記も自動的に revertable な同等品に変換されます。
リスト、辞書、セットを作成できる拡張アンパックのような他の Python 構文は、結果を revertable な同等のものに変換します。しかし、パフォーマンス上の理由から、関数やメソッドへの (追加のキーワード引数の辞書を作成する)二重アスタリスク付きのパラメーターは、 revertable オブジェクトへ変換されません。
revertable オブジェクトを自動的に継承するどの型も継承しないその他のクラス
さらに :
revertable 型のメソッドと演算子は変更され、リスト、辞書、セットの生成時に、 revertable なオブジェクトを返すようになります。
リスト、辞書、セットを返す組み込み関数は revertable な同等のものを返します。
Python のコードを呼び出しても一般的には revertable オブジェクトは生成されません。ロールバックに参加しない可能性のあるオブジェクトができる場合として次があります。 :
str.split メソッドのような組み込み型のメソッドの呼び出し
インポートされた python モジュールで作成され、 Ren'Py に返されたオブジェクト (例えば collections.defaultdict のインスタンスはロールバックに参加しません) 。
Ren'Py の API から返されるオブジェクトで特に記載のないもの。
そのようなデータをロールバックに参加させる必要があるなら、参加する型に変換すると良いです。例
# Calling list inside Python-in-Ren'Py converts a non-revertable list
# into a revertable one.
$ attrs = list(renpy.get_attributes("eileen"))
ほとんどの Ren'Py ステートメントは自動的にロールバックとロールフォワードをサポートしていますが、直接 ui.interact()
を呼び出すと、ロールバックとロールフォワードのサポートを自分で追加する必要があります
# This is None if we're not rolling back, or else the value that was
# passed to checkpoint last time if we're rolling forward.
roll_forward = renpy.roll_forward_info()
# Set up the screen here...
# Interact with the user.
rv = ui.interact(roll_forward=roll_forward)
# Store the result of the interaction.
renpy.checkpoint(rv)
renpy.checkpoint が呼び出された後はゲームとユーザーとのインタラクションがないことが重要です ( もしあれば、ユーザーはロールバック出来なくなります )。
renpy.
can_rollback
() linkロールバック可能なら True を返します。
renpy.
checkpoint
(data=None) link現在のステートメントをユーザーがロールバックできるチェックポイントにします。一旦この関数が呼ばれると、現在のステートメントではもうユーザーへのインタラクションはありません。
ゲームの保存に使用するスクリーンショットも消去します。
この data はゲームがロールバック中に renpy.roll_forward_info()
によって返されます。
True なら、これはロールバックが止まるハードチェックポイントです。 False なら、これはロールバックが止まらないソフトチェックポイントです。
renpy.
get_identifier_checkpoints
(identifier) linkHistoryEntry オブジェクトの rollback_identifier を指定して、その identifier に到達するのに renpy.rollback()
が通過する必要のあるチェックポイントの数を返します。
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ゲームの状態を最後のチェックポイントまで巻き戻します。
True の場合、いかなる状況であってもロールバックが発生します。それ以外の場合は、store、context、config で有効化されている場合のみ発生します。
Ren'Py は、ここで指定された renpy.checkpoint の数だけロールバックします。この条件を満たす範囲で、可能な限りロールバックします。
True の場合、呼び出しはメインコンテキストのコードが実行されるまで遅延されます。
True ならロールバッグは以前のチェックポイントの直後まで実行され、 False なら現在のチェックポイントの直前まで実行されます。
None を指定するか、ロールバックが完了した時に呼び出されるラベルを指定します。
デファルトは True で、トランジション後に実行されるスクリプトはトランジションをスキップするアブノーマルなモードで実行されます。アブノーマルモードはインタラクションが始まると終了します。
renpy.
suspend_rollback
(flag) linkロールバックはそれが停止している間の区間をスキップします。
flag が True なら、ロールバックは停止されます。 False ならロールバックは再開されます。
警告
ロールバックの禁止はユーザーフレンドリーではありません。ユーザーが間違えて意図しない選択肢をクリックしたら、そのミスを訂正できなくなります。ロールバックはセーブとロードに等しいので、ユーザーはさらにセーブを強要されてゲームの印象が悪くなります。
一部またはすべての場面でロールバックを無効化可能です。ロールバックが全く望まれていないなら、 config.rollback_enabled
を利用して簡単に無効化できます。
より一般的なのはロールバックの一時的な禁止です。これは renpy.block_rollback()
関数によって行われます。呼び出されると Ren'Py がその位置より前にロールバック出来ないようにします。例えば
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."
ラベル no_return に着いたら、 Ren'Py は選択肢へのロールバックを許可しなくなります。
固定ロールバックは自由なロールバックとロールバックの完全な禁止の中間に当たります。ロールバックは可能ですが、ユーザーは決定を変更出来ません。固定ロールバックは以下の例で示すように、renpy.fix_rollback()
関数で実行されます。
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 関数が呼び出された後もユーザーが選択肢へロールバックすることはまだ出来ます。しかし、違った選択をすることは出来ません。
fix_rollback についてゲームデザイン時にいくつか配慮すべき点があります。Ren'Py は自動的に checkpoint()
に与えられたすべてのデータをロックするよう気をつけますが、 Ren'Py の性質上、突破しして予測出来ない結果にする python コードを書くことは可能です。最も注目すべきは call screen
は固定ロールバックでは上手く動作しないことです。プログラムを使用する場所でロールバックを禁止するか、これをうまく扱うために追加のコードを書くかはゲームデザイナー次第です。
選択肢や renpy.input()
, renpy.imagemap()
のような、組込みのユーザーに対するインタラクションは固定ロールバックでも十分に動作するようにデザインされています。
fix_rollback は menu や imagemap の機能を変更するので、外見にこのことを反映するのが賢明です。このためにはmenu button のウィジェットの状態がどうやって変更されるのかを理解することが重要です。 config.fix_rollback_without_choice
を通して選択される2つのモードがあります。
デフォルトでは選択された選択肢を「選択状態」にします。つまり「 selected_ 」を接頭辞に持つスタイルプロパティーを有効化します。それ以外のすべてのボタンは無効化され、「 insensitive_ 」を接頭辞に持つプロパティーを有効化して表示されます。結果としてこれは選択肢にただ一つの選択可能な選択肢を残します。
config.fix_rollback_without_choice
が False に設定されると、すべてのボタンが無効化されます。つまり選択された選択肢はスタイルプロパティーに「 selected_insensitive_ 」の接頭辞を使用する一方、その他のボタンには「 insensitive_ 」を接頭辞に持つプロパティーを使用します。
固定ロールバックでも正常に動作する python の作業を書くためには、知っておくべきことが少しあります。まず最初に、 renpy.in_fixed_rollback()
関数を使用してゲームが現在固定ロールバック中かどうかを判断できます。第二に固定ロールバック中は ui.interact()
はどんなアクションが処理されても、常に与えられた roll_forward データを返します。これはつまり ui.interact()
/renpy.checkpoint()
関数が使用されると、動作のほとんどが決定されるということです。
カスタムスクリーンの作成を簡単にするために、ほとんど共通して使用される2つのアクションが提供されます。 ui.ChoiceReturn()
アクションはそれが関連づけられたボタンがクリックされると値を返し、 ui.ChoiceJump()
アクションはスクリプトのラベルにジャンプするために使用できますが、このアクションはそのスクリーンが 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現在ロールバックが実行され、現在のコンテキストが renpy.fix_rollback() ステートメントが実行されるより前なら True を返します。
ui.
ChoiceJump
(label, value, location=None, block_all=None, sensitive=True, args=None, kwargs=None) linkvalue を返す選択肢用のアクションで、固定ロールバックを考慮した方法でボタンを管理します ( 動作の詳細は block_all を参照 )。
ボタンのラベルとなるテキストです。 imagebutton や hotspot にもこれは与えられます。このラベルは現在のスクリーン内の選択肢のユニークな識別子として使用されます。また、 location と共に、この選択肢が選択されたことがあるかどうかを保存するために使用されます。
ジャンプする場所
現在のスクリーンに対してユニークな場所の識別子です。
False なら、そのボタンは選択されていたなら選択状態で、選択されていなかったら無効になります。
True なら固定ロールバック中ボタンは常に無効です。
None なら config.fix_rollback_without_choice
変数から値はとられます。
スクリーンのすべての要素に True が与えられると、 クリックが出来なくなります ( ロールフォワードはまだ動作します ) 。これは ui.interact()
の前に ui.saybehavior()
を呼び出して変更可能です。
ui.
ChoiceReturn
(label, value, location=None, block_all=None, sensitive=True, args=None, kwargs=None) linkvalue を返す選択肢用のアクションで、固定ロールバックを考慮した方法でボタンを管理します ( 動作の詳細は block_all を参照 )。
ボタンのラベルとなるテキストです。 imagebutton や hotspot にもこれは与えられます。このラベルは現在のスクリーン内の選択肢のユニークな識別子として使用されます。また、 location と共に、この選択肢が選択されたことがあるかどうかを保存するために使用されます。
その選択肢が選択されると返される値
現在のスクリーンに対してユニークな場所の識別子です。
False なら、そのボタンは選択されていたなら選択状態で、選択されていなかったら無効になります。
True なら固定ロールバック中ボタンは常に無効です。
None なら config.fix_rollback_without_choice
変数から値はとられます。
スクリーンのすべての要素に True が与えられると、 クリックが出来なくなります ( ロールフォワードはまだ動作します ) 。これは ui.interact()
の前に ui.saybehavior()
を呼び出して変更可能です。
NoRollback
linkこのクラスおよびこのクラスを継承したクラスのインスタンスはロールバックに参加しません。 NoRollback クラスのインスタンスからアクセス可能なオブジェクトはそれが他のルートからもアクセス可能な場合のみロールバックに参加します。
SlottedNoRollback
linkこのクラスおよびこのクラスを継承したクラスのインスタンスはロールバックに参加しません。このクラスは NoRollback
と違い関連づけられた辞書を持たないため、 __slots__
を使用してメモリー使用量を削減できます。
NoRollback クラスのインスタンスからアクセス可能なオブジェクトはそれが他のルートからもアクセス可能な場合のみロールバックに参加します。
例
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."