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,你就能永远重复这些事 —— 因为什么都不会结束。

demo 只是让你有东西可玩,游戏会告诉你什么时候赢、什么时候输。这个差别就是一个 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 就是你在 tab 02 里连着血条一起加到 CharacterStats 上的那个 event。Director 从不在场景里搜敌人 —— 每个敌人死的时候自己来报告。

  • Time.timeScale = 0f 一次停掉所有东西:敌人、动画、粒子。你不用挨个去禁用。

  • 重载场景是最便宜的重开方式,而且绝不会留下残余状态 —— 整个竞技场都是从文件重新建出来的。

第 2 步 —— 两个面板

  1. 1

    在 tab 01 那个 Canvas 里 —— 就是装着 HudPanel 和 PausePanel 的那个 —— 再加两个面板:VictoryPanel 和 DefeatPanel,每个都有一行标题和一个 Button。

  2. 2

    在 Inspector 里把两个都设为 inactive。它们只在这局结束的时候才出现。

  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);
    }
}