什么是状态机?
状态机(State Machine)是一种数学模型,用于描述对象在不同状态之间的转移和行为,由一组状态、一组事件和一组转换规则构成。
状态机的基本概念
- 状态:在某一时刻对象所处的形态,可以是一个值,也可以是一个抽象的概念。例如在游戏中,角色可以处于行走、跳跃、攻击等状态。
- 事件:触发状态转换的条件或信号。例如玩家按下空格键会触发角色从行走状态转换到跳跃状态。
- 转换规则:对象在不同状态之间转移条件和行为,当某个事件发生时对象会从一个状态切换到另一个状态。例如从行走状态切换到跳跃状态。
状态机实现
使用Cocos引擎和TypeScript语言来实现一个简单的状态机。实现之前先来看一张状态机的基本执行逻辑图。.
状态机的核心是一个循环,在循环中根据不同的状态,判断不同的条件,满足条件后切换状态,并执行当前状态下的逻辑。
状态数据
export class StateData {
//当前状态是否完成
public finish: boolean = false;
//状态结果
public result: number = -1;
//错误信息
public errMsg: string = null;
//重置数据
public reset(): void {
this.finish = false;
this.result = -1;
this.errMsg = null;
}
}
状态基类
export interface IState<SD extends StateData> {
/** 当前状态是否已经开始 */
started: boolean;
/** 状态开始的时间 */
startTime: number;
/** 当前状态的code */
code: number;
/** 重置状态 */
reset(): void;
/** 开始执行状态逻辑 */
start(): void;
/**
* 执行状态的初始化逻辑。
* @param data 状态数据
*/
initialize(data: SD): void;
/**
* 刷新状态逻辑。
* @param dt 帧间隔时间
* @param data 状态数据
*/
update(dt: number, data: SD): void;
/**
* 状态执行结束了。
* @param data 状态数据
*/
finish(data: SD): number;
}
状态机对象
class StateWrapper<SD extends StateData> {
readonly state: IState<SD>;
readonly data: SD;
constructor(state: IState<SD>, data: SD) {
if (state == null || data == null) {
throw new Error("Invalid state object!");
}
this.state = state;
this.data = data;
}
}
/**
* 状态机对象。
*/
export class StateMachine<SD extends StateData> {
private readonly _states: Map<number, StateWrapper<SD>> = new Map<number, StateWrapper<SD>>();
private _current: StateWrapper<SD> = null;
private _running: boolean = false;
get currentState(): IState<SD> {
if (this._current == null) {
return null;
}
return this._current.state;
}
/**
* 设置状态机包含的所有状态。
* @param states 状态列表。
* @param data 状态数据。
*/
setStates(states: IState<SD>[], data: SD): void {
if (!states) {
return;
}
this._states.clear();
states.forEach((state) => {
this._states.set(state.code, new StateWrapper<SD>(state, data));
});
}
/**
* 开始执行状态机的逻辑。
* @param firstState 第一个状态的code。
*/
start(firstState: number): void {
this._running = true;
this.switchToState(firstState);
}
/**
* 停止执行状态机
* @param stateCode
* @returns
*/
stop(): void {
this._running = false;
}
/**
* 切换到指定的状态。
* @param stateCode 要切换到的状态的code。
*/
switchToState(stateCode: number): void {
let targetState = this._states.get(stateCode);
if (targetState == null) {
console.info("Try to switch to invalid state: ", stateCode);
return;
}
this._current = targetState;
this._current.data.reset();
this._current.state.reset();
try {
this._current.state.start();
this._current.state.initialize(this._current.data);
} catch (e: any) {
this._current.state.onInitializeError(e, this._current.data);
}
}
/**
* 刷新状态机逻辑。
*/
update(dt: number): void {
if (!this._running) {
return;
}
let cur = this._current;
if (cur == null) {
return;
}
let state = cur.state;
let data = cur.data;
// 调用当前状态的Update函数
state.update(dt, data);
// 如果当前状态数据标记为完成,切换状态
if (data != null && data.finish) {
let nextState = state.finish(data);
this.switchToState(nextState);
}
}
}
示例
protected onStart(): void {
//创建状态机对象
this.fsm = new StateMachine();
//继承IState基类扩展状态
this.idleState = new BattleRoleDieState(this);
this.moveState = new BattleRoleMoveState(this);
this.attackState = new BattleRoleAttackState(this);
//继承StateData基类扩展数据
this.stateData = new BattleRoleStateData(this);
this.fsm.setStates([this.idleState, this.moveState, this.attackState], this.stateData);
this.fsm.start(BattleRoleStateEnum.IDLE);
}
当finish为true的时候自动切换到下一个状态,当然也可以手动调用switchToState函数手动切换状态
状态机的优缺点
优点:
- 逻辑清晰:状态机将复杂的系统行为分解为简单的状态和状态之间的转换,使系统逻辑更加清晰和易于理解。
- 可维护性高:由于状态机的逻辑是模块化的,任何状态的修改或扩展只需在该状态的实现部分进行,不会影响其他部分,增强了代码的可维护性。
- 可扩展性强:新的状态和转换可以方便地添加到现有的状态机中,使系统具有良好的扩展性。
缺点:
- 状态爆炸:对于复杂系统,状态和转换的数量可能非常庞大,导致状态机图变得复杂难以管理,这被称为“状态爆炸”问题。