状态模式

在Unity中使用C#实现状态模式(State Pattern),核心是将对象的不同状态封装为独立的状态类,让对象的行为随状态动态切换(替代大量if-else/switch判断)。结合游戏开发典型场景——游戏角色的状态机(Idle/Run/Attack/Hurt/Death),能直观体现状态模式“状态与行为解耦、扩展灵活”的核心特性。

一、核心思路(状态模式)

角色 对应实现类 作用
环境类(Context) PlayerStateMachine(玩家状态机) 维护当前状态,提供状态切换接口,转发行为请求到当前状态
抽象状态类 BasePlayerState(玩家基础状态) 定义所有状态的通用行为接口(Enter/Update/Exit),持有环境类引用
具体状态类 IdleState/RunState/AttackState/HurtState/DeathState 实现抽象状态接口,封装对应状态的具体行为
客户端 GameManager(挂载到Unity物体) 触发状态切换(如按键输入、受击事件)

二、完整代码实现

1. 枚举与抽象状态定义

1.1 玩家状态枚举
/// <summary>
/// 玩家状态枚举(标识不同状态)
/// </summary>
public enum PlayerStateType
{
    Idle,    // 闲置
    Run,     // 奔跑
    Attack,  // 攻击
    Hurt,    // 受击
    Death    // 死亡
}
1.2 抽象状态类:BasePlayerState
using UnityEngine;

/// <summary>
/// 玩家基础状态(抽象状态类)
/// </summary>
public abstract class BasePlayerState
{
    // 持有状态机引用(环境类)
    protected PlayerStateMachine stateMachine;
    // 状态类型
    public PlayerStateType StateType { get; protected set; }

    public BasePlayerState(PlayerStateMachine stateMachine, PlayerStateType stateType)
    {
        this.stateMachine = stateMachine;
        StateType = stateType;
    }

    /// <summary>
    /// 进入状态时执行(初始化逻辑)
    /// </summary>
    public abstract void EnterState();

    /// <summary>
    /// 状态帧更新(核心行为逻辑)
    /// </summary>
    public abstract void UpdateState();

    /// <summary>
    /// 退出状态时执行(清理逻辑)
    /// </summary>
    public abstract void ExitState();

    /// <summary>
    /// 处理状态切换请求(可重写实现状态校验)
    /// </summary>
    /// <param name="targetStateType">目标状态</param>
    /// <returns>是否允许切换</returns>
    public virtual bool CanSwitchState(PlayerStateType targetStateType)
    {
        // 死亡状态不可切换到其他状态
        if (stateMachine.CurrentState.StateType == PlayerStateType.Death)
            return false;
        // 受击状态可被攻击/死亡打断,其他状态可自由切换(可根据需求扩展)
        return targetStateType != PlayerStateType.Hurt || stateMachine.CurrentState.StateType != PlayerStateType.Attack;
    }
}

2. 环境类:PlayerStateMachine(玩家状态机)

using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 玩家状态机(环境类):管理所有状态,处理状态切换
/// </summary>
public class PlayerStateMachine : MonoBehaviour
{
    // 当前状态
    public BasePlayerState CurrentState { get; private set; }

    // 状态字典(缓存所有状态实例,避免重复创建)
    private Dictionary<PlayerStateType, BasePlayerState> _stateDict = new Dictionary<PlayerStateType, BasePlayerState>();

    // 玩家基础属性
    public int Hp = 100;
    public float MoveSpeed = 5f;

    private void Awake()
    {
        // 初始化所有状态并缓存
        _stateDict.Add(PlayerStateType.Idle, new IdleState(this, PlayerStateType.Idle));
        _stateDict.Add(PlayerStateType.Run, new RunState(this, PlayerStateType.Run));
        _stateDict.Add(PlayerStateType.Attack, new AttackState(this, PlayerStateType.Attack));
        _stateDict.Add(PlayerStateType.Hurt, new HurtState(this, PlayerStateType.Hurt));
        _stateDict.Add(PlayerStateType.Death, new DeathState(this, PlayerStateType.Death));
    }

    private void Start()
    {
        // 初始状态为闲置
        SwitchState(PlayerStateType.Idle);
    }

    private void Update()
    {
        // 帧更新当前状态
        CurrentState?.UpdateState();
    }

    /// <summary>
    /// 切换状态(核心方法)
    /// </summary>
    /// <param name="targetStateType">目标状态类型</param>
    public void SwitchState(PlayerStateType targetStateType)
    {
        // 校验状态是否可切换
        if (CurrentState != null && !CurrentState.CanSwitchState(targetStateType))
        {
            Debug.Log($"[状态机] 拒绝切换状态:{CurrentState.StateType} → {targetStateType}");
            return;
        }

        // 退出当前状态
        CurrentState?.ExitState();

        // 切换到新状态
        CurrentState = _stateDict[targetStateType];
        Debug.Log($"[状态机] 切换状态:{targetStateType}");

        // 进入新状态
        CurrentState.EnterState();
    }

    /// <summary>
    /// 模拟玩家受击
    /// </summary>
    /// <param name="damage">伤害值</param>
    public void TakeDamage(int damage)
    {
        if (CurrentState.StateType == PlayerStateType.Death) return;

        Hp -= damage;
        Debug.Log($"[玩家] 受到{damage}点伤害,剩余血量:{Hp}");

        if (Hp <= 0)
        {
            SwitchState(PlayerStateType.Death);
        }
        else
        {
            SwitchState(PlayerStateType.Hurt);
        }
    }
}

3. 具体状态实现(Idle/Run/Attack/Hurt/Death)

3.1 闲置状态:IdleState
using UnityEngine;

/// <summary>
/// 闲置状态(具体状态1)
/// </summary>
public class IdleState : BasePlayerState
{
    public IdleState(PlayerStateMachine stateMachine, PlayerStateType stateType) 
        : base(stateMachine, stateType) { }

    public override void EnterState()
    {
        Debug.Log("[闲置状态] 进入:播放闲置动画、停止移动");
        // Unity实际开发:播放闲置动画、重置移动速度
        // stateMachine.GetComponent<Animator>().Play("Idle");
    }

    public override void UpdateState()
    {
        // 监听输入:按下WASD切换到奔跑状态
        if (Input.GetAxisRaw("Horizontal") != 0 || Input.GetAxisRaw("Vertical") != 0)
        {
            stateMachine.SwitchState(PlayerStateType.Run);
        }
        // 监听输入:按下鼠标左键切换到攻击状态
        else if (Input.GetMouseButtonDown(0))
        {
            stateMachine.SwitchState(PlayerStateType.Attack);
        }
    }

    public override void ExitState()
    {
        Debug.Log("[闲置状态] 退出:停止闲置动画");
    }
}
3.2 奔跑状态:RunState
using UnityEngine;

/// <summary>
/// 奔跑状态(具体状态2)
/// </summary>
public class RunState : BasePlayerState
{
    public RunState(PlayerStateMachine stateMachine, PlayerStateType stateType) 
        : base(stateMachine, stateType) { }

    public override void EnterState()
    {
        Debug.Log("[奔跑状态] 进入:播放奔跑动画、开启移动");
        // stateMachine.GetComponent<Animator>().Play("Run");
    }

    public override void UpdateState()
    {
        // 移动逻辑
        float horizontal = Input.GetAxisRaw("Horizontal");
        float vertical = Input.GetAxisRaw("Vertical");
        Vector3 moveDir = new Vector3(horizontal, 0, vertical).normalized;
        stateMachine.transform.Translate(moveDir * stateMachine.MoveSpeed * Time.deltaTime);

        // 监听输入:松开方向键切换到闲置状态
        if (horizontal == 0 && vertical == 0)
        {
            stateMachine.SwitchState(PlayerStateType.Idle);
        }
        // 监听输入:按下鼠标左键切换到攻击状态
        else if (Input.GetMouseButtonDown(0))
        {
            stateMachine.SwitchState(PlayerStateType.Attack);
        }
    }

    public override void ExitState()
    {
        Debug.Log("[奔跑状态] 退出:停止奔跑动画");
    }
}
3.3 攻击状态:AttackState
using UnityEngine;

/// <summary>
/// 攻击状态(具体状态3)
/// </summary>
public class AttackState : BasePlayerState
{
    private float _attackDuration = 0.5f; // 攻击持续时间
    private float _timer;                 // 计时

    public AttackState(PlayerStateMachine stateMachine, PlayerStateType stateType) 
        : base(stateMachine, stateType) { }

    public override void EnterState()
    {
        Debug.Log("[攻击状态] 进入:播放攻击动画、触发攻击判定");
        // stateMachine.GetComponent<Animator>().Play("Attack");
        _timer = _attackDuration;
    }

    public override void UpdateState()
    {
        // 攻击持续计时
        _timer -= Time.deltaTime;
        if (_timer <= 0)
        {
            // 攻击结束后切回闲置状态
            stateMachine.SwitchState(PlayerStateType.Idle);
        }

        // 攻击状态不可被移动打断(CanSwitchState已限制)
    }

    public override void ExitState()
    {
        Debug.Log("[攻击状态] 退出:攻击动画结束、重置攻击CD");
    }

    // 重写:攻击状态仅允许被受击/死亡打断
    public override bool CanSwitchState(PlayerStateType targetStateType)
    {
        return targetStateType == PlayerStateType.Hurt || targetStateType == PlayerStateType.Death;
    }
}
3.4 受击状态:HurtState
using UnityEngine;

/// <summary>
/// 受击状态(具体状态4)
/// </summary>
public class HurtState : BasePlayerState
{
    private float _hurtDuration = 0.3f;
    private float _timer;

    public HurtState(PlayerStateMachine stateMachine, PlayerStateType stateType) 
        : base(stateMachine, stateType) { }

    public override void EnterState()
    {
        Debug.Log("[受击状态] 进入:播放受击动画、播放受击音效、触发无敌帧");
        // stateMachine.GetComponent<Animator>().Play("Hurt");
        _timer = _hurtDuration;
    }

    public override void UpdateState()
    {
        _timer -= Time.deltaTime;
        if (_timer <= 0)
        {
            // 受击结束切回闲置状态
            stateMachine.SwitchState(PlayerStateType.Idle);
        }
    }

    public override void ExitState()
    {
        Debug.Log("[受击状态] 退出:关闭无敌帧");
    }
}
3.5 死亡状态:DeathState
using UnityEngine;

/// <summary>
/// 死亡状态(具体状态5)
/// </summary>
public class DeathState : BasePlayerState
{
    public DeathState(PlayerStateMachine stateMachine, PlayerStateType stateType) 
        : base(stateMachine, stateType) { }

    public override void EnterState()
    {
        Debug.Log("[死亡状态] 进入:播放死亡动画、禁用移动、显示死亡UI");
        // stateMachine.GetComponent<Animator>().Play("Death");
        stateMachine.enabled = false; // 禁用状态机更新
    }

    public override void UpdateState()
    {
        // 死亡状态无帧更新逻辑
    }

    public override void ExitState()
    {
        // 死亡状态永不退出
        Debug.LogError("[死亡状态] 尝试退出死亡状态,操作无效!");
    }

    // 重写:死亡状态不允许切换到任何状态
    public override bool CanSwitchState(PlayerStateType targetStateType)
    {
        return false;
    }
}

4. 客户端调用:GameManager(Unity场景挂载)

using UnityEngine;

/// <summary>
/// 游戏管理器(客户端):模拟游戏事件触发状态切换
/// </summary>
public class GameManager : MonoBehaviour
{
    public PlayerStateMachine player; // 拖入玩家状态机物体

    private void Start()
    {
        // 模拟游戏流程:2秒后让玩家受击,5秒后再次受击致死
        Invoke(nameof(SimulateHurt), 2f);
        Invoke(nameof(SimulateDeath), 5f);
    }

    /// <summary>
    /// 模拟玩家受击
    /// </summary>
    private void SimulateHurt()
    {
        player.TakeDamage(30);
    }

    /// <summary>
    /// 模拟玩家死亡
    /// </summary>
    private void SimulateDeath()
    {
        player.TakeDamage(80);
    }

    // 测试:按下空格键手动触发攻击
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            player.SwitchState(PlayerStateType.Attack);
        }
    }
}

三、Unity场景配置与运行效果

1. 场景配置步骤

  1. 创建空物体命名为Player,挂载PlayerStateMachine脚本;
  2. 创建空物体命名为GameManager,挂载GameManager脚本,将Player拖入player字段;
  3. 运行场景,控制台输出如下(结合按键操作):
[状态机] 切换状态:Idle
[闲置状态] 进入:播放闲置动画、停止移动

// 按下WASD键
[闲置状态] 退出:停止闲置动画
[状态机] 切换状态:Run
[奔跑状态] 进入:播放奔跑动画、开启移动

// 松开WASD键
[奔跑状态] 退出:停止奔跑动画
[状态机] 切换状态:Idle
[闲置状态] 进入:播放闲置动画、停止移动

// 2秒后模拟受击
[玩家] 受到30点伤害,剩余血量:70
[闲置状态] 退出:停止闲置动画
[状态机] 切换状态:Hurt
[受击状态] 进入:播放受击动画、播放受击音效、触发无敌帧
[受击状态] 退出:关闭无敌帧
[状态机] 切换状态:Idle
[闲置状态] 进入:播放闲置动画、停止移动

// 按下空格键攻击
[闲置状态] 退出:停止闲置动画
[状态机] 切换状态:Attack
[攻击状态] 进入:播放攻击动画、触发攻击判定
[攻击状态] 退出:攻击动画结束、重置攻击CD
[状态机] 切换状态:Idle
[闲置状态] 进入:播放闲置动画、停止移动

// 5秒后模拟死亡
[玩家] 受到80点伤害,剩余血量:-10
[闲置状态] 退出:停止闲置动画
[状态机] 切换状态:Death
[死亡状态] 进入:播放死亡动画、禁用移动、显示死亡UI

四、Unity场景扩展(贴合实际开发)

1. 状态池优化(复用状态实例)

避免频繁创建状态对象,通过对象池复用:

public class PlayerStateMachine : MonoBehaviour
{
    // 状态池(复用状态实例)
    private ObjectPool<BasePlayerState> _statePool;

    private void Awake()
    {
        _statePool = new ObjectPool<BasePlayerState>(
            createFunc: (type) => 
            {
                switch (type)
                {
                    case PlayerStateType.Idle: return new IdleState(this, type);
                    case PlayerStateType.Run: return new RunState(this, type);
                    // 其他状态...
                    default: return null;
                }
            },
            actionOnGet: (state) => state.EnterState(),
            actionOnRelease: (state) => state.ExitState()
        );
    }

    // 切换状态时从池获取
    public void SwitchState(PlayerStateType targetType)
    {
        if (CurrentState != null)
        {
            _statePool.Release(CurrentState);
        }
        CurrentState = _statePool.Get(targetType);
    }
}

2. 状态参数配置(ScriptableObject)

将状态参数(如攻击时长、移动速度)封装为ScriptableObject,可视化配置:

[CreateAssetMenu(fileName = "PlayerStateConfig", menuName = "Game/PlayerStateConfig")]
public class PlayerStateConfig : ScriptableObject
{
    public float idleAnimSpeed = 1f;
    public float runSpeed = 5f;
    public float attackDuration = 0.5f;
    public float hurtDuration = 0.3f;
}

// 在状态机中引用配置
public class PlayerStateMachine : MonoBehaviour
{
    public PlayerStateConfig stateConfig; // 拖入配置文件

    private void Awake()
    {
        _stateDict.Add(PlayerStateType.Attack, new AttackState(this, PlayerStateType.Attack, stateConfig.attackDuration));
    }
}

// 攻击状态接收配置参数
public class AttackState : BasePlayerState
{
    private float _attackDuration;
    public AttackState(PlayerStateMachine stateMachine, PlayerStateType stateType, float attackDuration) 
        : base(stateMachine, stateType)
    {
        _attackDuration = attackDuration;
    }
}

3. 状态过渡动画(Animator融合树)

结合Unity Animator实现状态平滑过渡:

public class IdleState : BasePlayerState
{
    private Animator _animator;
    private int _idleHash = Animator.StringToHash("Idle");

    public IdleState(PlayerStateMachine stateMachine, PlayerStateType stateType) 
        : base(stateMachine, stateType)
    {
        _animator = stateMachine.GetComponent<Animator>();
    }

    public override void EnterState()
    {
        _animator.CrossFade(_idleHash, 0.1f); // 平滑切换动画
    }
}

4. 状态事件回调(解耦逻辑)

为状态添加事件回调,让外部系统监听状态变化:

public class BasePlayerState
{
    // 状态进入/退出回调
    public event Action OnStateEnter;
    public event Action OnStateExit;

    public override void EnterState()
    {
        OnStateEnter?.Invoke();
        // 原有逻辑...
    }

    public override void ExitState()
    {
        OnStateExit?.Invoke();
        // 原有逻辑...
    }
}

// 外部监听(如音效系统)
public class AudioSystem : MonoBehaviour
{
    private void Awake()
    {
        var player = FindObjectOfType<PlayerStateMachine>();
        player._stateDict[PlayerStateType.Attack].OnStateEnter += PlayAttackSound;
    }

    private void PlayAttackSound()
    {
        GetComponent<AudioSource>().PlayOneShot(attackClip);
    }
}

五、状态模式核心优势(Unity场景)

  1. 消除冗余判断:替代if (state == Idle) { ... } else if (state == Run) { ... }的嵌套逻辑,代码更清晰;
  2. 状态行为封装:每个状态的逻辑独立封装在对应类中,便于调试和维护(如修改攻击逻辑只需改AttackState);
  3. 灵活扩展:新增状态(如“跳跃”“闪避”)时,只需新增JumpState/DodgeState,无需修改原有状态和状态机代码,符合“开闭原则”;
  4. 状态切换可控:通过CanSwitchState统一控制状态切换规则,避免非法状态切换(如死亡后无法攻击)。

六、状态模式 vs 其他模式(Unity场景对比)

模式 核心差异 Unity适用场景
状态模式 状态封装为类,动态切换 角色状态机、AI行为树、UI状态管理
策略模式 算法封装为类,手动替换 攻击算法切换、移动方式切换
责任链模式 请求沿链传递,单一处理 伤害计算、输入事件处理

状态模式是Unity开发中角色/AI状态管理的首选模式,几乎所有动作游戏、RPG的角色状态机都会基于此模式实现,是解决“多状态行为切换”最优雅的方案。