Unity初心者が2Dタワーディフェンスゲームを制作しています。
今回は、ゲームオーバーとステージクリアを実装していきます。前回、ゲームの状態管理機能を実装したので、それを利用すれば簡単だろうと思っていましたが、意外と面倒でした。
- Mac mini (M1, 2020)
- Unity 2022.3.36f1
ゲームオーバーの実装
城のHPが0になったらゲームオーバー画面を表示します。ゲームオーバー画面には「タイトルへ戻る」ボタンがあって、押すとタイトル画面へ戻ります。
ゲームオーバー画面の作成
PausePanelオブジェクトを複製して新しいオブジェクトを作成し、名前を「GameOverPanel」に修正します。配下のオブジェクト名も修正します。

パネルの色、タイトル文字、ボタンのテキストなどを修正します。こんな感じにしました。

インスペクター左上のチェックを外して、パネルを非表示にしておきます。
ゲームオーバー画面の表示・非表示
城のHPが0になったらゲーム状態(GameState
)をGameOverにする処理はすでに実装済みです。なので、あとはゲーム状態に応じてゲームオーバー画面の表示・非表示をするだけです。
GameManagerスクリプトを修正します。
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI; // Buttonクラスを使用するために必要
using TMPro; // TextMeshProを使うために必要
public class GameManager : MonoBehaviour
{
// ...省略...
// ここから追加
[SerializeField] private GameObject gameOverPanel; // ゲームオーバーパネル
// ここまで
// ...省略...
/// <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); // タイトルパネルを表示
titlePanel.transform.SetAsLastSibling(); // タイトルパネルを最前面に移動
pausePanel.SetActive(false); // ポーズパネルを非表示
// ここから追加
gameOverPanel.SetActive(false); // ゲームオーバーパネルを非表示
// ここまで
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
case GameState.Preparing:
Debug.Log("ゲーム状態: Preparing");
Time.timeScale = 0; // 時間を停止
titlePanel.SetActive(false); // タイトルパネルを非表示
pausePanel.SetActive(false); // ポーズパネルを非表示
// ここから追加
gameOverPanel.SetActive(false); // ゲームオーバーパネルを非表示
// ここまで
// ボタンのアイコンとテキストを「スタート」に設定
startPauseButtonIcon.sprite = playIcon; // playIcon を設定
startPauseButtonText.text = "スタート";
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
case GameState.Playing:
Debug.Log("ゲーム状態: Playing");
Time.timeScale = 1; // 時間を通常速度に戻す
pausePanel.SetActive(false); // ポーズパネルを非表示
// ここから追加
gameOverPanel.SetActive(false); // ゲームオーバーパネルを非表示
// ここまで
// ボタンのアイコンとテキストを「ポーズ」に設定
startPauseButtonIcon.sprite = pauseIcon; // pauseIcon を設定
startPauseButtonText.text = "ポーズ";
nextWaveButton.interactable = true; // 次のウェーブボタンを活性化
break;
case GameState.Paused:
Debug.Log("ゲーム状態: Paused");
Time.timeScale = 0; // 時間を停止
pausePanel.SetActive(true); // ポーズパネルを表示
pausePanel.transform.SetAsLastSibling(); // ポーズパネルを最前面に移動
// ここから追加
gameOverPanel.SetActive(false); // ゲームオーバーパネルを非表示
// ここまで
// ボタンのアイコンとテキストを「ポーズ」に設定
startPauseButtonIcon.sprite = pauseIcon; // pauseIcon を設定
startPauseButtonText.text = "ポーズ";
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
case GameState.GameOver:
Debug.Log("ゲーム状態: GameOver");
Time.timeScale = 0; // 時間を停止
pausePanel.SetActive(false); // ポーズパネルを非表示
// ここから追加
gameOverPanel.SetActive(true); // ゲームオーバーパネルを表示
gameOverPanel.transform.SetAsLastSibling(); // ゲームオーバーパネルを最前面に移動
// ここまで
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
case GameState.Victory:
Debug.Log("ゲーム状態: Victory");
Time.timeScale = 0; // 時間を停止
pausePanel.SetActive(false); // ポーズパネルを非表示
// ここから追加
gameOverPanel.SetActive(false); // ゲームオーバーパネルを非表示
// ここまで
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
break;
}
}
// ...省略...
// ここから追加
/// <summary>
/// タイトルへ戻るボタンが押されたときの処理
/// </summary>
public void OnBackButtonClicked()
{
ChangeState(GameState.Title);
}
// ここまで
}
- ゲームオーバー画面のオブジェクトを入れる変数
gameOverPanel
を定義しました。 ChangeState()
メソッド内で、ゲーム状態がGameOver
のときはパネルを表示し、それ以外のときは非表示にするようにしました。transform.SetAsLastSibling()
で最前面に移動させています。OnBackButtonClicked()
メソッドを追加しました。ゲーム状態をTitle
に変更します。つまりタイトル画面が表示されます。
GameManagerオブジェクトのインスペクターで「Game Manager (Script) > Game Over Panel」にGameOverPanelオブジェクトをドラッグ&ドロップしてアサインします。

GameOverPanel/BackButtonオブジェクトのインスペクターで「On Click ()」にOnBackButtonClicked()
メソッドを設定します。

- 「+」をクリック。
- GameManagerオブジェクトを「None (Object)」にドラッグ&ドロップ。
- 「No Function」をクリックして「Gamemanager > OnBackButtonClicked」を選択。
ステージクリアの実装
最終ウェーブの敵をすべて倒したらステージクリア画面を表示します。ステージクリア画面には「タイトルへ戻る」ボタンがあって、押すとタイトル画面へ戻ります。
ステジクリア画面の作成
GameOverPanelオブジェクトを複製して新しいオブジェクトを作成し、名前を「StageClearPanel」に修正します。配下のオブジェクト名も修正します。
パネルの色、タイトル文字、ボタンのテキストなどを修正します。こんな感じにしました。

インスペクター左上のチェックを外して、パネルを非表示にしておきます。
ステージクリアの判定と画面の表示・非表示
GameManagerスクリプトを修正します。
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI; // Buttonクラスを使用するために必要
using TMPro; // TextMeshProを使うために必要
public class GameManager : MonoBehaviour
{
// ...省略...
// ここから 追加
[SerializeField] private GameObject stageClearPanel; // ステージクリアパネル
[SerializeField] private TextMeshProUGUI scoreText; // スコアテキスト
// ここまで
// ...省略...
/// <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); // タイトルパネルを表示
titlePanel.transform.SetAsLastSibling(); // タイトルパネルを最前面に移動
pausePanel.SetActive(false); // ポーズパネルを非表示
gameOverPanel.SetActive(false); // ゲームオーバーパネルを非表示
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
// ここから 追加
stageClearPanel.SetActive(false); // ステージクリアパネルを非表示
// ここまで
break;
case GameState.Preparing:
Debug.Log("ゲーム状態: Preparing");
Time.timeScale = 0; // 時間を停止
titlePanel.SetActive(false); // タイトルパネルを非表示
pausePanel.SetActive(false); // ポーズパネルを非表示
gameOverPanel.SetActive(false); // ゲームオーバーパネルを非表示
// ボタンのアイコンとテキストを「スタート」に設定
startPauseButtonIcon.sprite = playIcon; // playIcon を設定
startPauseButtonText.text = "スタート";
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
// ここから 追加
stageClearPanel.SetActive(false); // ステージクリアパネルを非表示
// ここまで
break;
case GameState.Playing:
Debug.Log("ゲーム状態: Playing");
Time.timeScale = 1; // 時間を通常速度に戻す
pausePanel.SetActive(false); // ポーズパネルを非表示
gameOverPanel.SetActive(false); // ゲームオーバーパネルを非表示
// ボタンのアイコンとテキストを「ポーズ」に設定
startPauseButtonIcon.sprite = pauseIcon; // pauseIcon を設定
startPauseButtonText.text = "ポーズ";
nextWaveButton.interactable = true; // 次のウェーブボタンを活性化
// ここから 追加
stageClearPanel.SetActive(false); // ステージクリアパネルを非表示
// ここまで
break;
case GameState.Paused:
Debug.Log("ゲーム状態: Paused");
Time.timeScale = 0; // 時間を停止
pausePanel.SetActive(true); // ポーズパネルを表示
pausePanel.transform.SetAsLastSibling(); // ポーズパネルを最前面に移動
gameOverPanel.SetActive(false); // ゲームオーバーパネルを非表示
// ボタンのアイコンとテキストを「ポーズ」に設定
startPauseButtonIcon.sprite = pauseIcon; // pauseIcon を設定
startPauseButtonText.text = "ポーズ";
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
// ここから 追加
stageClearPanel.SetActive(false); // ステージクリアパネルを非表示
// ここまで
break;
case GameState.GameOver:
Debug.Log("ゲーム状態: GameOver");
Time.timeScale = 0; // 時間を停止
pausePanel.SetActive(false); // ポーズパネルを非表示
gameOverPanel.SetActive(true); // ゲームオーバーパネルを表示
gameOverPanel.transform.SetAsLastSibling(); // ゲームオーバーパネルを最前面に移動
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
// ここから 追加
stageClearPanel.SetActive(false); // ステージクリアパネルを非表示
// ここまで
break;
case GameState.Victory:
Debug.Log("ゲーム状態: Victory");
Time.timeScale = 0; // 時間を停止
pausePanel.SetActive(false); // ポーズパネルを非表示
gameOverPanel.SetActive(false); // ゲームオーバーパネルを非表示
nextWaveButton.interactable = false; // 次のウェーブボタンを非活性化
// ここから 追加
stageClearPanel.SetActive(true); // ステージクリアパネルを表示
stageClearPanel.transform.SetAsLastSibling(); // ステージクリアパネルを最前面に移動
if (scoreText != null && ScoreManager.Instance != null)
scoreText.text = ScoreManager.Instance.CurrentScore.ToString(); // スコア表示
// ここまで
break;
}
}
// ...省略...
/// <summary>
/// 最終ウェーブをクリアしたときの処理
/// </summary>
public void OnFinalWaveCleared()
{
if (CurrentState == GameState.Playing)
{
ChangeState(GameState.Victory); // プレイ中 -> 勝利
}
}
// ...省略...
}
- 変数
stageClearPanel
とscoreText
を定義しました。 ChangeState()
メソッド内で、ゲーム状態がVictory
のときはパネルを表示し、スコアを表示するようにしました。Victory
以外のときはパネルを非表示にします。
ScoreManagerスクリプトを修正します。
using UnityEngine;
using TMPro; // TextMeshPro を使うために必要
using DG.Tweening; // DOTween を使うために必
public class ScoreManager : MonoBehaviour
{
// シングルトンインスタンス
public static ScoreManager Instance { get; private set; }
[SerializeField, Tooltip("現在のスコア")] private int currentScore = 0;
// ここから 追加
public int CurrentScore => currentScore;
// ここまで
[SerializeField, Tooltip("スコア表示用UIテキスト")] private TextMeshProUGUI scoreText;
// ...省略...
}
currentScore
を読み取り専用で外部のスクリプトから取得できるようにしました。GameManagerから呼び出すので。
EnemySpawnerスクリプトを修正します。
using System.Collections;
using System.Collections.Generic; // Listを使うために必要
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using TMPro; // TextMeshProを使うために必要
public class EnemySpawner : MonoBehaviour
{
// ...省略...
// ここから 追加
private int aliveEnemyCount = 0; // 現在生存している敵の数
// ここまで
// ここから 追加
public static EnemySpawner Instance { get; private set; } // シングルトンインスタンス
private void Awake()
{
// シングルトンパターンの実装
if (Instance == null)
{
Instance = this;
}
else if (Instance != this)
{
Debug.LogWarning("EnemySpawner のインスタンスが既に存在するため、新しいインスタンスを破棄します。", this);
Destroy(gameObject);
return;
}
}
// ここまで
// ...省略...
/// <summary>
/// 敵を生成する
/// </summary>
/// <param name="waveData">ウェーブデータ</param>
private void SpawnEnemy(WaveSetting.WaveData waveData)
{
// EnemyId に基づいて敵データを取得
EnemySetting.EnemyData enemyData = enemySetting.EnemyDataList.Find(e => e.EnemyId == waveData.EnemyId);
if (enemyData == null)
{
Debug.LogError($"EnemyId '{waveData.EnemyId}' に対応する敵データが見つかりません。");
return;
}
// 経路のスタート地点にプレハブから敵を生成
EnemyController enemyController = Instantiate(enemyPrefab, waveData.Path.StartPosition.position, Quaternion.identity);
// 敵データの初期化
enemyController.InitializeEnemy(waveData.Path, gameManager, enemyData);
// ここから 追加
// 生存している敵の数を増加
aliveEnemyCount++;
// ここまで
}
// ...省略...
// ここから 追加
/// <summary>
/// 敵が撃破されたときに呼ばれるメソッド
/// </summary>
public void OnEnemyDefeated()
{
aliveEnemyCount--;
// 最終ウェーブ中、敵が全滅したらVictory
if (currentWaveId == currentStageWaves.Count - 1 && aliveEnemyCount <= 0 && gameManager.CurrentState == GameManager.GameState.Playing)
{
if (gameManager != null)
{
gameManager.OnFinalWaveCleared();
}
}
}
// ここまで
}
aliveEnemyCount
を定義しました。現在生存している敵の数を入れておく変数です。- シングルトンパターンを実装しました。
SpawnEnemy()
メソッド内で、敵を生成するたびにaliveEnemyCount
を増やします。OnEnemyDefeated()
を追加しました。敵が撃破されたときに呼ばれるメソッドです。aliveEnemyCount
を減らし、最終ウェーブの敵が全滅したらOnFinalWaveCleared()
を実行します。
EnemyControllerスクリプトを修正します。
using UnityEngine;
using DG.Tweening;
using System.Linq;
public class EnemyController : MonoBehaviour
{
// ...省略...
/// <summary>
/// 敵オブジェクトのクリーンアップ処理(移動停止、HPバー削除、GameObject削除)
/// </summary>
private void CleanupEnemy()
{
// 既に破棄処理中の場合や、オブジェクトがnullの場合は何もしない
if (this == null || gameObject == null) return;
// tween変数に代入されている処理を終了する
moveTween?.Kill();
moveTween = null; // Killした後は参照をnullにしておくのが安全
// HPバーを破壊
if (hpBar != null)
{
Destroy(hpBar.gameObject);
hpBar = null; // 破棄後は参照をnullに
}
// ここから 追加
// 死亡時にSpawnerへ通知
if (EnemySpawner.Instance != null)
{
EnemySpawner.Instance.OnEnemyDefeated();
}
// ここまで
// 敵本体の破壊
Destroy(gameObject);
}
}
敵を撃破したときにOnEnemyDefeated()
を実行する処理を追加しました。
GameManagerオブジェクトのインスペクターで「Game Manager (Script) > Stage Clear Panel」にStageClearPanelオブジェクトをドラッグ&ドロップしてアサインします。
「Score Text」にはScoreTextオブジェクトををアサインします。

StageClearPanel/BackButtonオブジェクトのインスペクターで「On Click ()」にOnBackButtonClicked()
メソッドを設定します。
- 「+」をクリック。
- GameManagerオブジェクトを「None (Object)」にドラッグ&ドロップ。
- 「No Function」をクリックして「Gamemanager/OnBackButtonClicked」を選択。
ポーズ画面の修正
ポーズ画面からもタイトルに戻れるようにしましょう。
PausePanelに「タイトルへ戻る」ボタンを追加します。

PausePanel/BackButtonオブジェクトのインスペクターで「On Click ()」にOnBackButtonClicked()
メソッドを設定します。
- 「+」をクリック。
- GameManagerオブジェクトを「None (Object)」にドラッグ&ドロップ。
- 「No Function」をクリックして「Gamemanager/OnBackButtonClicked」を選択。
さいごに
今回は、ゲームオーバーとステージクリアを実装しました。これで、ひと通りゲームを遊べるようになりましたね。いえい。
次回は、ステージ選択機能を実装していきます。
でわでわ
コメント