本記事は サムザップ Advent Calendar 2025 の14日目の記事です。
はじめに
はじめまして!
サムザップでゲームクライアントエンジニアをしているの@mittan1330です。
直近、個人で請け負っているプロジェクトにてUnityScreenNavigatorを用いた、アウトゲーム(ゲームの遊びにかかわらない表示物など)開発基盤の整備を行う機会がありました。
今回は、そのUI遷移やModal(ダイアログ)の実装を簡略化しより安全な実装にするため、型安全なParam設計とModal自動生成ツールを導入した経験をまとめていきます。
※ 概念と実装例を詳しく見せるために、ある程度説明を割愛しておりますので、ご容赦頂けますと幸いです。
目次
- 基盤実装の経緯(課題と開発上の狙い)
- なぜParamを使うのか?設計意図とその動機
- 型安全Paramによるデータ受け渡し手法
- Modalの基底クラスと画面遷移管理の実例
- Paramを使ったModal表示の実例
- UnityScreenNavigatorのModal生成ライフサイクルとParam設計の仕組み
- この実装方式におけるメリットとデメリット
- Tips
1. 基盤実装の経緯(課題と開発上の狙い)
1.1 基盤開発の重要性について
Unityを用いたゲーム開発ではUI表示物が増えると、
画面遷移・データ受け渡しの実装負担や複雑さが増大します。
特に、基本的なUnityの機能を用いて実装すると、設計無視した実装が可能になり、複雑になっていくというのはあるあるだと思います。
実際に途中参画したあるプロジェクトでは、1つの神クラスで様々なUIの表示だし分けの機能が実装されていました。
(例:xxxDialog.gameObject.SetActive(true);を色々な条件で出し分けているなど)
そういった課題を解決するためにも、アウトゲーム基盤を整備することはどのようなプロジェクトであっても大切だと言えます。
1.2 選定した基盤
今回利用するアウトゲーム基盤としては、無料で公開されていて、尚且つ利便性の高いと感じたUnityScreenNavigatorを導入しました。
1.3 基盤を使う上で意識したいこと
今回のアウトゲーム基盤を整備するに当たって、「画面・Modalの追加」「遷移設計」「必要データの受け渡し」
などの運用が、より安全かつ効率的にしたいと考えていました。
そこで、今回は利用する基盤(UnityScreenNavigator)へ
「Param(パラメータ)」の設計にこだわった独自の改修を入れて以下を実現していきたいと思います。
- 型安全な情報受け渡し
- 拡張・保守がしやすい構造
- コード量・バグリスクの大幅削減
2. なぜParamを使うのか?設計意図とその動機
UnityScreenNavigatorを導入したばかりの頃、
画面やModal間のデータ受け渡しにSingletonやstaticクラスを用いていました。
Singletonやstaticクラスを用いた実装例
- ModalAで選択した内容を一時保存(Singleton/staticクラス等)
- ModalBを起動
- ModalB内で保存値を参照して画面表示
この方法だと、以下のようなという課題を感じていました。
- 保存・取得のバグが発生しやすい
- どこから来た値か分かりづらい
- 複数箇所で値を扱うと保守が困難になる
3. 型安全Paramによるデータ受け渡し手法
そこで、起動時に必要な値をParamクラスとして直接渡す方式に切り替えました。
不要な保存処理や参照ミスを防げ、シンプル・型安全・拡張性が高いUI設計になります。
パラメータ設計を使ったデータ受け渡し実装例
- ModalAで選択
- ModalB起動時に、選択値をParamで渡す
4. Modalの基底クラスと画面遷移管理の実例
4.1 Modalの基底クラス実装例と構造
Modalダイアログのパラメータ管理・受け渡しを安全かつ統一的に行うため、
ModalBase<TParam> という抽象基底クラスを作成しています。
■ ModalBase(サンプルコード)
using UnityScreenNavigator.Runtime.Core.Modal;
/// <summary>
/// Modalの基底クラス
/// ダイアログなどを表示する層
/// </summary>
public abstract class ModalBase<TParam> : Modal
where TParam : ModalBase<TParam>.ModalParam
{
/// <summary>
/// 遷移パラメータを渡す際に利用するクラス
/// NOTE: 利用元で継承して実装する
/// </summary>
public class ModalParam
{
}
/// <summary> 遷移パラメータ </summary>
protected TParam _param;
/// <summary>
/// 遷移パラメータをModalへ流し込む対応
/// </summary>
/// <param name="param">遷移パラメータ</param>
public virtual void SetParam(TParam param)
{
_param = param;
}
}
このパターンを使うことで、どんなModalにも「適切な型のパラメータだけ」を
安全・簡単に渡すことができます。
4.2 Modal管理の仕組み(ScreenNavigationManager)
Modal表示・非表示などの管理は、
ScreenNavigationManager というマネージャーを基盤にしています。
■ ScreenNavigationManager(Modalのみの実装抜粋)
using Cysharp.Threading.Tasks;
using ScreenNavigation.Base;
using UnityEngine;
using UnityScreenNavigator.Runtime.Core.Modal;
/// <summary>
/// 画面遷移を管理するマネージャークラス(ModalのみのSample実装版)
/// UnityScreenNavigatorをラップして、各レイヤーの管理を行う
/// </summary>
public class ScreenNavigationManager : MonoBehaviour
{
private static ScreenNavigationManager _instance;
/// <summary> モーダル </summary>
[SerializeField] private ModalContainer _modalContainer;
/*
* < ここに「ScreenNavigationManager」をシングルトンインスタンスにするための実装を記述 >
*/
// モーダルの制御
/// <summary>
/// Modalを表示する
/// </summary>
public async UniTask ShowModalAsync<TModal, TParam>(string id, TParam param, bool isAnimation = true)
where TModal : ModalBase<TParam>
where TParam : ModalBase<TParam>.ModalParam
{
await _modalContainer.Push(id, isAnimation,
onLoad: data => { ((TModal)data.modal).SetParam(param); });
}
/// <summary>
/// Modalを閉じる
/// </summary>
public async UniTask PopModalAsync(bool isAnimation = true, int popCount = 1)
=> await _modalContainer.Pop(isAnimation, popCount);
/// <summary>
/// デフォルトモーダルを全て閉じる
/// </summary>
public async UniTask PopAllModal(bool isAnimation = true)
=> await _modalContainer.Pop(isAnimation, _modalContainer.Modals.Count);
}
この仕組みを利用すれば、「表示したいModal」「渡すパラメータ」の型や個数が常に明確になり、バグ防止や新規追加にも強い構造で実装できます。
5. Paramを使ったModal表示の実例
5.1 スコアダイアログの表示パターン
スコアを表示するダイアログ(ScoreModal)を例に、
どのように「表示したい値」を安全に渡しているかを説明します。
ScreenNavigationManager.Instance.ShowModalAsync<Score, Score.Param>(
Modals.Name.Score.Path(), // EnumからPrefabパスを取得
new Score.Param {
Score = 10000 // 表示するスコア値
}
).Forget();
5.2 異なるパラメータ実装も柔軟
たとえば「メッセージ+選択肢」など複雑なダイアログも、
Paramクラスで柔軟に拡張できます。
public class Confirm : ModalBase<Confirm.Param>
{
public class Param : ModalParam
{
public string Message;
public Action OnYes;
public Action OnNo;
}
public override void SetParam(Param param)
{
_param = param;
}
}
5.3 この時のScoreダイアログ定義
public class Score : ModalBase<Score.Param>
{
[SerializeField] private TextMeshProUGUI _scoreText;
private int _score = 0;
public class Param : ModalParam
{
public int Score; // 表示したい情報だけを持つ
}
public override void SetParam(Param param)
{
_score = param.Score;
}
public override async Task WillPushEnter()
{
// _scoreにセットされた値をテキストラベルへ表示
_scoreText.text = _score.ToString();
}
}
5.4 Enum・パス管理例
public static class Modals
{
public enum Name
{
Score,
Dice,
// ...他ダイアログ名
}
public static string Path(this Name modalName)
{
switch (modalName)
{
case Name.Score: return "UiPanels/Modals/Score";
case Name.Dice: return "UiPanels/Modals/Dice";
default: return "";
}
}
}
6. UnityScreenNavigatorのModal生成ライフサイクルとParam設計の仕組み
6.1 UnityScreenNavigator Modalのライフサイクルについて
UnityScreenNavigatorのModal(ダイアログ)は、ライフサイクル管理のために以下の流れで処理されます。
- ModalContainer.Push()
- ユーザーの操作やシステムイベントを起点にModalの生成・表示要求が発生
- Modalインスタンス生成
- 指定されたPrefabからModalクラス(例:ScoreModal)が生成
- onLoadコールバック
- Modal生成直後(=ビューも生成された直後)に、コールバックで初期化や面倒なイベント登録、Paramの値渡しを行う
- Modal表示アニメーション
- isAnimation=trueの場合はアニメーション付きで表示開始
- ユーザー操作や状態変化に応じ、Modalの閉じるイベント・他Modalへの遷移イベントなどが発生
6.2 Param設計がどのように動いているか
今回の設計では、「onLoadコールバック」をユニークな設計ポイントとして活用しています。
Pushする際に、表示したいModalと一緒に渡したい値(Param)を直接指定できます。
▼ 仕組み解説
-
ScreenNavigationManager.ShowModalAsync
Modal表示時に、必要な情報(Param)を引数で渡す - ModalContainer.PushのonLoadコールバック
Modal生成直後にonLoadコールバックが実行され、
ここで「.SetParam(param)」を呼んで、受け取りModal側に値をセット - WillPushEnterメソッドにて、Paramで設定された値を用いた表示だし分けを実現
- Modal側では、このパラメータを描画・ロジックなどにそのまま利用
▼ フロー図
▼ 仕組みのメリット
- onLoadが生成直後であるため、初期化・イベントバインド・値セットが一箇所で完結
- Modal本体の実装は「Paramが確実にセットされている前提」で記述できる
- 複雑な条件分岐や後処理も、初期化のタイミングですべて記述できる
▼ 注意点
- onLoadコールバックが渡されない設計だと、値セットのタイミングミスや未設定バグになるので注意
- Paramクラスは必ず「必要な値のみ」にする(大きく複雑にしすぎない)
6.3 ここまでの実装解説をUMLにまとめる
今までに紹介した実装について詳細をUMLへまとめるとこのような設計になっています。
仕組みとしてわかりやすくなったと思います。
- モーダルとパラメータの型安全な関連づけ
- 責務の分離がわかりやすい
- モーダル一覧の集中管理
7. この実装方式におけるメリットとデメリット
メリット
-
データ流れや依存関係が明確
- 必要な値だけParamとして明示でき、「どこから何を渡すのか」が一目瞭然
-
余計な保存/参照パターンが減少
- Singleton/staticクラスに頼る必要がなくなり、状態管理の複雑さを回避
-
型安全性が高い
- Paramクラスで型を強制できるので、ミスやバグ、変換作業を減らせる
-
拡張性・量産性に優れる
- ページやModal追加時もParamを増やすだけで良い。自動生成ツールとも相性抜群
-
onLoadコールバック活用による初期化の明確化
- Modal生成直後に値セット・初期イベント登録などを一発で管理できる
デメリット
-
Param渡し忘れや拡張漏れリスク
- 起動時にParamを渡していないとnullが発生する。呼び出し側の記述・設計ミスに注意
-
Paramが肥大化しやすい
- 1つのParamに多目的な値を詰めすぎると、「本当に必要なもの」が不明になる
-
永続化や全画面共通データには向かない
- Paramは一時的な遷移時データに向いているため、グローバル管理には他方式が必要
改善の余地について
-
SingletonとResource.Load()を用いた実装をしている
- Unityからもアナウンスのある通り本来はSingletonとResource.Load()は非推奨になっています
- 今回は解説のために可読性の高いこちらを利用しましたが、実装においてはAddressableなどを利用することをおすすめします
-
モーダルの戻り値(結果)
- OK/CancelなどModalで選択した入力値を返したいことはよくあるとおもいます
- Paramの受け渡し実装はこれで完了していますが、結果の受け渡しもできればBestです
- (一旦、値を返す対応をしたい場合は、おすすめできませんがModalのParamへActionを設定することで実装が可能です)
-
モーダルの自動生成
- 毎回新しいモーダルを作成する際に、スクリプトとModalPrefabを生成するのは少々面倒です
- なので、Editor拡張などを用いて自動生成ツールを整備するとより利便性が向上します
8. Tips
- Prefab名とクラス名を揃えることで、自動ロード・管理ミスを防止できます
- Paramクラスは「その画面・ダイアログで絶対必要なデータだけ」を定義し、小分けに管理
- Enumやリソース管理もツールで自動化し、人的ミスや工数を削減
- 適材適所でParamと全体管理の設計を使い分けることが大切
まとめ
型安全Param設計と自動生成ツールの導入により、開発における保守・実装的ハードルを大きく下げられました。
UnityScreenNavigatorの仕組みを使いつつ、Modal生成ライフサイクル・onLoadコールバック・Param設計を掛け合わせることで、「狙い通りのデータ流れ&堅牢なアウトゲーム設計」が実現できます。
本記事が皆さんのUnityで開発されている皆さんの参考になれば幸いです!