Unity3D 进阶教程 - 01 高级架构模式

概述

入门系列中,我们把脚本挂到 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
// UI 面板订阅(在 OnEnable/OnDisable 中管理生命周期)
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
事件总线 ← 游戏管理器(单例)

状态机(玩家/敌人)

命令模式(输入处理/技能系统)
  • 单例:适合全局管理器,但控制数量
  • 事件总线:跨系统解耦通信的首选
  • 状态机:管理对象行为状态变迁
  • 命令模式:需要撤销/回放时使用

这些都是工具而非教条——根据场景选择最合适的模式,不要为了用模式而用模式。

下一篇将探讨 依赖注入与可编写脚本对象,进一步解耦代码组织。

ByteFisher
分享编程技术 · 记录钓鱼乐趣
扫码关注
▸ 扫码关注 ◂
分享: