游戏开发架构选型:第1篇|什么是架构?从进球事件看 MVC 的局限

游戏开发架构选型:第1篇|什么是架构?从进球事件看 MVC 的局限

同样是写一个"进球",为什么有的代码越改越崩溃,有的代码加功能像拼乐高?本文从三种主流架构的原理出发,用同一个足球游戏场景做实战对比,帮你理解"选架构"这件事到底选的是什么。


1 架构要解决的核心问题

写一个游戏功能不难。难的是这个功能在三个月后还能改得动。

游戏开发中,架构要解决的本质问题只有三个:

  1. 模块间如何通信?------记分牌更新了,音效怎么知道该播?摄像机怎么知道该切?
  2. 数据如何流转?------球员属性改了,比赛引擎要不要通知 UI?通知了 UI,UI 要不要再通知渲染层?
  3. 变化如何被隔离?------策划说"加一个成就系统",能不能只加一个文件而不动已有的 20 个文件?

三种主流架构------MVC、MVVM、事件驱动------分别给出了不同的答案。

需要说明的是,下面这些代码都来自一个真实跑起来的足球游戏项目------不是玩具示例,而是经过完整迭代的生产级代码。这套架构设计经历过从单机到联网、从原型到完整项目的完整验证。


2 场景引入:一个进球,五个模块同时响应

当一个球员在比赛中进球时,至少有五个模块需要同时响应:

模块 响应动作
记分牌面板 更新比分(主队 +1 / 客队 +1)
进球回放面板 显示进球球员头像和进球时间
音效系统 播放进球欢呼声
摄像机系统 切换到进球回放视角
比赛引擎 切换到 Freeze 暂停状态

如果这个需求都写不清楚,后面再加裁判播报、成就系统、比赛统计、队伍战术,项目很快就会变成"改一处炸一片"。


3 MVC 模式:分层的初心与现实的变异

MVC(Model-View-Controller)是游戏开发中最古老的架构思想之一。它的初衷非常朴素:把数据、显示、逻辑拆开。

三种角色各司其职:

角色 职责 举例
Model 持有数据,数据变时通知订阅者 HomeScore 从 1 变成 2
View 渲染显示,监听 Model 变化 记分牌 Text 从 "1" 变成 "2"
Controller 处理输入,把用户操作转为 Model 调用 玩家按下射门键

理想很美好,但 Unity 社区里落地 MVC 时,出现了两种截然不同的变体。理解它们的区别,比理解 MVC 本身更重要。

变体 A:上帝 Controller(Unity 社区最普遍的"伪 MVC")

在 Unity 里,很多人会把一个 MonoBehaviour 当成"总调度中心"------它持有所有模块的引用,任何事件发生时逐行调用:

痛点 :Controller 持有 5 个模块的具体类型引用。策划说"加个成就系统"------你得在 GoalMatchController 里加第六行代码,还得在 Inspector 拖一个新的引用。策划说"裁判播报也要响应进球"------再加第七行。Controller 越来越大,最终变成谁都不敢动的上帝类。

"上帝 Controller"的本质:把"谁响应什么"的决策逻辑硬编码在一个中心类里。这不是架构,这是把所有耦合集中到了一个地方。

变体 B:经典 MVC(Model → View 观察者模式)

回到 Smalltalk-80 定义的经典 MVC:Model 改变时通过观察者模式直接通知 View,Controller 只处理用户输入。这在 Unity 中的实现如下:

痛点:表面上看起来比上帝 Controller 好------Controller 瘦了。但实际上引入了新问题:

  • View 直接知道 Model 的类型------每个 View 都必须持有 MatchModel 引用,这和"Model 不依赖 View"的初衷矛盾(实际上变成了 Model ↔ View 双向知道)
  • Model 的 event 订阅者列表膨胀------当 50+ 个 View 都订阅 OnGoalScored,这个委托链就是一条串行 50 次的调用链,无法按优先级排队
  • Model 被动承担了调度职责------Model 的职责是比赛数据,它不应该区分"进球后摄像机干嘛 vs 音效的干嘛",但委托让它间接承担了调度逻辑------这本质上是把中枢从显式的 Controller 变成了隐式的 Model 内部委托

两种变体的本质区别

两者都没有真正解决耦合问题。区别只在于:耦合集中在一个类(上帝 Controller),还是散落在 Model 的委托链里(经典 MVC)


4 MVVM 模式:数据驱动的双向绑定

MVVM(Model-View-ViewModel)的核心思路是:在 View 和 Model 之间插入一个 ViewModel,通过数据绑定自动同步状态

ViewModel 持有 Observable<int> 这样的可观察属性,View 绑定到这些属性上。当 Model 的数据变化时,ViewModel 更新 Observable.Value,UI 自动刷新------不需要手动调用 scoreText.text = "1"

在 Unity 中落地 MVVM 需要自己造轮子,或者引入第三方库:

痛点:MVVM 在 WPF、Android、iOS 中非常成熟,但在 Unity 中面临几个现实困境:

  1. Unity 原生不支持数据绑定框架------得自己造 Observable<T>,或者引入 UniRx/R3 等第三方库,增加打包体积和学习成本
  2. 绑定关系变成"看不见的网"------改了某个字段会触发谁?靠 grep 找 .OnChanged += 调用链,比事件总线更难调试
  3. 不适合一次性触发场景------进球是一次性动作,但 Observable.Value = newVal 每次都触发 OnChanged,多了一层不必要的调度;比赛中的分支事件(普通进球 / 乌龙球 / 点球)需要给每个分支单独建字段,不如事件驱动发不同事件类型灵活

5 事件驱动:发布/订阅的松耦合哲学

事件驱动(Pub/Sub)的核心思想只有一句话:发布者只管发事件,订阅者各管各的,中间用一个事件总线中转

实战代码:一行 Trigger,各模块独立订阅

优势

  • MatchEngine 不知道 5 个订阅者是谁------它只知道"触发一次"
  • 新增订阅者零侵入:成就系统只需要新建一个 MonoBehaviour + 在 OnEnable 里加一行 EventManager.Subscribe<GoalScoredEvent>(...),MatchEngine 完全不用动
  • 解绑自然:每个 MonoBehaviour 在 OnDisableUnSubscribe,对象销毁即失联,不存在野指针或内存泄漏
  • 静态类型安全:泛型约束 where T : IBaseEvent 保证订阅者拿到的就是目标事件,IDE 跳转和重构都友好
  • 天然支持条件分支:PenaltyScoredEventOwnGoalEvent 各自只订阅自己关心的事件,互不干扰

发布者和订阅者互不知晓。 进球判定模块不知道自己触发的事件会被谁处理,记分牌模块不知道是谁触发了进球事件。

三种方案的耦合度对比

结语:架构不是为了"写得像样",而是为了"后面还能改得动"

如果你把一个进球事件看成一个真实的工程问题,就会发现:

  • 上帝 Controller 的问题,不是它"写得大",而是它"函数调用变成结构耦合"
  • 经典 MVC 的问题,不是它没有分层,而是它把事件调度压进 Model
  • MVVM 的问题,不是绑定不对,而是它在 Unity 里的隐式依赖太强
  • 事件驱动的优点,不是"很流行",而是它严格把"发射事件"和"处理事件"的职责分开了

下一篇,我们就继续往前:把 MVC、MVVM 和事件驱动放进一张表里做定量对比,再结合真实项目做一次架构取舍。

相关推荐
加多2 小时前
DeepSeek Harness 深度分析:用途、问题、架构原理与使用指南
人工智能·架构
小马过河R2 小时前
数据仓库入门:是什么、怎么做、和数据库有什么区别?
大数据·数据库·数据仓库·架构·驾驭工程·fde
火云牌神3 小时前
分层整洁架构:标准化工程目录结构,防范 AI 越界调用
人工智能·架构·ai编程·分层架构·vibecoding
SmalBox3 小时前
【节点】[PolarCoordinates节点]原理解析与实际应用
unity3d·游戏开发·图形学
Dr.kangder3 小时前
嵌入式面试总结(二十二)——指针
java·面试·职场和发展·架构·嵌入式
淼澄研学3 小时前
Python调用通义千问API实现长尾搜题内容自动化生成实战
前端·react.js·架构
AI枫林晚3 小时前
当 AI 突然“失忆“:DeepSeek Harness 的 Session Log 怎么做单一事实源
架构
刘立军4 小时前
依赖注入:禁止 AI 硬编码实例化,提升可测试性与扩展性
架构·ai编程
Dr.kangder4 小时前
嵌入式面试总结(二十一)——C语言关键字
c语言·开发语言·面试·职场和发展·架构·虚拟化