将状态表示为类
本课属于 Coddy Unity 学习旅程的“Unity 游戏架构”单元。第 35 课,共 64 课。
第 3 单元中,基于枚举的 switch 把每个状态的工作都放在一个 Update 里。状态开始携带自己的数据后,这种做法就难以扩展了:只有一个状态使用的计时器最终会成为 MonoBehaviour 的字段,和其他所有状态的字段放在一起,而且每个 case 都必须记得重置自己用到的内容。C# 的状态模式会为每个状态创建一个独立的类,并让每个类实现同一个接口。这个道路信标会闪烁或保持常亮,按 Space 键可以在两种状态之间切换:
public interface IState
{
void Enter();
void Tick();
void Exit();
}
public class BlinkState : IState
{
private Renderer lamp;
private float timer;
public BlinkState(Renderer lamp)
{
this.lamp = lamp;
}
public void Enter() { timer = 0f; }
public void Tick()
{
timer += Time.deltaTime;
if (timer >= 0.5f)
{
timer = 0f;
lamp.enabled = !lamp.enabled;
}
}
public void Exit() { lamp.enabled = true; }
}
public class SteadyState : IState
{
public void Enter() { }
public void Tick() { }
public void Exit() { }
}
public class Beacon : MonoBehaviour
{
public Renderer lamp;
private IState blink;
private IState steady;
private IState current;
void Start()
{
blink = new BlinkState(lamp);
steady = new SteadyState();
SetState(steady);
}
void Update()
{
if (Keyboard.current.spaceKey.wasPressedThisFrame) SetState(current == steady ? blink : steady);
current.Tick();
}
void SetState(IState next)
{
if (current != null) current.Exit();
current = next;
current.Enter();
}
}这些状态是普通的 C# 类,不是 MonoBehaviour。你可以用 new 创建它们,并通过构造函数把它们需要的内容传进去,这里传入的是灯的 Renderer,因为它们不是组件,不能调用 GetComponent,也不能在 Inspector 中显示字段。Unity 也不会调用它们的方法:Beacon 才是 MonoBehaviour,因此它的 Update 会在每一帧调用 current.Tick()。
timer 只属于 BlinkState,而它的 Enter 会在信标每次开始闪烁时把计时器设为 0。状态结束时,Exit 会进行清理:灯熄灭时也可能按下 Space 键,而 Exit 会重新点亮它,因此 SteadyState 开始时灯不会是暗的。SetState 会先运行旧状态的 Exit,再运行新状态的 Enter。第三种状态,例如慢速脉冲,只需再添加一个类,Beacon 的 Update 无需改变。
挑战
中等编写炮塔的扫描状态。
- 在
IState.cs中,声明public interface IState,并包含void Enter();、void Tick();和void Exit();。 - 在
TurretStates.cs中,编写ScanState : IState,并传入炮塔头部。进入时:角度设为 0,头部颜色设为Color.green。每次更新时:让头部的局部 y 角度在-45和45之间以每秒90度的速度来回扫动。 Turret:在 Start 中进入一个状态,在 Update 中更新它。
按下“开始”:绿色头部会来回扫动。然后按“检查”。
亲手试试
using UnityEngine;
// ScanState
Unity 游戏架构中的所有课
自己练习: Unity 在线编辑器