セーブ、ロード、ロールバック link

Ren'Py はゲーム状態のセーブ、ゲーム状態のロード、そして以前のゲーム状態へのロールバックをサポートしています。少し流行とは違いますが、ロールバックはユーザーとのインタラクションを持つ各ステートメントの開始でセーブし、ユーザーがロールバックするとそのセーブをロードするものとして考えられます。

注釈

通常リリース間でのセーブの互換性を保とうとはしますが、この互換性は保証されません。十分大きな利益があればセーブの互換性を破壊する決定をします。

何がセーブされるか link

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

bc だけがセーブされます。 A は一旦ゲームが開始されてからは変更されていないのでセーブされません。 O はそれが参照するオブジェクトは変更されましたが、変数自体は変更されていないのでセーブされません。

何がセーブされないか link

Python 変数のうち、ゲーム開始時から変更されていないものは保存されません。これは、ある保存される変数が別の保存されないオブジェクトを参照(エイリアス)している場合に問題となりえます。例

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

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

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

ab はエイリアスです。セーブとロードはおそらくこのエイリアスを破壊し、 ab に違うオブジェクトを参照させます。これはとても面倒なことになり得るので、保存された変数と保存されない変数のエイリアスは避けるべきです( これは直接的には滅多にないことですが、保存されない変数と保存されるフィールドのエイリアスで起こるかもしれません)。

その他いくつかの種類の、セーブされない状態を示します:

制御フローのパス

Ren'Py がセーブするものは、現在のステートメントと、呼び出し先から戻るために必要なステートメントだけです。どうやってそこに到達したかは記憶しません。重要なことは、(変数の代入のような)コードがゲームに追加された場合、それは実行されないことです。

画像名から displayable へのマッピング

このマッピングはセーブされないので、画像はゲームをもう一度ロードするときに新しい画像に変わっているかもしれません。これにより画像はゲームの更新とともに新しいファイルに変更できるようになります。

設定変数、スタイル、スタイルプロパティー

設定変数とスタイルはゲームの一部としてはセーブされません。それ故、それらは init ブロックでのみ変更され、一旦ゲームが開始されたらそのままであるべきです。

どこで Ren'Py はセーブされるか link

セーブは最も外側のインタラクションコンテキストにある 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 がセーブできるもの link

Ren'Py はゲーム状態をセーブするために python pickle システムを使用しています。 このモジュールは以下をセーブできます :

  • True, False, None, int, str, float, complex, str, unicode オブジェクトのような基本的な型

  • list, tuple, set, dict のようなコレクション型

  • ユーザー定義オブジェクト、クラス、関数、メソッド、 bound method 。これらの関数の保存を成功させるためには、それらをオリジナルの名前で利用可能なままにしておく必要があります。

  • Character, Displayable, Transform, Transition オブジェクト

Ren'Py がセーブできないもの link

ある特定の型は pickle できません :

  • Render オブジェクト

  • Iterator オブジェクト

  • ジェネレーターオブジェクト

  • asyncawait で作成されるようなコルーチンタスクやフューチャーのこと。

  • 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")

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

正常に動作します。

セーブ関数と変数 link

高級なセーブシステムによって使用される一つの変数があります。

save_name = ... link

これは各セーブに保存される文字列です。セーブに名前を与えて、ユーザーがそれらを区別するのを助けます。

さらなるセーブデートのカスタマイズが 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=u'.', fast=False) link

セーブをリストアップします。各セーブに対して以下をタプルに含んで返します :

  • セーブのファイル名

  • 渡された extra_info

  • ゲームセーブ時に使用されたスクリーンショットを表示する displayable

  • ゲームが開始されてからの UNIX epoch の秒数での時間

regexp

ファイル名の先頭に対してマッチするリストを絞り込むための正規表現

fast

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

セーブスロットの名前の文字列です。変数名は filename ですが、ファイル名の一部に対応するだけです。

extra_info

セーブファイルにセーブされる追加の文字列です。通常これは save_name の値です。

renpy.take_screenshot() がこの関数の前に呼び出されるべきです。

renpy.slot_json(slotname) link

slotname の json 情報を返すか、スロットが空のときは None を返します。

var:config.save_json_callbacks 関数への d 引数と同じように辞書として返されます。より正確には、この辞書にはゲームが保存されたときと同じデータが含まれます。

renpy.slot_mtime(slotname) link

slot の更新日時を返すか、スロットが空のときは None を返します。

renpy.slot_screenshot(slotname) link

slotname に対するスクリーンショットで利用可能な画面を返すか、スロットが空のときは None を返します。

renpy.take_screenshot(scale=None, background=False) link

スクリーンショットを撮ります。このスクリーンショットはセーブの一部として保存されます。

与えられた名前のセーブスロットを削除します。

ロード後のデータ保持 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

現在のステートメントと次のチェックポイントを含むステートメントの間で変更されたデータを、ロード時に保持します。

ロールバック link

ロールバックはユーザーがほとんどの現代的なアプリケーションで利用可能なアンドゥーやリドゥーとほとんど同様に、以前の状態にゲームを再現できるようにします。ロールバックの間システムは外見とゲーム変数の維持に注意していますが、ゲーム作成時にはいくつか配慮するべきところがあります。

どのデータがロールバックされますか? 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"))

ロールバックとロールフォワードのサポート 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

この data はゲームがロールバック中に renpy.roll_forward_info() によって返されます。

hard

True なら、これはロールバックが止まるハードチェックポイントです。 False なら、これはロールバックが止まらないソフトチェックポイントです。

renpy.get_identifier_checkpoints(identifier) link

HistoryEntry オブジェクトの 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

ゲームの状態を最後のチェックポイントまで巻き戻します。

force

True の場合、いかなる状況であってもロールバックが発生します。それ以外の場合は、store、context、config で有効化されている場合のみ発生します。

checkpoints

Ren'Py は、ここで指定された renpy.checkpoint の数だけロールバックします。この条件を満たす範囲で、可能な限りロールバックします。

defer

True の場合、呼び出しはメインコンテキストのコードが実行されるまで遅延されます。

greedy

True ならロールバッグは以前のチェックポイントの直後まで実行され、 False なら現在のチェックポイントの直前まで実行されます。

label

None を指定するか、ロールバックが完了した時に呼び出されるラベルを指定します。

abnormal

デファルトは True で、トランジション後に実行されるスクリプトはトランジションをスキップするアブノーマルなモードで実行されます。アブノーマルモードはインタラクションが始まると終了します。

renpy.suspend_rollback(flag) link

ロールバックはそれが停止している間の区間をスキップします。

flag:

flag が True なら、ロールバックは停止されます。 False ならロールバックは再開されます。

ロールバックの禁止 link

警告

ロールバックの禁止はユーザーフレンドリーではありません。ユーザーが間違えて意図しない選択肢をクリックしたら、そのミスを訂正できなくなります。ロールバックはセーブとロードに等しいので、ユーザーはさらにセーブを強要されてゲームの印象が悪くなります。

一部またはすべての場面でロールバックを無効化可能です。ロールバックが全く望まれていないなら、 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 は選択肢へのロールバックを許可しなくなります。

固定ロールバック link

固定ロールバックは自由なロールバックとロールバックの完全な禁止の中間に当たります。ロールバックは可能ですが、ユーザーは決定を変更出来ません。固定ロールバックは以下の例で示すように、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() のような、組込みのユーザーに対するインタラクションは固定ロールバックでも十分に動作するようにデザインされています。

固定ロールバックのスタイル link

fix_rollback は menu や imagemap の機能を変更するので、外見にこのことを反映するのが賢明です。このためにはmenu button のウィジェットの状態がどうやって変更されるのかを理解することが重要です。 config.fix_rollback_without_choice を通して選択される2つのモードがあります。

デフォルトでは選択された選択肢を「選択状態」にします。つまり「 selected_ 」を接頭辞に持つスタイルプロパティーを有効化します。それ以外のすべてのボタンは無効化され、「 insensitive_ 」を接頭辞に持つプロパティーを有効化して表示されます。結果としてこれは選択肢にただ一つの選択可能な選択肢を残します。

config.fix_rollback_without_choice が False に設定されると、すべてのボタンが無効化されます。つまり選択された選択肢はスタイルプロパティーに「 selected_insensitive_ 」の接頭辞を使用する一方、その他のボタンには「 insensitive_ 」を接頭辞に持つプロパティーを使用します。

固定ロールバックとスクリーンのカスタム link

固定ロールバックでも正常に動作する 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]!"

ロールバックの禁止と固定用関数 link

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

value を返す選択肢用のアクションで、固定ロールバックを考慮した方法でボタンを管理します ( 動作の詳細は block_all を参照 )。

label

ボタンのラベルとなるテキストです。 imagebutton や hotspot にもこれは与えられます。このラベルは現在のスクリーン内の選択肢のユニークな識別子として使用されます。また、 location と共に、この選択肢が選択されたことがあるかどうかを保存するために使用されます。

value

ジャンプする場所

location

現在のスクリーンに対してユニークな場所の識別子です。

block_all

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

value を返す選択肢用のアクションで、固定ロールバックを考慮した方法でボタンを管理します ( 動作の詳細は block_all を参照 )。

label

ボタンのラベルとなるテキストです。 imagebutton や hotspot にもこれは与えられます。このラベルは現在のスクリーン内の選択肢のユニークな識別子として使用されます。また、 location と共に、この選択肢が選択されたことがあるかどうかを保存するために使用されます。

value

その選択肢が選択されると返される値

location

現在のスクリーンに対してユニークな場所の識別子です。

block_all

False なら、そのボタンは選択されていたなら選択状態で、選択されていなかったら無効になります。

True なら固定ロールバック中ボタンは常に無効です。

None なら config.fix_rollback_without_choice 変数から値はとられます。

スクリーンのすべての要素に True が与えられると、 クリックが出来なくなります ( ロールフォワードはまだ動作します ) 。これは ui.interact() の前に ui.saybehavior() を呼び出して変更可能です。

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

    "Welcome!"

    $ o.value += 1

    "o.value is [o.value]. It will increase each time you rolllback and then click ahead."