« Lua 5.3.4 的一个 bug | 返回首页 | 日程表服务 »

MMORPG 的同步设计

前段时间有一个友商来了一个技术团队到我公司交流,主要是想探讨一下他们即将上线的一款 MMORPG 游戏在内部测试中发现的网络卡顿问题是否有好的解决手段。

经过了解,卡顿主要是在压力测试中表现出的网络消息流量过大造成的,又没有找到合适的方案减少流量。

关于 MMORPG 的网络部分的设计,我之前写过很多 blog 。最近两年写过这样两篇:MMORPG 客户端的网络消息框架如何只基于请求回应模式实现 MMO 级别的场景服务

昨天,我们自己的一个 MMORPG 项目中发生一个小 bug ,就这个 bug 我们做了一些讨论。我想借这个问题展开,在一个抽象层面谈谈我觉得 MMORPG 的网络同步应该怎样做。

具体 bug 是这样的:

在客户端登录后,服务器会推送个玩家背包的信息,客户端应根据这些信息初始化背包对象。

在场景中的某些掉落品会自动进入玩家的背包,客户端会根据这类信息修改背包的状态。

一般情况下,推送玩家背包信息的网络包一定会先抵达。但是由于一些有关断线自动重连的设计,导致了后一种掉落品信息包抢先在了初始化过程之前。

固然,这个 bug 有简单的解决方案。比如在 UI 系统中 model 和 view 完全分离(我们现在的确是这样做的),而 model 应该先由默认值初始化,不必等待登录完成再触发背包的初始化流程。

但我认为,应该在一个更高的抽象层面来看待这个问题。


物品掉落自动进入背包这条消息是服务器推送过来的,但是这条推送到底意味着什么?

如果是一个银行的客户端。当你的账户下发生了转账行为,别人向你转了一笔钱,如果你刚刚查询过账户余额,那么转账信息抵达后,是否应该由客户端将转账事件中的金额和上次查询的余额合并,自动计算出新的余额?

通常银行客户端软件绝对不会这样做,只会提供一个余额查询的功能。用户收到转账信息后,会再次查询。

这个例子或许并不恰当,账户转账发生并不频繁,用户对余额的关心也没有太多的实时性需求。但是它可以说明一件事情:事件的发生、即使事件会导致状态的变化是双方的共有知识、它和状态的变化本质上还是两件事。

对于网络游戏来说,玩家需要了解虚拟世界中发生的事件,也需要了解和他相关的对象的当前状态,这其实是两个不同的需求。


固然,如果依靠一些预先设计好的规则,客户端和服务器可以达成共识,根据虚拟世界中发生的事件,以及对象的初始状态,就可以同步对象的当前状态。很多网络游戏都基于这个方法设计,但我们应该认清本质:事件传达和状态同步是两件事,之所以选择这样的做法,其实是一种优化手段。因为我们只需要传达事件,省去了状态同步的网络流量。这个方法能成立,基于的是事先部署好的规则共识,以及事件的完整性。

MMORPG 类型的游戏的不同点在于,MMO 的世界很大,几乎不可能做全状态同步,也不可能传达世界中的所有事件。每个客户端都只关心整个世界的极小部分。所以 MMORPG 的设计中,几乎不会采用完全的初始状态 + 所有事件 的方法来同步世界状态。

也就是一部分事件仅仅只用来传达字面的意义,比如“ A 用 X 技能攻击了 B 造成了 Y 点伤害” 这种消息,可能仅用来做视觉表现以及文字 log 在屏幕上传达给玩家,而不会真的和服务器一样执行完全一样的逻辑(在 MOBA 类游戏中,参与人数较少,则可能采用完全逻辑去计算)。比如这个 X 技能导致 B 不能移动,很可能是单独的消息通知的,这样可以避免服务器和客户端由于了解的信息不对等而产生差异。

很多设计者对 “事件通知” 和 “状态同步” 这两个问题认识模糊,导致设计上含混不清。有的地方利用 “事件通知” 去计算状态变化,有的地方又单独设计状态同步协议,是很多 bug 的根源。


我的观点是:应当把 事件通知状态同步 在设计层次严格分离。所有事件通知的网络包都是可以丢弃和乱序的,它仅作为客户端的视觉呈现使用;而状态同步则是应该根据客户端的实际需求来严格同步,客户端处于什么状况,需要关心哪些对象,多少对象,根据这些需要来同步 MMO 世界中的一个子集。这些需要同步的对象既包括玩家自身的数值、背包,也包括了玩家所处的场景,他附近的其他玩家和 NPC ,还可以包括聊天频道信息、任务、排行榜、拍卖行信息。

例如,玩家受伤(HP 减少),自动拾取物件,这些都是事件,如果客户端收到这些事件,可以做出对应的动画表现;而当前玩家的 HP ,背包里有些什么物品则属于状态同步管理的范畴。我们应该使用合适的同步策略来做。可以是客户端推断出状态会发生变化去查询一次和上个已知版本的差异,也可以是向服务器订阅对应的对象的状态差异变化,还可以是简单的请求对象的全量状态数据。采取何种同步方案就属于设计和优化细节了。


如果因为网络流量问题,我们需要做出限制的话,事件通知是可以按信息优先级丢弃或推迟送达的。怎样丢弃可以由设计人员判断信息丢失后会给玩家客户端的视觉表现造成怎样的影响。即使全部丢弃了,客户端也不应该出错,并保持基本可玩。

状态同步应该在一个更高的抽象层面设计实现。所谓同步,无非是将服务器上的一个数据结构复制到客户端。最粗笨的方法是客户端提起请求,服务器全量返回序列化后的数据结构。最早期的 web 服务都是这样做的,相当于玩家不断的在一个他关心的页面按 F5 刷新页面。在这个基础上,优化是必须做的。

我们有很多方法来做差异更新,我在 “如何只基于请求回应模式实现 MMO 级别的场景服务” 一文中提出过我的方案。这未必是唯一的方法。不依赖客户端请求,服务器直接推送差异也是 MMORPG 用的比较多的方法。我认为这些都是针对状态同步的具体优化,可以结合具体情况具体考量。基于客户端请求和服务器直接推送的区别仅在于:基于客户端请求需要消耗更多的客户端上行流量、而服务器直接推送则需要消耗服务器额外的开销去维护客户端到底需要同步哪些信息。至于同步延迟的问题,两者没有什么区别。因为即使是基于客户端请求,也可以提前发起请求,只在状态变化的时候服务器再回应;而不必等到客户端需要时再请求状态差异。

例如大多数 MMORPG 服务器都会在服务器计算玩家周围的对象,并在周围对象移动时推送这些移动信息。我个人认为在流量有限的时候,在场景中树立若干灯塔,灯塔管理灯塔范围内的玩家 id,再由客户端主动查询灯塔要好一些。可以避免服务器在计算每个玩家周围的对象时陷入一个 O(n^2) 的复杂度,也能回避无脑推送巨量的消息。玩家可以跟踪一个灯塔的状态,然后再根据一个玩家列表,进一步选择同步有限几个玩家的坐标(不同设备和不同网络条件下能同时观测的对象数量是不同的)。这样可以更加有效。

回到文初的 bug 。由于断线重连问题导致物品掉落自动拾取和背包初始化乱序的问题。那个自动拾取的消息推送就只会显示一段拾取动画、在屏幕上显示一行 log 文字,最多触发一次背包状态更新的查询,而不会去操作一个未初始化的背包对象了。

Comments

req/resp 模式下实时性很难保证吧,比如mmo中aoi的同步,客户端发起灯塔查询的话如何进行校验也是一个问题

不错

订阅推送本身也是分“订阅”和“推送”双向的。

我主张的是,在大多数情况下,让订阅对应一次“推送”然后就自动取消订阅更好。

订阅/推送/取消订阅 这才是增加了实现复杂度,且并没有简化和提高有效性,也谈不上在“面对和处理大量信息”有什么优势。

从实现者的心理上谈:我需要什么你就给我什么,比起我不知道你会给我什么信息,等收到了再过滤更省心和可靠一些。

本篇再一次表达了@cloud喜欢客户端查询而非推送的状态同步理念,
但订阅推送是现实的一个普遍的模型,它仍然是客户端行为,它解决的问题是使得个体能够更有效地面对和处理大量信息,
毕竟对于现实的个体来说,接受服务并过滤是要比主动请求琐碎事件是一件更省心的事,

非常赞成。
有在项目中推行这种理念,可惜效果不明显,最主要原因还是人都是偏懒得,一些原则做着做着就不遵守了。

好办法,可行。
做项目的时候,状态同步的部分尽可能封装好,尽可能统一。
事件通知的部分就和动画效果一样,不做也不会对逻辑造成影响,只要完成系统时候补齐就行。

状态独立于事件的思路很棒,我们的系统也采用了类似的机制来确保终端能够稳定地显示最新的资金、持仓状态。
状态独立更新的另一个好处在于这个过程是机械化的、可抽象的,开发者不必为此编写额外的代码,从而也避免了编码过程中的非技术性失误,提升稳定性。

正文我已经解释了:采取什么实现策略都可以做到相同的延迟。

怎么及时同步属于细节设计以及实现问题。

我觉得这个想法会不会太「理想化」了 从程序设计的角度上来看这样很干净 但从用户的角度来看如果收到了一个事件通知却发现状态还没有同步就不怎么合理了

不知道您对这个问题怎么看? 

Post a comment

非这个主题相关的留言请到:留言本