设计模式这个东西,在Unity项目里是个挺微妙的话题。你说它重要吧,很多小项目从头到尾就用了个单例,跑得也挺好;你说它不重要吧,面试必问,项目一复杂,代码腐化速度第一个收拾的就是你。尤其是“其他设计模式”这个部分,往往是很多人的知识盲区——单例、工厂这些用得勤,但观察者、命令、状态、备忘录、中介者这些,平时写业务代码很少主动去碰,真到要用的时候又不知道从哪儿下手。这篇就把Unity里那些“不常用但关键时刻能救命”的设计模式捋一遍,结合游戏开发的实际场景讲清楚它们的应用价值,顺便把常见坑也一并解决掉。
我用Unity做客户端开发有十来年了,大大小小的项目都趟过。说实话,设计模式这东西,纯粹背概念一点用都没有,必须放到具体场景里才能理解它在解决什么问题。比如说观察者模式,如果你没接过一个“几十个UI界面都要监听同一份数据变化”的需求,你就很难体会事件系统有多香。再比如说对象池模式,你要是没被频繁实例化特效卡到掉帧,也不会明白“复用”这两个字在游戏优化里的分量。
这篇文章主要适合这几类人看:刚学完C#和Unity基础、准备进阶的初中级开发者,被面试官问过“你用过哪些设计模式”但答不出花来的求职者,以及在项目里代码越写越乱、正琢磨怎么重构的客户端程序员。我会从实际项目视角出发,不仅讲每个模式是什么,更会重点拆解它在Unity里的实现方式、为什么用这种方式、以及哪些坑是文档里不会写的。
1. 内容整体设计与思路拆解
1.1 为什么Unity项目特别需要“其他设计模式”
很多Unity开发者对设计模式的理解停留在“单例模式是全局访问”“工厂模式是创建对象”这个层级。这种理解在面对具体需求时是撑不起来的,因为Unity本身有一套自己的生命周期和组件系统,直接套用传统的面向对象设计模式,有时候不仅不能解决问题,反而会制造新的麻烦。
举几个很典型的例子。观察者模式在纯C#项目里用事件委托很容易做,但放到Unity里就要考虑GameObject销毁后事件是否还持有引用,处理不好就是内存泄漏。命令模式在处理玩家输入时很好用,可以轻松实现撤销、重做和按键重映射,但如果不考虑Input System的异步回调,代码就会变得非常割裂。状态模式在写角色行为AI时几乎是必选项,但Unity的MonoBehaviour生命周期和状态切换之间怎么协同,很多人第一次写都会栽跟头。
这就是“其他设计模式”存在的意义——它是为应对复杂度而生的工具箱。单例解决的是“全局唯一”的问题,工厂解决的是“创建过程复杂”的问题,但一个真实游戏项目里,还有大量问题是关于“对象之间怎么通信”“行为怎么切换”“历史状态怎么保存”的。这些问题如果不用合适的模式去组织代码,随着功能增加,代码耦合度会指数级上升,最后变成改一行崩三处的地狱现场。
1.2 模式选择背后的逻辑:少即是多
实际开发中,最怕的就是为了用模式而用模式。我刚带团队那会儿,有个同事把项目里所有系统都提成了接口,每个接口都配了一个实现类,还搞了个服务定位器,看起来架构特别“干净”,结果加个新功能要改十几个文件,维护成本比不用模式还高。
所以我在这篇文章里不会把24种设计模式全讲一遍,那没有意义。我要讲的是在Unity项目里真正有使用场景、能直接提升代码质量的六种模式:观察者模式、命令模式、状态模式、对象池模式、备忘录模式、中介者模式。这些模式有一个共同特征——它们都是在应对游戏开发里特别常见的那几类复杂问题,而不是为了体现“面向对象”这个概念而存在的。
选这几个模式还有一层考量:它们之间有很强的互补性。观察者模式解决通信,命令模式解决操作记录,状态模式解决行为切换,对象池模式解决性能瓶颈,备忘录模式解决存档回溯,中介者模式解决多系统协作。组合起来用,基本能覆盖一个中型Unity项目里70%以上的架构设计需求。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 观察者模式:Unity事件系统的正确打开方式
2.1 事件驱动到底在解决什么问题
观察者模式的核心思想很简单:当一个对象的状态发生变化时,所有依赖它的对象都能收到通知。在Unity里最常见的实现方式就是事件委托、UnityEvent和C#的event关键字。
但如果你只知道这个定义,那你还是没理解它真正的价值。观察者模式解决的是“反向依赖”的问题——数据的持有者不需要知道谁在关心它的变化,关心它的人自己来订阅通知。这在UI系统里尤其重要,比如玩家的血量变化,可能有血条UI、伤害数字、成就系统、任务追踪、音效管理器等多个系统需要响应,如果让角色类直接持有这些系统的引用,耦合度直接爆炸。用事件的话,角色类只需要声明一个OnHealthChanged事件,谁想监听谁自己订阅,互不干扰。
2.2 三种事件实现方式的选型对比
在Unity里实现事件驱动,常用的方案有三种:
| 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| C# event | 语法简洁,性能好,支持 +=/-= | 无序列化能力,编辑器里不可视 | 纯逻辑层,非MonoBehaviour类之间的通信 |
| UnityEvent | 可在Inspector里拖拽绑定,支持持久化监听 | 有额外开销,调试困难,GC压力较大 | UI按钮点击等需要可视化配置的场景 |
| 委托+自定义事件中心 | 解耦彻底,全局可访问 | 滥用会导致架构混乱,定位问题难 | 跨系统广播,全局性的游戏事件 |
实际项目里我推荐混合使用:模块内部的通信用C# event,跨模块的广播用事件中心。UnityEvent尽量少用,只保留给UI按钮这种要在Inspector里配的场景。原因很简单,UnityEvent的性能在频繁调用的时候会有明显压力,而且断点调试的时候追踪起来特别费劲,一旦事件链复杂起来,根本不知道是哪一步掉的链子。
2.3 事件系统的内存泄漏陷阱
这个坑值得单独拎出来说。在Unity里用事件,最经典的内存泄漏场景就是:A对象订阅了B对象的事件,然后A被销毁了,但B还活着,B触发事件时就会调到一个已经销毁的对象的方法。如果A是MonoBehaviour还好,调一下== null判断还能发现;如果A是纯C#对象,这个过程完全是静默的,内存泄漏就这么积累下来了。
解决这个问题,核心思路是两条。第一条是做到“谁订阅谁退订”,在OnDestroy里把所有订阅关系都解除。第二条是善用WeakReference或者更现代的`IDis
