0x08. 状态模式
状态模式
在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. 场景配置步骤
- 创建空物体命名为
Player,挂载PlayerStateMachine脚本; - 创建空物体命名为
GameManager,挂载GameManager脚本,将Player拖入player字段; - 运行场景,控制台输出如下(结合按键操作):
[状态机] 切换状态: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场景)
- 消除冗余判断:替代
if (state == Idle) { ... } else if (state == Run) { ... }的嵌套逻辑,代码更清晰; - 状态行为封装:每个状态的逻辑独立封装在对应类中,便于调试和维护(如修改攻击逻辑只需改
AttackState); - 灵活扩展:新增状态(如“跳跃”“闪避”)时,只需新增
JumpState/DodgeState,无需修改原有状态和状态机代码,符合“开闭原则”; - 状态切换可控:通过
CanSwitchState统一控制状态切换规则,避免非法状态切换(如死亡后无法攻击)。
六、状态模式 vs 其他模式(Unity场景对比)
| 模式 | 核心差异 | Unity适用场景 |
|---|---|---|
| 状态模式 | 状态封装为类,动态切换 | 角色状态机、AI行为树、UI状态管理 |
| 策略模式 | 算法封装为类,手动替换 | 攻击算法切换、移动方式切换 |
| 责任链模式 | 请求沿链传递,单一处理 | 伤害计算、输入事件处理 |
状态模式是Unity开发中角色/AI状态管理的首选模式,几乎所有动作游戏、RPG的角色状态机都会基于此模式实现,是解决“多状态行为切换”最优雅的方案。