设计模式详解(Unity + C# 方向)
一、什么是设计模式
设计模式(Design Pattern)是软件开发中对反复出现的设计问题的可复用解决方案。它不是具体的代码,而是一种"套路"——告诉你类和对象该怎么组织,才能写出可扩展、可维护、低耦合的代码。
它的理论源头是 1994 年"四人帮"(GoF)的《设计模式:可复用面向对象软件的基础》,书中总结了 23 种经典模式,分为三大类:
| 分类 | 解决什么问题 |
|---|---|
| 创建型(5 种) | 对象怎么"new"出来,创建过程与使用解耦 |
| 结构型(7 种) | 类和对象怎么"拼"在一起,组成更大的结构 |
| 行为型(11 种) | 对象之间怎么"交互",职责和算法怎么分配 |
学习设计模式之前,建议先记住背后的核心思想——SOLID 原则(单一职责、开闭原则、里氏替换、接口隔离、依赖倒置)。几乎所有设计模式都是在落实"开闭原则":对扩展开放,对修改关闭。
二、23 种 GoF 模式速览
创建型模式
| 模式 | 一句话说明 | Unity 场景举例 |
|---|---|---|
| 单例 Singleton | 保证全局只有一个实例 | GameManager、AudioManager |
| 工厂方法 Factory Method | 用方法封装对象的创建 | 根据类型生成不同敌人 |
| 抽象工厂 Abstract Factory | 创建"一族"相关对象 | 不同关卡主题的一整套怪物+场景 |
| 建造者 Builder | 分步骤构建复杂对象 | 角色创建器(捏脸系统) |
| 原型 Prototype | 通过克隆创建对象 | Instantiate(prefab) 本质就是原型 |
结构型模式
| 模式 | 一句话说明 | Unity 场景举例 |
|---|---|---|
| 适配器 Adapter | 接口不兼容时做转换 | 接入第三方 SDK 统一接口 |
| 桥接 Bridge | 抽象与实现分离,各自独立变化 | 渲染逻辑与平台分离 |
| 组合 Composite | 树形结构统一对待个体与整体 | 场景层级、UI 层级、技能树 |
| 装饰 Decorator | 动态给对象加功能 | 武器附魔、Buff 叠加 |
| 外观 Facade | 给复杂子系统一个简单入口 | 封装存档系统对外只暴露 Save/Load |
| 享元 Flyweight | 共享细粒度对象省内存 | 瓦片地图、大量树木/子弹共享数据 |
| 代理 Proxy | 控制对对象的访问 | 资源懒加载、网络代理 |
行为型模式
| 模式 | 一句话说明 | Unity 场景举例 |
|---|---|---|
| 责任链 Chain of Responsibility | 请求沿链传递直到被处理 | 事件冒泡、伤害结算流程 |
| 命令 Command | 把请求封装成对象 | 撤销/重做、输入录制回放 |
| 解释器 Interpreter | 定义语法并解释执行 | 技能脚本、对话 DSL |
| 迭代器 Iterator | 顺序访问集合元素 | C# 的 foreach / IEnumerator |
| 中介者 Mediator | 对象间通过中介通信,避免网状依赖 | UI 面板之间通过 UIManager 通信 |
| 备忘录 Memento | 保存和恢复状态 | 存档/读档、快照回滚 |
| 观察者 Observer | 一对多订阅通知 | 事件系统、成就系统 |
| 状态 State | 状态改变时行为改变 | AI 状态机、角色状态(跑/跳/攻击) |
| 策略 Strategy | 算法族可互换 | 武器攻击方式、寻路算法切换 |
| 模板方法 Template Method | 骨架固定,步骤留给子类 | 关卡流程基类 |
| 访问者 Visitor | 对结构中元素做操作而不改类 | 对场景中对象批量导出/统计 |
三、Unity 开发中最常用的模式 + 代码实战
理论说完,下面挑游戏开发中真正高频使用的模式,逐一给出可直接用的 Unity C# 实现。
1. 单例模式(Singleton)—— 使用频率最高
场景:GameManager、AudioManager、UIManager 这类全局唯一的管理者。
最实用的是泛型 MonoBehaviour 单例,任何管理器继承它即可:
using UnityEngine;
public class MonoSingleton<T> : MonoBehaviour where T : MonoSingleton<T>
{
private static T _instance;
public static T Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<T>();
if (_instance == null)
{
var go = new GameObject(typeof(T).Name);
_instance = go.AddComponent<T>();
}
}
return _instance;
}
}
protected virtual void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject); // 场景中已存在,销毁重复者
return;
}
_instance = (T)this;
DontDestroyOnLoad(gameObject); // 切场景不销毁
}
}
使用:
public class AudioManager : MonoSingleton<AudioManager>
{
public void PlaySFX(AudioClip clip)
{
// 播放音效……
}
}
// 任意地方调用:
AudioManager.Instance.PlaySFX(clip);
警告:单例是最容易被滥用的模式。满项目都是
XxxManager.Instance会形成隐式全局依赖,难以测试。只给真正的全局服务用。
2. 观察者模式(Observer)—— 解耦的利器
场景:玩家掉血 → UI 更新血条、播放受击音效、触发成就。如果让 Player 直接引用这三个系统,耦合就爆炸了。用事件订阅解决。
C# 原生就支持:event + Action。
using System;
using UnityEngine;
public class PlayerHealth : MonoBehaviour
{
public event Action<int, int> OnHealthChanged; // 当前血量, 最大血量
[SerializeField] private int maxHp = 100;
private int _hp;
private void Start()
{
_hp = maxHp;
OnHealthChanged?.Invoke(_hp, maxHp);
}
public void TakeDamage(int damage)
{
_hp = Mathf.Max(0, _hp - damage);
OnHealthChanged?.Invoke(_hp, maxHp); // 发布事件,谁关心谁订阅
}
}
订阅方(UI):
public class HealthBarUI : MonoBehaviour
{
[SerializeField] private PlayerHealth player;
[SerializeField] private UnityEngine.UI.Slider slider;
private void OnEnable() => player.OnHealthChanged += UpdateBar; // 订阅
private void OnDisable() => player.OnHealthChanged -= UpdateBar; // 必须退订!
private void UpdateBar(int hp, int max)
{
slider.value = (float)hp / max;
}
}
再进一步,可以做一个全局事件中心(EventBus),让完全不相干的系统也能通信:
using System;
using System.Collections.Generic;
public static class EventCenter
{
private static readonly Dictionary<string, Delegate> _table = new();
public static void Subscribe(string eventName, Action callback)
{
_table[eventName] = _table.TryGetValue(eventName, out var d)
? Delegate.Combine(d, callback)
: callback;
}
public static void Unsubscribe(string eventName, Action callback)
{
if (!_table.TryGetValue(eventName, out var d)) return;
var result = Delegate.Remove(d, callback);
if (result == null) _table.Remove(eventName);
else _table[eventName] = result;
}
public static void Publish(string eventName)
{
if (_table.TryGetValue(eventName, out var d))
(d as Action)?.Invoke();
}
}
// 使用:
EventCenter.Subscribe("GameOver", ShowGameOverPanel);
EventCenter.Publish("GameOver");
Unity 特色做法:还可以用
UnityEvent(可在 Inspector 里拖连线)或 ScriptableObject 事件,把事件本身做成资产文件,进一步降低代码依赖。
3. 状态模式(State)—— AI 与角色控制的核心
场景:敌人有"巡逻 → 追击 → 攻击 → 死亡"等状态。如果用一堆 if-else 和 bool 标志位写,状态一多就是灾难。
// 状态接口
public interface IState
{
void Enter();
void Tick(); // 每帧执行
void Exit();
}
// 状态机(纯 C# 类,不继承 MonoBehaviour,可复用)
public class StateMachine
{
public IState Current { get; private set; }
public void ChangeState(IState next)
{
Current?.Exit();
Current = next;
Current.Enter();
}
public void Tick() => Current?.Tick();
}
具体状态(以敌人 AI 为例):
public class PatrolState : IState
{
private readonly Enemy _enemy;
public PatrolState(Enemy enemy) => _enemy = enemy;
public void Enter() => _enemy.Animator.Play("Walk");
public void Tick()
{
_enemy.MoveAlongWaypoints();
if (_enemy.CanSeePlayer)
_enemy.FSM.ChangeState(new ChaseState(_enemy)); // 切换状态
}
public void Exit() { }
}
public class ChaseState : IState
{
private readonly Enemy _enemy;
public ChaseState(Enemy enemy) => _enemy = enemy;
public void Enter() => _enemy.Animator.Play("Run");
public void Tick()
{
_enemy.ChasePlayer();
if (_enemy.DistanceToPlayer < 2f)
_enemy.FSM.ChangeState(new AttackState(_enemy));
else if (!_enemy.CanSeePlayer)
_enemy.FSM.ChangeState(new PatrolState(_enemy));
}
public void Exit() { }
}
驱动者:
public class Enemy : MonoBehaviour
{
public StateMachine FSM { get; private set; } = new StateMachine();
private void Start() => FSM.ChangeState(new PatrolState(this));
private void Update() => FSM.Tick();
}
每加一个状态只是新增一个类,完全不改动旧代码——这就是开闭原则的体现。
4. 对象池(Object Pool)—— 性能优化必备
场景:子弹、特效、敌人尸体频繁 Instantiate/Destroy 会产生 GC 卡顿。对象池"用完归还、循环使用"。
Unity 2021+ 内置了 UnityEngine.Pool:
using UnityEngine;
using UnityEngine.Pool;
public class BulletSpawner : MonoBehaviour
{
[SerializeField] private Bullet bulletPrefab;
private ObjectPool<Bullet> _pool;
private void Awake()
{
_pool = new ObjectPool<Bullet>(
createFunc: () => Instantiate(bulletPrefab), // 池空时创建
actionOnGet: b => b.gameObject.SetActive(true), // 取出时激活
actionOnRelease: b => b.gameObject.SetActive(false), // 归还时隐藏
actionOnDestroy: b => Destroy(b.gameObject), // 超出容量销毁
defaultCapacity: 20,
maxSize: 100);
}
public void Fire(Vector3 pos, Vector3 dir)
{
Bullet b = _pool.Get();
b.transform.position = pos;
b.Launch(dir, onDead: () => _pool.Release(b)); // 子弹消亡时归还
}
}
5. 命令模式(Command)—— 撤销/重做、输入系统
场景:RTS 操作队列、棋类游戏悔棋、解谜游戏回退步数。把"一次操作"封装成对象,就能存起来、排队、撤销。
public interface ICommand
{
void Execute();
void Undo();
}
public class MoveCommand : ICommand
{
private readonly Transform _target;
private readonly Vector3 _from;
private readonly Vector3 _to;
public MoveCommand(Transform target, Vector3 to)
{
_target = target;
_from = target.position;
_to = to;
}
public void Execute() => _target.position = _to;
public void Undo() => _target.position = _from;
}
public class CommandManager
{
private readonly Stack<ICommand> _history = new();
public void Do(ICommand command)
{
command.Execute();
_history.Push(command);
}
public void Undo()
{
if (_history.Count > 0)
_history.Pop().Undo();
}
}
在 MonoBehaviour 里使用:
public class ChessController : MonoBehaviour
{
private readonly CommandManager _cmdManager = new();
public void MovePiece(Transform piece, Vector3 targetPos)
=> _cmdManager.Do(new MoveCommand(piece, targetPos));
private void Update()
{
if (Input.GetKeyDown(KeyCode.Z))
_cmdManager.Undo(); // 按 Z 悔棋
}
}
6. 工厂模式(Factory)—— 统一创建逻辑
场景:刷怪系统。调用方只说"我要一只骷髅",不关心它怎么初始化。
public enum EnemyType { Slime, Skeleton, Dragon }
public class EnemyFactory : MonoBehaviour
{
[SerializeField] private GameObject slimePrefab;
[SerializeField] private GameObject skeletonPrefab;
[SerializeField] private GameObject dragonPrefab;
public Enemy Create(EnemyType type, Vector3 position)
{
GameObject prefab = type switch
{
EnemyType.Slime => slimePrefab,
EnemyType.Skeleton => skeletonPrefab,
EnemyType.Dragon => dragonPrefab,
_ => throw new System.ArgumentOutOfRangeException(nameof(type))
};
var go = Instantiate(prefab, position, Quaternion.identity);
var enemy = go.GetComponent<Enemy>();
enemy.Init(); // 统一初始化入口(读配置表、绑定事件等)
return enemy;
}
}
更进阶的做法是结合 ScriptableObject 存怪物数据,工厂查表生成,新增怪物时零代码改动。
7. 策略模式(Strategy)—— 可插拔的算法
场景:同一个士兵可以拿剑(近战)、拿弓(远程)、用法杖(AOE)。攻击方式是可换的"算法"。
public interface IAttackStrategy
{
void Attack(Vector3 origin, Vector3 targetPos);
}
public class MeleeAttack : IAttackStrategy
{
public void Attack(Vector3 origin, Vector3 targetPos)
{
// 近战判定:检测 origin 前方扇形范围
Debug.Log("挥剑近战攻击!");
}
}
public class RangedAttack : IAttackStrategy
{
private readonly BulletSpawner _spawner;
public RangedAttack(BulletSpawner spawner) => _spawner = spawner;
public void Attack(Vector3 origin, Vector3 targetPos)
{
_spawner.Fire(origin, (targetPos - origin).normalized);
Debug.Log("射箭远程攻击!");
}
}
使用者运行时自由切换:
public class Soldier : MonoBehaviour
{
private IAttackStrategy _strategy;
[SerializeField] private Transform target;
public void EquipWeapon(IAttackStrategy strategy) => _strategy = strategy;
private void Update()
{
if (Input.GetMouseButtonDown(0))
_strategy?.Attack(transform.position, target.position);
}
}
新增一种攻击方式 = 新增一个实现
IAttackStrategy的类,士兵的代码一行不用改。
8. 组件模式(Component)—— Unity 的立身之本
Unity 引擎本身就是组件模式的教科书:
GameObject只是一个容器,本身没有任何行为;- 所有能力(渲染、碰撞、移动、音效)都是
Component——Rigidbody、Collider、AudioSource、你写的脚本…… - 想给物体加功能 = 组合组件,而不是继承出
MonsterWithFlyableAndShootable这种继承地狱。
这就是为什么官方反复强调"组合优于继承"。自己写代码时同理:把"血量""移动""背包"拆成独立组件挂到物体上,而不是写一个巨大的 CharacterBase 基类。
四、补充:游戏开发特有的模式
GoF 23 式之外,游戏圈还有一本经典《Game Programming Patterns》(Robert Nystrom),其中和 Unity 强相关的:
- 游戏循环(Game Loop):
Update()+FixedUpdate()就是; - 更新方法(Update Method):每帧逐对象更新;
- 双缓冲(Double Buffer):Unity 渲染管线的帧缓冲;
- 事件队列(Event Queue):解耦输入与处理时机;
- 服务定位器(Service Locator):单例的替代方案,用注册表查找服务,方便替换实现(比如真机/编辑器用不同存档服务)。
五、避坑指南:不要过度设计
- 先写能跑的代码,再重构出模式——模式是重构的终点,不是设计的起点。小项目里生搬抽象工厂只会增加复杂度。
- 单例别滥用,优先考虑依赖注入或 ScriptableObject 共享数据。
- 模式之间常组合使用:状态机里的状态可以用工厂创建,AI 的攻击可以注入策略,UI 用观察者监听数据。
- 判断标准很简单:如果加一个新需求(新怪物、新武器、新状态)时,你需要改动的旧代码越少,说明结构越好。
建议学习路径:单例 → 观察者 → 状态 → 对象池 → 策略/工厂 → 命令,这六个吃透了,Unity 项目里 90% 的架构问题都能应对。剩下的模式遇到具体场景时再翻出来查。