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 オブジェクト
File-like オブジェクト
内部関数とラムダ関数
デフォルトで Ren'Py はゲームを保存するために cPickle module を使用します。 config.use_cpickle
を設定すると代わりに Ren'Py は pickle module を使用します。これはゲームを遅くしますが、python2.xではよりよいセーブエラーを送ります。この設定はPython3では無効で、システムが自動的に適切なものを選びます。
高級なセーブシステムによって使用される一つの変数があります。
save_name
= ... linkこれは各セーブに保存される文字列です。セーブに名前を与え、ユーザーがそれらを区別するのを助けるために使われます。
高級なセーブ用のアクションが アクション で説明されています。さらに以下の低級なセーブロード用の関数があります。
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 を返します。
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現在のステートメントと次のチェックポイントを含むステートメントの間で変更されたデータを、ロード時に保持します。
ロールバックはユーザーがほとんどの現代的なアプリケーションで利用可能なアンドゥーやリドゥーとほとんど同様に、以前の状態にゲームを再現できるようにします。ロールバックの間システムは外見とゲーム変数の維持に注意していますが、ゲーム作成時にはいくつか配慮するべきところがあります。
ほとんどの 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 コードを書いてこれを突破し、結果を予測出来ない方法でこれらを変更することが可能です。プログラムを使用する場所でロールバックを禁止するか、これをうまく扱うために追加のコードを書くかはゲームデザイナー次第です。
選択肢や renpy.input()
, renpy.imagemap()
のような、組込みのユーザーに対するインタラクションは固定ロールバックでも十分に動作するようにデザインされています。
fix_rollback は menu や imagemap の機能を変更するので、外見にこのことを反映するのが賢明です。このためにはmenu button のウィジェットの状態がどうやって変更されるのかを理解することが重要です。 config.fix_rollback_without_choice
を通して選択される2つのモードがあります。
デフォルトでは「selected」で、それゆえ「 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 クラスのインスタンスからアクセス可能なオブジェクトはそれが他のルートからもアクセス可能な場合のみロールバックに参加します。
例
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."