Menu
CoddyTech

状态转换

本课属于 Coddy Unity 学习旅程的“Unity 游戏架构”单元。第 38 课,共 64 课。

谁来决定状态何时改变?如果由拥有者来决定,就会把每条规则都放回同一个 Update 中,就像之前的切换逻辑一样。通常的做法是让每个状态都知道如何退出:在自己的 Tick 中检查条件,并向状态机请求下一个状态。这个炸弹会等到玩家靠近,然后点燃引信;如果玩家跑开,就回到等待状态:

public class Bomb : MonoBehaviour
{
    public Transform player;
    public StateMachine machine = new StateMachine();
    public IState idle;
    public IState fuse;

    void Start()
    {
        idle = new IdleState(this);
        fuse = new FuseState(this);
        machine.ChangeState(idle);
    }

    void Update() { machine.Tick(); }

    public bool PlayerNear()
    {
        return Vector3.Distance(transform.position, player.position) < 3f;
    }
}

public class IdleState : IState
{
    private Bomb bomb;
    public IdleState(Bomb bomb) { this.bomb = bomb; }

    public void Enter() { }
    public void Exit() { }

    public void Tick()
    {
        if (bomb.PlayerNear()) bomb.machine.ChangeState(bomb.fuse);
    }
}

public class FuseState : IState
{
    private Bomb bomb;
    private float timer;
    public FuseState(Bomb bomb) { this.bomb = bomb; }

    public void Enter() { timer = 2f; }
    public void Exit() { }

    public void Tick()
    {
        if (!bomb.PlayerNear())
        {
            bomb.machine.ChangeState(bomb.idle);
            return;
        }
        timer -= Time.deltaTime;
        if (timer <= 0f)
        {
            Debug.Log("Boom!");
            Object.Destroy(bomb.gameObject);
        }
    }
}

每个状态都会在构造函数中获取炸弹,并通过它访问状态机、玩家检测和其他状态。炸弹会在 Start 中创建每个状态一次,因此状态不会再用 new 创建其他状态:如果两个状态在各自的构造函数中创建对方,就会不断循环,直到堆栈溢出。炸弹的 Update 只会调用状态机的 Tick,规则则保存在所属的状态中:2 秒引信完全由 FuseState 负责。

ChangeState 会立即运行 FuseState 的 Exit 和 IdleState 的 Enter,但调用之后,FuseState 的 Tick 中其余部分仍会继续运行。return 会停止后续执行,否则引信会在状态已经结束后再多烧一帧。Destroy 写作 Object.Destroy:MonoBehaviour 从 Unity 的 Object 类继承了它,而普通类必须写出类名。

challenge icon

挑战

中等

在 FlytrapStates.cs 中的每个 Tick 里编写捕蝇草的状态转换。

  1. Open:当满足 trap.PlayerInReach() 时,调用 trap.machine.ChangeState(trap.snap);。
  2. Snap:将 Time.deltaTime 加到 timer 上;到达 0.5 时,如果玩家仍在范围内,就切换到 trap.chew,否则切换到 trap.open。
  3. Chew:让 timer 从 3 开始倒计时;到 0 时,切换到 trap.open。

按“开始”,走进陷阱,然后躲开它。接着按“检查”。

亲手试试

using UnityEngine;

public class OpenState : IState
{
    private Flytrap trap;

    public OpenState(Flytrap trap)
    {
        this.trap = trap;
    }

    public void Enter()
    {
        trap.Paint(new Color(0.3f, 0.8f, 0.2f));
    }

    public void Tick()
    {
        // 玩家进入攻击范围时,迅速合拢
    }

    public void Exit()
    {
    }
}

public class SnapState : IState
{
    private Flytrap trap;
    private float timer;

    public SnapState(Flytrap trap)
    {
        this.trap = trap;
    }

    public void Enter()
    {
        timer = 0f;
        trap.Paint(new Color(1f, 0.55f, 0.1f));
    }

    public void Tick()
    {
        // 经过 0.5 秒:如果玩家仍在攻击范围内,就开始咀嚼;否则重新张开
    }

    public void Exit()
    {
    }
}

public class ChewState : IState
{
    private Flytrap trap;
    private float timer;

    public ChewState(Flytrap trap)
    {
        this.trap = trap;
    }

    public void Enter()
    {
        timer = 3f;
        trap.Paint(new Color(0.5f, 0.05f, 0.1f));
        Debug.Log("Chomp!");
    }

    public void Tick()
    {
        // 经过 3 秒:重新张开
    }

    public void Exit()
    {
    }
}

Unity 游戏架构中的所有课

自己练习: Unity 在线编辑器