前言
做游戏开发时,我们经常遇到这样的场景:游戏从主菜单进入游戏,然后暂停,再返回游戏,最后回到主菜单。这背后就是状态机在工作。
传统写法用 if-else 或者 switch 来管理,代码量一多就乱成一团。本文要讲的状态机系统,解决了三个核心问题:
- 状态切换异步化 - 支持保存数据、加载资源等耗时操作
- 状态可回退 - 像浏览器后退按钮一样返回上一个状态
- 零配置使用 - 新增状态不需要注册,直接就能用
核心设计:泛型 + 多态
状态基类的设计
1 2 3 4 5 6 7 8 9 10 11
| public abstract class StateBase<T> where T : class { public string TypeName { get; set; } public bool AllowReentry { get; protected set; } public bool HaveExitTask { get; protected set; } public Action ExitFinishCall { get; set; }
public virtual void EnterState(int enterCount = 1, object parameters = null) { } public virtual void UpdateState() { } public virtual void ExitState() { } }
|
这个设计有几个关键点:
1. 泛型参数 T 的作用
T 看起来多余,其实是为了类型隔离。你可以同时存在多个独立的状态机系统:
1 2 3
| StateBase<GameStateBase> StateBase<CharacterState> StateBase<UIState>
|
三个系统互不干扰,各自管理自己的状态。
2. HaveExitTask 机制 - 核心创新
这是这个系统最巧妙的设计。先看问题:传统状态切换是这样的:
1 2
| ExitState() EnterState()
|
但实际开发中,ExitState 可能需要保存数据、上传服务器,这些操作是异步的。如果直接写成同步代码,要么阻塞主线程,要么状态切换不完整。
解决方案:
1 2
| public bool HaveExitTask { get; protected set; } public Action ExitFinishCall { get; set; }
|
状态管理器这样处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| if (currentState.HaveExitTask) { currentState.ExitFinishCall = () => { currentState = nextState; nextState.EnterState(); }; currentState.ExitState(); } else { currentState.ExitState(); currentState = nextState; nextState.EnterState(); }
|
使用时这样写:
1 2 3 4 5 6 7 8 9 10 11 12
| public class GamePauseState : GameStateBase { protected override bool DefaultHaveExitTask => true;
public override void ExitState() { SaveGameAsync(() => { ExitFinishCall?.Invoke(); }); } }
|
3. enterCount 参数 - 区分首次进入和重入
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public override void EnterState(int enterCount, object parameters) { if (enterCount == 1) { LoadGameWorld(); } else { ClearGameWorld(); LoadGameWorld(); } }
|
这个参数特别适合”重新开始游戏”的场景。
状态管理器:双泛型的妙用
CRTP 设计模式
1 2 3
| public class StateManagerBase<T, K> : MonoBehaviour where T : StateBase<T> where K : StateManagerBase<T, K>
|
为什么要两个泛型参数?
T 是状态类型,确保注册的状态类型正确。
K 是管理器类型,用了 CRTP(奇异递归模板模式):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public static K Instance { get { instance = FindObjectOfType<K>(); if (instance == null) { GameObject go = new GameObject(typeof(K).Name); instance = go.AddComponent<K>(); } return instance; } }
|
好处是:Instance 返回的是具体类型,不需要强制转换。
1 2
| GameStateManager.Instance.SetState<PlayingState>();
|
状态切换的核心流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| public void SetState<E>(object parameters = null) where E : StateBase<T> { if (_isTransitioning) return; if (!_statesDict.TryGetValue(name, out var nextState)) { nextState = CreateAndRegisterState<E>(); }
if (_currentState?.TypeName != nextState?.TypeName || nextState.AllowReentry) { _isTransitioning = true; if (_currentState != null) _stateStack.Push(_currentState); if (_currentState?.HaveExitTask == true) { _currentState.ExitFinishCall = () => { _currentState = nextState; nextState.EnterState(); _isTransitioning = false; }; _currentState.ExitState(); } else { _currentState?.ExitState(); _currentState = nextState; nextState.EnterState(); _isTransitioning = false; } } }
|
整个流程就像接力棒:当前状态完成退出任务后,通过回调通知管理器切换到新状态。
状态栈:像浏览器一样后退
1 2 3 4 5 6 7
| private Stack<StateBase<T>> _stateStack = new Stack<StateBase<T>>();
public void PopState(object parameters = null) { var prevState = _stateStack.Pop(); SetStateInternal(prevState, parameters); }
|
每个 SetState 都会把当前状态压栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 初始状态:Menu
SetState<Playing>() → 栈:[Menu],当前:Playing
SetState<Pause>() → 栈:[Menu, Playing],当前:Pause
SetState<Settings>() → 栈:[Menu, Playing, Pause],当前:Settings
PopState() → 栈:[Menu, Playing],当前:Pause
PopState() → 栈:[Menu],当前:Playing
PopState() → 栈:[],当前:Menu
|
这个机制特别适合多级菜单:主菜单 → 设置 → 音频 → 返回音频 → 返回设置 → 返回主菜单。
自动注册:反射的威力
传统方式需要手动注册每个状态:
1 2 3 4 5
| gameStateManager.RegisterState(new MenuState()); gameStateManager.RegisterState(new PlayingState()); gameStateManager.RegisterState(new PauseState());
|
这个系统用反射实现自动注册:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| private StateBase<T> CreateAndRegisterState<E>() where E : StateBase<T> { try { E stateInstance = (E)Activator.CreateInstance(typeof(E)); RegisterState(stateInstance); return stateInstance; } catch (Exception ex) { Debug.LogError($"创建状态失败: {ex.Message}"); return null; } }
|
状态只在第一次使用时创建,之后永久缓存在字典里:
1 2 3 4 5 6 7 8 9 10 11
| private Dictionary<string, StateBase<T>> _statesDict;
public void SetState<E>() { if (!_statesDict.TryGetValue(name, out var state)) { state = CreateAndRegisterState<E>(); _statesDict[name] = state; } }
|
性能对比:
- 反射创建:约 1ms(只在第一次)
- 字典查找:约 0.00001ms(O(1))
- 游戏运行时性能几乎无影响
编辑器工具:反射 + 缓存
自动查找所有状态类
编辑器工具需要知道项目中有哪些状态类。用反射扫描程序集:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public static Dictionary<Type, List<Type>> GetStateHierarchy() { var allTypes = GetAllUserTypes(); var stateBaseType = typeof(StateBase<>); var intermediateTypes = allTypes.Where(type => type.IsClass && !type.IsAbstract && type.BaseType?.IsGenericType == true && type.BaseType.GetGenericTypeDefinition() == stateBaseType ).ToList(); var hierarchy = new Dictionary<Type, List<Type>>(); foreach (var intermediateType in intermediateTypes) { var derivedTypes = allTypes.Where(type => type.IsSubclassOf(intermediateType) ).ToList(); hierarchy[intermediateType] = derivedTypes; } return hierarchy; }
|
扫描出来的结构是这样的:
1 2 3 4 5
| StateBase<T> └─ GameStateBase (中间类) ├─ MenuState ├─ PlayingState └─ PauseState
|
缓存优化
反射扫描所有程序集很慢(约 50-200ms),所以用了 30 秒缓存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| private static Dictionary<Type, List<Type>> _cachedHierarchy; private static DateTime _lastCacheTime = DateTime.MinValue; private static readonly TimeSpan CACHE_DURATION = TimeSpan.FromSeconds(30);
public static Dictionary<Type, List<Type>> GetStateHierarchy(bool useCache = true) { if (useCache && _cachedHierarchy != null && DateTime.Now - _lastCacheTime < CACHE_DURATION) { return _cachedHierarchy; } var hierarchy = ComputeHierarchy(); _cachedHierarchy = hierarchy; _lastCacheTime = DateTime.Now; return hierarchy; }
|
性能提升:
- 无缓存:50-200ms
- 有缓存:0.1ms
- 性能提升 显著
线程安全:双重检查锁定
单例模式用了双重检查锁定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| private static readonly object _lock = new object(); private static K instance;
public static K Instance { get { if (instance == null) { lock (_lock) { if (instance == null) { instance = FindObjectOfType<K>(); if (instance == null) { GameObject go = new GameObject(typeof(K).Name); instance = go.AddComponent<K>(); } DontDestroyOnLoad(instance.gameObject); instance.Init(); } } } return instance; } }
|
为什么要双重检查?
单次检查的话,两个线程可能同时通过第一次检查,导致创建两个实例。双重检查确保只创建一个实例,同时又保持高性能。
完整示例
看一个完整的使用场景:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| public class GameController : MonoBehaviour { void Start() { GameStateManager.Instance.SetState<MenuState>(); } }
public class MenuState : GameStateBase { public override void EnterState(int enterCount, object parameters) { Debug.Log("进入主菜单"); } }
public class PlayingState : GameStateBase { protected override bool DefaultAllowReentry => true; public override void EnterState(int enterCount, object parameters) { if (enterCount == 1) Debug.Log("首次进入游戏"); else Debug.Log("重入游戏(重新开始)"); } public override void UpdateState() { } }
public class PauseState : GameStateBase { protected override bool DefaultHaveExitTask => true; public override void ExitState() { Debug.Log("保存游戏数据..."); CoroutineScheduler.Instance.DelaySecondsExecute(1f, () => { Debug.Log("保存完成"); ExitFinishCall?.Invoke(); }); } }
void OnButtonClick() { GameStateManager.Instance.SetState<PlayingState>(); GameStateManager.Instance.SetState<PauseState>(); GameStateManager.Instance.PopState(); }
|
设计背后的思考
为什么用泛型而不是接口?
接口需要运行时类型检查,泛型可以在编译时检查类型。而且泛型基类可以提供默认实现,代码复用性更高。
为什么用单例而不是依赖注入?
Unity 生态中单例是主流,全局访问更方便。依赖注入需要额外的容器,学习曲线陡峭。
为什么用反射自动注册?
手动注册容易遗漏,每次新增状态都要修改代码。反射自动注册是”一次编写,永久使用”。
总结
这套状态机系统的核心价值:
- 异步切换 - 用回调机制优雅地处理异步状态转换
- 状态回退 - 用栈实现浏览器式的后退功能
- 零配置 - 用反射自动创建和注册状态
- 类型安全 - 用泛型在编译时检查类型
- 高效缓存 - 用 30 秒缓存将反射性能提升N倍
技术上,它结合了多个经典设计模式:状态模式、模板方法模式、工厂模式,通过泛型、反射、闭包等现代语言特性,将状态机这个经典模式提升到了新的高度。
实际项目中,这套系统大幅降低了状态管理的心智负担。新增一个状态只需要写一个类,不需要修改任何框架代码,符合开闭原则。调试时可以通过编辑器工具一目了然地看到所有状态结构,大大提升了开发效率。
状态机虽是老生常谈,但做对了细节,依然是游戏开发中不可或缺的基础设施。