|
文/树袋熊 转载需和作者取得联系
虽然现在连主机游戏都纷纷加入了网战部分,不过其身份主要充当状态同步,矛盾点集中在同步即时性上。以大量数值逻辑为主的业务功能侧重点则不同。如果说写代码就是用状态的操作给问题建模,那么编程范式和设计模式种种的目的就是用一种稳健的方式管理状态和划分操作权。游戏的业务部分往往状态繁多,且有很多异步操作。这么多状态没有一个行之有效的管理模式代码就成一锅汤了。这里记录一下我最近的思考。
游戏业务的关注点无外乎主动 get 数据并呈现,主动把界面上的输入 set 到数据,数据改变时被动刷新回界面,这三点而已。那么很容易为游戏业务按照经典的 MVP (Model Presenter View) 模式来设计框架。
在笔者的设计中,View 即业务相关 GUI,这一层的实现仅关注如何简洁的构造 GUI 及相关效果,当然,没有哪个效果会和逻辑数据耦合。
Model 包含所有业务相关数据,只对 Presenter 提供 get,set,callback 注册接口。这一层诚如 DB,Presender 并不需要关心数据从哪儿来,当前又存储在哪儿。国内游戏开发圈经历了这么多年引擎用的越来越溜恐怕没人把渲染和逻辑搅在一起了,同理网络读写也应该封装在 Model 层内部。
Presenter 是业务逻辑代码所在,负责向 View 推数据做展示,并注册用户输入处理回调;还会从 Model 层主动 get,set 数据,并接收数据改变回调。
其中 Presenter 和 View 之间的绑定属于体力活,完全可以通过内部或外部 DSL 解决,这样会省下相当多的由数据生成并填充界面,或界面操作反映到数据的代码。伪代码如下:
- presenter = new Presenter()
- view = presenter.show("layout.dat")
- model = new Model("config.dat", network)
- presenter.bind(
- model["player/exp"],
- view["window/label"]
- )
- presenter.reg(
- model["game/fight"],
- view["window/button.clicked"]
- )
复制代码
如果需要根据数据动态创建对象,则需使用 GUI 模板:
- presenter.bindWithTemplate(
- model["player/props"].where(true),
- view["window/panel"],
- "prop_item_layout.dat"
- )
复制代码
既然被称为 DSL,就应该做到支持 where,orderby,group,join 等子句,甚至 precedure。Presenter 中会用一点点代码实现通用数据桥接器,负责被绑定到一起的 View 控件和 Model 值的转换和传递。对于注册到 Model 的 View 层动作,Presenter 同样桥接并传递给 Model。Model 中数据并不一定表示某个具象的实体,还可以是抽象的操作,比如一个对象表示“升级某个英雄”这一操作,在 DSL 中这样表示一次具体的升级操作:
- presenter.regWithObject(
- model["player/hero/upgrade_operation"],
- view["window/upgrade/button.clicked"],
- view["window/upgrade/invisible/id"]
- )
复制代码
当然,你可以选择像我这个例子中一样使用 View 层一个纯数据、不可见对象存储当前操作的英雄 ID,也可以在 DSL 内用一个变量保存,DSL 的制定灵活得多,完全没必要拘泥。
使用脚本实现的 DSL 叫做外部 DSL,相对的使用原生语言实现的 DSL 叫做内部 DSL,但无疑主流脚本的一类函数、动态性和内置异步支持会让 DSL 更具声明式特性。
业务逻辑的特点是有很多异步操作,MVP 模式不会把异步的发起者和执行者混到一起,仅专注解决异步的两个核心问题:异步回调,以及异步互斥。这很像是在谈论异步编程模型是不是,没错,游戏业务中的异步问题简化的多,这里不存在并发的情景。我们只是在业务层面使用异步搞定数据修改发起,数据修改执行,数据读(写)互斥这三个问题,这比在底层做大规模并发容易多了。在我的设计中,只有当在 View 层的下一步操作所依赖的 Model 层数据需要等待一个异步结果时才加锁,我们一开始就已经说明,数据流向有三种,get,set 和 callback,读写只会发生在 Model 层,那么不难想到完整的异步读写访问流程如下文所述。
Set 数据时先判断是否有未完成的 set (即是否上锁),如果有则忽略当前 set 操作,注意这时不需要对 View 层锁定操作;否则对数据进行设置并加锁,锁会对关联数据同时加锁,同理,set 前亦需对关联数据判断写入操作是否已被锁定。
Get 数据时判断是否上锁,有则异步等待(通常做法在 View 层转加载圆圈并禁止操作,但不是必需的,下文会做解释),没有锁直接取数据。
数据改变的 callback 由 Model 层的网络消息触发,这一行为有可能是因 set 操作引起的,也有可能是由远端机主动触发的。对于第一种情况,因为 set 时加了锁,所以这里需要解锁对应数据(及关联数据),然后调用数据改变 callback;第二种情况中只回调即可。
还是用英雄升级举例,假定升级英雄会消耗碎片和金币,Model 层的伪代码如下:
- class Hero {
- Async Level {
- get {
- if (_locked("level"))
- return Async(() => _level);
- return _level;
- }
- private set {
- _level = value;
- raiseChanged("level", _level);
- }
- }
- Async Gold {
- get {
- if (_locked("gold"))
- return Async(() => _gold);
- return _gold;
- }
- private set {
- _gold = value;
- raiseChanged("gold", _gold);
- }
- }
- Async ChipCount {
- get {
- if (_locked("chip_count"))
- return Async(() => _chipCount);
- return _chipCount;
- }
- private set {
- _chipCount = value;
- raiseChanged("chip_count", _chipCount);
- }
- }
- bool Upgradable {
- get {
- return Gold >= m && ChipCount >= n;
- }
- }
- void upgrade() {
- if (!lock("upgrade"))
- return;
- network.requestUpgrade();
- }
- void upgradeResponsed(Msg data) {
- Level = data.level;
- Gold = data.gold;
- ChipCount = data.chipCount;
- unlock("upgrade");
- }
- void goldChangedNotification(Msg data) {
- Gold = data.gold;
- }
- bool locked(string op) {
- if (op == "upgrade")
- return _locked("upgrade") || _locked("gold") || _locked("chip_count");
- }
- bool lock(string op) {
- if (locked(op))
- return false;
- if (op == "upgrade") {
- _lock("upgrade"); _lock("gold"); _lock("chip_count"); _lock("level");
- return true;
- }
- }
- void unlock(string op) {
- if (op == "upgrade") {
- _unlock("upgrade"); _unlock("gold"); _unlock("chip_count"); _unlock("level");
- }
- }
- }
复制代码
在 Presenter 层,get 数据和 callback 的伪代码如下:
- def getter(path)
- ret = model[path].get()
- if (ret is Async) then
- schedule(ret)
- else
- return ret
- end if
- end def
- def callback(path, value)
- raise(path, value)
- end def
复制代码
更进一步,甚至可以把 Model 层的读取时对锁的互斥去掉,立即返回本机的当前数据,在 Presenter 层也不需要对异步 getter 做延迟处理。有了写入锁,就可以保证对数据有写入的操作间的互斥,而且不阻碍玩家在 View 层忽略暂时被锁住的操作转而跳去做其他事情。没错,就像 SUPERCELL 的游戏做到的那样。除非你的界面上花了大把力气做了升级、升星、升品、道具获得等等特效,玩家非得看完才对得起你的诚意?
事情谈到这离完整的设计还差另一半,即在 Presenter 层用同步的风格写出异步的代码,这是写出清爽代码的重要因素,我能从网上找到大把各种语言的异步写法,得益于异步无刷新 Web 体验的兴趣,JavaScript 的各种技巧可能是最容易搜到的例子。而具体怎样实现依赖于你的 DSL 实现环境,以及个人风格喜好,用同步风格写异步操作的手感比零碎的函数干净多了。
|
|