1. はじめに
まずは、ゲーム全体の大枠から実装していきます。
本記事では、いきなりキャラクターの移動やステージ生成といった、ゲームのコアとなるロジックには着手しません。
なぜ大枠から実装するのか?
今回採用するのは、全体の下書きを描いてから徐々に細部を書き込んでいく、という進め方です。
絵を描く場合でも、まずラフを描いて全体像を確認し、その後に必要な部分を徐々に描き込んでいく、という進め方があります。
ゲーム開発も同様で、先にタイトル画面・ゲーム本編・結果画面といった 全体の構造 を用意しておくことで、後から追加する個々の処理を「どの状態に属する処理か」を意識しながら実装できるようになります。
逆に、最初からゲームロジックの実装に入ってしまうと
- 処理の置き場が曖昧になる
- 後から画面や状態を追加するたびに構造を崩すことになる
- 結果として作り直しが発生しやすくなる
といった問題が起きやすくなります。
そこで今回は、ゲームの振る舞いを支える大枠だけを先に実装し、具体的なロジックはその中に後から描き込んでいく、という方針を取ります。
2. 3つの状態と遷移
今回のゲームは、大きく分けて次の3つのモードを持ちます。
- Title:タイトル画面
- Game:ゲーム本編
- Result:リザルト画面
これらは画面の違いというだけでなく、ゲームが取りうる状態(モード) として扱います。
まずは、これら3つのモードと、その間の遷移を状態遷移図で表すと、以下のようになります。
起動直後は Title モードから始まり、ユーザー操作やゲームの進行に応じて、次のようにモードが切り替わります。
-
Title → Game
ゲーム開始の操作を受け取ったとき(StartGame) -
Game → Result
ゲーム終了と判定されたとき(GameOver) -
Result → Game
リトライを選択したとき(Retry) -
Result → Title
タイトルに戻る操作を行ったとき(ReturnTitle)
ここで重要なのは、モードの切り替えは、必ず何らかの「シグナル」をきっかけに行われるという点です。
本記事では、これらの遷移を SignalKind という列挙型で表現し、各モードから発行されたシグナルを元に、状態遷移を制御していきます。
次のセクションでは、この状態遷移をどのようなクラス構成で実現するかを見ていきます。
3. 状態遷移を支えるクラス設計
前のセクションで整理した状態遷移(Title / Game / Result)を、コードとして実現するためのクラス構成を整理します。
本記事では、まず全体像を俯瞰できるように、最初にクラス図を提示します。
個々のクラスの実装や細部は、この後のセクションで順番に説明していきます。
クラス図の読み方(ポイント)
-
Managerクラスが全体の司令塔となり、現在のモード(Mode)を保持しつつ、モードの切り替えを制御します。 -
TitleManager/GameManager/ResultManagerは、それぞれのモード内の処理だけを担当します(UI表示や入力など)。 - モード遷移のきっかけは
SignalKindとして表現し、各モードから「シグナル」をManagerに通知します。
Managerは受け取ったSignalKindを元に、次のモードへ遷移します。
※本図に登場するクラスはすべて MonoBehaviour を継承していますが、視認性を優先して継承関係は省略しています。
Unity上ではこのように Managers という GameObject にアタッチしています。
次のセクションでは、まず状態遷移の中心となる Manager クラスから見ていきます。
4. 状態遷移を一元管理する Manager クラス
このセクションでは、状態遷移の中心となる Manager クラスを掘り下げます。
Manager の役割は大きく2つです。
- 現在のモード(Title / Game / Result)を保持する
- シグナル(
SignalKind)を受け取り、適切な次モードへ切り替える
実装としては、次の2つのメソッドが肝になります。
-
SwitchTo():モード切り替えの共通処理(OnExit / OnEnter)をまとめる -
OnSignal():現在モードとシグナルから、次のモードを決定する
4.1 Mode と各 Manager の参照
まず Manager は内部 enum として Mode を持ち、各モード用の Manager を参照します。
// ゲームが取りうる状態
private enum Mode
{
None = -1,
Title,
Game,
Result
}
// 各モードManagerインスタンス
[SerializeField] private TitleManager _title;
[SerializeField] private GameManager _game;
[SerializeField] private ResultManager _result;
// 現在のモードを格納
private Mode _mode = Mode.None;
4.2 SwitchTo:OnExit / OnEnter を必ず通す
SwitchTo() では「同じモードへの遷移は無視する」「現在モードの OnExit を呼ぶ」「次モードの OnEnter を呼ぶ」を一箇所に集約しています。
private void SwitchTo(Mode next)
{
Debug.Log($"Manager.SwitchTo next:[{next}] mode:[{this._mode}]");
// 現在のモードと変わりなければスキップ
if(this._mode == next)
{
return;
}
// 現在の状態がNoneならば終了処理のコールをスキップ
if(this._mode != Mode.None)
{
// 現モードの終了処理をコール
switch (this._mode)
{
case Mode.Title:
this._title.OnExit();
break;
case Mode.Game:
this._game.OnExit();
break;
case Mode.Result:
this._result.OnExit();
break;
}
}
// 現モードの変更
this._mode = next;
// 現モードの開始処理をコール
switch (_mode)
{
case Mode.Title:
_title.OnEnter();
break;
case Mode.Game:
_game.OnEnter();
break;
case Mode.Result:
_result.OnEnter();
break;
}
}
4.3 OnSignal:状態×シグナルで遷移を決める
各モードから通知された SignalKind は OnSignal() に集約されます。
現在のモードとシグナルの組み合わせによって、次に遷移するモードを決定します。
private void OnSignal(SignalKind signalKind)
{
switch (this._mode)
{
case Mode.Title:
if (signalKind == SignalKind.StartGame)
{
SwitchTo(Mode.Game);
}
break;
case Mode.Game:
if (signalKind == SignalKind.GameOver)
{
SwitchTo(Mode.Result);
}
break;
case Mode.Result:
if (signalKind == SignalKind.Retry)
{
SwitchTo(Mode.Game);
}
if (signalKind == SignalKind.ReturnTitle)
{
SwitchTo(Mode.Title);
}
break;
}
}
5. 各モード専用の Manager クラス(Title / Game / Result)
このセクションでは、各モード内の処理を担当する TitleManager / GameManager / ResultManager を見ていきます。
先に結論を書くと、3つのクラスは同じ骨格を持っています。
- モードの表示・非表示を
CanvasGroupで制御する - モード遷移のきっかけは
SignalKindとしてManagerに通知する -
OnEnter / OnExitで開始・終了処理をまとめる
そのため、ここでは共通部分を深追いせず、各クラスの「差分」に絞って紹介します。
5.1 共通で持つ骨格
3つのクラスはいずれも Init() で Action<SignalKind> を受け取り、必要なタイミングで _raiseSignal(SignalKind.XXX) を呼び出します。
また OnEnter() と OnExit() では CanvasGroup を使って表示と入力を切り替えています。
[SerializeField] private CanvasGroup _canvas;
public void OnEnter() {
Debug.Log("TitleManager.OnEnter");
this.SetVisible(true);
}
public void OnExit()
{
Debug.Log("TitleManager.OnExit");
this.SetVisible(false);
}
private void SetVisible(bool on)
{
if (!_canvas) return;
_canvas.alpha = on ? 1f : 0f;
_canvas.interactable = on;
_canvas.blocksRaycasts = on;
}
5.2 TitleManager:開始操作を検知して StartGame を通知する
TitleManager はタイトル画面での入力を監視し、タップを検知したら StartGame を通知します。
private Action<SignalKind> _raiseSignal;
private InputAction _clickAction;
public void Tick(float deltaTime)
{
// Tapを検知したらゲーム開始のシグナルを発行する
if (this._clickAction.IsPressed())
{
Debug.Log("TitleManager.Tick ClickAction");
this._raiseSignal.Invoke(SignalKind.StartGame);
}
}
5.3 GameManager:仮の GameOver をボタンで発火する
現時点ではゲームのコアロジックがまだ無いため、GameOver はボタン押下で代用しています。
public void OnClickGameOverButton()
{
this._raiseSignal(SignalKind.GameOver);
}
5.4 ResultManager:Retry / ReturnTitle を通知する
ResultManager は結果画面での選択肢(Retry / Title)を受け取り、それぞれ対応するシグナルを通知します。
public void OnClickTitle()
{
this._raiseSignal(SignalKind.ReturnTitle);
}
public void OnClickRetry()
{
this._raiseSignal(SignalKind.Retry);
}
ここまでで、各モードは「自分のモード内の処理」と「シグナル通知」だけを担当し、状態遷移そのものは Manager に集約できました。
次のセクションでは、各モードからの通知をどのように受け渡すか(コールバックと SignalKind)を整理します。
6. SignalKind を使った疎結合な状態遷移
ここまでの実装では、各モードからの状態遷移のきっかけを SignalKind という列挙型で表現しています。
まずは、その定義を示します。
public enum SignalKind
{
StartGame,
GameOver,
Retry,
ReturnTitle
}
この列挙型は、「どのモードから、どのような理由で遷移したいか」を表すためのものです。
各モードの Manager クラスは、直接ほかのモードや Manager クラスを呼び出すことはせず、単に対応する SignalKind をコールバック経由で通知します。
一方で、実際にどのモードへ遷移するかの判断は、すべて Manager クラスに集約されています。
このようにすることで、
- 各モードは「自分の役割」と「通知」だけを知っていればよい
- 状態遷移の条件が 1 箇所にまとまる
- 後からモードや遷移が増えても、影響範囲を限定しやすい
といった構造になります。
今回はシンプルな例のため SignalKind は 4 種類ですが、状態遷移の全体像を enum で表現できている点が重要です。
7. CanvasGroup を使ったモードごとのUI制御
各モードの UI 表示・非表示は、CanvasGroup を使って制御しています。
現時点では、各画面に配置されている UI はボタンのみですが、モードの切り替えに応じて「表示されるかどうか」「入力を受け付けるか」をまとめて制御できるようにしています。
具体的には、OnEnter() で表示を有効にし、OnExit() で非表示かつ入力不可にする、というシンプルな方針です。
CanvasGroup を使うことで
- 表示・非表示
- 入力の有効・無効
を一箇所でまとめて切り替えることができます。
この方法であれば、後から UI 要素が増えた場合でも、モードごとの表示制御を大きく変更する必要がありません。
今回は演出やアニメーションは行っていませんが、フェードイン・フェードアウトなども CanvasGroup を使えば自然に拡張できる構成になっています。
8. 今回やっていないことと、次回以降の予定
ここまでで、タイトル・ゲーム本編・リザルトという3つのモードと、それらを切り替えるための基本的な構造を用意しました。
一方で、本記事ではあえて実装していない要素も多くあります。
- キャラクターの移動や当たり判定
- ステージ生成やスコア計算
- ゲームオーバー判定の実装
- UI 演出やアニメーション
これらはすべて、今回用意した「モード遷移の枠組み」の中に後から組み込んでいくことを前提としています。
また、現時点では TitleManager / GameManager / ResultManager は、共通の振る舞いを持ちながらも、明示的なインターフェイスを定義していません。
今後モードが増えたり、共通処理が増えてきた段階では、これらを統合するインターフェイスを定義することで設計をより整理できる余地があります。
今回はまず構造をシンプルに保つことを優先し、必要になったタイミングで抽象化を進めていく方針としました。
次回以降は、この枠組みの中にゲーム本編のロジックを追加し、無限ランナーとして実際に「床を敷設する」「走り続ける」処理を実装していく予定です。


