設定UI画面を管理するシステム★【Unity6】

絶対調味ロジック

ここまで「バックログ」「セーブロード」を作成しました。
専用Canvas」が増え、「DontDestroyOnLoad設定」も個別設定で増えました。
ヒエラルキーの見た目もごちゃついてきています。
そして、まだ未実装コンフィグとして「音量設定」や「テキスト設定」などが残っています。

今後の複数のシステム設定画面の実装を踏まえて、いまのうちに、
一元管理」できるようなシステムが作れないかAiに相談してみました。
(そもそも何をどう管理するのか分かっていないので、完全に丸投げ状態です)

現状の全体把握

ヒエラルキー

ヒエラルキーを見ると、Canvasがすでに5枚ある。

Dynamic_Canvas  ← ノベル本体(Msg, Msg_UI等)
Backlog_Canvas  ← バックログ専用
SaveLoad_Canvas  ← セーブロード画面
Black_Canvas   ← 暗幕(FadeManager)
Dialog_Canvas   ← ダイアログ

DontDestroyOnLoad対象

DontDestroyOnLoad対象も複数散在している。

SaveManager(ルート)
SaveLoad_Canvas(ルート扱い)
Black_Canvas(ルート扱い)
Dialog_Canvas(ルート扱い)
ChapterManager(System_set以下?)

問題の核心

Canvas単位でSortOrder手動管理している。

Black_Canvas → SortOrder: 999
Dialog_Canvas → SortOrder: 200
SaveLoad_Canvas→ SortOrder: 100
Backlog_Canvas → SortOrder: 10(推定)

今は4枚で済んでいますが、設定画面・音量画面・メニュー画面が増えると番号管理が破綻します

設定UI画面の管理システム設計案

方針Canvas1枚に統一 + Sys_UIRoot + Sys_InputManagerで一元管理

つまりは、
・DontDestroyOnLoadがあるCanvasを1つのオブジェクトにまとめる:機能の一元化と見た目の整理
・まとめたものをDontDestroyOnLoad設定し、最終的にはタイトルSceneにだけの設置で済む設計。
・各設定UI画面での入力キー・ボタンの制御を統一化。
・将来は、各設定UI画面の重なりの順番を自動化。(ソートを手動で設定しないで済む)

Canvas構成

ルート・DontDestroyOnLoad
SaveManager       ← ゲームデータ系・既存ルール通り
UI_Root(Canvas 1枚)  ← UI系を全部ここに統合(Sys_UIRoot.cs
 ├ Sys_InputManager.cs  ← 入力ブロック管理
 ├ Sys_FadeManager.cs  ← 既存・移動
 ├ Black_Screen
 ├ SaveLoad_Panel    ← 既存・移動
 └ Dialog_Panel      ← 既存・移動

シーン内
System_set
 ├ CutManager
 ├ ScreenshotManager
 └ ChapterManager  ← ゲームデータ系・シーン内で十分
Backlog_Canvas    ← シーンリセットでOKなので

シーン外
UI_PanelBase.cs  ← 置いておくだけの設計図

Sys_UIRoot.cs

DontDestroyOnLoad(Scene共有設定)のCanvasを一元管理するスクリプト。

Sys_InputManager.cs

入力ブロック管理(カウンター方式)
  Block() / Unblock() / IsBlocked

パネル開閉管理
  パネルが開くたびにSortOrderを自動で最前面にする【将来対応
  → 番号の手動管理が不要になる

パネル登録
  UI_PanelBaseを継承したパネルが自動登録される

UI_PanelBase.cs

・全UIパネルの基底クラス
・Open() → UIManagerにBlock()を通知 + SortOrder更新
・Close() → UIManagerにUnblock()を通知
・継承するだけで各パネルが自動登録される

UI_PanelBase.csはどこにも直接アタッチしない。
継承したスクリプトにOpen()/Close()の共通処理が自動的に組み込まれる設計図

実装の手順

STEP 1:スクリプトの差し替えと追加

新規スクリプトを作成:Sys_UIRoot.cs・Sys_InputManager.cs・UI_PanelBase.cs

既存ファイルを差し替え(上書き保存):Sys_Messenger.cs・UI_SaveLoadPanel.cs・Sys_DialogManager.cs

STEP 2:UI_Rootオブジェクトを作成する

これが今回の核心です。
散らばっていたUI系オブジェクトをここに集約します。

① Hierarchyの何もない場所で右クリック
  →「Create Empty」を選択
  → 名前を「UI_Root」に変更

② UI_Rootの位置をルート(最上位階層)に置く
  SaveManagerと同じ階層になるようにドラッグ

③ UI_RootにSys_UIRoot.csをアタッチ
  → これでDontDestroyOnLoadが有効になる

④ UI_RootにSys_InputManager.csをアタッチ
  → 入力ブロック管理の本体

既存のCanvas3つをUI_Rootの子に移動する

移動するもの(UI_Rootの子にする):
 Black_Canvas  ← 暗幕・FadeManager
 SaveLoad_Canvas ← セーブロード画面
 Dialog_Canvas  ← ダイアログ画面

移動しないもの(シーン内に残す):
 Backlog_Canvas ← シーンごとにリセットが必要なため
 System_set   ← ゲームデータ系のため

移動後のヒエラルキーイメージ:

ルート階層
 SaveManager     ← 既存・変更なし
 UI_Root       ← 今回新規作成
  ├ Black_Canvas   ← ここに移動
  ├ SaveLoad_Canvas ← ここに移動
  └ Dialog_Canvas  ← ここに移動

シーン内・変更なし
 Backlog_Canvas
 Dynamic_Canvas
 System_set
  └ ChapterManager ← 話し合い通りここに置く
 CUT_OBJECTs
 CAMERA_Main
 Cut_001〜

STEP 4:動作確認チェックリスト

□ Unityコンパイルエラーがない

□ Play開始時にUI_RootがDontDestroyOnLoadに移動している
  (再生中にHierarchyの「DontDestroyOnLoad」欄に
  UI_RootとSaveManagerが見えればOK)

□ セーブ画面を開く
  → パネルが表示される

□ セーブ画面を開いた状態でホイールダウン
  → ノベルが進まない ←今回の修正目的

□ セーブ画面を閉じてホイールダウン
  → ノベルが正常に進む

□ ダイアログが出ている間もホイールダウンで進まない

□ 右クリックでセーブ画面が閉じる

□ バックログを開いてホイールスクロール
  → ログがスクロールされる(従来通り)

□ バックログを閉じてホイールダウン
  → ノベルが進む(従来通り)

設定UI画面の管理システム構築 まとめ

共通できるものを管理し、入力操作での挙動を直し、UI画面への統一操作となりました。

現状では、ノベルパート1での実装となっており、このまま各設定をここで作ってしまって良いものかと迷っていました。
探索パート論争パートへの適用もどうなるか分かりません。
軽く聞いてみたところ、
各パート・変数に対しても、今回のセーブシステムは適用可能な感じだったので、安心しています。

早いうちに制御管理を構築できて良かったです(*´ω`*)
これで心置きなく「テキスト設定」「音量設定」にとりかかれます。

今回の目的

・セーブ画面表示中のホイールダウンでノベルが進行してしまう入力貫通問題の解消
・将来のUI画面追加を見越した共通管理システムの構築
・DontDestroyOnLoadの一元管理
・ヒエラルキーの整理

解決した問題

原因:Sys_Messenger.csのUpdate()にセーブ画面が開いているかのチェックが存在しなかった

解決:Sys_InputManager.csを新規作成し、UI画面が開いている間はIsBlocked = trueになる仕組みを導入。
Sys_Messenger.csのUpdate()冒頭にIsBlockedチェックを1行追加。

新規作成スクリプト

Sys_UIRoot.cs
・UI_RootオブジェクトをDontDestroyOnLoadに登録する
・このスクリプトをUI_Rootにアタッチするだけでよい
・UI系オブジェクトの親として機能する

Sys_InputManager.cs
・カウンター方式で入力ブロックを管理する
・Block() → カウンター+1
・Unblock() → カウンター-1
・IsBlocked → カウンターが1以上のときtrue
・複数UIが同時に開いていても正しく制御できる

UI_PanelBase.cs
・全UI画面の基底クラス
・Open() → InputManagerのBlock()を自動呼び出し
・Close() → InputManagerのUnblock()を自動呼び出し
・継承するだけで入力ブロック管理が自動化される

改修スクリプト

UI_SaveLoadPanel.cs
・UI_PanelBaseを継承
・Open(SaveLoadMode mode)でbase.Open()を呼ぶ
・Close()でbase.Close()を呼ぶ
・DontDestroyOnLoadの記述を削除(UI_Rootが管理)

Sys_DialogManager.cs
・UI_PanelBaseを継承
・Show()内でbase.Open()を呼ぶ
・OnYesClicked/OnNoClicked内でbase.Close()を呼ぶ
・DontDestroyOnLoadの記述を削除(UI_Rootが管理)

Sys_Messenger.cs
・Update()冒頭にIsBlockedチェックを追加
・IsBlocked == trueのとき全入力を無視する
・バックログのホイール操作には影響しない(IsBlockedチェックの後でbacklogActiveチェックがある)

Sys_FadeManager.cs
・DontDestroyOnLoadの記述を削除(Black_CanvasがUI_Rootの子になったため不要)

Sys_ChapterManager.cs
・DontDestroyOnLoadの記述を削除
・System_setの子としてシーン内に残す設計に変更
・章テキストはシーンごとに変わる情報のため、シーンをまたいで保持する必要がない

DontDestroyOnLoad管理ルール

呼んでよいのはルート直置きオブジェクトのスクリプトのみ
 Sys_UIRoot.cs  ← UI系を一括管理
 Sys_SaveManager.cs ← ゲームデータ系

禁止
 UI_Rootの子オブジェクトのスクリプト
  └ Sys_FadeManager / UI_SaveLoadPanel / Sys_DialogManager
 シーン内オブジェクトのスクリプト
  └ Sys_ChapterManager / Sys_BacklogManager

整理後のヒエラルキー構成

DontDestroyOnLoad対象・ルート
 UI_Root
  ├ Sys_UIRoot.cs   ← DontDestroyOnLoad管理
  ├ Sys_InputManager.cs ← 入力ブロック管理
  ├ Black_Canvas   ← Sys_FadeManager
  ├ SaveLoad_Canvas ← UI_SaveLoadPanel
  └ Dialog_Canvas  ← Sys_DialogManager
 SaveManager ← Sys_SaveManager・既存ルール通り

シーン内・DontDestroyOnLoad不要
 System_set
  ├ ChapterManager ← Sys_ChapterManager
  ├ CutManager
  └ ScreenshotManager
 Backlog_Canvas ← シーンごとにリセット
 Dynamic_Canvas
 TestSave_Canvas
 CUT_OBJECTs
 CAMERA_Main
 Cut_001〜

UI_Rootの配置ルール

・現在:ノベルScene1に配置(B案)
 → 現段階ではノベルScene1のみ開発中のため

・タイトルScene作成時:タイトルSceneにも追加(C案)
 → 重複はSys_UIRootのAwakeで自動削除される

・最終形:タイトルSceneのみに置く

・RuntimeInitializeOnLoadによる自動生成は採用しない
 → InspectorアサインによるUI構造を維持するため
 → Sys_Flag_Managerとは方針が異なる点に注意

将来UIを追加するときのルール

① UI_Rootの子にCanvasを新規作成する
② スクリプトはUI_PanelBaseを継承する
③ PanelObjectプロパティを実装する
  protected override GameObject PanelObject => panel;
④ 開くときはbase.Open()を呼ぶ
⑤ 閉じるときはbase.Close()を呼ぶ
⑥ DontDestroyOnLoadは書かない

→ 入力ブロックとDontDestroyOnLoadは自動管理される

現在までの設計ルール(継続・追記分含む)

・キャラクター・カメラはCutの外に置く
・Cutをまたいで使うオブジェクトはCutの外に置く
・SaveManagerはルートオブジェクト必須
・DontDestroyOnLoad対象はルートに置く
・演出のみのCutには必ずCmd_EndCutをアタッチ
・複数Cmd_Move_xyzがある場合
 EndCutを呼ぶのは最後の1つだけ
・DontDestroyOnLoadはルート直置きのスクリプトのみ記述
・UI_Rootの子・シーン内オブジェクトへの記述は禁止
・将来のUI画面は全てUI_PanelBaseを継承する

改善★0525 自動ソート問題の解決

1. 発生していた問題(前提の弱点)

原因:
従来の UI_Manager.GetNextSortOrder() は、画面が開くたびに数値を無限にカウントアップ(++)し、全UIが閉じきるまでリセットされない仕様でした。

現象:
この「増え続けるローカルな連番(1, 2, 3…)」を Canvas の sortingOrder に直接上書き代入していたため、インスペクター上で 150 などに設定していた初期値が強制的に 1 や 2 に書き換わり、他のCanvasの下に潜り込むソート崩れが発生していました。

2. 施した修正(リファクタリング内容)
既存の仕組み(Stack管理、入力ブロック等)を一切壊さず、ロジックを最適化しました。

UI_Manager.cs の最適化:
不確実なカウントアップ変数(nextSortOrder)を完全に廃止。
GetNextSortOrder() は、単純に「現在スタックに積まれているUIの枚数(stack.Count)」を返すだけの超軽量・安全な設計に変更しました。

UI_PanelBase.cs の設計変更:
インスペクターで設定された初期の sortingOrder を Awake 時に「ベース値(基準の高さ)」としてキャッシュ。
画面を開く際は、[インスペクターの初期値] + [現在のスタック枚数(オフセット)] を代入する仕様に改良しました。

3. 修正後のソート挙動(仕様)

単体で開く場合(通常時):
スタック数が 0 なので、インスペクターの設定値(例: 140)のまま表示されます。

重ねて開く場合(Additive時):
1枚目が 140 なら、その上に重ねて開いた2枚目は 140 + 1 = 141 となり、
後から開いたUIが確実に前面にくる自動ソートが実現します。

自動ソートの動作確認結果
修正後のログで Offset:0 が続くことを確認したが、これは正常。
現運用では設定画面はすべて OpenMode.Replace のため、スタックには常に1枚しか存在しない。
Offset が加算されるのは Additive モードで複数パネルを重ねた場合のみで、その際は +1, +2… と正しく機能する設計になっている。

■ 今後の運用・開発ガイド

既存の他スクリプト(セーブ・設定等)の修正は「一切不要」です。
新しい画面(Canvas)を追加する場合:
 1. 通常通り、Canvasに UI_PanelBase を継承したスクリプトをアタッチする。
 2. インスペクター上で、そのCanvasの Sorting Order を 140 や 150(システムUI用の基準値)に設定する。

■ 修正での警告

Assets\Script\System_set\UI_MenuPanel.cs(63,10): warning CS0114: ‘UI_MenuPanel.Awake()’ hides inherited member ‘UI_PanelBase.Awake()’. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.

UI_PanelBase に protected virtual void Awake() を新設したことで、既存の子クラスが持っていた void Awake() との間に隠蔽(hiding)が発生し、CS0114警告が7クラスで出ていた。

施した修正(全7クラス共通)

// 修正前
void Awake()
{
    // 既存処理
}

// 修正後
protected override void Awake()
{
    base.Awake(); // ← 必ず先頭に置く
    // 既存処理はそのまま
}

base.Awake() を先頭に置く理由:
UI_PanelBase.Awake() 内で CacheBaseSortOrder() が走るため、
子クラスの処理より先にベースのソート値キャッシュを確定させる必要がある。

クラス修正備考
UI_SaveLoadPaneloverride + base.Awake()panel がインスペクターで最初からアクティブかどうか確認推奨
Sys_DialogManageroverride + base.Awake()Show() 内で sortingOrder = 200 を直接代入しており、UI_PanelBase のソート計算と二重管理になっている。動作上は問題ないが、将来的にはインスペクターの baseSortOrder を 200 に統一することを検討
UI_AudioSettingsPaneoverride + base.Awake()修正のみで完結。問題なし
Sys_BacklogManageroverride + base.Awake()GameObject.Find(“BacklogLockController”) はDDOLとシーン側のライフサイクル差による意図的な設計(資料参照)。触らない
UI_MenuPaneloverride + base.Awake()Co_ReturnTitle() 内の //Close() コメントアウトは意図的。キャンセル時はメニューが開いたままになる仕様
UI_SystemSettingsPaneloverride + base.Awake()修正のみで完結。問題なし
UI_TextSettingsPaneloverride + base.Awake()コルーチン停止時は必ず null 代入とセットで行うこと

改善★0601 入力ブロック機能追加

変更ファイル一覧

Sys_InputManager.cs:修正 BlockAll/UnblockAll 追加
Sys_InputBlocker.cs:新規 アタッチ式ブロッカー

設計方針

既存の Sys_InputManager(UI用カウンター式ブロック)を拡張する形で実装。
新規クラスは追加せず、影響範囲を最小化した。

ブロックの評価式:IsBlocked = (blockCount > 0) || isFullBlocked
UI用ブロック(カウンター)と演出用ブロック(フラグ)は独立しており、互いに干渉しない。

使い方

演出・チャプター表示オブジェクトに Sys_InputBlocker をアタッチするだけ。

・オブジェクトアクティブ時 → BlockAll() 自動呼び出し
・オブジェクト非アクティブ・破棄時 → UnblockAll() 自動呼び出し

コードから直接制御する場合:

Sys_InputManager.Instance.BlockAll();
// 演出処理
Sys_InputManager.Instance.UnblockAll();

運用上の制約

・Sys_InputBlocker の複数同時アタッチは非推奨(解除が競合する)
・将来複数同時使用が必要になった場合は isFullBlocked をカウンター方式に変更する

チャプター表示のスキップ(左クリックのみ通す)は現状未実装。
仕様が固まった段階で BlockAll にオプション引数を追加する方針。

コメント