Skip to content

会話ロールバック(一つ前へ)

会話ロールバックは、一つ前の表示行(台詞・選択肢・全画面テキスト)に戻り、そこから続行できる機能です。既定の会話テンプレートは機能バーに「戻る」ボタンを用意します。

ランタイム API

gdscript
@export var dialogue_manager: KonadoDialogueManager
gdscript
# 戻れるか(到達可能な一つ前の行があり、境界に阻まれていないか)
var can: bool = dialogue_manager.timeline.can_step_back()

# 一つ前の行へ戻る。成功で true
if dialogue_manager.timeline.can_step_back():
    dialogue_manager.timeline.step_back()

# 戻るのに必要なコミット済み命令数(0 は不可)
var steps: int = dialogue_manager.timeline.previous_dialogue_steps()

# Backlog 条目回退:条目が持つ VM コミット連番で直接クエリ/実行できる
var reachable: bool = dialogue_manager.timeline.can_rollback_to_entry(serial)
dialogue_manager.timeline.rollback_to_entry(serial)

原子性

ロールバックは仮想マシンの可逆トランザクションを再利用し、さらに目標行の完全スナップショットから場景を復元してから決定論的に再生します。

  • 進行中のトランザクション(打字・ボイス・待機中の非同期処理)は取り消され、トークンも無効化されます;
  • スナップショットはカメラ・俳優・背景・音声・UI・変数を正確に復元します(逐次命令の差分累積に依存しないため、非同期カメラやトランジション付き演出も正確に戻せます。最新 128 行分を保持);
  • 再生は同一フレーム内に完了してから画面が更新されるため、以前の画面が一瞬表示されることはありません;
  • 復元に失敗した場合は安全停止状態に入り、中途半端な画面を残しません。

境界

境界となるのは endhalt)だけです。jumpjump.script)は跨ぐことができます:ロールバックはその行が属する脚本ごと復元するため、「前の行」は飛んできた脚本へ戻り、そのまま進めると jump が再実行されます。

跨げる副作用:signalasyncam は再生がそこを通過するときに再実行されます(信号は再発火し、カメラは再び動きます)。したがって選び直した分岐では信号ハンドラが再度呼ばれます。実績命令(achievement unlock / increment / set_flag)は外部カウンタで、跨いでも再生されません(実績は取り消されず、二重計上もされません)。非同期カメラは戻す際に進行中の Tween をキャンセルし、スナップショットから変換を復元します。ハンドラがスナップショットの外を書き換える場合は冪等にしてください(「変数と副作用」参照)。

次の場合は引き続き戻れません(can_step_back()false)。

  • 一つ前の行より前に endhalt)がある;
  • 未処理のランタイムエラーがある;
  • 実行履歴が消去済み、または保持容量(既定 512 命令)を超えている;あるいは必要な行のスナップショットが既に破棄されている(スナップショットキャッシュは直近 128 行を 4 MiB の予算内で保持します);

分岐のロールバック

一つ前の行が選択肢の場合、戻ると全選択肢が再表示され、選び直せます。

gdscript
dialogue_manager.timeline.step_back()  # 選択肢が再表示される

選び直すと新しい選択記録が書き込まれます。既定の TRIM ポリシーでは破棄した選択が履歴から除かれ、KEEP では両方が残ります。

さらにもう一度戻すと、問いかけの台詞のの行に移動します。選択肢はテキストボックスを書き換えないため、選択肢が重なっている台詞はすでに画面に出ており、変化のない一手を踏ませないように一度で飛ばします。キャンセルされた選択肢の表示も同時に破棄されるため、以前の台詞の上にボタンが残ることはありません。

Backlog ジャンプ(任意の行へ戻る)

既定テンプレートの Backlog パネルは任意の行をクリックしてそこへ戻れます——Ren'Py の履歴画面は読み取り専用で、ジャンプには自分でコードを書く必要があります:

gdscript
# Backlog のエントリは対応する VM コミット連番を持ちます
var entries: Array[Dictionary] = dialogue_manager.dialogue_history.entries(0, false)

# そのエントリが戻り先として使えるか
if dialogue_manager.timeline.can_rollback_to_entry(entries[0]["serial"]):
    # その行へ戻る:「一つ前」と同じ原子パスを通ります
    dialogue_manager.timeline.rollback_to_entry(entries[0]["serial"])

意味論は「一つ前」と完全に同じです:その行のスナップショットで場面を復元し、同一フレームで再生し、脚本を跨ぐ jump も越えられ、越えた不可逆な副作用はポリシーに従い、復元に失敗すれば安全停止に入ります。

戻れないエントリ:

  • いま表示している行(未コミット、連番 0)——パネルでは表示のみでクリックできません;
  • 無効な連番、またはスナップショットが保持範囲(直近 128 行、「境界」参照)を超えた行。

戻した後はパネルの記録も対象行まで切り詰められ、そのまま進めると越えた可重放副作用(signal)が再実行されるため、最終状態は初回プレイと同じになります。

変数と副作用

ロールバックが巻き戻すのはスナップショット内の状態(スクリプト変数、カメラ、アクター、背景、オーディオ、UI、実行位置)だけです。それ以外は次の規則に従います:

書き込み先ロールバック時再生がその命令を通過するとき
$ 一時変数 / % 永続変数(setadd など)対象行へ正確に復元(スナップショットは集合ごと置き換えるため、後から作った変数も削除されます)通常どおり再実行
signalハンドラがスナップショット内に加えた変更も行と一緒に巻き戻る再発火し、ハンドラが再び実行される
achievement unlock / increment / set_flag取り消されない(実績は増えるだけ)再生されない(二重計上なし)
スナップショット外の状態(シングルトン、外部セーブ、独自 Resource)取り消せないハンドラ側で冪等にする(例 3 参照)

永続変数(%)と一時変数($)はどちらもスナップショット内にあり、ロールバックは対象行の状態へまとめて復元します。違いは「いつ変わるか」です:

  • % 永続変数はシーン(jump)を跨いでも保持され、セーブにも保存されます。ロールバックは対象行の時点の値へ戻します。
  • $ 一時変数はシーン変更(jump)でクリアされます。ロールバックは対象行の時点の値へ戻すため、脚本を跨いで戻ると前の脚本の一時変数が再び現れます。

⚠️ % は対象行のスナップショットで集合ごと置き換わるため、その 2 行の間に対話の外(ショップ、メニュー、variable_store を直接書く外部コード)で変更された % も一緒に巻き戻ります。これはタイムトラベル的な意味付けです。ロールバックの影響を受けては困る数値(プラットフォーム通貨、アカウント進行度)は % ではなく Konado スナップショットの外に置いてください。

例 1:数値をスクリプト変数に置く(推奨)

text
set %love = 0
Kona "基礎教学を始めますか?"
choice "基礎教学を始める" -> start_choice
choice "また今度" -> exit_choice

branch start_choice
    add %love 1
    Kona "一緒に学びましょう!"
    end

選択肢まで戻して同じ分岐を選び直すと、%love はまずスナップショットで 0 に戻り、再生時に add %love 1 がもう一度実行されます → 結果は 1 のままです(2 に残ることも、失われることもありません)。

例 2:signal + スナップショット内だけを書き換えるハンドラ

同梱デモが採用している形です(sample/demo/demo.gd):

gdscript
func _on_konado_custom_signal(content: Variant) -> void:
    if content == "好感度上昇":
        # Konado の永続変数を書き換える:スナップショット内の状態
        dialogue_manager.variable_store.apply_operation(
            "love", KonadoVariableStore.Operation.ADD, 1
        )
text
choice "基礎教学を始める" -> start_choice

branch start_choice
    ...
    signal 好感度上昇
    Kona "ご利用ありがとうございました!"
    jump res://sample/demo/demo_02.ks

その分岐を跨いで戻して選び直すと、信号は再発火してハンドラが再び +1 し、書き込まれた %love も行と一緒に巻き戻されています。したがって正味の結果は +1 のままです(+2 にも 0 にもなりません)。

例 3:signal + スナップショット外を書き換えるハンドラ(冪等が必須)

gdscript
func _on_konado_custom_signal(content: Variant) -> void:
    if content != "ギャラリー解放":
        return
    # スナップショット外だけに作用する:ロールバックでは戻せない
    external_save.unlock_gallery("cg_01")

この命令を跨いで戻すと信号は再発火し、unlock_gallery がもう一度呼ばれます。もともと冪等にするか、スナップショット内の変数をガードに使ってください:

gdscript
func _on_konado_custom_signal(content: Variant) -> void:
    if content != "ギャラリー解放":
        return
    if dialogue_manager.variable_store.get_bool("gallery_cg_01"):
        return
    dialogue_manager.variable_store.set_value("gallery_cg_01", true)
    external_save.unlock_gallery("cg_01")

ロールバックを完全に予測可能にするには、権威となる数値% 永続変数か $ 一時変数に置き、外部システムは表現層(サウンド、UI、プラットフォーム実績の同期)に限定して、何度呼んでも安全にしてください。

実績は再生されない

achievement unlock / achievement increment / achievement set_flag は外部カウンタです。ロールバックは跨げますが、再生時に再実行されません——実績は取り消されず、二重にも記録されません。ロールバック後に再び発火させたいものは、スクリプト変数で表現してください。

セーブと履歴

ロールバックはメモリ内操作で、セーブ形式は変わりません。履歴の整理は KonadoDialogueHistory.RollbackPolicy に従います(「会話履歴」を参照)。

Released under BSD3-Clause License.