题主的描述很详细,基本看懂了。
很多时候,问出正确的问题,很容易得到正确的答案;而如果疑问的角度不好,就会陷入矛盾,无法得到有效的结果。
这个问题就属于后者——实际上,题主是找错了解耦的角度。
就 PickUp 消息来说。题主的设计问题在于 耦合不足 ,具体来说是PickUp包没有做完该做的事,无效信息过多、有效信息不足,给网络同步带来疑难。
1、PickUp消息包应该包含哪些内容?
客户端和服务器的同步是网络游戏的核心问题之一。无论现代硬件如何进化,仍然有一些基本原则,比如:
- 需要同步的数据,应当尽可能小。
- 不要去反复更新没有变化的数据。
- 不要同步与此次操作无关的数据。
所以在比较标准的设计中,「拾取道具 PickUp」这件事,应有C2S和S2C两个网络包:
C2S ,客户端到服务器的请求。应当包含:
- 道具在场景中的ID。(或者道具位置,只要是能够标识出掉落道具的参数)。
消息完毕。
只有这一个参数?
仔细想想,确实如此。在真正的网游中,客户端只要告诉服务器「我要捡哪个道具」就够了,至于那个道具是否存在?距离多远?是什么东西?能不能捡?都是由服务器说了算,不需要也不能够由客户端来决定。
甚至客户端具体是哪个玩家,即PlayerID也是由网络层决定的,客户端无权定义自己的身份。
S2C ,服务器对PickUp事件的响应。应当包含:
- 拾取成功或失败,失败原因用错误码表示即可。
- 如果拾取失败,无需任何其它数据。消息完毕。
- 如果拾取成功,则消息中仅包含 有变化 的道具信息。比如第几个背包格子添加什么道具,或者某个道具的叠加数量+1。总之仅包含变化的道具信息。消息完毕。
这些数据足够满足绝大多数典型的网络游戏,不需要其它任何数据,也不应该添加其它数据。
为什么呢?
2、问题:由此操作引发的其它数据变化,如何处理?
显然,如果游戏比较复杂,比如WOW那种的,拾取新道具的操作可能引发其它一系列变化,包括不限于:
- 角色负重变化,某些属性变化。
- 任务列表变化。
- 技能变化。
- 状态(buff)变化。
……等等很多。
但是注意,就算变化再多,这些变化一般也不加入到PickUp消息中。
以任务变化来说,当拾取新的任务道具时,新增道具触发了任务系统的刷新。假如任务有变化,自然会触发任务系统的同步机制,任务系统自然会推送「 有改变的任务 」到客户端。
同理,角色数据、技能系统也都像这样有自己的同步机制,都会在发生改变时,发送「最小改变数据」同步给客户端。
各个模块各司其职。无论如何,它们与「拾取道具」这件事没有直接关系,不归拾取道具的逻辑负责。
3、题主的设计有哪些不妥?
单说从问题描述中看到的一些不妥之处。
1、Net.SendItemUpdate、Net.SendPlayerUpdate等函数,疑似发送了全部数据。
前面提过,全量更新违背了信息最小化原则。不仅让网络包变大,更糟糕的是:当玩家同时发送多个请求时,多个响应的全量包之间的数据是重复的甚至不一致的,数据正确性与处理顺序密切相关,容易引起数据同步的BUG。
2、一个PickUp请求,产生了三个独立的响应。
一个请求产生多个响应,本身没问题。但是具体到题主的代码中,与前文描述的情况不一样。
主要是请求包和响应包应当包含哪些数据、不应当包含哪些数据需要想清楚。
4、关于网络同步能否解耦的问
网络同步有些情况下能解耦,有些情况下应该强耦合,慢慢来谈。
首先,网络同步有三种基本方式:
1、状态同步
服务器和客户端持续高频率同步「所有状态」,经典的例子是早年的Quake网络版。
不要以为同步「所有状态」会很慢。其实,利用diff算法,自动识别差异化信息并同步,能得到极为优异的网络性能。
简单、高速、有效,是写得好的状态同步算法最大的特点。
2、帧同步
帧同步是网络上讨论很多的概念。它的核心就是「仅同步用户操作,不发送任何状态数据」。
帧同步的前提是「操作相同,结果一定相同」。帧同步在原理上不难理解,但实际开发起来有很多疑难。主要是对逻辑代码的编写提出了很高要求,任何一点点逻辑瑕疵都会导致客户端服务器数据不一致。所以真正完全采用帧同步的游戏并不是那么多。
但是从性能和效率上来讲,帧同步确实是王者级别的存在,适合很多高频高速操作的移动端网络游戏。
3、★ 事件同步
虽然前两种同步方式经常听说,但实际上,介于前两者之间、或者说结合了前两者的同步方式才是大部分网络游戏的真实方案。
真正网游项目中,特别是MMO中,很难有一种同步方式打天下的情况。要完美解决所有问题,还是只能定义各种各样不同的逻辑包,具体问题具体分析。
像题主的「拾取道具PickUp」这个包,客户端到服务器的是一个操作(操作:拾取周围某个道具),服务器返回的是一些状态(成功或失败,背包状态、角色状态)。
有时是操作、有时是状态,无法统一命名,我称之为事件同步。或者说是一种很自由的同步方式,完全取决于每一个包的处理怎么写。
搞清楚了几种同步方式的区别,你会发现:帧同步和状态同步,都能做到与具体逻辑代码无关,也就是真正意义上的「解耦」。
在帧同步和状态同步框架下,只要逻辑代码写对了,所有的状态自然就由底层同步了。网络的事情完全不用操心。
而更普遍的「事件同步」方式下,同步机制本身就与游戏逻辑密切相关,开发者不得不精心设计每一个消息包,把每一个包都写对了,才能做到无漏洞、数据一致。
没想清楚这一点的情况下,强行在每一个操作中同步所有数据,模仿「状态同步」的做法,自然就会踩到坑里~~
网络联机游戏的机遇
近几年独立游戏蓬勃发展,但 联机功能完善的创新游戏 发展水平并不高。
这既是市场的短板,也是历史的机遇,希望有志气的小伙伴们考虑这一条充满挑战的道路~~~