Parallels Desktop 35%OFF セール

【Unity】タワーディフェンス(20) ゲームオーバーとステージクリア【クソゲー制作】

タワーディフェンスを作る(20)

Unity初心者が2Dタワーディフェンスゲームを制作しています。

今回は、ゲームオーバーステージクリアを実装していきます。前回、ゲームの状態管理機能を実装したので、それを利用すれば簡単だろうと思っていましたが、意外と面倒でした。

環境
  • Mac mini (M1, 2020)
  • Unity 2022.3.36f1
目次

ゲームオーバーの実装

城のHPが0になったらゲームオーバー画面を表示します。ゲームオーバー画面には「タイトルへ戻る」ボタンがあって、押すとタイトル画面へ戻ります。

ゲームオーバー画面の作成

STEP

PausePanelオブジェクトを複製して新しいオブジェクトを作成し、名前を「GameOverPanel」に修正します。配下のオブジェクト名も修正します。

タワーディフェンス209
STEP

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

タワーディフェンス210
STEP

インスペクター左上のチェックを外して、パネルを非表示にしておきます。

ゲームオーバー画面の表示・非表示

城のHPが0になったらゲーム状態(GameState)をGameOverにする処理はすでに実装済みです。なので、あとはゲーム状態に応じてゲームオーバー画面の表示・非表示をするだけです。

STEP

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に変更します。つまりタイトル画面が表示されます。
STEP

GameManagerオブジェクトのインスペクターで「Game Manager (Script) > Game Over Panel」GameOverPanelオブジェクトをドラッグ&ドロップしてアサインします。

タワーディフェンス211
STEP

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

タワーディフェンス212
  1. 「+」をクリック。
  2. GameManagerオブジェクトを「None (Object)」にドラッグ&ドロップ。
  3. 「No Function」をクリックして「Gamemanager > OnBackButtonClicked」を選択。

ステージクリアの実装

最終ウェーブの敵をすべて倒したらステージクリア画面を表示します。ステージクリア画面には「タイトルへ戻る」ボタンがあって、押すとタイトル画面へ戻ります。

ステジクリア画面の作成

STEP

GameOverPanelオブジェクトを複製して新しいオブジェクトを作成し、名前を「StageClearPanel」に修正します。配下のオブジェクト名も修正します。

STEP

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

タワーディフェンス213
STEP

インスペクター左上のチェックを外して、パネルを非表示にしておきます。

ステージクリアの判定と画面の表示・非表示

STEP

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); // プレイ中 -> 勝利
        }
    }

    // ...省略...
}
  • 変数stageClearPanelscoreTextを定義しました。
  • ChangeState()メソッド内で、ゲーム状態がVictoryのときはパネルを表示し、スコアを表示するようにしました。Victory以外のときはパネルを非表示にします。
STEP

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から呼び出すので。

STEP

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()を実行します。
STEP

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()を実行する処理を追加しました。

STEP

GameManagerオブジェクトのインスペクターで「Game Manager (Script) > Stage Clear Panel」StageClearPanelオブジェクトをドラッグ&ドロップしてアサインします。

「Score Text」にはScoreTextオブジェクトををアサインします。

タワーディフェンス214
STEP

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

  1. 「+」をクリック。
  2. GameManagerオブジェクトを「None (Object)」にドラッグ&ドロップ。
  3. 「No Function」をクリックして「Gamemanager/OnBackButtonClicked」を選択。

ポーズ画面の修正

ポーズ画面からもタイトルに戻れるようにしましょう。

STEP

PausePanelに「タイトルへ戻る」ボタンを追加します。

タワーディフェンス215
STEP

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

  1. 「+」をクリック。
  2. GameManagerオブジェクトを「None (Object)」にドラッグ&ドロップ。
  3. 「No Function」をクリックして「Gamemanager/OnBackButtonClicked」を選択。

さいごに

今回は、ゲームオーバーとステージクリアを実装しました。これで、ひと通りゲームを遊べるようになりましたね。いえい。

次回は、ステージ選択機能を実装していきます。

でわでわ

タワーディフェンスを作る(20)

この記事が気に入ったら
いいね または フォローしてね!

シェアしてね

コメント

コメントする

目次