第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 包含三个核心要素 :

  1. 状态(State):系统可以处于的有限状况之一。在游戏中,可以是“站立”、“行走”、“攻击”、“受伤”等。同一时刻只能有一个状态处于激活状态。

  2. 事件(Event):触发状态转移的外部或内部刺激。可以是玩家的按键输入、定时器超时、与其他物体的碰撞、血量降到阈值等。

  3. 转移(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 状态与实体的交互规范

在设计状态类与实体类的交互时,需要遵循一些原则,避免循环依赖和职责混淆:

  1. 状态只读数据,不修改实体私有状态。实体暴露的公共属性应该只读,状态的修改通过调用实体的公共方法进行。

  2. 实体提供行为方法,不包含条件判断。比如 Move()Attack()PlayAnimation(),至于什么时候调用这些方法,由状态决定。

  3. 复杂计算放在实体中,状态保持轻量。比如寻路计算、伤害公式,应该放在实体或独立的服务类中,状态只调用结果。

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 会暴露出一些问题 :

  1. 状态爆炸:当状态数量超过 20 个时,状态之间的转移矩阵变得极其复杂,难以维护。比如一个角色有站立、行走、奔跑、跳跃、二段跳、攀爬、游泳、攻击、技能1、技能2… 等状态,两两之间的转移条件会成百上千。

  2. 状态复用困难:多个状态可能共享部分逻辑。比如“奔跑”和“行走”都受移动输入控制,都依赖地面检测,但这些逻辑无法复用,只能在两个状态里各写一遍。

  3. 历史状态丢失:从“攻击”状态切回时,应该回到攻击前的状态(可能是站立或行走),但经典 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 和技能系统。

在下一章中,我们将从行为组织转向数据处理,探讨如何在游戏中实现高效的数据管理。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐