Unity初心者が2Dタワーディフェンスを作っています。
今回はゲームの状態管理機能を実装します。現在のゲームの状態が、タイトル画面なのかプレイ中なのかポーズ中なのかなどを管理することで、コードの見通しがよくなり保守しやすくなります。
タイトル画面の作成
まず、ゲームのタイトル画面を作成します。というのは、状態管理で「タイトル→準備中→プレイ中」みたいな流れを確認するのにタイトル画面が必要だからですね。
ヒエラルキーで右クリック「UI > Panel」を選択してパネルを作成します。

Canvasの下にパネルオブジェクトができるので、名前を「TitlePanel」にします。
TitlePanelのインスペクターで設定をします。
Rect Transform はそのままでOKです。パネルがゲーム画面全体を覆う状態ですね。
「Image > Source Image」を「None」に変更します。これでパネルの四隅がほんのり丸くなっている問題を解消できます。「Color」をお好みの色に変更します。

TitlePanelの配下に各種オブジェクトを配置して、タイトル画面を作ります。

ゲームタイトルのテキストとステージ選択ボタンなどを配置しました。
ステージ選択機能の実装
とりあえず、ステージ1の選択ボタンを押したらステージ1が開始するようにします。ちゃんとしたステージ選択機能はあとでやります。
GameManagerスクリプトを修正します。
using UnityEngine;
using UnityEngine.EventSystems;
public class GameManager : MonoBehaviour
{
[SerializeField] private SideBarManager sideBarManager;
[SerializeField] private GameObject tileHighlighter; // ハイライト用オブジェクト
public GameObject TileHighlighter => tileHighlighter; // tileHighlighterにアクセスするためのプロパティ
private Vector2 tileSize = new Vector2(1, 1); // マス目のサイズ(必要に応じて変更)
private int currentStageId = 1; // 現在のステージID
public int CurrentStageId => currentStageId;
// ここから 追加
[SerializeField] private GameObject titlePanel; // タイトルパネル
// ここまで
void Update()
{
//...省略...
}
// ここから 追加
/// <summary>
/// ステージ選択ボタンが押されたときに呼び出されるメソッド
/// </summary>
/// <param name="stageId">選択されたステージID</param>
public void SelectStage(int stageId)
{
currentStageId = stageId; // ステージIDを設定
// タイトルパネルを非表示にする
if (titlePanel != null)
{
titlePanel.SetActive(false);
}
else
{
Debug.LogWarning("titlePanel が設定されていません。");
}
Debug.Log($"ステージ {stageId} が選択されました。");
}
// ここまで
/// <summary>
/// マウスのクリックを検出
/// </summary>
private void DetectClick()
{
// ...省略...
}
}
titlePanel
変数を定義しました。SelectStage()
メソッドを追加しました。currentStageId
に選択されたステージ番号を設定し、タイトルパネルを非表示にします。
GameManagerオブジェクトのインスペクターで「Game Manager (Script) > Title Panel」にTitlePanelオブジェクトをドラッグ&ドロップしてアサインします。

ステージ選択ボタン(Stage1Button〜Stage5Button)の「On Click ()」にGameManager.SelectStage()
メソッドを設定します。

- 「On Click ()」の「+」をクリックする。
- 「None (Object)」にGamemanagerオブジェクトをドラッグ&ドロップする。
- 「No Function」をクリックして「GameManager > SelectStage (int)」を選択する。
- 引数を設定する。Stage1Buttonは「1」、Stage2Buttonは「2」(以下省略)。
ゲームの状態管理
今回のメインディッシュ、ゲームの状態管理を実装します。ゲームの状態をあらわす定数は以下の6種類にしました。
- Title (タイトル画面)
- Preparing (準備中)
- Playing (プレイ中)
- Paused (ポーズ中)
- GameOver (ゲームオーバー)
- Victory (ステージクリア)
フローチャートで表現するとこんな感じです。

コードを書いていきましょう。
GameManagerスクリプトを修正します。
using UnityEngine;
using UnityEngine.EventSystems;
// ここから 追加
using UnityEngine.UI; // Buttonクラスを使用するために必要
// ここまで
public class GameManager : MonoBehaviour
{
// ここから 追加
public enum GameState
{
Title, // タイトル画面
Preparing, // 準備中
Playing, // プレイ中
Paused, // 一時停止
GameOver, // ゲームオーバー
Victory // 勝利
}
// ここまで
[SerializeField] private SideBarManager sideBarManager;
[SerializeField] private GameObject tileHighlighter; // ハイライト用オブジェクト
public GameObject TileHighlighter => tileHighlighter; // tileHighlighterにアクセスするためのプロパティ
private Vector2 tileSize = new Vector2(1, 1); // マス目のサイズ(必要に応じて変更)
private int currentStageId = 1; // 現在のステージID
public int CurrentStageId => currentStageId;
[SerializeField] private GameObject titlePanel; // タイトルパネル
// ここから 追加
public GameState CurrentState = GameState.Title; // 現在のゲーム状態
[SerializeField] private Button nextWaveButton; // 次のウェーブボタン
[SerializeField] private Button startPauseButton; // スタート・ポーズボタン
// ここまで
// ここから 追加
private void Start()
{
// ボタンのクリックイベントを登録
startPauseButton.onClick.AddListener(OnStartPauseButtonClicked);
}
// ここまで
void Update()
{
// ここから 追加
if (CurrentState == GameState.Playing && FortressController.Instance.CurrentHp <= 0)
{
ChangeState(GameState.GameOver); // 城のHPが0になったらゲームオーバー
}
// ここまで
if (Input.GetMouseButtonDown(0)) // マウスの左ボタンがクリックされたら
{
DetectClick();
}
}
// ここから 追加
/// <summary>
/// ゲーム状態を変更する
/// </summary>
/// <param name="newState">新しいゲーム状態</param>
public void ChangeState(GameState newState)
{
CurrentState = newState;
switch (newState)
{
case GameState.Title:
Debug.Log("ゲーム状態: Title");
titlePanel.SetActive(true); // タイトルパネルを表示
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
case GameState.Preparing:
Debug.Log("ゲーム状態: Preparing");
titlePanel.SetActive(false); // タイトルパネルを非表示
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
case GameState.Playing:
Debug.Log("ゲーム状態: Playing");
nextWaveButton.interactable = true; // 次のウェーブボタンを活性化
break;
case GameState.Paused:
Debug.Log("ゲーム状態: Paused");
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
case GameState.GameOver:
Debug.Log("ゲーム状態: GameOver");
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
case GameState.Victory:
Debug.Log("ゲーム状態: Victory");
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
}
}
/// <summary>
/// スタート・ポーズボタンが押されたときの処理
/// </summary>
private void OnStartPauseButtonClicked()
{
if (CurrentState == GameState.Preparing)
{
ChangeState(GameState.Playing); // 準備中 -> プレイ中
}
else if (CurrentState == GameState.Playing)
{
ChangeState(GameState.Paused); // プレイ中 -> ポーズ
}
}
/// <summary>
/// 再開ボタンが押されたときの処理
/// </summary>
private void OnRestartButtonClicked()
{
if (CurrentState == GameState.Paused)
{
ChangeState(GameState.Playing); // ポーズ -> プレイ中
}
}
/// <summary>
/// 最終ウェーブをクリアしたときの処理
/// </summary>
public void OnFinalWaveCleared()
{
if (CurrentState == GameState.Playing)
{
ChangeState(GameState.Victory); // プレイ中 -> 勝利
}
}
// ここまで
/// <summary>
/// ステージ選択ボタンが押されたときに呼び出されるメソッド
/// </summary>
/// <param name="stageId">選択されたステージID</param>
public void SelectStage(int stageId)
{
currentStageId = stageId; // ステージIDを設定
// ここから 削除
// タイトルパネルを非表示にする
//if (titlePanel != null)
//{
// titlePanel.SetActive(false);
//}
//else
//{
// Debug.LogWarning("titlePanel が設定されていません。");
//}
// ここまで
Debug.Log($"ステージ {stageId} が選択されました。");
// ここから 追加
// ゲーム状態を Title から Preparing に変更
if (CurrentState == GameState.Title)
{
ChangeState(GameState.Preparing);
}
// ここまで
}
/// <summary>
/// マウスのクリックを検出
/// </summary>
private void DetectClick()
{
// ...省略...
}
}
- ゲームの状態をあらわす
GameSate
を定義しました。列挙型(enum)を使っています。 - 以下の3つの変数を定義しました。
CurrentState
: 現在のゲーム状態。初期値はTitle。nextWaveButton
: 次のウェーブボタン。startPauseButton
: スタート・ポーズボタン。
Start()
メソッドを追加して、スタート・ポーズボタンのクリックイベントを追加しました。Update()
メソッド内に、城のHPが0になったらゲーム状態をGameOverにする処理を追加しました。ChangeState()
メソッドを追加しました。ゲーム状態を変更し、パネルの表示・非表示、ボタンの活性・非活性を切り替えます。OnStartPauseButtonClicked()
メソッドを追加しました。スタート・ポーズボタンが押されたときにゲーム状態をPlayingまたはPausedに切り替えます。OnRestartButtonClicked()
メソッドを追加しました。再開ボタンが押されたときにゲーム状態をPlayingに切り替えます。OnFinalWaveCleared()
メソッドを追加しました。最終ウェーブをクリアしたときにゲーム状態をVictoryに切り替えます。- SelectStage()メソッドを修正しました。
- タイトルパネルを非表示にする処理は
ChangeState()
メソッドに任せることにしたので削除します。 - ゲーム状態をPreparingに切り替える処理を追加しました。
- タイトルパネルを非表示にする処理は
GameManagerのインスペクターで「Game Manager (Script) > NetWaveButton」にNextWaveButtonオブジェクトをドラッグ&ドロップしてアサインします。
「Game Manager (Script) > StartPauseButton」にはStartPauseButtonオブジェクトをアサインします。

スタート・ポーズ機能の実装
状態がPlayingのとき以外はゲーム時間を止める
ゲーム状態がPlayingのとき以外はゲーム内の時間を止めたい。というわけで、GameManagerスクリプトを修正します。
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI; // Buttonクラスを使用するために必要
public class GameManager : MonoBehaviour
{
// ...省略...
private void Start()
{
// ボタンのクリックイベントを登録
startPauseButton.onClick.AddListener(OnStartPauseButtonClicked);
// ここから 追加
// ゲーム開始時に GameState が Title の場合、時間を停止
if (CurrentState == GameState.Title)
{
Time.timeScale = 0; // 時間を停止
}
// ここまで
}
void Update()
{
// ...省略...
}
/// <summary>
/// ゲーム状態を変更する
/// </summary>
/// <param name="newState">新しいゲーム状態</param>
public void ChangeState(GameState newState)
{
CurrentState = newState;
switch (newState)
{
case GameState.Title:
Debug.Log("ゲーム状態: Title");
// ここから 追加
Time.timeScale = 0; // 時間を停止
// ここまで
titlePanel.SetActive(true); // タイトルパネルを表示
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
case GameState.Preparing:
Debug.Log("ゲーム状態: Preparing");
// ここから 追加
Time.timeScale = 0; // 時間を停止
// ここまで
titlePanel.SetActive(false); // タイトルパネルを非表示
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
case GameState.Playing:
Debug.Log("ゲーム状態: Playing");
// ここから 追加
Time.timeScale = 1; // 時間を通常速度に戻す
// ここまで
nextWaveButton.interactable = true; // 次のウェーブボタンを活性化
break;
case GameState.Paused:
Debug.Log("ゲーム状態: Paused");
// ここから 追加
Time.timeScale = 0; // 時間を停止
// ここまで
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
case GameState.GameOver:
Debug.Log("ゲーム状態: GameOver");
// ここから 追加
Time.timeScale = 0; // 時間を停止
// ここまで
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
case GameState.Victory:
Debug.Log("ゲーム状態: Victory");
// ここから 追加
Time.timeScale = 0; // 時間を停止
// ここまで
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
}
}
// ...省略...
}
Time.timeScale = 0
でゲーム内時間は停止し、Time.timeScale = 1
で通常の速度になります。
ポーズ画面の作成
ゲームがポーズ中のときに表示するパネルを作成します。
ヒエラルキーで右クリック「UI > Panel」を選択してパネルを作成します。
名前を「PausePanel」に変更します。
PausePanelのインスペクターで設定をします。
Rect Transform はそのままでOKです。パネルがゲーム画面全体を覆う状態です。ポーズ中はゲーム画面のボタン類を押せないようにしたいので、このようにしました。
「Image > Source Image」を「None」に変更し、「Color」をお好みの色に変更します。
PausePanelの配下に各種オブジェクトを配置して、ポーズ画面を作ります。

PausePanelのインスペクターで左上のチェックを外してオブジェクトを非表示にしておきます。
ポーズ画面の表示・非表示
ポーズ中にはポーズ画面を表示し、それ以外の状態のときは表示しないようにコードを修正します。スタート・ポーズボタンのアイコンとテキストの切り替えもします。
GameManagerスクリプトを修正します。
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI; // Buttonクラスを使用するために必要
// ここから 追加
using TMPro; // TextMeshProを使うために必要
// ここまで
public class GameManager : MonoBehaviour
{
// ...省略...
// ここから 追加
[SerializeField] private Image startPauseButtonIcon; // スタート・ポーズボタンアイコン
[SerializeField] private TextMeshProUGUI startPauseButtonText; // スタート・ポーズボタンテキスト
[SerializeField] private GameObject pausePanel; // ポーズパネル
[SerializeField] private Button restartButton; // 再開ボタン
[SerializeField] private Sprite playIcon; // スタートボタン用のアイコン
[SerializeField] private Sprite pauseIcon; // ポーズボタン用のアイコン
// ここまで
private void Start()
{
// ボタンのクリックイベントを登録
startPauseButton.onClick.AddListener(OnStartPauseButtonClicked);
// ここから 追加
restartButton.onClick.AddListener(OnRestartButtonClicked);
// ここまで
// ゲーム開始時に GameState が Title の場合、時間を停止
if (CurrentState == GameState.Title)
{
Time.timeScale = 0; // 時間を停止
}
}
void Update()
{
// ...省略...
}
/// <summary>
/// ゲーム状態を変更する
/// </summary>
/// <param name="newState">新しいゲーム状態</param>
public void ChangeState(GameState newState)
{
CurrentState = newState;
switch (newState)
{
case GameState.Title:
Debug.Log("ゲーム状態: Title");
Time.timeScale = 0; // 時間を停止
titlePanel.SetActive(true); // タイトルパネルを表示
// ここから 追加
pausePanel.SetActive(false); // ポーズパネルを非表示
// ここまで
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
case GameState.Preparing:
Debug.Log("ゲーム状態: Preparing");
Time.timeScale = 0; // 時間を停止
titlePanel.SetActive(false); // タイトルパネルを非表示
// ここから 追加
pausePanel.SetActive(false); // ポーズパネルを非表示
// ボタンのアイコンとテキストを「スタート」に設定
startPauseButtonIcon.sprite = playIcon; // playIcon を設定
startPauseButtonText.text = "スタート";
// ここまで
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
case GameState.Playing:
Debug.Log("ゲーム状態: Playing");
Time.timeScale = 1; // 時間を通常速度に戻す
// ここから 追加
pausePanel.SetActive(false); // ポーズパネルを非表示
// ボタンのアイコンとテキストを「ポーズ」に設定
startPauseButtonIcon.sprite = pauseIcon; // pauseIcon を設定
startPauseButtonText.text = "ポーズ";
// ここまで
nextWaveButton.interactable = true; // 次のウェーブボタンを活性化
break;
case GameState.Paused:
Debug.Log("ゲーム状態: Paused");
Time.timeScale = 0; // 時間を停止
// ここから 追加
pausePanel.SetActive(true); // ポーズパネルを表示
// ボタンのアイコンとテキストを「ポーズ」に設定
startPauseButtonIcon.sprite = pauseIcon; // pauseIcon を設定
startPauseButtonText.text = "ポーズ";
// ここまで
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
case GameState.GameOver:
Debug.Log("ゲーム状態: GameOver");
Time.timeScale = 0; // 時間を停止
// ここから 追加
pausePanel.SetActive(false); // ポーズパネルを非表示
// ここまで
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
case GameState.Victory:
Debug.Log("ゲーム状態: Victory");
Time.timeScale = 0; // 時間を停止
// ここから 追加
pausePanel.SetActive(false); // ポーズパネルを非表示
// ここまで
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
}
}
// ...省略...
}
- 以下の6つの変数を定義しました。
startPauseButtonIcon
: スタート・ポーズボタンのアイコン。startPauseButtonText
: スタート・ポーズボタンのテキスト。pausePanel
: ポーズパネル。restartButton
: 再開ボタン。playIcon
: スタートボタン用のアイコン画像。pauseIcon
: ポーズボタン用のアイコン画像。
Start()
メソッド内に、再開ボタンのクリックイベントを追加しました。ChangeState()
メソッド内に、ゲーム状態に応じてポーズパネルの表示非表示、スタート・ポーズボタンのアイコンとテキストを切り替える処理を追加しました。
GameManagerのインスペクターで、先ほど追加した6つの変数にオブジェクトをドラッグ&ドロップしてアサインします。

ポーズ画面を最前面に表示する
ポーズ画面はあらゆるオブジェクトを覆うように最前面に表示させたいのですが、今のところそうなっていません。敵のHPバーやフローティングテキストはポーズ画面よりも前に表示されてしまいます。
これは、これらのオブジェクトがプレハブから生成されるためです。プレハブから生成されるオブジェクトはヒエラルキーの下に追加されるので、前面に表示されてしまいます。
この問題を解消するためにGameManagerスクリプトを修正します。
/// <summary>
/// ゲーム状態を変更する
/// </summary>
/// <param name="newState">新しいゲーム状態</param>
public void ChangeState(GameState newState)
{
CurrentState = newState;
switch (newState)
{
// ...省略...
case GameState.Paused:
Debug.Log("ゲーム状態: Paused");
Time.timeScale = 0; // 時間を停止
pausePanel.SetActive(true); // ポーズパネルを表示
// ボタンのアイコンとテキストを「ポーズ」に設定
startPauseButtonIcon.sprite = pauseIcon; // pauseIcon を設定
startPauseButtonText.text = "ポーズ";
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
// ここから 追加
// 最前面に移動
pausePanel.transform.SetAsLastSibling();
// ここまで
break;
// ...省略...
}
}
SetAsLastSibling()
は、そのオブジェクトをヒエラルキーの兄弟オブジェクトの中で一番最後に移動させるメソッドです。つまり、最前面に表示されるということです。
時間を止めているときもアニメーションは動かす
ゲーム状態がPreparing(準備中)のときに、砲台を設置してもゴールドが減らなかったり、フローティングテキストのアニメーションが動かないという問題が発生しました。これは、DOTweenのデフォルト設定ではTime.timeScale
の影響を受け、アニメーションが停止するためです。
DOTweenのアニメーションがTime.timeScale
の影響を受けないようにします。
FloatingTextManagerスクリプトを修正します。
/// <summary>
/// フローティングテキストを表示する
/// </summary>
/// <param name="text">表示するテキスト</param>
/// <param name="color">テキストの色</param>
/// <param name="position">ワールド座標での表示位置</param>
/// <param name="offset">表示位置のオフセット</param>
public void ShowFloatingText(string text, Color color, Vector3 position, Vector3 offset)
{
// ...省略...
// テキストの設定
TextMeshProUGUI textMesh = floatingText.GetComponent<TextMeshProUGUI>();
if (textMesh != null)
{
// ...省略...
// DOTween を使用してアニメーションを設定
rectTransform.DOMoveY(rectTransform.position.y + 50f, 1.5f) // 上方向に移動
.SetEase(Ease.OutQuad)
.SetUpdate(true); // 追加 Unscaled Time を使用
textMesh.DOFade(0f, 1.5f) // フェードアウト
.SetEase(Ease.InQuad)
.SetUpdate(true); // 追加 Unscaled Time を使用
}
// ...省略...
}
DOTweenのメソッドチェーンに.SetUpdate(true)
を追加すると、Time.timeScale
に依存せずにアニメーションが動作します。
GoldManagerスクリプトを修正します。
/// <summary>
/// ゴールドを増減する
/// </summary>
/// <param name="amount">増減するゴールド量(正の数で増加、負の数で減少)</param>
/// <returns>ゴールドが足りて処理できた場合は true、足りない場合は false</returns>
private bool ChangeGold(int amount)
{
// ...省略...
// DOTween を使用してカウントアップ・カウントダウンアニメーション
DOTween.To(() => startValue, x =>
{
startValue = x;
if (goldText != null)
{
goldText.text = startValue.ToString();
}
}, endValue, 0.5f) // 0.5秒でアニメーション
.SetEase(Ease.OutQuad) // スムーズなアニメーション
.SetUpdate(true) // 追加 Unscaled Time を使用
.OnComplete(() =>
{
// アニメーション完了後に実際のゴールド値を更新
SetGold(endValue);
});
// ...省略...
}
ScoreManagerスクリプトを修正します。
/// <summary>
/// スコアを加算する
/// </summary>
/// <param name="amount">加算するスコア量</param>
public void AddScore(int amount)
{
// ...省略...
// DOTween を使用してカウントアップアニメーション
DOTween.To(() => startValue, x =>
{
startValue = x;
if (scoreText != null)
{
scoreText.text = startValue.ToString();
}
}, endValue, 0.5f) // 0.5秒でカウントアップ
.SetEase(Ease.OutQuad) // スムーズなアニメーション
.SetUpdate(true) // 追加 Unscaled Time を使用
.OnComplete(() =>
{
// アニメーション完了後に実際のスコア値を更新
currentScore = endValue;
});
}
以上で、Time.timeScale = 0
のときにもDOTweenのアニメーションが動くようになりました。
さいごに
ゲームの状態管理を実装したことで、今後の開発がやりやすくなったのではないかと思います。いえい。
でわでわ
コメント