网络通信 - scutrobotlab/RM2021_simulation GitHub Wiki

网络通信是融合在所有代码中的,所以主要分三个部分写一下一些思考。在阅读下面这段前建议先了解一下 Mirror 网络框架

数据同步与 Rpc

在 Mirror 框架中,数据同步是几乎无感的。通过使用 SyncVar 等注解,基本类型的变量、基本类型组成的对象、数组等可以在服务端和客户端之间同步。通过定义 Command 和 Rpc,客户端服务端代码之间可以互相调用。数据同步和远程调用的细节被框架封装了,下面主要在更高的层次谈谈编码过程中一些需要注意的点。

首先是关于数据的组织。虽然框架提供了同步数组和字典的能力,但随着同步数据量的增加,数据同步对带宽的压力也会增加。按 FixedUpdate 速率每秒 50 帧计算,如果每一帧都把整个赛场所有的数据在所有客户端和服务端之间同步,服务器 10Mbps 的带宽都可能不够用。所以同步数据的组织对模拟器的网络优化有很大影响。

一种可行的组织策略是,统一同步影响表现层的数据。例如步兵机器人,影响其视觉呈现的数据主要有位置、旋转、行进速度(影响车轮)、云台旋转与俯仰、血量等。这几个值表现为数据大概不会超过 64 byte,每一帧都进行同步是完全可以接受的。而且针对位置、旋转等信息,Mirror 封装了 NetworkTransform 等组件,除同步之外加入了预测、插值等功能,可以直接使用。而在控制机器人过程中产生的插值变量、标志量等数据,保留在操作手本地就好了,没有必要进行同步。对 Mirror 自定义同步对象进行赋值,会触发自定义对象的异步数据同步,所以一些裁判系统数值,例如 Buff、子弹数量等,可以封装在对象中,利用 Mirror 异步同步对象的机制进行同步,控制同步的帧率。

除了数据的同步,Rpc 也是网络优化的一个要点。在 Mirror 网络编程模型中,同步调用是比较少见的。更多见的方式是客户端发起一个请求(调用 Command),服务端以另一个 Rpc 调用的方式返回结果(调用 ClientRpc 等)。所以在设计过程中,应按照对应框架的编程模型设计数据流动方式,防止出现开发中途发现拿不到数据,用 Hack 的方式解决后,又引入新的不确定性因素,最后导致差错困难,代码库难以维护。2021 模拟器工程就有些这样的问题,希望引以为戒。

上述的数据同步和 Rpc 调用都非常直观。由于服务端与客户端使用同一套代码库,很容易写着写着陷入单机程序开发的思维。比如在上一行修改了同步字典中的值,在下一行直接假定数据已经在全部客户端同步,进行其他的操作。这样是非常危险的!每一次数据同步或者 Rpc 调用之后一定要思考一下网络时延的问题!数据一定要考虑时序问题并校验!否则你可能会在调试过程中发现无法稳定复现的问题,从代码逻辑上非常难肉眼看出问题,浪费大量的后期查错时间。

典例:上面提过的玩家机器人互认过程,由于各客户端加载场景、确认权限、同步到服务端的时间可能有很大差异,同步开始比赛问题在很长一段时间里没有有效解决,表现出来的问题就是某些客户端在开始游戏后直接胜利,另一些客户端则变成了哨兵。直到在服务端统一做了二次确认,问题才没有复现。

一致性

网络应用分散在不同的地理位置执行,端到端的通信延迟、客户端的计算速度、网络状况各不相同。要保证模拟的正常进行,就要将一致性保持在合理范围内。这里涉及到的一致性可以分为数据上的一致性和视觉上的一致性。 (2021 模拟器在这方面实现得非常 Hack,很多实现方式千万不要学。)

数据上的一致性,就是说机器人血量、Buff、建筑血量、比赛结果等重要的数据必须在各端显示一致。实现这一点要注意的是对重要数据,一定要完整同步,每一次都将数据重新发一遍。某些数据同步操作是基于变化量的,比如靠 Rpc 通知每个客户端神符激活情况等。但对于扣血等事件,这样的同步方式不可取。比如服务端发了基地扣血 Rpc 数据包,但是在同步过程中某个客户端没有成功接收,那么他所显示的基地血量就永远比其他客户端多一些。这显然会影响比赛进行。

关于数据一致性还有一点值得商榷:弹丸检测问题。在 RM 比赛中,弹丸是一个带物理效果的物体,不像某些游戏中的激光枪。在英雄吊射时,大弹丸的飞行时间甚至可以超过两秒。在这两秒中,任何网络波动、同步延迟,都会导致弹着点的变化。在 2021 模拟器中,我们最终选择在发弹的客户端进行弹丸物理模拟和检测,即谁发弹,谁检测,保证不会出现“看着打到了但没扣血”的情况。这样实现的缺点是对于网络卡顿的客户端,弹丸击中的很可能是敌方车辆一段时间前的位置,但依然判定了扣血。在比赛中就出现过英雄被视距外弹丸莫名其妙击杀的情况。另一种弹丸检测方式是统一在服务端进行检测,但随之而来的是视觉一致性问题,下面会继续这个话题。

节选自 Controller/GroundControllerBase.cs

视觉上的一致性,是指在保证不占用大量网络流量前提下,保证每个客户端的视觉效果基本一致。典型例子就是 RM 赛场上会出现等大量弹丸。在每一局模拟比赛中,都有上千发大小弹丸被发射。如果为每个弹丸进行姿态同步,虽然保证了视觉强一致性,却很可能拖垮数据同步。2021 模拟器的实现方式是,对于每一次弹丸发射,进行 Rpc 调用,在所有客户端的同一位置,以同样的初始姿态和速度生成弹丸,保证弹丸的飞行轨迹基本一致。加上车辆的位置同步,实现了可以接受的击打效果。实际模拟中还有很多视觉一致性问题,需要根据场景解决。

然后是所有网络游戏都会讨论的反作弊问题。虽然相信对于模拟器目标用户而言,这个问题并不存在,但还是顺路提一下。2021 模拟器的子弹检测机制就是直接无视反作弊问题的典范,在客户端执行这样重要的逻辑。靠这个漏洞,只要稍加逆向工程,客户端就有机会直接击杀场上所有目标。解决这个问题的办法就是把重要逻辑全都移到服务端执行,将结果同步回客户端。但网络同步是有延迟的,想必如果操控机器人很顺畅,发弹却需要等半秒的话,模拟体验会非常糟糕。

综上,分享一个新的网络同步思路:考虑到模拟器有模拟延迟的需求,输入管理器可以直接将所有输入发送到服务端,服务端检测延迟、动态调整延迟后,将输入数据分发给机器人。在服务端完成所有模拟,再将机器人姿态、血量、发弹等数据同步回客户端。服务端可以通过调整输入延迟来平滑任何时候的同步延迟。例如,某客户端的网络延迟是 30ms,设定希望模拟的延迟是 150ms,那么服务端接收到输入后,就需要等待 80ms 左右,开始模拟,再将结果送回客户端。这样客户端的体感延迟就可以稳定在 150ms,既解决了一致性问题,又实现了模拟延迟的需求。

关于服务端

适配 Unity 引擎的另一个网络框架是 Photon,这个框架提供了分离服务端与客户端的方式,可以开发独立的服务端应用程序。官方还提供服务器托管服务。这个框架由于没有使用过这里不展开讲.

另一个关于服务端的想法是能否利用 Serverless 技术,用连续的、短生命周期的容器作为服务端,接力进行 UDP 通信。由于 Serverless 冷启动问题,生命周期问题,这个思路目前还没有成熟的方案。其实目前的 Serverless 基础设施也并不适合实时游戏这种有状态服务端,只不过,国内大带宽服务器实在是太贵啦。

⚠️ **GitHub.com Fallback** ⚠️