U
タブ 04世界とゲームループ7 レッスン — タップで開く
06 / 07 · 12 分
世界とゲームループ

敵の波、敗北、そしてやり直し

ここまでのプロジェクト

Assets/

Scenes/

Arena.unity

VictoryPanelDefeatPanelSpawnPoint ×4

Scripts/

CharacterStats.OnDied eventHealthBar.csGameDirector.csEndPanels.cs

Prefabs/

Enemy.prefab

dressed arena · waves spawned by GameDirector

すでにあるものこのレッスンで追加このレッスンで置き換え

すべて動いています。Knight はブロックを跳び、三つのスキルは当たり、敵は追ってきて傷つけ、アリーナには光も音もあり、動きも速い。Play を押せば、あとは永遠に同じことをできます——何も終わらないので。

デモは何かをさせてくれますが、ゲームは勝ちと負けを教えてくれます。その違いは enum ひとつと、四十行ほどのコードです。

アニメーション図

三つのウェーブのあとに二つの結末——どちらからも最初に戻る道があります。

ステップ 1——enum ひとつ、一カ所

状態を複数のスクリプトに散らかすと、勝っているのに敗北画面が出ているのはなぜかを一晩問い続けることになります。一つのオブジェクトに置いておけば、ほかのスクリプトはそれを読むだけです。

GameDirector.cs
using System.Collections;
using UnityEngine;

public enum GameState { Ready, Fighting, Cleared, Defeat }

public class GameDirector : MonoBehaviour
{
    public static GameDirector I;              // one director, reachable from anywhere

    public GameState State { get; private set; } = GameState.Ready;

    public Transform[] spawnPoints;
    public GameObject enemyPrefab;
    public CharacterStats player;
    public int[] waveSizes = { 2, 3, 4 };

    int alive;

    void Awake() => I = this;

    void Start()
    {
        player.OnDied += () => Finish(GameState.Defeat);
        StartCoroutine(RunWaves());
    }

    IEnumerator RunWaves()
    {
        State = GameState.Fighting;

        foreach (int size in waveSizes)
        {
            alive = size;
            for (int i = 0; i < size; i++)
            {
                Transform where = spawnPoints[Random.Range(0, spawnPoints.Length)];
                GameObject e = Instantiate(enemyPrefab, where.position, where.rotation);
                e.GetComponent<CharacterStats>().OnDied += () => alive--;
            }

            // wait until the player clears this wave, or dies trying
            while (alive > 0 && State == GameState.Fighting) yield return null;
            if (State != GameState.Fighting) yield break;

            yield return new WaitForSeconds(2f);   // a breath between waves
        }

        Finish(GameState.Cleared);
    }

    void Finish(GameState ending)
    {
        State = ending;
        Time.timeScale = 0f;                        // freeze the arena behind the panel
    }

    public void Restart()
    {
        Time.timeScale = 1f;
        UnityEngine.SceneManagement.SceneManager.LoadScene(
            UnityEngine.SceneManagement.SceneManager.GetActiveScene().buildIndex);
    }
}
  • OnDied はタブ 02 で体力バーと一緒に CharacterStats へ追加したあの event です。Director はシーンの中で敵を探しません——敵は倒れるとき自分から知らせてくれます。

  • Time.timeScale = 0f は一発で全部を止めます。敵も、アニメーションも、パーティクルも。一つずつ無効化しなくても済みます。

  • シーンを読み直すのがいちばん安い再スタートで、古い状態が残りようがありません——アリーナ全体がファイルから作り直されるので。

ステップ 2——パネル二つ

  1. 1

    タブ 01 の Canvas——HudPanel と PausePanel が入っているあれ——の中にパネルを二つ追加します。VictoryPanel と DefeatPanel、それぞれにタイトルと Button を一つずつ。

  2. 2

    両方とも Inspector で非アクティブに。存在するのは勝負がついたときだけです。

  3. 3

    各 Button の OnClick に GameDirector.Restart を割り当てます。

  4. 4

    GameDirector.I.State を見張って、該当するパネルを出す小さなスクリプトを一つ足します。

EndPanels.cs
using UnityEngine;

public class EndPanels : MonoBehaviour
{
    public GameObject victory;
    public GameObject defeat;

    void Update()
    {
        // Update still runs at timeScale 0 — that is why the panel can appear.
        victory.SetActive(GameDirector.I.State == GameState.Cleared);
        defeat.SetActive(GameDirector.I.State == GameState.Defeat);
    }
}