同步模型
本文的分析环境设定为:服务器不接入物理引擎,纯物理游戏(偏向于强操作)以开房间的方式运行。
主要分析点包括:同步模型、服务器框架、防作弊、主客户端热切换(保证任意玩家掉线都不影响游戏进行)。本文更多是个人思考的记录。
服务器不接入物理引擎时,只能依赖从客户端中选举出一个主客户端来负责物理处理,再将状态同步到其他客户端,其他客户端做插值处理。(服务器也可以接入部分逻辑,取代主客户端的某些功能,甚至完全取消主客户端角色,改为服务器记录来自客户端的状态快照并进行逻辑处理。)
其他客户端的"物理效果"表现力取决于插值算法和同步频率,目前有以下两种方案:
方案一:主客户端全物理
所有玩家均处于主客户端的物理控制下。其他玩家将操作发送给主客户端,接收来自主客户端的状态信息后做插值。这种方案的好处是游戏控制权集中在主客户端,降低了同步模型设计的难度。
主客户端只需接收其他玩家的操作信息即可。由于同步频率远低于游戏帧数,若主客户端将每次收到的操作都立即执行,会出现操作不连贯的问题(如移动时一走一停)。因此,主客户端应将操作处理为输入状态的改变——例如,某客户端的移动操作发送给主客户端的数据应该是"开始左移动"/"停止左移动",而不是重复发送"左移动"。
方案二:各客户端独立物理
每个客户端的玩家自身物理效果可以实时处理(自身是一系列刚体的力的组合,玩家的移动操作直接作用于自身刚体),让玩家看到自己的表现非常流畅(有点像常规网络架构中客户端预处理、表现先行的思想)。然后定时将自己的状态(BodyPart 的 position、rotation)发送给主客户端,主客户端对该玩家信息插值后,负责逻辑仲裁。
这种方案下,所有客户端玩家自身的物理状态都是独立控制的,自己看自己角色的物理表现(比如动作动画)非常完美。主客户端扮演的角色是逻辑仲裁者,负责伤血判定、捡武器、道具产生等。代码实现复杂度相对不高。
此外,可以考虑让服务器接入部分逻辑,来修正客户端的同步错误。
对抗网络抖动与延迟
建议先用 TCP 快速实现功能(网络优化属于优化层面,费时费力,应先以实现功能为主),再用可靠 UDP 加速传输。在接入可靠 UDP 时,尝试过 UDT,但效果不理想(文档中没有明显的调优方法,未继续深入研究)。在 Android 真机 WIFI 环境下,整体延时的确有所下降,但游戏体验提升并不明显。
以下是几种改进思路:
- 思路一:采用更合适的可靠 UDP 方案。KCP 似乎比 UDT 更优,值得尝试。
- 思路二:在同步模型二下,position、rotation 这类消息同步频率高且频繁。一旦发生丢包,为保证可靠性可能导致一个 tick 内收到多个包,角色出现明显抖动。因此可以在逻辑层选择性丢弃旧包,只使用最新的包。这个思路进一步引申出:既然最新包才有用,丢包了也无所谓,那为什么还要保证可靠?由此引出思路三。
- 思路三:将消息分为三类(channel):
- 保证到达(如掉血、道具生成、子弹发射)
- 保证按顺序到达(逻辑前后有强顺序性的消息)
- 无需保证可靠性(位置、旋转等)
个人最理想的方案是(asio + kcp)或(go + kcp)的组合(协程非常好用),再加入消息分类策略。如果效果仍不够理想,再考虑 P2P。P2P 也有一定的问题:打洞(NAT 穿透)不能 100% 成功,因此需要服务器为无法打洞的客户端提供中转,同时可能会丧失服务器验证功能。
服务器架构
游戏分为战斗和非战斗两部分。战斗的消息同步应考虑用可靠 UDP 实现基本协议;非战斗同步消息用常规 TCP(RPC 或消息模型)均可。
游戏逻辑上分为两大块:
- 非战斗:主要涉及匹配、商城、好友、任务等常规需求。一个玩家大部分游戏时间不会处于非战斗状态,因此常规 RPC 或消息模型即可。这类服务器称为 LogicServer,较为综合且通用。
- 战斗:涉及大量消息转发,主要负载是流量。
防作弊
以下三个方案层层递进:
-
服务器在战斗同步中只扮演中间件角色,仅负责转发消息。防作弊手段由所有客户端共同承担——每个客户端都是仲裁者,负责对一些逻辑上限进行验证,比如瞬移、无限产生道具、秒杀等极端情况。但这种方案防不住所有客户端同时作弊,也存在被恶意攻击的风险(类似于 GTA5 中某玩家开作弊让服务器认为是你在作弊的情况),这种防作弊机制本身提供了这种可能性。
-
在方案一的基础上,将验证代码移至服务器。服务器也充当一个客户端进行消息仲裁。为节省开发时间,可以接入 C# 的客户端代码用 mono 来跑——客户端可以专门针对这种验证写一个模块,服务器和客户端共用;也可以只放在服务器端(去掉物理引擎)。基本模式是:一个房间在转发消息的同时做消息合法性的防作弊验证。
这种模式意味着客户端游戏的全局状态会分为两大块:物理输出和状态描述(可以只是对象类型和 position)。验证模式下只需要状态描述集合,无需物理输出;状态描述可以是状态快照,也是物理输出的快照。由此引出一个新问题:这份状态描述该信任哪个客户端的?每个客户端都有可能作弊,作为仲裁依据的状态描述该信任谁,还是只信任部分客户端?
-
针对方案二的问题进一步优化:服务器可以接入部分逻辑。将客户端的 mono 代码去掉物理引擎后,逻辑仍在 C# 层运行。例如:道具产生的合法性(不能无限产生)、扣血的合法性(不能秒杀)、位移的合法性(不能瞬移),由服务器告知主客户端,非法消息直接丢弃或认定为作弊。
此外,主客户端不唯一也是一种方案——主客户端可以随机切换,比如该局是玩家 1,下一局可能是玩家 2,甚至在一局中也可以切换。// TODO
///TODO