1780 字
9 分钟
MVCS 实践方法论——如何运用一种改良于 MVC 的游戏开发架构
首先要说明一些事情:
框架不应该包罗万象。归根结底它不过是适用于某些场景的方法论,代表一种对具体业务的抽象思考。
实现更应该做的是在需求场景里对抽象做出合理阐释,而不是生搬硬套某些方法。
当然,以上只是我个人的理解,如果有相当稳固的流程规范,还是放下那点个人的执念吧。
架构总览
需要注意的是,该架构一般在数据条目足以提取,并且能够构建一个相当大小的 Model 时使用。也就是当状态需要稳定生命周期、多业务,或者有多个访问者的时候。
┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ ╌ 反馈可能形成下一次输入 ╌ ╌ 游戏循环自发推动状态 ╌ └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ ▼ ▲ 写路径:世界改数据 读路径:数据变显示输入源 View按钮点击 / SO 事件通道 / 碰撞 SetXxx 只写控件 │ ▲ ▼ │输入侧 Controller 显示侧 Controller翻译成意图;击退、切画布这类 把数据翻译成显示参数即时反应在这里编排 ▲ │ │ ▼ │Service │唯一写入口:跨聚合规则、生命周期 │ │ │ ▼ │Model ───────── 状态改变,发 C# 事件 ───────┘聚合内规则落地环的走法:一次输入沿写路径下行到 Model;Model 的事件从底部拐进读路径,上行把新状态画到 View;世界对新显示做出反应,产生下一轮输入——推动状态的不只玩家,还有战斗、时间、AI 和系统。
| 层 | 职责 | 形态 | 它知道什么 | 它不碰什么 |
|---|---|---|---|---|
| Model | 一个数据聚合的状态、聚合内规则、状态变化事件 | 纯 C# 类,一般情况下由service托管 | 自己的字段 | Unity 与其它一切层 |
| Service | 统一的写入口、建/销与存档生命周期、跨聚合规则 | mono单例组件 | 自己的 Model、别的 Service | 场景对象、UI 控件 |
| Controller(输入侧) | 订阅输入源,翻译成 Service 写调用,编排即时反应 | MonoBehaviour,挂在输入发生的实体或面板上 | 事件通道、Service、要编排的场景组件 | 显示细节、持久规则 |
| Controller(显示侧) | 订阅 Model 事件,把数据翻译成 View 的显示调用 | 纯 C# 类,由 View 创建并托管 | Model、View 的 SetXxx | 场景、输入 |
| View | 持控件引用;管显隐、焦点、显示生命周期;点击转发出去 | 面板的场景组件 | 自己的控件、自己的显示侧 Controller | Model、Service、订阅逻辑 |
各层职责
Service
- MVCS多出来的 S 是 Service,StrangeIoC 使用了这个命名和结构。它的作用类似于 Manager 类,但是更轻量,职责也更清晰。
- 不过这里有区别的是,原版的 Service 偏无状态的外部交互,比如网络、IO;这里的 Service 是域的写数据入口,并且持有 Model。
- 在这套实践里,Service 是统一的写入口,通常作为 Mono 单例;真正的写动作使用 Model里的方法完成 。
- Model 可能碰到多个Service或者事务的写入冲突,此时一次写必须原子地动两个聚合。
- 这里的事件管线主要占两段:跨系统的输入走 SO 事件通道,Model 的状态变化走 C# 事件。
- 事件是给”变了之后需要跟着变”的消费者的,显示用事件通知;规则执行用拉,要用时读取当前值,不订阅。
Model
- 状态与规则摘进普通 C# Model。拷贝边界有三处:配置模板只提供初始值,运行时状态由模板拷贝创建,运行时修改不反写配置,避免 SO 被改脏;持久化只传快照,一进一出各拷一次。运行时状态由 Model 持有并负责变更。
Model 的访问
- Model一般是由 Service 持有的纯 C# 类,并且由 Service 对外提供访问。数据只能通过 Model 的方法改动,对同一份数据不存在两条入口。
Controller
- Controller 担任翻译角色,把原始的数据流转换成真实的业务,并且调用对应的 Service 来完成任务。
输入侧 Controller
- 需要自行控制生命周期,用于翻译直接的玩家反馈,以及输入引发的数据变更。
- 向外通过 Service 调用正确的业务逻辑,这正是其翻译职责的体现。
显示侧 Controller
- 让 View 托管,生命周期直接和 View 的画布组相统一,因此可以做成纯 C# 类,能减少单例和 Mono 的使用。
- 订阅事件作为通知,收到后读取 Model 当前状态,转换成显示参数调用 View 的功能;事件不是显示层唯一的数据来源。
所有权和类的形态
综合来说,由于生命周期的需要,以及一些测试、解耦规范的便利,我对于所有权和类的形态有如下的实践方案。
Service 持有 Model
- 让 Service 持有 Model,并且 Model 跟随其生命周期:
- Model 可能涉及 SO 数据的载入和部分初始化,因此需要跟随一个 Mono 类。
- 再有就是就近原则,Service 类在模型里的流程紧贴 Model,不论是更新还是调用,因此成为了好的跟随选择。
View 持有显示侧 Controller
- 让 View 持有显示侧的 Controller 对象,并且跟随 View 生命周期:
- 随身携带的翻译,隐含构造时传入依赖的要求,也属于一种对解耦范式的推广。
场景中的 Mono 脚本
- Service 作为 Mono 单例挂载,Controller 直接通过单例调用处理事务。
- 输入侧 Controller 和 View 成为场景里挂载的 Mono 脚本:
- 前者依然是就近原则,在最靠近数据变化的地方,也就是场景内的游戏对象上挂载,把玩家的反馈传导到处理框架内,是一个入口。
- 后者则是框架对于玩家反馈的处理结果的呈现,同样直接挂到游戏对象上。
纯 C# 类的提取
- 提取 Model 和显示侧的 Controller 为纯 C# 类。业务请求走直接调用,事件只串联“状态变化到显示更新”这一段通知链:
- 作用就是脱离编辑器而能测试相当部分的核心逻辑,利用生命周期和纯 C# 的冲突这一限制来减少耦合产生,或者迫使开发者通过提取、拆分逻辑以达到上述目的。
与传统 MVC 的关系
就以上两部分的讨论而言,虽然在构造和生命周期管理上有部分的依赖,但是整体来说依然是符合传统的 MVC 范式的。毕竟游戏开发里的 MVC 是面向数据流的一种分层方法,而此处依旧遵循这一基本思想。
因而此处谈到的 MVCS,是为对接引擎的生命周期而在MVC基础上做出调整,或者说改进后的版本。
MVCS 实践方法论——如何运用一种改良于 MVC 的游戏开发架构
https://www.yonagi.world/posts/mvcs-architecture-methodology/ 部分信息可能已经过时
粤公网安备44011102484817号