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行为。到时候你会看到,状态模式只是起点,真正的实战还有不少坑等着我们踩。