第12章 角色的灵魂:有限状态机在复杂游戏AI与技能系统中的工业化实践
本章探讨了有限状态机(FSM)在游戏AI与技能系统中的工业化实践。FSM通过将角色行为分解为离散状态(如站立、行走、攻击)并明确定义状态转换条件,解决了传统if-else逻辑的四大痛点:逻辑扩散、状态转移混乱、代码复用困难和调试复杂度。文中详细介绍了FSM的三要素(状态、事件、转移)及其在商业项目中的价值,如降低40%的bug率。通过状态基类设计展示了完整生命周期管理(OnEnter/Update
第12章 角色的灵魂:有限状态机在复杂游戏AI与技能系统中的工业化实践
在游戏开发的世界里,角色的行为逻辑就像人的神经系统——它决定了角色何时移动、何时攻击、何时做出反应。如果你打开一个角色的 Update 方法,看到的是几十个布尔变量交织的 if-else 判断,那么恭喜你,你已经成功制造了一个逻辑炸弹。这个炸弹可能今天不炸,但当你试图添加一个新状态的时候,它会把你炸得怀疑人生。
有限状态机(Finite State Machine,FSM)正是为了解决这个问题而生的经典模式。它不是什么新鲜的技术,从上世纪八十年代开始就在游戏开发中广泛使用,直到今天,从超级马里奥到原神,FSM 依然是构建角色行为的基础架构 。它的核心思想极其简单:任何一个时刻,角色只能处于有限状态集合中的某一个状态,当特定事件发生时,从当前状态切换到另一个状态 。
但这简单的思想背后,有着从简单枚举到分层状态机的演进之路。本章从最基础的 FSM 基类设计开始,逐步构建一个支持状态生命周期、事件驱动、层次嵌套的完整框架,并通过 NPC AI 和角色技能系统两个实战案例,展示 FSM 在商业项目中的落地技巧。
12.1 FSM 的本质:为什么 if-else 会腐烂
12.1.1 if-else 式状态管理的困境
让我们先看一段典型的初学者代码:
public class Player : MonoBehaviour
{
public enum PlayerState { Idle, Walk, Run, Attack, Jump, Fall }
public PlayerState currentState;
private bool isGrounded;
private float verticalSpeed;
private void Update()
{
// 处理状态逻辑
if (currentState == PlayerState.Idle)
{
// 空闲逻辑
if (Input.GetKey(KeyCode.W))
{
currentState = PlayerState.Walk;
}
if (Input.GetKeyDown(KeyCode.Space) && isGrounded)
{
currentState = PlayerState.Jump;
verticalSpeed = 10f;
}
}
else if (currentState == PlayerState.Walk)
{
// 行走逻辑
// ... 几十行代码
if (!Input.GetKey(KeyCode.W))
{
currentState = PlayerState.Idle;
}
if (Input.GetKey(KeyCode.LeftShift))
{
currentState = PlayerState.Run;
}
}
else if (currentState == PlayerState.Run)
{
// 奔跑逻辑
// ... 又是几十行
}
// ... 其他状态
}
}
这段代码的问题随着状态增多而指数级恶化 :
问题一:逻辑扩散。每个状态的逻辑散落在巨大的 switch-case 或 if-else 块中,想要理解一个状态的完整行为,需要在几百行代码里上下翻找。
问题二:状态转移混乱。状态之间的转移条件穿插在代码各处,形成一张无形的蜘蛛网。添加一个新状态时,你永远不知道需要修改哪些地方的转移条件。
问题三:代码复用困难。多个状态共用的逻辑(比如播放动画、检测地面)被迫在每个状态里重复实现,或者用 helper 函数勉强提取,但依然混乱。
问题四:调试噩梦。当出现一个 bug(比如角色在空中还能攻击),你需要在数十个条件分支里猜测是哪个状态的处理出了问题。
12.1.2 FSM 的核心三要素
FSM 通过把状态封装成独立的类,从根本上解决了上述问题 。一个标准的 FSM 包含三个核心要素 :
-
状态(State):系统可以处于的有限状况之一。在游戏中,可以是“站立”、“行走”、“攻击”、“受伤”等。同一时刻只能有一个状态处于激活状态。
-
事件(Event):触发状态转移的外部或内部刺激。可以是玩家的按键输入、定时器超时、与其他物体的碰撞、血量降到阈值等。
-
转移(Transition):定义了从源状态到目标状态的路径,以及该转移触发时执行的动作。通常表示为:在状态 S 下,当事件 E 发生时,执行动作 A,然后切换到状态 S’ 。
用公式表达就是:State(S) x Event(E) -> Actions (A), State(S‘) 。
12.1.3 FSM 在商业项目中的价值
根据行业经验,采用 FSM 架构的项目在维护阶段能够减少约 40% 的 bug 率 。这不是因为 FSM 能自动生成正确逻辑,而是因为它强制实现了关注点分离——每个状态的代码物理隔离,状态转移逻辑集中在控制器中,行为逻辑分布在各个状态类里。这种结构使得:
- 可读性:要理解“攻击”状态的行为,只需要打开 AttackState.cs
- 可扩展性:增加新状态时,新建一个类,在控制器中注册转移条件,无需修改任何已有状态的代码
- 可测试性:可以单独实例化一个状态进行单元测试
- 协作性:多个程序员可以同时开发不同的状态,合并冲突的概率极低
12.2 FSM 基类设计:构建可复用的骨架
12.2.1 状态生命周期的定义
一个健壮的状态应该具备完整的生命周期。参考 Unity 的 MonoBehaviour 设计,我们为状态定义四个阶段 :
- OnEnter:进入状态时调用,适合初始化动画、重置计时器、播放音效
- OnUpdate:状态持续期间的每帧逻辑,适合处理移动、检测条件
- OnFixedUpdate:物理更新时调用,适合处理刚体受力
- OnExit:离开状态时调用,适合清理临时数据、停止动画
12.2.2 状态基类的实现
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 状态基类
/// 所有具体状态继承此类
/// </summary>
/// <typeparam name="T">拥有此状态的实体类型</typeparam>
public abstract class FSMState<T>
{
// 状态名称,用于调试
public string StateName { get; protected set; }
// 持有该状态的实体
protected T entity;
// 状态机引用,用于切换状态
protected FSM<T> fsm;
public FSMState(T entity, FSM<T> fsm, string stateName = null)
{
this.entity = entity;
this.fsm = fsm;
this.StateName = stateName ?? GetType().Name;
}
/// <summary>
/// 进入状态时调用
/// </summary>
public virtual void OnEnter()
{
#if UNITY_EDITOR
Debug.Log($"[FSM] 进入状态: {StateName}");
#endif
}
/// <summary>
/// 每帧更新
/// </summary>
public virtual void OnUpdate()
{
}
/// <summary>
/// 物理更新
/// </summary>
public virtual void OnFixedUpdate()
{
}
/// <summary>
/// 退出状态时调用
/// </summary>
public virtual void OnExit()
{
#if UNITY_EDITOR
Debug.Log($"[FSM] 退出状态: {StateName}");
#endif
}
/// <summary>
/// 处理自定义事件(可选)
/// </summary>
public virtual void OnEvent(string eventName, object eventData = null)
{
}
}
12.2.3 状态机控制器的实现
控制器负责维护当前状态、处理状态切换、转发生命周期调用:
using System;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 有限状态机控制器
/// </summary>
/// <typeparam name="T">拥有此状态机的实体类型</typeparam>
public class FSM<T>
{
// 当前状态
private FSMState<T> currentState;
// 所有状态的缓存(可选,用于防止频繁实例化)
private Dictionary<Type, FSMState<T>> stateCache = new Dictionary<Type, FSMState<T>>();
// 持有此状态机的实体
private T entity;
// 是否正在切换状态(防止递归)
private bool isTransitioning;
// 当前状态只读属性
public FSMState<T> CurrentState => currentState;
public Type CurrentStateType => currentState?.GetType();
public FSM(T entity)
{
this.entity = entity;
}
/// <summary>
/// 添加状态到缓存
/// </summary>
public void AddState(FSMState<T> state)
{
Type type = state.GetType();
if (!stateCache.ContainsKey(type))
{
stateCache[type] = state;
}
}
/// <summary>
/// 切换状态
/// </summary>
public void ChangeState<TState>() where TState : FSMState<T>
{
Type newStateType = typeof(TState);
ChangeState(newStateType);
}
/// <summary>
/// 切换状态(通过类型)
/// </summary>
public void ChangeState(Type newStateType)
{
if (isTransitioning)
{
Debug.LogWarning($"正在状态切换中,忽略 {newStateType.Name} 的切换请求");
return;
}
if (!stateCache.TryGetValue(newStateType, out FSMState<T> newState))
{
Debug.LogError($"状态 {newStateType.Name} 未注册到 FSM");
return;
}
// 如果已经在目标状态,忽略
if (currentState != null && currentState.GetType() == newStateType)
{
return;
}
isTransitioning = true;
// 退出当前状态
if (currentState != null)
{
currentState.OnExit();
}
// 切换状态
currentState = newState;
// 进入新状态
currentState.OnEnter();
isTransitioning = false;
}
/// <summary>
/// 更新当前状态
/// </summary>
public void Update()
{
if (currentState != null)
{
currentState.OnUpdate();
}
}
/// <summary>
/// 物理更新
/// </summary>
public void FixedUpdate()
{
if (currentState != null)
{
currentState.OnFixedUpdate();
}
}
/// <summary>
/// 发送事件到当前状态
/// </summary>
public void SendEvent(string eventName, object eventData = null)
{
if (currentState != null)
{
currentState.OnEvent(eventName, eventData);
}
}
/// <summary>
/// 清空状态缓存(通常用于销毁时)
/// </summary>
public void ClearCache()
{
stateCache.Clear();
}
}
12.2.4 状态转移的两种模式
在商业项目中,状态转移通常有两种实现方式,各有适用场景:
模式一:由状态自身决定转移。每个状态在自己的 Update 中检测条件,调用 fsm.ChangeState。这种模式直观简单,适合状态数量少、转移条件明确的场景 。
public override void OnUpdate()
{
// 检测到玩家,切换到追击状态
if (CanSeePlayer())
{
fsm.ChangeState<ChaseState>();
}
}
模式二:由控制器统一管理转移。将所有转移条件集中在一个地方(如实体类或独立的转移表)。这种模式适合状态数量多、转移复杂的场景,便于宏观掌控 。
// 在实体类的 Update 中
public void Update()
{
// 统一处理全局条件
if (health <= 0)
{
fsm.ChangeState<DeadState>();
return;
}
// 转发给当前状态
fsm.Update();
}
本章的示例采用第一种模式,因为它更直观,也符合大多数中小型项目的需求。
12.3 子类设计:从抽象到具体的状态实现
有了基类和控制器,我们来设计几个具体的状态类,感受一下 FSM 的代码组织方式。
12.3.1 空闲状态 IdleState
using UnityEngine;
/// <summary>
/// 空闲状态
/// 角色静止不动,等待输入
/// </summary>
public class IdleState : FSMState<PlayerController>
{
private float idleTimer;
private float idleAnimationInterval = 3f; // 每3秒播放一次空闲动画
public IdleState(PlayerController entity, FSM<PlayerController> fsm)
: base(entity, fsm, "空闲")
{
}
public override void OnEnter()
{
base.OnEnter();
// 重置计时器
idleTimer = 0f;
// 设置动画
entity.animator.SetBool("IsMoving", false);
entity.animator.SetBool("IsRunning", false);
// 停止移动
entity.StopMovement();
}
public override void OnUpdate()
{
base.OnUpdate();
// 检测移动输入
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
if (Mathf.Abs(horizontal) > 0.1f || Mathf.Abs(vertical) > 0.1f)
{
// 检测是否奔跑
if (Input.GetKey(KeyCode.LeftShift))
{
fsm.ChangeState<RunState>();
}
else
{
fsm.ChangeState<WalkState>();
}
return;
}
// 检测跳跃
if (Input.GetButtonDown("Jump") && entity.isGrounded)
{
fsm.ChangeState<JumpState>();
return;
}
// 空闲动画计时
idleTimer += Time.deltaTime;
if (idleTimer >= idleAnimationInterval)
{
entity.animator.SetTrigger("IdleSpecial");
idleTimer = 0f;
}
}
}
12.3.2 行走状态 WalkState
using UnityEngine;
/// <summary>
/// 行走状态
/// 角色以基础速度移动
/// </summary>
public class WalkState : FSMState<PlayerController>
{
private Vector3 moveDirection;
public WalkState(PlayerController entity, FSM<PlayerController> fsm)
: base(entity, fsm, "行走")
{
}
public override void OnEnter()
{
base.OnEnter();
// 设置动画
entity.animator.SetBool("IsMoving", true);
entity.animator.SetBool("IsRunning", false);
// 设置移动速度
entity.currentSpeed = entity.walkSpeed;
}
public override void OnUpdate()
{
base.OnUpdate();
// 获取输入
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
// 如果没有输入,回到空闲
if (Mathf.Abs(horizontal) < 0.1f && Mathf.Abs(vertical) < 0.1f)
{
fsm.ChangeState<IdleState>();
return;
}
// 检测奔跑
if (Input.GetKey(KeyCode.LeftShift))
{
fsm.ChangeState<RunState>();
return;
}
// 检测跳跃
if (Input.GetButtonDown("Jump") && entity.isGrounded)
{
fsm.ChangeState<JumpState>();
return;
}
// 计算移动方向
moveDirection = new Vector3(horizontal, 0, vertical).normalized;
moveDirection = entity.transform.TransformDirection(moveDirection);
// 应用移动
entity.Move(moveDirection * entity.currentSpeed * Time.deltaTime);
// 旋转角色朝向移动方向
if (moveDirection.magnitude > 0.1f)
{
Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
entity.transform.rotation = Quaternion.Slerp(
entity.transform.rotation,
targetRotation,
Time.deltaTime * entity.rotationSpeed
);
}
}
public override void OnExit()
{
// 退出时确保停止移动动画?基类已经做了一些,但这里可以加额外清理
base.OnExit();
}
}
12.3.3 跳跃状态 JumpState
using UnityEngine;
/// <summary>
/// 跳跃状态
/// 角色离开地面,有抛物线轨迹
/// </summary>
public class JumpState : FSMState<PlayerController>
{
private float jumpStartTime;
private Vector3 jumpStartPosition;
private bool hasReachedApex; // 是否达到最高点
public JumpState(PlayerController entity, FSM<PlayerController> fsm)
: base(entity, fsm, "跳跃")
{
}
public override void OnEnter()
{
base.OnEnter();
// 记录跳跃开始时间
jumpStartTime = Time.time;
jumpStartPosition = entity.transform.position;
hasReachedApex = false;
// 设置动画
entity.animator.SetBool("IsJumping", true);
entity.animator.SetTrigger("Jump");
// 应用向上速度
if (entity.rb != null)
{
entity.rb.velocity = new Vector3(
entity.rb.velocity.x,
entity.jumpForce,
entity.rb.velocity.z
);
}
}
public override void OnUpdate()
{
base.OnUpdate();
// 检测是否达到最高点(竖直速度接近0)
if (entity.rb != null && !hasReachedApex)
{
if (entity.rb.velocity.y <= 0.1f)
{
hasReachedApex = true;
entity.animator.SetBool("IsFalling", true);
}
}
// 检测落地
if (entity.isGrounded && entity.rb.velocity.y <= 0)
{
// 根据落地时的输入决定下一个状态
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
if (Mathf.Abs(horizontal) > 0.1f || Mathf.Abs(vertical) > 0.1f)
{
if (Input.GetKey(KeyCode.LeftShift))
{
fsm.ChangeState<RunState>();
}
else
{
fsm.ChangeState<WalkState>();
}
}
else
{
fsm.ChangeState<IdleState>();
}
}
}
public override void OnExit()
{
base.OnExit();
// 清理动画状态
entity.animator.SetBool("IsJumping", false);
entity.animator.SetBool("IsFalling", false);
}
}
12.4 实体类设计:状态机的载体
有了状态和控制器,我们需要一个实体来持有它们。这个实体类通常是一个 MonoBehaviour,负责初始化状态机、在 Update 中驱动它、并提供状态需要的公共方法和数据。
12.4.1 玩家控制器实现
using UnityEngine;
/// <summary>
/// 玩家控制器
/// 持有FSM,提供公共属性和方法
/// </summary>
public class PlayerController : MonoBehaviour
{
[Header("移动参数")]
public float walkSpeed = 3f;
public float runSpeed = 6f;
public float jumpForce = 8f;
public float rotationSpeed = 10f;
[Header("组件引用")]
public Animator animator;
public Rigidbody rb;
public Transform cameraTransform;
[Header("状态")]
public string currentStateName; // 仅用于调试显示
// 运行时变量
[HideInInspector] public float currentSpeed;
[HideInInspector] public bool isGrounded;
// 状态机
private FSM<PlayerController> fsm;
private void Awake()
{
// 确保必要组件存在
if (animator == null) animator = GetComponent<Animator>();
if (rb == null) rb = GetComponent<Rigidbody>();
// 初始化状态机
InitializeFSM();
}
private void InitializeFSM()
{
fsm = new FSM<PlayerController>(this);
// 创建并注册所有状态
fsm.AddState(new IdleState(this, fsm));
fsm.AddState(new WalkState(this, fsm));
fsm.AddState(new RunState(this, fsm));
fsm.AddState(new JumpState(this, fsm));
fsm.AddState(new FallState(this, fsm));
// 设置初始状态
fsm.ChangeState<IdleState>();
}
private void Update()
{
// 更新地面检测
CheckGrounded();
// 更新当前状态
fsm.Update();
// 更新调试信息
currentStateName = fsm.CurrentState?.StateName ?? "None";
}
private void FixedUpdate()
{
// 物理更新
fsm.FixedUpdate();
}
private void CheckGrounded()
{
// 简单的射线检测
isGrounded = Physics.Raycast(
transform.position + Vector3.up * 0.1f,
Vector3.down,
0.2f
);
}
/// <summary>
/// 提供给状态类调用的移动方法
/// </summary>
public void Move(Vector3 deltaPosition)
{
if (rb != null)
{
rb.MovePosition(transform.position + deltaPosition);
}
else
{
transform.position += deltaPosition;
}
}
/// <summary>
/// 停止移动
/// </summary>
public void StopMovement()
{
if (rb != null)
{
rb.velocity = new Vector3(0, rb.velocity.y, 0);
}
}
/// <summary>
/// 发送事件到状态机
/// </summary>
public void SendEvent(string eventName, object eventData = null)
{
fsm.SendEvent(eventName, eventData);
}
}
12.4.2 状态与实体的交互规范
在设计状态类与实体类的交互时,需要遵循一些原则,避免循环依赖和职责混淆:
-
状态只读数据,不修改实体私有状态。实体暴露的公共属性应该只读,状态的修改通过调用实体的公共方法进行。
-
实体提供行为方法,不包含条件判断。比如
Move()、Attack()、PlayAnimation(),至于什么时候调用这些方法,由状态决定。 -
复杂计算放在实体中,状态保持轻量。比如寻路计算、伤害公式,应该放在实体或独立的服务类中,状态只调用结果。
12.5 技能系统的 FSM 实现
FSM 不仅适用于角色的基础状态,也非常适合实现技能系统。一个技能通常有多个阶段:准备、释放、生效、冷却、被打断等,天然适合用状态机建模。
12.5.1 技能状态的设计
/// <summary>
/// 技能基类
/// 继承自FSMState,所以每个技能本质上是一个状态机
/// </summary>
public abstract class SkillBase : FSMState<SkillComponent>
{
protected SkillData data;
protected float skillTimer;
protected bool isFinished;
public SkillBase(SkillComponent entity, FSM<SkillComponent> fsm, SkillData data)
: base(entity, fsm, data.skillName)
{
this.data = data;
}
/// <summary>
/// 尝试释放技能(由外部调用)
/// </summary>
public virtual bool TryCast()
{
// 检查冷却
if (Time.time < data.lastCastTime + data.cooldown)
{
return false;
}
// 检查消耗
if (!entity.HasEnoughMana(data.manaCost))
{
return false;
}
return true;
}
}
/// <summary>
/// 技能数据(可配置)
/// </summary>
[System.Serializable]
public class SkillData
{
public string skillName;
public float cooldown = 3f;
public float manaCost = 10f;
public float castTime = 0.5f; // 吟唱时间
public float activeTime = 0.2f; // 伤害判定持续时间
public float effectRadius = 5f;
public int damage = 20;
[HideInInspector] public float lastCastTime = -100f;
}
12.5.2 具体技能:火球术的完整实现
using UnityEngine;
/// <summary>
/// 火球术技能
/// 三个阶段:吟唱 -> 飞行 -> 冷却
/// </summary>
public class FireballSkill : SkillBase
{
// 子状态定义(也可以单独写成类,这里为了紧凑使用内部枚举)
private enum FireballPhase
{
Casting,
Flying,
Cooling
}
private FireballPhase currentPhase;
private GameObject fireballProjectile;
private Vector3 targetPosition;
public FireballSkill(SkillComponent entity, FSM<SkillComponent> fsm, SkillData data)
: base(entity, fsm, data)
{
}
public override void OnEnter()
{
base.OnEnter();
// 进入吟唱阶段
currentPhase = FireballPhase.Casting;
skillTimer = 0f;
// 扣除消耗
entity.ConsumeMana(data.manaCost);
// 播放吟唱动画
entity.animator.SetTrigger("CastSpell");
// 显示吟唱特效
entity.PlayEffect("CastingEffect", data.castTime);
Debug.Log("开始吟唱火球术");
}
public override void OnUpdate()
{
base.OnUpdate();
skillTimer += Time.deltaTime;
switch (currentPhase)
{
case FireballPhase.Casting:
UpdateCasting();
break;
case FireballPhase.Flying:
UpdateFlying();
break;
case FireballPhase.Cooling:
UpdateCooling();
break;
}
}
private void UpdateCasting()
{
// 吟唱过程中可以被玩家移动打断
if (Input.GetAxis("Horizontal") != 0 || Input.GetAxis("Vertical") != 0)
{
// 打断技能
Debug.Log("技能被打断");
fsm.ChangeState<IdleSkillState>(); // 回到空闲技能状态
return;
}
// 吟唱完成
if (skillTimer >= data.castTime)
{
// 进入飞行阶段
currentPhase = FireballPhase.Flying;
skillTimer = 0f;
// 获取目标位置(玩家鼠标指向的位置)
targetPosition = entity.GetTargetPosition();
// 生成火球
fireballProjectile = entity.SpawnProjectile("Fireball", targetPosition);
Debug.Log("火球发射");
}
}
private void UpdateFlying()
{
if (fireballProjectile == null)
{
// 火球被销毁,直接进入冷却
currentPhase = FireballPhase.Cooling;
skillTimer = 0f;
return;
}
// 火球飞向目标
float remainingDistance = Vector3.Distance(
fireballProjectile.transform.position,
targetPosition
);
if (remainingDistance < 0.5f)
{
// 到达目标,爆炸
entity.ExplodeAt(targetPosition, data.effectRadius, data.damage);
// 销毁火球
GameObject.Destroy(fireballProjectile);
// 进入冷却
currentPhase = FireballPhase.Cooling;
skillTimer = 0f;
}
}
private void UpdateCooling()
{
// 冷却计时
if (skillTimer >= data.cooldown)
{
// 冷却结束,回到空闲状态
fsm.ChangeState<IdleSkillState>();
}
}
public override void OnExit()
{
base.OnExit();
// 记录施法时间,用于冷却计算
data.lastCastTime = Time.time;
// 清理可能残留的对象
if (fireballProjectile != null)
{
GameObject.Destroy(fireballProjectile);
}
}
}
/// <summary>
/// 技能空闲状态
/// </summary>
public class IdleSkillState : FSMState<SkillComponent>
{
public IdleSkillState(SkillComponent entity, FSM<SkillComponent> fsm)
: base(entity, fsm, "技能空闲")
{
}
public override void OnUpdate()
{
// 检测技能按键
if (Input.GetKeyDown(KeyCode.Q))
{
// 尝试施放火球术
SkillBase fireball = fsm.CurrentState as FireballSkill;
// 实际项目中应该从技能管理器中获取
}
}
}
12.5.3 技能组件:管理多个技能
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 技能组件
/// 管理多个技能的状态机
/// </summary>
public class SkillComponent : MonoBehaviour
{
[Header("技能配置")]
public SkillData[] skillDatas;
[Header("组件引用")]
public Animator animator;
public GameObject effectPrefab;
public GameObject projectilePrefab;
// 当前激活的技能
private FSM<SkillComponent> skillFSM;
private Dictionary<string, SkillBase> skills = new Dictionary<string, SkillBase>();
private void Awake()
{
skillFSM = new FSM<SkillComponent>(this);
// 创建技能
foreach (var data in skillDatas)
{
// 根据技能名称创建对应的技能实例
SkillBase skill = CreateSkill(data);
if (skill != null)
{
skills[data.skillName] = skill;
skillFSM.AddState(skill);
}
}
// 初始状态为技能空闲
skillFSM.AddState(new IdleSkillState(this, skillFSM));
skillFSM.ChangeState<IdleSkillState>();
}
private SkillBase CreateSkill(SkillData data)
{
// 简单的工厂模式
switch (data.skillName)
{
case "Fireball":
return new FireballSkill(this, skillFSM, data);
// 可以添加更多技能
default:
return null;
}
}
private void Update()
{
skillFSM.Update();
}
// 以下是提供给技能调用的公共方法
public bool HasEnoughMana(float cost)
{
// 从玩家属性获取
return true;
}
public void ConsumeMana(float cost)
{
// 扣蓝逻辑
}
public Vector3 GetTargetPosition()
{
// 获取鼠标指向的世界坐标
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
return hit.point;
}
return Vector3.zero;
}
public GameObject SpawnProjectile(string type, Vector3 targetPos)
{
// 实例化 projectile
return Instantiate(projectilePrefab, transform.position, Quaternion.identity);
}
public void PlayEffect(string effectName, float duration)
{
// 播放特效
}
public void ExplodeAt(Vector3 position, float radius, int damage)
{
// 爆炸伤害逻辑
Collider[] hits = Physics.OverlapSphere(position, radius);
foreach (var hit in hits)
{
if (hit.CompareTag("Enemy"))
{
// 造成伤害
}
}
}
}
12.6 FSM 的局限与进阶模式
12.6.1 经典 FSM 的三大局限
尽管 FSM 非常强大,但在大型项目中,经典的单层 FSM 会暴露出一些问题 :
-
状态爆炸:当状态数量超过 20 个时,状态之间的转移矩阵变得极其复杂,难以维护。比如一个角色有站立、行走、奔跑、跳跃、二段跳、攀爬、游泳、攻击、技能1、技能2… 等状态,两两之间的转移条件会成百上千。
-
状态复用困难:多个状态可能共享部分逻辑。比如“奔跑”和“行走”都受移动输入控制,都依赖地面检测,但这些逻辑无法复用,只能在两个状态里各写一遍。
-
历史状态丢失:从“攻击”状态切回时,应该回到攻击前的状态(可能是站立或行走),但经典 FSM 无法记住历史。
6.2 分层状态机:解决状态爆炸
分层状态机(Hierarchical FSM)通过引入状态的父子关系来解决状态爆炸问题 。子状态继承父状态的行为,当子状态无法处理某个事件时,由父状态处理。
/// <summary>
/// 移动基类状态
/// 所有与移动相关的状态继承此类
/// </summary>
public abstract class BaseMovementState : FSMState<PlayerController>
{
public BaseMovementState(PlayerController entity, FSM<PlayerController> fsm, string name)
: base(entity, fsm, name)
{
}
public override void OnUpdate()
{
// 处理所有移动状态通用的逻辑,比如地面检测
entity.CheckGrounded();
}
}
/// <summary>
/// 行走状态继承自移动基类
/// </summary>
public class WalkState : BaseMovementState
{
public WalkState(PlayerController entity, FSM<PlayerController> fsm)
: base(entity, fsm, "行走")
{
}
public override void OnUpdate()
{
base.OnUpdate(); // 先执行基类的通用逻辑
// 行走特有的逻辑
// ...
}
}
12.6.3 下推状态机:解决历史记录
下推状态机(Pushdown Automaton)使用栈来记录状态历史,常用于需要“返回上一个状态”的场景,比如射击游戏的开镜状态 。
/// <summary>
/// 下推状态机
/// </summary>
public class PushdownFSM<T>
{
private Stack<FSMState<T>> stateStack = new Stack<FSMState<T>>();
private T entity;
public PushdownFSM(T entity)
{
this.entity = entity;
}
/// <summary>
/// 压入新状态(暂停当前状态)
/// </summary>
public void PushState(FSMState<T> newState)
{
if (stateStack.Count > 0)
{
stateStack.Peek().OnPause(); // 需要状态类支持OnPause
}
stateStack.Push(newState);
newState.OnEnter();
}
/// <summary>
/// 弹出当前状态(返回上一个)
/// </summary>
public void PopState()
{
if (stateStack.Count > 0)
{
stateStack.Pop().OnExit();
}
if (stateStack.Count > 0)
{
stateStack.Peek().OnResume(); // 需要状态类支持OnResume
}
}
/// <summary>
/// 替换当前状态(不保留历史)
/// </summary>
public void ChangeState(FSMState<T> newState)
{
if (stateStack.Count > 0)
{
stateStack.Pop().OnExit();
}
stateStack.Push(newState);
newState.OnEnter();
}
}
应用场景:角色在奔跑时按下瞄准键,应该切换到瞄准状态,同时记住之前是奔跑状态;松开瞄准键时,回到奔跑状态。
12.6.4 并发状态机:同时运行多个状态
有些行为需要同时进行,比如角色可以一边奔跑一边攻击。并发状态机通过运行多个独立的状态机来实现 。
public class ConcurrencyFSM<T>
{
private List<FSM<T>> fsms = new List<FSM<T>>();
public void AddFSM(FSM<T> fsm)
{
fsms.Add(fsm);
}
public void Update()
{
foreach (var fsm in fsms)
{
fsm.Update();
}
}
}
实际项目中,可以有一个状态机控制移动(Idle/Walk/Run),另一个状态机控制动作(Idle/Attack/Skill),两者独立运行但共享同一个实体数据。
12.7 游戏案例分享:从简单到复杂
12.7.1 案例一:平台跳跃游戏的敌人 AI
在一个平台跳跃游戏中,我们需要实现一个经典的敌人:史莱姆。它的行为很简单:在平台上左右巡逻,遇到悬崖停止,发现玩家后跳跃追击。
public class SlimeAI : MonoBehaviour
{
public float patrolSpeed = 2f;
public float chaseSpeed = 4f;
public float jumpForce = 8f;
public float detectionRange = 5f;
private FSM<SlimeAI> fsm;
private Transform player;
private Rigidbody2D rb;
private bool facingRight = true;
private void Start()
{
player = GameObject.FindGameObjectWithTag("Player").transform;
rb = GetComponent<Rigidbody2D>();
fsm = new FSM<SlimeAI>(this);
fsm.AddState(new SlimePatrolState(this, fsm));
fsm.AddState(new SlimeChaseState(this, fsm));
fsm.ChangeState<SlimePatrolState>();
}
private void Update()
{
fsm.Update();
}
// 提供给状态的公共方法
public void Move(float speed, bool shouldJump = false)
{
rb.velocity = new Vector2(
(facingRight ? speed : -speed),
shouldJump ? jumpForce : rb.velocity.y
);
}
public void FlipDirection()
{
facingRight = !facingRight;
transform.localScale = new Vector3(
-transform.localScale.x,
transform.localScale.y,
transform.localScale.z
);
}
public bool DetectPlayer()
{
float distanceToPlayer = Vector2.Distance(transform.position, player.position);
return distanceToPlayer <= detectionRange;
}
public bool IsAtEdge()
{
// 检测前方是否有地面
RaycastHit2D hit = Physics2D.Raycast(
transform.position + Vector3.right * (facingRight ? 1 : -1) * 0.5f,
Vector2.down, 1f
);
return hit.collider == null;
}
}
// 巡逻状态
public class SlimePatrolState : FSMState<SlimeAI>
{
public SlimePatrolState(SlimeAI entity, FSM<SlimeAI> fsm) : base(entity, fsm, "Patrol")
{
}
public override void OnUpdate()
{
// 检测玩家
if (entity.DetectPlayer())
{
fsm.ChangeState<SlimeChaseState>();
return;
}
// 向前移动
entity.Move(entity.patrolSpeed);
// 检测边缘,调头
if (entity.IsAtEdge())
{
entity.FlipDirection();
}
}
}
// 追击状态
public class SlimeChaseState : FSMState<SlimeAI>
{
private float chaseTimer;
public SlimeChaseState(SlimeAI entity, FSM<SlimeAI> fsm) : base(entity, fsm, "Chase")
{
}
public override void OnEnter()
{
base.OnEnter();
chaseTimer = 0f;
}
public override void OnUpdate()
{
chaseTimer += Time.deltaTime;
// 如果失去玩家视野超过3秒,回到巡逻
if (!entity.DetectPlayer())
{
if (chaseTimer > 3f)
{
fsm.ChangeState<SlimePatrolState>();
}
return;
}
chaseTimer = 0f;
// 判断玩家在左边还是右边
float playerDirection = Mathf.Sign(entity.player.position.x - entity.transform.position.x);
// 如果需要调头
if ((playerDirection > 0 && !entity.facingRight) ||
(playerDirection < 0 && entity.facingRight))
{
entity.FlipDirection();
}
// 判断是否需要跳跃(遇到障碍)
bool needJump = Physics2D.Raycast(
entity.transform.position,
Vector2.right * playerDirection,
1f
);
// 追击
entity.Move(entity.chaseSpeed, needJump);
}
}
12.7.2 案例二:格斗游戏的角色连招系统
格斗游戏的连招系统是 FSM 的典型应用。每个招式是一个状态,通过精确的按键时机和帧数据实现连招。
public class FightingGameCharacter : MonoBehaviour
{
public Animator anim;
public float attackCooldown = 0.5f;
private FSM<FightingGameCharacter> fsm;
private void Start()
{
fsm = new FSM<FightingGameCharacter>(this);
fsm.AddState(new IdleFightingState(this, fsm));
fsm.AddState(new LightAttackState(this, fsm));
fsm.AddState(new HeavyAttackState(this, fsm));
fsm.AddState(new HitState(this, fsm));
fsm.ChangeState<IdleFightingState>();
}
private void Update()
{
fsm.Update();
}
public void TakeDamage(int damage)
{
// 受伤逻辑
fsm.ChangeState<HitState>();
}
}
// 轻攻击状态
public class LightAttackState : FSMState<FightingGameCharacter>
{
private float attackTimer;
private bool canCombo; // 是否可连招
public LightAttackState(FightingGameCharacter entity, FSM<FightingGameCharacter> fsm)
: base(entity, fsm, "LightAttack")
{
}
public override void OnEnter()
{
base.OnEnter();
attackTimer = 0f;
canCombo = false;
// 播放攻击动画
entity.anim.SetTrigger("LightAttack");
// 在动画的第3帧开启连招窗口(实际项目中通过AnimationEvent实现)
Invoke(nameof(EnableCombo), 0.1f);
}
private void EnableCombo()
{
canCombo = true;
}
public override void OnUpdate()
{
attackTimer += Time.deltaTime;
// 攻击动作持续0.5秒
if (attackTimer >= 0.5f)
{
fsm.ChangeState<IdleFightingState>();
return;
}
// 在连招窗口内再次按下攻击键,触发重攻击
if (canCombo && Input.GetButtonDown("Fire1"))
{
fsm.ChangeState<HeavyAttackState>();
}
}
}
12.7.3 案例三:开放世界 NPC 的日常行为
开放世界 NPC 需要处理复杂的日常行为:工作、吃饭、睡觉、社交等。通过分层状态机可以优雅地实现。
顶层状态: Alive (活着)
├─ Working (工作)
│ ├─ AtDesk (在工位)
│ ├─ WalkingToDesk (走去工位)
│ └─ UsingComputer (用电脑)
├─ Resting (休息)
│ ├─ Sitting (坐着)
│ └─ WalkingToRestArea (走去休息区)
├─ Eating (吃饭)
│ ├─ InCafeteria (在食堂)
│ └─ WaitingInLine (排队)
└─ Sleeping (睡觉)
└─ InBed (在床上)
这种结构的优点是:每个子状态只关心自己这一层的逻辑,公共逻辑(比如被玩家攻击)由顶层状态处理。
12.8 性能优化与调试技巧
12.8.1 对象池优化
在技能系统中,频繁创建和销毁技能状态对象会产生 GC 压力。解决方案是使用对象池复用状态实例:
public class SkillStatePool
{
private Dictionary<Type, Queue<SkillBase>> pools = new Dictionary<Type, Queue<SkillBase>>();
public T Get<T>() where T : SkillBase, new()
{
Type type = typeof(T);
if (!pools.ContainsKey(type))
{
pools[type] = new Queue<SkillBase>();
}
if (pools[type].Count > 0)
{
return (T)pools[type].Dequeue();
}
return new T(); // 实际项目中需要传入参数
}
public void Return(SkillBase skill)
{
Type type = skill.GetType();
pools[type].Enqueue(skill);
}
}
12.8.2 可视化调试工具
在开发过程中,能够看到当前状态和状态转移历史对调试非常有帮助:
public class FSMGizmo : MonoBehaviour
{
private FSM<MonoBehaviour> fsm;
private List<string> stateHistory = new List<string>();
private const int MaxHistory = 10;
public void RegisterFSM(FSM<MonoBehaviour> fsm)
{
this.fsm = fsm;
}
public void RecordStateChange(string from, string to)
{
stateHistory.Add($"{from} -> {to}");
if (stateHistory.Count > MaxHistory)
{
stateHistory.RemoveAt(0);
}
}
private void OnGUI()
{
if (fsm?.CurrentState == null) return;
// 在屏幕左上角显示当前状态
GUI.Label(new Rect(10, 10, 300, 30),
$"当前状态: {fsm.CurrentState.StateName}");
// 显示历史记录
for (int i = 0; i < stateHistory.Count; i++)
{
GUI.Label(new Rect(10, 40 + i * 20, 500, 20),
stateHistory[i]);
}
}
}
12.8.3 性能监控
通过记录每个状态消耗的时间,可以发现性能瓶颈:
public class PerformanceState : FSMState<MonoBehaviour>
{
private System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
public override void OnEnter()
{
base.OnEnter();
stopwatch.Reset();
}
public override void OnUpdate()
{
stopwatch.Start();
// ... 实际逻辑
stopwatch.Stop();
// 如果超过阈值,报警
if (stopwatch.ElapsedMilliseconds > 5)
{
Debug.LogWarning($"状态 {StateName} 耗时过长: {stopwatch.ElapsedMilliseconds}ms");
}
}
}
通过本章的学习,我们从零构建了一个完整的 FSM 框架,并通过 NPC AI、角色基础状态、技能系统、格斗连招等多个案例展示了它的应用。这套框架具备状态生命周期、泛型支持、事件处理等商业项目所需的核心特性,同时通过分层状态机、下推状态机等进阶模式,可以应对更复杂的场景需求。
FSM 的美妙之处在于,它把复杂的行为逻辑拆解成一个个孤岛,每个岛上的事情都很简单,岛与岛之间的联系通过明确的转移条件定义。这种“分而治之”的思想,正是软件工程对抗复杂性的核心武器。掌握了它,你就能构建出既健壮又灵活的 AI 和技能系统。
在下一章中,我们将从行为组织转向数据处理,探讨如何在游戏中实现高效的数据管理。
更多推荐



所有评论(0)