☰
游戏战斗系统学习第2篇:有限状态机 (FSM)
2026/9/30 2:47:40 网站建设 项目流程

一、 FSM 的核心概念详解

1. 原理与实现

FSM的核心由状态(State) 、转移(Transition) 和条件(Condition) 三要素构成。AI始终处于某一个确定状态中,持续执行该状态的逻辑,直到满足某个转移条件才切换到新状态。典型的实现是一个while(true)循环包裹switch(state)语句块,每个case对应一个状态的执行逻辑和转移判断。

2. 概念详解
2.1 状态(State)

状态表示 AI 当前的行为模式,而不是具体动作。

比如: 在战斗AI中,一个典型的状态集合是:Idle(待机 / 空闲) → Patrol(巡逻) → Alert(警戒) → Chase(追击) → Attack(攻击) → Retreat(撤退)。状态数量少于7个时,FSM是最经济的选择。

注意:一个设计良好的状态应该是高内聚的:状态内部逻辑围绕一个明确的行为模式组织。如果 Attack 状态内部包含了前摇、判定、后摇、连招分支等复杂逻辑,就应该考虑将其拆分为子状态机(HFSM)

2.2 转移(Transition)

转移定义了状态之间的切换关系。一个完整的转移包含:

  • 源状态:从哪个状态出发
  • 目标状态:切换到哪个状态
  • 触发事件:什么事件触发转移,如 SeePlayer
  • 守卫条件:转移必须满足的条件,如 distance < 10 && hasAmmo
  • 转移动作:转移瞬间执行的动作,如播放音效、设置变量
  • 优先级:多个转移同时满足时的选择顺序

转移的触发方式有两种:

  • 轮询式:每帧检查所有转移条件。简单但可能浪费性能。
  • 事件驱动式:只在特定事件发生时检查相关转移。更高效,但需要事件系统支持。
2.3 动作(Action)

动作是状态机在特定时刻执行的逻辑。通常分为三类:

  • 进入动作(Entry Action):进入状态时执行一次,如播放动画、设置速度、重置计时器
  • 更新动作(Update Action):状态持续期间每帧执行,如移动、检测玩家、更新计时器
  • 退出动作(Exit Action):离开状态时执行一次,如停止移动、清理资源、恢复默认值

将动作按进入/更新/退出分离,是状态模式实现的关键,能有效避免资源泄漏和状态残留。

3. 适用场景

FSM非常适合状态少且稳定、转移关系明确、行为模式化的场景。例如2D游戏中的简单敌人AI、Boss战的阶段切换、塔防游戏中怪物的行进逻辑等。在《星际争霸》这类RTS中,FSM也被用于控制单位的底层行为。

技术选型要点:状态 ≤ 7个 → FSM;状态转移条件清晰且不随功能迭代频繁增加 → FSM;需要精确调试每个状态行为 → FSM(因为状态显式,调试最直观)。

二、分层有限状态机 (HFSM)

HFSM是对FSM的直接改进,通过将状态分组并层级化来解决状态转移的组合爆炸问题。

1. 原理与实现

HFSM的核心思想是:把一组相关的低层状态封装到一个高层状态中,低层状态之间的转移在内部处理,不需要暴露给外部状态。高层状态之间只维护粗粒度的转移关系,从而大幅减少需要显式管理的转移数量。

示例:战斗AI可以这样分层

战斗(Combat)[高层状态] ├── 近战攻击(MeleeAttack)[低层状态] ├── 远程攻击(RangedAttack)[低层状态] └── 防御(Defend)[低层状态] 非战斗(NonCombat)[高层状态] ├── 巡逻(Patrol) └── 待机(Idle)

从NonCombat进入Combat只需要一条高层转移,而不是从Patrol、Idle分别连接到MeleeAttack、RangedAttack、Defend的九条转移。HFSM还支持转移的重用——高层状态的转移条件可以被所有低层状态共享,减少了代码冗余

2. 为什么需要 HFSM:从状态爆炸说起
2.1 传统 FSM 的状态爆炸问题

假设一个战斗 AI 有以下状态:

Idle, Patrol, Alert, Chase, MeleeAttack, RangedAttack, Block, Dodge, Flee, Dead

如果采用扁平 FSM,需要考虑的转移数量是:

  • 每个状态平均需要检查 3~5 个转移条件

  • 10 个状态 → 约 30~50 条转移

  • 每新增一个状态,需要接入的转移数随已有状态数线性增长

更严重的是语义重复。例如:

  • Idle、Patrol、Alert都可能因为“发现玩家”转移到Chase

  • MeleeAttack、RangedAttack、Block、Dodge都可能因为“生命值过低”转移到Flee

  • 所有状态都可能因为“生命值归零”转移到Dead

这些转移条件在每个状态中重复编写,修改时需要同步修改多处,维护成本随状态数增长而急剧上升。

2.2 HFSM 的核心洞察

HFSM 的核心洞察是:很多状态共享相同的转移逻辑,很多转移只在特定状态组内才有意义。

将状态分组后:

  • 组内转移:只在组内状态之间发生,外部不需要知道

  • 组间转移:由父状态统一管理,子状态不需要重复编写

  • 通用转移:定义在高层,所有子状态自动继承

示例:将上述 10 个状态分组:

Root ├── NonCombat(非战斗) │ ├── Idle │ ├── Patrol │ └── Alert └── Combat(战斗) ├── Chase ├── MeleeAttack ├── RangedAttack ├── Block └── Dodge └── Flee(逃跑) └── Dead(死亡)

分组后:

  • NonCombat内部的Idle → Patrol → Alert转移只在组内处理

  • NonCombat → Combat只需要一条高层转移(发现玩家)

  • Combat → Flee只需要一条高层转移(生命值过低)

  • Any → Dead只需要一条全局转移(生命值归零)

  • 转移数量从 30~50 条降至 15~20 条,且新增状态时只需接入组内转移。

3. HFSM 的设计原则

原则一:按语义分组,而非按实现分组

好的分组原则:

Combat(战斗) ├── Chase(追击) ├── Attack(攻击) └── Defend(防御)

不好的分组:

StatesWithTimer(带计时器的状态) ├── Idle ├── Attack └── Flee

原则二:父状态应该有明确的语义

父状态名应该能概括所有子状态的共同特征。如果找不到合适的名字,说明分组不合理

原则三:层级深度控制在 3~4 层以内

过深的层级会增加调试复杂度。大多数战斗 AI 的层级深度在 3 层左右:

Root → Combat → MeleeAttack

三、状态机代码实现(TypeScript)

1. 状态模式(State Pattern)

每个状态一个类,符合开闭原则,易于扩展和复用。

1.1 接口定义
interface IState<C> { /** 进入状态时调用一次 */ enter(context: C): void; /** 每帧调用 */ update(context: C, deltaTime: number): void; /** 离开状态时调用一次 */ exit(context: C): void; /** 状态名称,用于调试 */ readonly name: string; } interface IStateMachine<C> { currentState: IState<C>; changeState(newState: IState<C>): void; update(deltaTime: number): void; }
1.2 战斗 AI 上下文与状态实现
//状态类型枚举 export enum EnemyStateType { Idle="Idle", //待机 Patrol= "Patrol", //巡逻 Chase="Chase", //追击 Attack="Attack", //攻击 Flee="Flee", //逃跑 Dead="Dead", //死亡 } //所有状态列表 const EnemyFsmState = { StateList: { [EnemyStateType.Idle]: new IdleState, [EnemyStateType.Patrol]: new PatrolState, [EnemyStateType.Chase]: new ChaseState, [EnemyStateType.Attack]: new AttackState, [EnemyStateType.Flee]: new FleeState, [EnemyStateType.Dead]: new DeadState, }, Default: EnemyStateType.Idle, } // --- 上下文 --- class EnemyContext { public health: number = 100; public maxHealth: number = 100; public position = { x: 0, y: 0 }; public playerPosition = { x: 0, y: 0 }; public attackRange: number = 2.0; public chaseRange: number = 10.0; public fleeThreshold: number = 30; public stateMachine!: StateMachine<EnemyContext>; public distanceToPlayer(): number { const dx = this.position.x - this.playerPosition.x; const dy = this.position.y - this.playerPosition.y; return Math.sqrt(dx * dx + dy * dy); } public canSeePlayer(): boolean { return this.distanceToPlayer() <= this.chaseRange; } public inAttackRange(): boolean { return this.distanceToPlayer() <= this.attackRange; } public isDead(): boolean { return this.health <= 0; } } // --- 待机状态 --- class IdleState implements IState<EnemyContext> { public readonly name = "Idle"; private timer = 0; public enter(ctx: EnemyContext): void { this.timer = 0; console.log("[AI] 进入待机"); } public update(ctx: EnemyContext, dt: number): void { this.timer += dt; if (ctx.canSeePlayer()) { ctx.stateMachine.changeState(new ChaseState()); return; } if (this.timer > 2.0) { ctx.stateMachine.changeState(new PatrolState()); } } public exit(ctx: EnemyContext): void { console.log("[AI] 离开待机"); } } // --- 巡逻状态 --- class PatrolState implements IState<EnemyContext> { public readonly name = "Patrol"; private pathIndex = 0; private patrolPath = [ { x: 0, y: 0 }, { x: 5, y: 0 }, { x: 5, y: 5 }, { x: 0, y: 5 }, ]; public enter(ctx: EnemyContext): void { console.log("[AI] 进入巡逻"); } public update(ctx: EnemyContext, dt: number): void { if (ctx.isDead()) { ctx.stateMachine.changeState(new DeadState()); return; } if (ctx.canSeePlayer()) { ctx.stateMachine.changeState(new ChaseState()); return; } this.moveAlongPath(ctx, dt); } public exit(ctx: EnemyContext): void { console.log("[AI] 离开巡逻"); } private moveAlongPath(ctx: EnemyContext, dt: number): void { const target = this.patrolPath[this.pathIndex]; const dx = target.x - ctx.position.x; const dy = target.y - ctx.position.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 0.1) { this.pathIndex = (this.pathIndex + 1) % this.patrolPath.length; return; } const speed = 2.0; ctx.position.x += (dx / dist) * speed * dt; ctx.position.y += (dy / dist) * speed * dt; } } // --- 追击状态 --- class ChaseState implements IState<EnemyContext> { public readonly name = "Chase"; public enter(ctx: EnemyContext): void { console.log("[AI] 进入追击"); } public update(ctx: EnemyContext, dt: number): void { if (ctx.isDead()) { ctx.stateMachine.changeState(new DeadState()); return; } if (ctx.health < ctx.fleeThreshold) { ctx.stateMachine.changeState(new FleeState()); return; } if (ctx.inAttackRange()) { ctx.stateMachine.changeState(new AttackState()); return; } if (!ctx.canSeePlayer()) { ctx.stateMachine.changeState(new PatrolState()); return; } this.moveTowardPlayer(ctx, dt); } public exit(ctx: EnemyContext): void { console.log("[AI] 离开追击"); } private moveTowardPlayer(ctx: EnemyContext, dt: number): void { const dx = ctx.playerPosition.x - ctx.position.x; const dy = ctx.playerPosition.y - ctx.position.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 0.01) return; const speed = 4.0; ctx.position.x += (dx / dist) * speed * dt; ctx.position.y += (dy / dist) * speed * dt; } } // --- 攻击状态 --- class AttackState implements IState<EnemyContext> { public readonly name = "Attack"; private attackTimer = 0; private readonly attackDuration = 0.8; private hasDealtDamage = false; public enter(ctx: EnemyContext): void { console.log("[AI] 进入攻击"); this.attackTimer = 0; this.hasDealtDamage = false; } public update(ctx: EnemyContext, dt: number): void { if (ctx.isDead()) { ctx.stateMachine.changeState(new DeadState()); return; } this.attackTimer += dt; // 前摇 0~0.3s,判定 0.3s,后摇 0.3~0.8s if (this.attackTimer >= 0.3 && !this.hasDealtDamage) { this.dealDamage(ctx); this.hasDealtDamage = true; } if (this.attackTimer >= this.attackDuration) { if (ctx.inAttackRange()) { ctx.stateMachine.changeState(new AttackState()); } else { ctx.stateMachine.changeState(new ChaseState()); } } } public exit(ctx: EnemyContext): void { console.log("[AI] 离开攻击"); } private dealDamage(ctx: EnemyContext): void { if (ctx.inAttackRange()) { console.log("[AI] 攻击命中玩家!"); } } } // --- 逃跑状态 --- class FleeState implements IState<EnemyContext> { public readonly name = "Flee"; public enter(ctx: EnemyContext): void { console.log("[AI] 进入逃跑"); } public update(ctx: EnemyContext, dt: number): void { if (ctx.isDead()) { ctx.stateMachine.changeState(new DeadState()); return; } if (ctx.health > ctx.fleeThreshold * 1.5) { ctx.stateMachine.changeState(new ChaseState()); return; } this.moveAwayFromPlayer(ctx, dt); } public exit(ctx: EnemyContext): void { console.log("[AI] 离开逃跑"); } private moveAwayFromPlayer(ctx: EnemyContext, dt: number): void { const dx = ctx.position.x - ctx.playerPosition.x; const dy = ctx.position.y - ctx.playerPosition.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 0.01) return; const speed = 5.0; ctx.position.x += (dx / dist) * speed * dt; ctx.position.y += (dy / dist) * speed * dt; } } // --- 死亡状态 --- class DeadState implements IState<EnemyContext> { public readonly name = "Dead"; public enter(ctx: EnemyContext): void { console.log("[AI] 进入死亡状态"); // 播放死亡动画、禁用碰撞等 } public update(ctx: EnemyContext, dt: number): void { // 死亡状态不处理任何逻辑 } public exit(ctx: EnemyContext): void { // 通常不会离开死亡状态 } }
1.3 状态机核心
class StateMachine<C> implements IStateMachine<C> { private _currentState: IState<C>; private _defaultState: string; private context: C; private stateTime: number = 0; private previousState: IState<C> | null = null; private states: Map<string, FSMState> = new Map(); constructor(context: C) { this.context = context; this._defaultState = EnemyFsmState.Default; this.states.clear(); Object.entries(EnemyFsmState.stateList).forEach(([key, state]) => { this.states.set(key, state); }); this._currentState = this.states.get(this._defaultState); this._currentState.enter(context); } public get currentState(): IState<C> { return this._currentState; } public get timeInState(): number { return this.stateTime; } public changeState(stateType:EnemyStateType): void { let newState = this.states.get(stateType); if (newState === this._currentState) { return; // 避免重复进入同一状态 } this._currentState.exit(this.context); this.previousState = this._currentState; this._currentState = newState; this.stateTime = 0; this._currentState.enter(this.context); } /** 返回到上一个状态 */ public revertToPreviousState(): void { if (this.previousState) { this.changeState(this.previousState); } } public update(deltaTime: number): void { this.stateTime += deltaTime; this._currentState.update(this.context, deltaTime); } }

使用示例

function main(): void { const ctx = new EnemyContext(); const stateMachine = new StateMachine<EnemyContext>(ctx); ctx.stateMachine = stateMachine; // 模拟游戏循环 const fixedDeltaTime = 1 / 60; let elapsed = 0; const gameLoop = setInterval(() => { stateMachine.update(fixedDeltaTime); elapsed += fixedDeltaTime; // 模拟玩家靠近 if (elapsed > 1.0 && elapsed < 5.0) { ctx.playerPosition = { x: ctx.position.x + 3, y: ctx.position.y }; } if (elapsed >= 10.0) { clearInterval(gameLoop); console.log("模拟结束"); } }, fixedDeltaTime * 1000); }
2. 分层有限状态机(HFSM)

HFSM 通过将状态分组并层级化,解决 FSM 的状态爆炸问题。

2.1 核心类型定义
enum TransitionResult { /** 转移已处理,不需要冒泡到父状态 */ Handled = 0, /** 未处理,冒泡到父状态 */ Unhandled = 1, } interface IHState<C> { readonly name: string; enter(ctx: C): void; update(ctx: C, dt: number): void; exit(ctx: C): void; /** 处理转移,返回是否已处理 */ handleTransition(ctx: C): TransitionResult; /** 获取当前活跃的子状态(用于调试) */ getActiveChild(): IHState<C> | null; }
2.2 分层状态基类
abstract class HierarchicalState<C> implements IHState<C> { public readonly name: string; protected parent: HierarchicalState<C> | null = null; constructor(name: string) { this.name = name; } public setParent(parent: HierarchicalState<C>): void { this.parent = parent; } public enter(ctx: C): void { console.log(`[HFSM] 进入 ${this.name}`); } public update(ctx: C, dt: number): void { // 先让子状态处理 const child = this.getActiveChild(); if (child) { child.update(ctx, dt); } } public exit(ctx: C): void { console.log(`[HFSM] 离开 ${this.name}`); } public handleTransition(ctx: C): TransitionResult { // 先让子状态处理 const child = this.getActiveChild(); if (child) { const result = child.handleTransition(ctx); if (result === TransitionResult.Handled) { return TransitionResult.Handled; } } // 子状态未处理,当前状态处理 return this.onHandleTransition(ctx); } protected onHandleTransition(ctx: C): TransitionResult { return TransitionResult.Unhandled; } public getActiveChild(): IHState<C> | null { return null; } }
2.3 复合状态(带子状态机)
class CompositeState<C> extends HierarchicalState<C> { private children: Map<string, HierarchicalState<C>> = new Map(); private activeChild: HierarchicalState<C> | null = null; private initialStateName: string; constructor(name: string, initialStateName: string) { super(name); this.initialStateName = initialStateName; } public addChild(child: HierarchicalState<C>): this { child.setParent(this); this.children.set(child.name, child); return this; } public enter(ctx: C): void { super.enter(ctx); const initial = this.children.get(this.initialStateName); if (initial) { this.activeChild = initial; initial.enter(ctx); } } public update(ctx: C, dt: number): void { // 先检查转移 const result = this.handleTransition(ctx); if (result === TransitionResult.Handled) { return; } // 再更新 super.update(ctx, dt); } public exit(ctx: C): void { this.activeChild?.exit(ctx); this.activeChild = null; super.exit(ctx); } public getActiveChild(): IHState<C> | null { return this.activeChild; } /** 切换子状态 */ public changeChild(newChildName: string, ctx: C): void { const newChild = this.children.get(newChildName); if (!newChild) { console.warn(`[HFSM] 子状态 ${newChildName} 不存在于 ${this.name}`); return; } if (this.activeChild === newChild) return; this.activeChild?.exit(ctx); this.activeChild = newChild; newChild.enter(ctx); } }
2.4 战斗 AI 的 HFSM 实现
// --- 复合状态:非战斗 --- class NonCombatState<C extends EnemyContext> extends CompositeState<C> { constructor() { super("NonCombat", "Idle"); } protected onHandleTransition(ctx: C): TransitionResult { if (ctx.canSeePlayer()) { // 冒泡到根状态,由根状态处理到 Combat 的转移 return TransitionResult.Unhandled; } return TransitionResult.Unhandled; } } // --- 复合状态:战斗 --- class CombatState<C extends EnemyContext> extends CompositeState<C> { constructor() { super("Combat", "Chase"); } protected onHandleTransition(ctx: C): TransitionResult { if (!ctx.canSeePlayer() && !ctx.inAttackRange()) { // 冒泡,由根状态处理回 NonCombat return TransitionResult.Unhandled; } return TransitionResult.Unhandled; } } // --- 叶子状态:待机 --- class IdleLeaf<C extends EnemyContext> extends HierarchicalState<C> { private timer = 0; constructor() { super("Idle"); } public enter(ctx: C): void { super.enter(ctx); this.timer = 0; } public update(ctx: C, dt: number): void { this.timer += dt; // 转移由父状态处理 } protected onHandleTransition(ctx: C): TransitionResult { if (this.timer > 2.0) { const parent = this.parent as CompositeState<C>; parent.changeChild("Patrol", ctx); return TransitionResult.Handled; } return TransitionResult.Unhandled; } } // --- 叶子状态:巡逻 --- class PatrolLeaf<C extends EnemyContext> extends HierarchicalState<C> { constructor() { super("Patrol"); } public update(ctx: C, dt: number): void { // 巡逻移动逻辑 } } // --- 叶子状态:追击 --- class ChaseLeaf<C extends EnemyContext> extends HierarchicalState<C> { constructor() { super("Chase"); } public update(ctx: C, dt: number): void { // 追击移动逻辑 } protected onHandleTransition(ctx: C): TransitionResult { if (ctx.health < ctx.fleeThreshold) { // 在战斗复合状态内切换到逃跑 const parent = this.parent as CompositeState<C>; parent.changeChild("Flee", ctx); return TransitionResult.Handled; } if (ctx.inAttackRange()) { const parent = this.parent as CompositeState<C>; parent.changeChild("Attack", ctx); return TransitionResult.Handled; } return TransitionResult.Unhandled; } } // --- 根状态 --- class RootState<C extends EnemyContext> extends CompositeState<C> { constructor() { super("Root", "NonCombat"); } protected onHandleTransition(ctx: C): TransitionResult { const activeChild = this.getActiveChild(); // 全局转移:死亡 if (ctx.isDead()) { this.changeChild("Dead", ctx); return TransitionResult.Handled; } // NonCombat <-> Combat 切换 if (activeChild?.name === "NonCombat" && ctx.canSeePlayer()) { this.changeChild("Combat", ctx); return TransitionResult.Handled; } if (activeChild?.name === "Combat" && !ctx.canSeePlayer() && !ctx.inAttackRange()) { this.changeChild("NonCombat", ctx); return TransitionResult.Handled; } return TransitionResult.Unhandled; } }
2.5 HFSM 的优缺点

优点:

  • 大幅减少转移数量,解决状态爆炸

  • 保留 FSM 的低开销和显式控制

  • 支持转移冒泡,父状态可处理通用转移

缺点:

  • 调试复杂度高于纯 FSM

  • 可能出现高层与低层状态的语义冲突

3. 并行状态机 与 栈式状态机
3.1 并行状态机

并行状态机允许同时运行多个独立的状态机,处理不同维度的行为。

实现:

class ParallelStateMachine<C> { private machines: Map<string, StateMachine<C>> = new Map(); private context: C; constructor(context: C) { this.context = context; } public addMachine(name: string, machine: StateMachine<C>): void { this.machines.set(name, machine); } public getMachine(name: string): StateMachine<C> | undefined { return this.machines.get(name); } public update(deltaTime: number): void { for (const machine of this.machines.values()) { machine.update(deltaTime); } } }

使用示例:角色多维度行为

class CharacterContext { public health = 100; public isMoving = false; public isAttacking = false; public isCasting = false; public movementMachine!: StateMachine<CharacterContext>; public actionMachine!: StateMachine<CharacterContext>; } // 移动状态机:Idle / Walk / Run / Jump // 动作状态机:None / Attack / Block / Cast // 两个状态机并行运行,通过共享上下文协调

并行状态机适合复杂角色的行为组合,但需要注意:

  • 状态机之间的互斥规则(如攻击时不能移动)

  • 优先级冲突解决

  • 共享状态的同步

3.2 栈式状态机

用栈管理状态,支持 Push / Pop 操作,适合需要"返回"的场景。

class StackStateMachine<C> { private stack: IState<C>[] = []; private context: C; constructor(context: C) { this.context = context; } public get currentState(): IState<C> | null { return this.stack.length > 0 ? this.stack[this.stack.length - 1] : null; } /** 压入新状态,当前状态暂停 */ public push(state: IState<C>): void { this.currentState?.exit(this.context); this.stack.push(state); state.enter(this.context); } /** 弹出当前状态,恢复到上一个状态 */ public pop(): void { if (this.stack.length === 0) return; const state = this.stack.pop()!; state.exit(this.context); this.currentState?.enter(this.context); } /** 替换当前状态 */ public replace(state: IState<C>): void { if (this.stack.length > 0) { const old = this.stack.pop()!; old.exit(this.context); } this.stack.push(state); state.enter(this.context); } public update(deltaTime: number): void { this.currentState?.update(this.context, deltaTime); } }

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询