システム適応・探索パート★【Unity6】

絶対調味ロジック

ここまでノベルパートにてノベル用システムを構築してきました。
探索パートにもほぼ流用できますが、一部分の分離・統合を行う必要があります。

  • セリフシステム:Cut進行専用になっているため、探索にも対応できるように
  • セーブロード:Cut進行専用になっているため、探索用保存ステータスなども追加し復元可能に

以前の開発バージョンUnity2023版とは比べ物にならないくらいに、ノベルパートでのシステムを実装しているので、
まずは探索パートに対応させ基礎システムを再構築していきます。

プロジェクトの目的(初期設計意図)

  • ノベル(Cut)と探索(Location)の共存
  • Messenger(セリフシステム)を共通UIとして利用
  • Save/Loadで両モードを復元可能にする
  • 進行構造は完全分離する

全体アーキテクチャ

基本構造

Cut(ノベル:1カット)
 ├ Sys_CutManager
 ├ Sys_CutController
 └ Sys_Messenger(共通利用)

Location(探索:場所)
 ├ Sys_LocationController
 ├ Sys_LocationUIManager(拡張用)
 └ Sys_Messenger(共通利用)

探索パートもCut進行と構造を同じようし、Location(場所)進行としています。
Cut内に状況を作る代わりに「Location内に状況を作る」となっています。
探索パート(その1) 独自構築
探索パート(その2) 独自構築

Cutバージョン・Locationバージョンに分け、それぞれに対応するシステムに再構築していきます。

設計思想

  • Cut = 直線進行(演出単位)
  • Location = 空間状態(状態単位)
  • Messenger = 共通会話UI
  • Save = モード依存分岐

フェーズ進行まとめ

フェーズ1:Cut依存分離

■ 目的:MessengerをCut依存から解放

■ 成果:
Explore(探索)でもMessenger動作可能
・CutとExplore(探索)のUI共通化成立

フェーズ2:Explore(探索)基礎構築

■ 目的:Location単位で探索を成立

■ 成果:
LocationController導入
Active/Inactiveで切替
Location遷移構造完成

Sys_LocationController
・移動時に表示されるメッセージを登録。
・現在のLocationから移動する・移動できるLocationを事前に登録。
 ※現状はボタンの方で機能しているため、Sys_LocationUIManagerともに不要。
・タイミングはディレイで設定可能に。
・セーブスロットに表示させていたセルフ部分はロケーション名に変更。

Sys_LocationButton
・Location移動には専用ボタンを使用。
・現在Locationと移動先Locationを登録。

フェーズ3:Save/Loadシステム再構築

Save/Load分岐

■ 目的:ノベルと探索の保存分離

■ 成果:
PlayMode(Novel / Explore)導入
Scene復帰対応
Location単位復元

探索状態保存拡張

■ 目的:探索内部状態の保存

■ 成果:
メッセージ進行保存
フラグ管理基盤
ギミック状態設計追加
イベントフラグ構造追加

探索完全復元コア

■ 目的:探索状態の再現性確保

■ 成果:
カメラ状態保存・復元
キャラクター状態保存・復元
Location単位完全復元
・Save→Load 一貫性確立

セーブ・ロード設計の改善★

保存構造(Sys_LocationState):
・カメラ位置・回転
・アクティブなMessenger1つの相対パス・messageIndex・isFinished

フラグ変数:
SaveData.savVariables(SAV_接頭辞)に一元管理。
Sys_LocationState には持たせない。

復元フロー(LoadRoutine):
 1. 全Locationを非アクティブ化
 2. 対象Locationを SetActive(true)
 3. ApplyState で全Messengerを停止・非アクティブ化
 4. 対象Messengerの親チェーンをアクティブ化
 5. ReserveMessageIndex でindex予約 → SetActive(true)
 6. IsLoading = false

Messengerのロード復元:
OnEnable 時に reservedMessageIndex を参照して再開位置にジャンプ。
コルーチンのタイミング問題(IsLoadingがfalseになるタイミングとの競合)は reservedSkip フラグで解決。
会話途中 → 指定indexから即時全表示で再開
会話終了済み → FinishConversation() を直接呼び出し

ロード中の副作用対策:以下のスクリプトに IsLoading チェックを追加
・Sys_Flag_Controller.RegisterTriggers() / OnEnable()
・Sys_FlagTrigger.OnEnable()
・Cmd_Activate_Object.OnEnable()
これによりロード中のフラグ発火・オブジェクト制御をスキップし、ApplyState による復元と競合しない。

現在の完成状態

■ 探索システムは以下を満たしている

  • Location単位で独立
  • Messenger共有
  • Save/Loadで完全復帰
  • フラグ・イベント管理可能
  • カメラ・キャラ復元可能

データ構造の中心

■ SaveData

  • Scene名
  • PlayMode(Novel / Explore)
  • CutIndex / MessageIndex
  • LocationName
  • LocationState

■ LocationState

  • メッセージ進行
  • カメラ状態
  • キャラ状態
  • ギミック状態
  • イベントフラグ

設計の重要原則

探索はCutにしない → 状態が空間依存のため
Location単位で完結 → 全状態はLocationStateに集約
Find依存は許容(現段階) → 将来最適化対象
シンプル優先 → Manager層増やさない

分別統合システムの改善★

■ 修正・整理の全体方針

削除:
Sys_GameModeManager — 完全削除、シーン分離で不要
・Sys_CutController の GameModeManager 参照
・Sys_CutManager.LoadLocationByName() — 残骸
・Sys_SaveManager の RestoreNovel() / RestoreExplore() / GetActiveLocationName() — 未使用デッドコード
・Sys_LocationState の activeCharacterName / localFlags / eventFlags — 未使用・重複

修正:
・モード判定を CutManager.Instance != null で統一
・static フラグの残留リセット
・SaveRoutine と AutoSaveRoutine の判定ロジック統一
・Sys_LocationState をシンプルに整理
スキップ・既読の探索対応
  Sys_Messenger の cachedCutIndex 初期値を 0 → -1 に変更。
  探索パートでは CutManager が存在しないため -1 を使用し、ノベルパートの cutIndex=0 との衝突を回避。

■ 運用ルール

シーン構成:
ノベルシーン:Sys_CutManager を置く。Sys_LocationController は置かない。
探索シーン:Sys_LocationController を置く。Sys_CutManager は置かない。

フラグ変数の保存:
・セーブに含めたい変数は SAV_ 接頭辞をつければ自動的にセーブデータに入ります。
・LocationState にフラグを持たせる必要はありません。

項目ルール
ノベルシーンSys_CutManager を置く。Sys_LocationController は置かない
探索シーンSys_LocationController を置く。Sys_CutManager は置かない
探索Messengerデフォルト非アクティブ。起動は各スクリプトで手動制御
会話終了後探索パートのみ自動で非アクティブ化(CutManager.Instance == null で判定)
フラグ保存SAV_ 接頭辞をつければ自動的にセーブデータに含まれる
セーブデータ変更後既存セーブは互換性がないため新規セーブし直してテスト

■ PlayModeの判定箇所

判定箇所:Sys_SaveManager.DetectPlayMode()

static PlayMode DetectPlayMode(out Sys_LocationController activeLocation)
{
    activeLocation = null;

    if (Sys_CutManager.Instance != null)
        return PlayMode.Novel;

    foreach (var loc in FindObjectsByType<Sys_LocationController>(...))
    {
        if (loc.gameObject.activeInHierarchy)
        {
            activeLocation = loc;
            return PlayMode.Explore;
        }
    }

    return PlayMode.Explore; // フォールバック
}

判定ロジック:
Sys_CutManager がシーンに存在する → ノベル
・アクティブな Sys_LocationController が存在する → 探索

判定タイミング:
・Save() 実行時
・AutoSave() 実行時
・ロード時は判定を行いません。セーブデータに記録された PlayMode をそのまま使います。

csharpif (data.playMode == PlayMode.Novel) { ... }
else { ... }

ノベルシーンには Sys_CutManager を置く、探索シーンには置かない。 それだけで全て自動的に判定されます。

改善★0523 ロード後のUI復元

探索パートでメッセージ終了後に表示されるUIが、セーブ→ロード後に復元されない問題。

■ 原因の構造

メッセージ終了
 → activateOnFinishedObjects(Msg_Front_End)をSetActive(true)
  → Cmd_Activate_Object.OnEnable()
   → IsLoading=true のためreturn(ブロック)
   → UIが表示されない

加えてMessenger自身も終了後にSetActive(false)されるため、CaptureStateで状態を捕捉できていなかった。

■ 解決方針

複雑な状態保存・復元ではなく、LocationControllerに復元対象を直接登録するシンプルな方式を採用。

■ 変更内容

Sys_LocationState.cs:変更なし(シンプルなまま維持)
Sys_LocationController.cs
・loadActivateObjectsフィールドを追加
・ApplyState内でisFinished=trueの場合のみloadActivateObjectsをアクティブ化

■ 運用ルール

・メッセージ終了後に表示したいUIはloadActivateObjectsにInspectorから直接アサインする
・isFinishedフラグで条件制御されるため、メッセージ未終了時には表示されない
・DontDestroyOnLoad配下のオブジェクトも直接アサインなので参照問題が発生しない

改善★0525 Messengerの初期化ルール

修正を重ねている間に、
LocationControllerでの初期起動Messenger指定がなくなったことにより、
通常のロケーション移動の制御漏れが発生していた問題を解決しました。

以前のようにデフォルトMessengerの明示的指定に戻しました。

修正ポイント

① 既存機能(セーブ・ロード)は100%そのまま維持
Sys_SaveManager がロード処理を行う際、
Sys_SaveManager.IsLoading = true の状態でこのLocationを SetActive(true) にします。
今回の修正により、ロード時は OnEnable の先頭で即座に return されるため、
ロード時の挙動(ApplyState による正確な復元)には一切干渉しません。

② 通常遷移時の「ゴミ」を完全に排除
普通にマップ移動してきたときは、
まず配下の全Messengerに対して ForceStop() と SetActive(false) を一括実行します。
これにより、「Unityエディタ上でうっかりアクティブのまま保存してしまった別のMessenger」が誤作動する原因を根本からシャットアウトします。

③ インスペクターで一目瞭然に
新しく追加した defaultMessenger 枠に、そのロケーションに入った瞬間に動かしたいMessenger(GameObject)をドラッグ&ドロップするだけで設定が完了します。ヒエラルキーの上下順を変更しても、バグらなくなります。

拡張の懸念点

「同じロケーションに、ゲームの進行度(フラグ)によって違うMessengerを初期起動させたいケース」
もし「このロケーションは、いつ来ても最初の会話(defaultMessenger)は1種類だけ」であれば、今回のシンプルな修正がベスト(最短ルート)です。
もしフラグによる分岐が必要な場合は、InitializeForNormalEntry の中でフラグマネージャーの値を参照する処理を追加する必要がありますが、現時点ではこれで意図通りの挙動になるはずです。

改善★0528 探索パートCinemachine対応・セーブロード修正

■ 発端の問題

Unity6 URP + Cinemachine導入後、
探索パートのLocation内でカメラが移動した状態でセーブしても、ロード後にデフォルトカメラ位置に戻ってしまう問題。

■ 原因の特定

CaptureState()・ApplyState()がCamera.mainのTransformを保存・復元していたが、
CinemachineはCinemachineBrainが毎フレームCamera.mainを上書きするため、保存も復元も機能していなかった。

■ 解決の方向性

「Camera.mainのTransformではなくどのVCamがアクティブかを保存・復元する」 方針に変更。

■ ヒエラルキーの再構成

変更前の問題:
・VCamがCmd_Activate_Objectで制御される状態セットの子に入っていた
・Cmd_Activate_ObjectがVCamごとON/OFFしてしまい復元と干渉

変更後の構造:

Location
├── VCams(VCamだけを独立して管理する親)
│     ├── CM_Cam_Default(常時Active)
│     └── CM_Cam_Chara(会話時用)
├── Msg_Sato(Cmd_Activate_Object)
│     ├── Sato_A(Messenger・Cmd_VCam_Switch・holdOnDisable=false)
│     └── Sato_B(Messenger・Cmd_VCam_Switch・holdOnDisable=true)
├── Msg_Sato_Choice(Cmd_Activate_Object・Cmd_LocationState_Marker)
└── Msg_Sato_Answer(Messenger・Cmd_VCam_Switch・Cmd_Activate_Object)

■ 新規作成スクリプト

Cmd_VCam_Switch.cs:MessengerのOnEnable/OnDisableに連動してVCamを切り替えるコンポーネント。

・役割:MessengerなどのGameObjectのアクティブ状態に連動して、VCamを切り替えるコンポーネント。

・設定項目:

フィールド内容
targetVCam会話時のVCam
アクティブ時に表示したいVCam
defaultVCamデフォルトVCam
非アクティブ時に戻すVCam
holdOnDisabletrueにすると非アクティブ時にデフォルトに戻さない
(連続する状態間でカメラを維持したい場合に使用)

・動作:

タイミング動作
OnEnabletargetVCamをON、defaultVCamをOFF
OnDisabletargetVCamをOFF、defaultVCamをON(holdOnDisable=falseの場合)
OnDisable何もしない(holdOnDisable=trueの場合)

・使い方パターン:

状況設定
キャラに寄るtargetVCam=キャラカメラ、
defaultVCam=デフォルトカメラ
カメラ位置を動かさずデフォルトに戻したいtargetVCam=デフォルトカメラ、
defaultVCam=デフォルトカメラ
連続する状態間でカメラを維持したいholdOnDisable=true

・運用ルール:
 必ずVCams配下のVCamをアサインする
 デフォルトカメラ(CM_Cam_Default相当)は常時Activeにしておく
 カメラを切り替えたいGameObjectにアタッチする
 ロード時はIsLoadingチェックにより発火しない(ApplyStateが復元を担当)

カメラ移動の発火タイミングが重要
OnEnable(アクティブになるとき) → カメラを寄せる
OnDisable(非アクティブになるとき) → カメラを戻す

Cmd_LocationState_Marker.cs:Messengerを持たない状態オブジェクト(選択肢UIなど)に付けるマーカー。

  • loadActivateObjects:ロード時にアクティブにしたいUIを登録

ロード後に表示したいUI(Marker状態)が重なっているような構築の場合、
状態の区切り目となるGameObjectCmd_LocationState_Markerをアタッチし、
ロード後に表示したいものをloadActivateObjectsに登録します。
ただし、直前の状態オブジェクトは非アクティブにする必要があります。

■ 修正したスクリプト

Sys_LocationState.cs:
・cameraPosition・cameraRotationを削除
activeVCamPaths(List)を追加:アクティブなVCamのパスを複数保存
activeStatePathを追加:Messengerなし状態オブジェクトのパスを保存

Sys_LocationController.cs:
CaptureState():VCams配下のアクティブVCamパスを保存、Markerのアクティブ状態を保存、アクティブMessengerを優先・終了済みMessengerをフォールバックで保存
ApplyState():VCamを復元、Marker状態を復元しつつActivateLoadObjects()を呼ぶ、Messengerを復元
IsApplyingState静的フラグを追加:ApplyState中はCmd_Activate_ObjectのOnEnableをスキップ

Cmd_Activate_Object.cs:
・OnEnableにSys_LocationController.IsApplyingStateチェックを追加

■ 運用ルール

状況対応
VCamの追加必ずVCams配下に置く
※現状は名前は必ず「VCams」にすること
会話時のカメラ切り替えCmd_VCam_Switchをアタッチ
連続状態でカメラ維持holdOnDisable=trueに設定
Messengerなしの状態オブジェクトCmd_LocationState_Markerをアタッチ
ロード後に表示したいUI(Messenger終了後)LocationControllerのloadActivateObjectsに登録
ロード後に表示したいUI(Marker状態)Cmd_LocationState_MarkerのloadActivateObjectsに登録
セーブデータ構造変更後既存セーブデータを削除してから再テスト

改善★0529 ロード後の再セーブ問題

■ 発生した不具合

あるLocationでセーブ→ロード後、そのまま再セーブ→再ロードするとloadActivateObjectsが発火しない。
Locationを移動してセーブした場合は問題なし。

■ 原因

ApplyState()で全MessengerがForceStop()・SetActive(false)される。
ロード後はアクティブなMessengerも終了済みMessengerも存在しない状態になるため、
再セーブ時にisFinished=false・activeMessengerPathが空で保存されてしまい、
次のロード時にloadActivateObjectsが発火しなかった。

同様の問題がCmd_LocationState_Markerのケースでも将来起きる可能性があった。

■ 修正内容

CaptureState()に2つの判定を追加。

1. loadActivateObjectsの再セーブ対応
Messengerが見つからない場合、loadActivateObjects内のいずれかがアクティブならisFinished=trueとみなす。

2. Cmd_LocationState_Markerの再セーブ対応
Markerがアクティブでない場合、Marker内のloadActivateObjectsがアクティブならactiveStatePathを保存する。

あわせてCmd_LocationState_MarkerにGetLoadActivateObjects()メソッドを追加。

■ 修正後のCaptureState()の判定優先順位

  1. VCams配下のアクティブVCamを保存
  2. アクティブなMarkerがあれば activeStatePath に保存
  3. Markerが非アクティブでも loadActivateObjects がアクティブなら activeStatePath に保存
  4. アクティブなMessengerがあれば会話途中として保存
  5. 終了済みMessengerがあれば isFinished=true として保存
  6. どちらもなく loadActivateObjects がアクティブなら isFinished=true とみなす

■ 変更ファイル

・Cmd_LocationState_Marker.cs:GetLoadActivateObjects()メソッド追加
・Sys_LocationController.cs:CaptureState()に判定ロジック2件追加

コメント