概述 入门系列中,我们把脚本挂到 GameObject 上,用 GetComponent 互相调用,这种模式在小项目中够用。但当项目膨胀到几十上百个脚本时,缺乏组织会导致耦合严重、难以维护。
架构模式 就是解决这个问题的——它们提供了一套可复用的代码组织范式,让系统间解耦、职责清晰、易于扩展。
本篇覆盖四个最常用的架构模式:
模式
解决的核心问题
Unity 中的典型场景
单例
全局唯一访问点
游戏管理器、音频管理器
事件总线
松耦合跨系统通信
UI 响应游戏逻辑、成就系统
状态机
有限状态切换
玩家状态(待机/移动/攻击/受伤)
命令模式
操作封装与可撤销
输入绑定、回放系统
1. 单例模式(Singleton) 1.1 基础实现 单例确保一个类只有一个实例,并提供全局访问点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class GameManager : MonoBehaviour { public static GameManager Instance { get ; private set ; } private void Awake () { if (Instance != null && Instance != this ) { Destroy(gameObject); return ; } Instance = this ; DontDestroyOnLoad(gameObject); } }
1 2 GameManager.Instance.StartGame();
1.2 泛型单例基类 项目中有多个管理器时,抽取基类避免重复:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public abstract class Singleton <T > : MonoBehaviour where T : Singleton <T >{ public static T Instance { get ; private set ; } protected virtual void Awake () { if (Instance != null && Instance != this ) { Destroy(gameObject); return ; } Instance = (T)this ; } } public class AudioManager : Singleton <AudioManager > { }public class ScoreManager : Singleton <ScoreManager > { }
1.3 非 MonoBehaviour 单例 对于纯 C# 类,使用更简洁的模式:
1 2 3 4 5 6 7 8 public class SaveSystem { private static readonly Lazy<SaveSystem> _instance = new (() => new SaveSystem()); public static SaveSystem Instance => _instance.Value; private SaveSystem () { } }
Lazy<T> 保证线程安全且延迟初始化。
1.4 注意事项
不要滥用 :单例本质上是全局变量,过度使用会让依赖关系不透明
测试困难 :单例难以 mock,建议用事件总线或 DI 替代跨系统通信
场景切换 :DontDestroyOnLoad 记得配合销毁逻辑,避免重复实例
2. 事件总线(Event Bus) 事件总线让 发布者 和 订阅者 完全解耦——发布者不知道谁在监听,订阅者不知道谁触发了事件。
2.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 27 28 29 30 public static class EventBus { private static readonly Dictionary<Type, Delegate> _handlers = new (); public static void Subscribe <T >(Action<T> handler ) where T : struct { if (_handlers.TryGetValue(typeof (T), out var existing)) _handlers[typeof (T)] = Delegate.Combine(existing, handler); else _handlers[typeof (T)] = handler; } public static void Unsubscribe <T >(Action<T> handler ) where T : struct { if (_handlers.TryGetValue(typeof (T), out var existing)) { var result = Delegate.Remove(existing, handler); if (result == null ) _handlers.Remove(typeof (T)); else _handlers[typeof (T)] = result; } } public static void Publish <T >(T eventData ) where T : struct { if (_handlers.TryGetValue(typeof (T), out var handler)) (handler as Action<T>)?.Invoke(eventData); } }
2.2 定义事件 1 2 3 4 5 6 7 8 9 10 11 public struct PlayerDiedEvent{ public GameObject Player; public string KillerTag; } public struct ScoreChangedEvent{ public int NewScore; public int Delta; }
2.3 订阅与发布 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class ScoreUI : MonoBehaviour { private void OnEnable () => EventBus.Subscribe<ScoreChangedEvent>(OnScoreChanged); private void OnDisable () => EventBus.Unsubscribe<ScoreChangedEvent>(OnScoreChanged); private void OnScoreChanged (ScoreChangedEvent e ) { _scoreText.text = $"分数: {e.NewScore} " ; } } public class Enemy : MonoBehaviour { private void OnDestroy () { EventBus.Publish(new ScoreChangedEvent { NewScore = ScoreManager.Instance.Score += 100 , Delta = 100 }); } }
2.4 何时使用
UI 响应游戏逻辑(分数变化、生命值变化)
成就/统计系统监听各种游戏事件
音频系统响应游戏状态
避免在频繁调用的性能敏感路径中使用(每帧 Update 中触发事件开销大)
明确的一对一调用用直接方法调用更清晰
3. 状态机(State Machine) Unity 开发中,对象的行为往往随状态变化——玩家待机时可以移动,攻击时不能。用 if-else 堆叠状态逻辑会让代码迅速腐烂,状态机 将每个状态封装为独立类。
3.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 27 28 public interface IState { void Enter () ; void Update () ; void FixedUpdate () ; void Exit () ; } public class StateMachine { public IState CurrentState { get ; private set ; } public void Initialize (IState startState ) { CurrentState = startState; CurrentState.Enter(); } public void ChangeState (IState newState ) { CurrentState?.Exit(); CurrentState = newState; CurrentState.Enter(); } public void Update () => CurrentState?.Update(); public void FixedUpdate () => CurrentState?.FixedUpdate(); }
3.2 玩家状态示例 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 public class PlayerIdleState : IState { private readonly Player _player; public PlayerIdleState (Player player ) => _player = player; public void Enter () { _player.Animator.Play("Idle" ); } public void Update () { if (_player.Input.MoveDirection != Vector2.zero) _player.StateMachine.ChangeState(new PlayerMoveState(_player)); if (_player.Input.IsAttackPressed) _player.StateMachine.ChangeState(new PlayerAttackState(_player)); } public void FixedUpdate () { } public void Exit () { } } public class PlayerMoveState : IState { private readonly Player _player; public PlayerMoveState (Player player ) => _player = player; public void Enter () { _player.Animator.Play("Run" ); } public void Update () { if (_player.Input.MoveDirection == Vector2.zero) _player.StateMachine.ChangeState(new PlayerIdleState(_player)); } public void FixedUpdate () { var velocity = _player.Input.MoveDirection * _player.MoveSpeed; _player.Rigidbody.velocity = new Vector2(velocity.x, _player.Rigidbody.velocity.y); } public void Exit () { } }
3.3 组合使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class Player : MonoBehaviour { public StateMachine StateMachine { get ; private set ; } public PlayerInput Input { get ; private set ; } public Animator Animator { get ; private set ; } public Rigidbody2D Rigidbody { get ; private set ; } public float MoveSpeed = 5f ; private void Awake () { StateMachine = new StateMachine(); Input = GetComponent<PlayerInput>(); Animator = GetComponent<Animator>(); Rigidbody = GetComponent<Rigidbody2D>(); } private void Start () { StateMachine.Initialize(new PlayerIdleState(this )); } private void Update () => StateMachine.Update(); private void FixedUpdate () => StateMachine.FixedUpdate(); }
每个状态类只关心自己该做的事,新增状态只需加新类、改转换条件,原有代码不动——开闭原则 的体现。
4. 命令模式(Command Pattern) 命令模式将 动作 封装为对象,支持撤销、队列、宏、回放等高级功能。
4.1 基础接口 1 2 3 4 5 public interface ICommand { void Execute () ; void Undo () ; }
4.2 具体命令 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 public class MoveCommand : ICommand { private readonly Transform _transform; private readonly Vector3 _direction; private readonly float _distance; private Vector3 _previousPosition; public MoveCommand (Transform transform, Vector3 direction, float distance ) { _transform = transform; _direction = direction; _distance = distance; } public void Execute () { _previousPosition = _transform.position; _transform.position += _direction * _distance; } public void Undo () { _transform.position = _previousPosition; } }
4.3 命令管理器 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 public class CommandManager { private readonly Stack<ICommand> _history = new (); private const int MaxHistory = 50 ; public void ExecuteCommand (ICommand command ) { command.Execute(); _history.Push(command); if (_history.Count > MaxHistory) { var temp = new Stack<ICommand>(_history.ToArray()[1. .]); _history.Clear(); foreach (var cmd in temp) _history.Push(cmd); } } public void UndoLast () { if (_history.TryPop(out var command)) command.Undo(); } }
4.4 在 Unity 中的典型应用
场景
说明
玩家操作撤销
移动、放置、建造等可撤销操作
回放系统
录制命令序列,重播实现录像
输入绑定
将按键映射为命令对象,方便配置
AI 决策队列
AI 排布一系列命令延迟执行
总结 四种模式各有侧重,实际项目中往往组合使用:
1 2 3 4 5 事件总线 ← 游戏管理器(单例) ↓ 状态机(玩家/敌人) ↓ 命令模式(输入处理/技能系统)
单例 :适合全局管理器,但控制数量
事件总线 :跨系统解耦通信的首选
状态机 :管理对象行为状态变迁
命令模式 :需要撤销/回放时使用
这些都是工具而非教条——根据场景选择最合适的模式,不要为了用模式而用模式。
下一篇将探讨 依赖注入与可编写脚本对象 ,进一步解耦代码组织。