Unity状态机的实现方法二
Unity状态机核心实现分析
回顾:
上一期投稿写了Unity中简单的状态机实现方式,但在项目实战中,枚举方法并不适用,通过不断选择判断语句,大大影响性能,在项目中,更常用的是通过实现接口方式的方式并多态出不同的状态类从而管理;
1. 状态接口
public interface IState
{
void Enter(); // 状态进入时调用,用于初始化状态相关的数据
void Update(); // 状态更新时调用,用于处理状态的主要逻辑
void Exit(); // 状态退出时调用,用于清理状态相关的数据
}
接口分析:
Enter()
:状态的入口点,用于设置初始状态,如初始化变量、播放动画等Update()
:状态的主循环,处理状态的核心逻辑,如检测输入、更新状态等Exit()
:状态的清理方法,用于重置变量、清除效果等
使用接口而不用父类继承的原因:
- 由于C#语言单继承多实现的特性,可能每个状态之间的需求不完全相同,所以继承父类并不合适
- 遵循单一职责原则(SRP)
- 当你使用接口时,添加新的状态或行为模式变得更加简单,无需修改现有的类结构。只需定义新接口并让相应的类实现它们即可。
2. 状态机演变过程
2.1 原始版本
public class StateMachine
{
private IState currentState;
public IState CurrentState => currentState;
public void Update()
{
currentState?.Update();
}
public void ChangeState(IState newState)
{
currentState?.Exit();
currentState = newState;
currentState.Enter();
}
}
原始版本分析:
-
核心功能
- 维护当前状态引用
- 提供状态更新机制
- 实现基本状态切换
-
局限性
- 每次切换都需要创建新状态实例
- 状态实例无法复用
- 类型安全性较弱
2.2 改进版本
public class StateMachine
{
private IState currentState;
private Dictionary<Type,IState> states = new Dictionary<Type,IState>();
public IState CurrentState => currentState;
public void Update()
{
currentState?.Update();
}
public void AddState<T>(T state) where T:IState
{
states[typeof(T)] = state;
}
public void ChangeState<T>() where T:IState
{
if (!states.ContainsKey(typeof(T)))
{
throw new Exception($"状态 {typeof(T).Name} 未注册");
}
currentState?.Exit();
currentState = states[typeof(T)];
currentState.Enter();
}
public bool HasState<T>() where T:IState
{
return states.ContainsKey(typeof(T));
}
public T GetState<T>() where T:IState
{
if (states.TryGetValue(typeof(T), out var state))
{
return (T)state;
}
return default;
}
}
改进版本分析:
-
状态管理增强
- 使用字典存储状态实例
- 支持状态的注册和查询
- 提供类型安全的状态访问
-
错误处理
- 检查状态是否已注册
- 提供状态存在性验证
- 安全的状态获取方法
-
接口完整性
- 状态注册:
AddState<T>
- 状态切换:
ChangeState<T>
- 状态查询:
HasState<T>
,GetState<T>
- 状态更新:
Update
- 状态注册:
3. 演变目的分析
3.1 状态管理的改进
- 原始版本需要每次创建新的状态实例
- 新版本使用字典缓存所有状态,避免重复创建对象
- 减少了内存开销和垃圾回收压力
3.2 类型安全
- 使用泛型约束
where T:IState
确保类型安全 - 避免了运行时类型错误
- 提供了更好的编译时检查
3.3 使用方式的优化
// 状态机初始化示例
void Start()
{
stateMachine.AddState(new PlayerNormalState(this));
stateMachine.AddState(new PlayerAttackState(this));
stateMachine.ChangeState<PlayerNormalState>();
}
3.4 状态切换的简化
- 原始版本:
stateMachine.ChangeState(new PlayerAttackState(player))
- 新版本:
stateMachine.ChangeState<PlayerAttackState>()
- 不需要每次都创建新的状态实例
- 代码更简洁,使用更方便
3.5 内存管理的改进
- 状态实例只在初始化时创建一次
- 通过字典重用状态实例
- 避免了频繁的对象创建和销毁
4. 改进的主要目的
4.1 性能优化
- 减少对象创建
- 降低内存压力
- 提高运行效率
4.2 代码质量提升
- 更好的类型安全
- 更清晰的状态管理
- 更简洁的API
4.3 维护性增强
- 状态集中管理
- 更容易调试和跟踪
- 代码结构更清晰
4.4 使用便利性
- 简化了状态切换操作
- 减少了使用时的代码量
- 降低了出错可能性
5. 使用建议
5.1 状态切换
// 正确的使用方式
stateMachine.ChangeState<PlayerAttackState>();
// 避免直接访问或修改当前状态
// stateMachine.CurrentState = new PlayerAttackState(player); // 错误
5.2 状态更新
// 在MonoBehaviour的Update中调用
void Update()
{
stateMachine.Update();
}
5.3 状态初始化
void Start()
{
stateMachine = new StateMachine();
stateMachine.AddState(new PlayerNormalState(this));
stateMachine.AddState(new PlayerAttackState(this));
stateMachine.ChangeState<PlayerNormalState>();
}
6. 高级用法与扩展
6.1 状态机与组件通信
// 状态基类
public abstract class BaseState : IState
{
protected readonly Character character;
protected BaseState(Character character)
{
this.character = character;
}
// IState 接口实现
public abstract void Enter();
public abstract void Update();
public abstract void Exit();
}
// 具体状态示例
public class PlayerAttackState : BaseState
{
public PlayerAttackState(Character character) : base(character) { }
public override void Enter()
{
character.animator.SetTrigger("Attack");
}
}
6.2 状态过渡条件
public class PlayerNormalState : BaseState
{
public override void Update()
{
// 检查状态转换条件
if (Input.GetButtonDown("Fire1"))
{
character.stateMachine.ChangeState<PlayerAttackState>();
}
}
}
7. 最佳实践总结
7.1 设计原则
-
单一职责
- 每个状态只处理一种行为模式
- 避免状态逻辑过于复杂
-
开闭原则
- 易于添加新状态
- 不修改现有状态逻辑
-
依赖倒置
- 状态依赖抽象接口
- 避免直接依赖具体实现
7.2 代码组织
-
状态分类
- 按功能模块组织状态
- 使用命名空间隔离不同类型状态
-
状态复用
- 抽象共同行为到基类
- 使用组合而非继承
7.3 测试策略
-
单元测试
- 测试状态转换逻辑
- 验证状态行为正确性
-
集成测试
- 测试状态机整体流程
- 验证状态间交互