4. FSM实现(二):基于状态模式的状态机实现

好,咱们接着聊。上一章我们用最朴素的if-else搭了个状态机,说实话,那玩意儿在小项目里够用。但一旦状态多了,代码就开始发臭。我当年在做一个动作游戏AI时,角色有十几种状态,if-else嵌套得跟意大利面条似的,改一个状态要翻半天代码。

那怎么办?用状态模式(State Pattern)。

说白了,就是把每个状态封装成一个独立的类。状态之间的切换,由状态自己决定,或者由状态机统一调度。这样代码就清爽多了。

4.1 状态模式的核心思想

状态模式,本质上是「将状态的行为局部化」。每个状态类只关心自己该做什么,以及什么条件下切换到哪个状态。

我习惯把状态机拆成三个角色:

  • 状态接口(State):定义所有状态必须实现的方法,比如Enter、Update、Exit。
  • 具体状态(ConcreteState):实现具体的行为逻辑。
  • 状态机(StateMachine):持有当前状态,负责状态切换。

你想想看,这样做的好处是什么?每个状态都是独立的单元,新增一个状态,你只需要写一个新类,完全不用动其他状态的代码。这就是开闭原则——对扩展开放,对修改关闭。

4.2 C++ 示例:一个简单的巡逻-追击AI

咱们还是拿游戏AI举例。假设一个敌人,平时在巡逻,发现玩家就追击,追丢了就回去继续巡逻。

先定义状态接口:

// State.h
class State {
public:
    virtual ~State() = default;
    virtual void Enter() = 0;
    virtual void Update(float deltaTime) = 0;
    virtual void Exit() = 0;
    virtual const char* GetName() const = 0;
};

然后是两个具体状态:

// PatrolState.h
class PatrolState : public State {
public:
    void Enter() override {
        // 我习惯在这里重置巡逻路径点
        currentWaypoint = 0;
        std::cout << "进入巡逻状态" << std::endl;
    }

    void Update(float deltaTime) override {
        // 巡逻逻辑:沿着路径点移动
        MoveToWaypoint();

        // 检测到玩家?切换追击
        if (DetectPlayer()) {
            // 注意:这里不直接切换,而是通过状态机
            // 具体切换逻辑在状态机里处理
            owner->ChangeState("Chase");
        }
    }

    void Exit() override {
        std::cout << "离开巡逻状态" << std::endl;
    }

    const char* GetName() const override { return "Patrol"; }

private:
    int currentWaypoint = 0;
    StateMachine* owner; // 持有状态机引用
};
// ChaseState.h
class ChaseState : public State {
public:
    void Enter() override {
        std::cout << "进入追击状态" << std::endl;
        // 我记得有一次忘了设置追击速度,结果敌人追得比乌龟还慢...
        speed = chaseSpeed;
    }

    void Update(float deltaTime) override {
        // 追击逻辑:朝玩家移动
        MoveTowardPlayer();

        // 追丢了?回去巡逻
        if (LostPlayer()) {
            owner->ChangeState("Patrol");
        }

        // 追上了?攻击(这里可以再切到攻击状态)
        if (ReachedPlayer()) {
            owner->ChangeState("Attack");
        }
    }

    void Exit() override {
        std::cout << "离开追击状态" << std::endl;
    }

    const char* GetName() const override { return "Chase"; }

private:
    float speed;
    StateMachine* owner;
};

最后是状态机本身:

// StateMachine.h
class StateMachine {
public:
    void AddState(const std::string& name, State* state) {
        states[name] = state;
        state->SetOwner(this); // 注入状态机引用
    }

    void ChangeState(const std::string& name) {
        if (currentState) {
            currentState->Exit();
        }
        currentState = states[name];
        currentState->Enter();
    }

    void Update(float deltaTime) {
        if (currentState) {
            currentState->Update(deltaTime);
        }
    }

private:
    std::unordered_map<std::string, State*> states;
    State* currentState = nullptr;
};

关键点:状态切换时,先调用Exit()清理旧状态,再调用Enter()初始化新状态。这个顺序不能乱,否则容易出现资源泄漏或状态不一致。

4.3 Python 示例:同样的逻辑,更简洁的写法

Python写起来就轻快多了。我个人觉得Python特别适合快速原型验证,但大型项目里还是C++更稳。

# state_machine.py
from abc import ABC, abstractmethod

class State(ABC):
    @abstractmethod
    def enter(self):
        pass

    @abstractmethod
    def update(self, dt):
        pass

    @abstractmethod
    def exit(self):
        pass

class PatrolState(State):
    def enter(self):
        self.current_waypoint = 0
        print("进入巡逻状态")

    def update(self, dt):
        self.move_to_waypoint()
        if self.detect_player():
            self.owner.change_state("Chase")

    def exit(self):
        print("离开巡逻状态")

class ChaseState(State):
    def enter(self):
        print("进入追击状态")
        self.speed = 5.0  # 追击速度

    def update(self, dt):
        self.move_toward_player()
        if self.lost_player():
            self.owner.change_state("Patrol")

    def exit(self):
        print("离开追击状态")

class StateMachine:
    def __init__(self):
        self.states = {}
        self.current_state = None

    def add_state(self, name, state):
        self.states[name] = state
        state.owner = self  # 注入状态机引用

    def change_state(self, name):
        if self.current_state:
            self.current_state.exit()
        self.current_state = self.states[name]
        self.current_state.enter()

    def update(self, dt):
        if self.current_state:
            self.current_state.update(dt)

小技巧:Python里用@property装饰器可以优雅地暴露状态机的当前状态名,方便调试时打印日志。

4.4 状态模式的优缺点

用了这么多年,我总结一下状态模式的利弊:

优点 缺点
每个状态独立,易于扩展 状态类数量增多,小项目显得臃肿
状态切换逻辑清晰 状态之间可能有隐式耦合(比如共享数据)
符合开闭原则 需要额外管理状态实例的生命周期
便于单元测试 状态切换的时序问题可能引入bug

避坑指南:我曾经在一个项目里,所有状态都共享同一个「黑板」数据对象。结果某个状态在Exit时忘了清理数据,导致下一个状态读到脏数据,AI行为变得诡异。排查了整整两天……所以,状态切换时一定要做好数据清理。

4.5 什么时候用状态模式?

我个人建议:

  • 状态数超过5个:if-else已经难以维护了,果断上状态模式。
  • 状态切换逻辑复杂:比如一个状态可以切换到多个状态,或者切换条件很复杂。
  • 需要频繁增删状态:项目迭代期,状态模式能让你少改很多代码。
  • 团队协作:不同人负责不同状态,状态模式天然支持分工。

但如果你只有两三个状态,而且永远不会变,那用if-else反而更直接。别为了设计模式而设计模式,这是很多新手容易犯的毛病。

4.6 小结

嗯,这一章我们讲了状态模式实现FSM的核心思路。说白了就是把状态变成对象,让每个状态自己管好自己的事。C++和Python的示例都跑了一遍,你应该能感受到状态模式的清爽感。

下一章,我会聊聊状态机的一些进阶玩法——比如状态栈、全局状态、以及如何用状态机处理复杂的AI行为。到时候你会看到,状态模式只是起点,真正的实战还有不少坑等着我们踩。