セーブシステムの改善【Unity6】

絶対調味ロジック

現状のCut進行セーブの問題点

Cut進行でセーブロードを完璧に行うには、そのCut内に以前で起きた変化も重複で設置設定しないといけない状況。

具体例
Cut001で、3つのオブジェクトを非表示にした場合、
Cut002でも、Cut001で非表示にした3つのオブジェクトの非表示設定を設置しないと、
Cut002でセーブロードした際に、3つのオブジェクトが非表示になっていない挙動になる。

・002~以降もその設定を毎回行わないといけない
・さらに、途中で他にそういう変化を行ったら、それ以降もその設定を追加しないといけない

これだと地獄設計だったので、大改造覚悟でClaudeに相談です。
結果は、たった数行追加のベスト改修!

解決アプローチ

問題の本質

毎Cutで画面状態を再構築する
どのCutから再生しても正常な画面が表示される

それを実現するために各Cutが「その時点の全状態を自前で持たなければならない」というのが起きている。
再構築」と言いながら「差分管理」になっているのが矛盾点。

解決 3案

案A:「状態スナップショット方式」(最もシンプル)

各Cutが「自分が担当する変化だけ」を定義し、
ロード時にCut001〜指定Cutまでの変化を順番に高速再生して状態を積み上げる。

ロード時の処理:Cut001の変化を適用 → Cut002の変化を適用 → … → Cut003で停止

  • IsLoadingフラグが既にあるので、このサイレント再生中は演出をスキップできる
  • 既存の IsLoading 仕組みとの相性が良い
  • 各Cutは「自分の変化だけ」書けばよくなる

懸念点:Cutが増えると起動が遅くなる可能性がある(ただし演出スキップ済みなのでほぼ問題ない

案B:「Cutにオブジェクト状態テーブルを持たせる」(現在の延長)

Cutを開始するたびに、全オブジェクトの現在状態をDictionaryに自動キャプチャし、セーブに含める。
ロード時はそのスナップショットを復元する。

  • 手作業が不要になる
  • ただしセーブデータが肥大化する
  • オブジェクトが増えるたびに管理対象を増やす手間が別途発生する

案C:「管理対象オブジェクトをマネージャーで一元管理」(より大胆)

Sys_StageManager のようなものを作り、シーン上の「状態変化しうるオブジェクト」をすべて登録しておく。

各Cutは StageManager に「このIDのオブジェクトをこうしろ」という命令リストだけ持つ。
StageManager が現在状態を常時保持し、セーブ・ロード時にその状態テーブルを丸ごと保存・復元する。

率直な意見

案Aが最速で安全です。

「本当にそれでいいですか?」
現在のCut数と今後の規模次第では、案Aの「遡り再生」がそもそも破綻します。
Cut数が100・200になったとき、ロードのたびにCut001から全再生するのは設計として脆い。

個人開発・初心者という前提を考慮すると、案C(StageManager方式)が最終的に一番メンテナブルですが、
今の設計に大きく手を入れる必要があります。

案Aは:

「Cut100個をロード時に遡り再生」が重いという懸念を先ほど出しましたが、
IsLoadingフラグで演出を完全スキップする前提なら、実態はDictionaryへの値代入を100回ループするだけです。
低スペックPCでも体感ゼロに近い。

対して案Cは:

  • StageManager新規実装
  • 全Cutの命令形式を変更(既存20Cutの改修)
  • セーブデータ構造の変更
  • 今後の全Cutでの記述ルール変更

工数とバグリスクが、案Aの数倍になります。

まとめ

案Aを採用。

Cut300個でも問題ない
例えば、状態変化スクリプトの Cmd_Activate_Object の処理は OnEnable() で実行されます。
つまり「Cut001〜002を高速再生する」実態はSetActive(true/false)をループするだけです。
300Cut × オブジェクト数十個 = 数千回のSetActive呼び出し。1フレームで終わります。

具体的な実装LoadRoutine に数行追加するだけです。

やること規模
既存の状態変化スクリプトに IsLoadingチェック追加数行
LoadRoutine に遡り再生ループ追加10行程度
既存Cutの改修不要

既存の全Cutはそのままで動きます。
他にDOTweenや状態変化を持つCmdスクリプトがあれば、同様にIsLoadingチェックの有無を確認してください。

設計案概要:ロード時遡り再生方式

問題の本質

Cut進行方式は「毎Cutで画面状態を再構築する」設計だが、現状は各Cutが過去の変化を自前で持たないと再構築できない
これは管理コストが線形以上に増大する。

解決方針

ロード時に、指定Cutの直前までを高速サイレント再生して状態を積み上げる。
各Cutは「自分の変化だけ」を定義すればよくなる。

処理フロー

ロード開始
  ↓
シーンロード・全Cut非アクティブ化(既存)
  ↓
★ Cut[0] → SetActive(true) → 1f待機 → SetActive(false)
★ Cut[1] → SetActive(true) → 1f待機 → SetActive(false)
★ Cut[2] → ...(目標Cutの直前まで繰り返し)
  ↓
目標CutをStartCut()で通常実行(既存)

改修箇所

① Sys_SaveManager.cs — LoadRoutineに追加(約10行)
遡り再生ループを、既存のStartCut呼び出しの直前に挿入する。

② 既存のオブジェクト状態変化スクリプト — Start() / OnEnable() に IsLoadingチェック追加
遅延コルーチンをスキップして即時実行するパスを追加する。
Start() / OnEnable() で遅延処理を持つスクリプトがあれば同様に対応する。

既存への影響

  • 全Cutオブジェクト:改修不要
  • セーブデータ構造:変更なし
  • パフォーマンス:SetActiveループはIsLoadingフラグで演出スキップ済みのため、Cut300個でも実用上問題なし

改修

Sys_SaveManager.cs の LoadRoutine 改修

数行の追記のみ。

状態変化スクリプト改修 + IsLoadingチェックのテンプレート

void OnEnable()
{
    // ★ IsLoadingチェック(テンプレート)
    // OnEnable()に遅延処理がある全Cmdスクリプトに必ずこのブロックを追加する。
    // ロード中は演出・遅延をスキップして状態だけ即時適用する。
    if (Sys_SaveManager.IsLoading)
    {
        foreach (var obj in activateObjects)   obj?.SetActive(true);
        foreach (var obj in deactivateObjects) obj?.SetActive(false);
        return;
    }

    StartCoroutine(ActivateAfterDelay());
    StartCoroutine(DeactivateAfterDelay());
}

テンプレートの原則

  • if (Sys_SaveManager.IsLoading) で分岐
  • ブロック内は「最終状態を即時適用」するコードだけ書く
  • 必ず return で通常処理を抜ける
Start() / OnEnable() の中に以下があるか?
  ├─ StartCoroutine()            → 必要
  ├─ DOTween (.DOFade等)         → 必要
  ├─ Invoke() / InvokeRepeating  → 必要
  └─ 即時代入・SetActive のみ    → 不要

OnEnable() か Start() かは関係なく、遅延・Tween処理があるか否かが判断基準です。

今後の追加Cmdスクリプトへの対応ルール

Start() または OnEnable() 内にDOTween・コルーチン・Invokeなど
遅延・アニメーション処理がある場合は必ずIsLoadingチェックを追加する。

即時代入・SetActiveのみの場合は不要

SAV_(保存変数)の紐づけ

今回のセーブ改修に合わせて「SAV_(保存変数)」の保存方法も改修します。

確定バグ(必ず修正)

ロード時のSAV_残留

  • slot1→slot2とロードした場合、slot1のSAV_変数が残留する
  • 修正:RestoreSavVariables()の先頭にSAV_全削除を2行追加

ニューゲーム時のSAV_残留

  • 前回プレイのSAV_変数がメモリに残ったまま新ゲームが始まる
  • 修正:ClearSavVariables()を新設+UI_TitleManager.csを新規作成

遡り再生中のSAV_上書きリスク:中長期リスク(設計ルールで対処)

  • 今は発生しないが、将来CutControllerにSAV_書き込みを追加した瞬間に顕在化
  • 修正:コードではなく設計ルールとして明文化するにとどめる

確定設計

修正1:Sys_Flag_Manager.cs に2つ追加

  • ClearSavVariables()メソッドを新設
  • RestoreSavVariables()の先頭にSAV_全削除を追加

修正2:UI_TitleManager.cs を新規作加

  • ニューゲームボタン → ClearSavVariables()→ シーンロード
  • ロードボタン → UI_ButtonsBaseのOnTitleLoadClick()に委譲

修正3:

Unityエディタ上でタイトルのニューゲームボタンのOnClickをSys_SceneLoaderからUI_TitleManager.OnClickNewGame()に差し替え

  • UI_TitleManagerとUI_ButtonsBaseを同オブジェクトにアタッチ
  • ニューゲームボタン・ロードボタンのOnClickを差し替え

Unityエディタ上での作業

UI_TitleManager をタイトルシーンの適切なオブジェクトにアタッチして:

  1. New Game Scene Index にゲーム開始シーンのインデックスを設定
  2. ニューゲームボタンのOnClickを UI_TitleManager.OnClickNewGame() に変更
  3. ロードボタンのOnClickを UI_TitleManager.OnClickLoad() に変更
  4. UI_ButtonsBase も同じオブジェクトにアタッチ

残っている設計上の注意事項(コードではなくルール)

将来CutControllerにOnEnable()でSAV_変数を書く処理を追加する場合、
ロード時の遡り再生で復元済みのSAV_値が上書きされるリスクがあります。
その際は IsLoadingフラグを見てSAV_書き込みをスキップする実装を必ず入れてください。

コメント