DOOM3网络同步体系

概述

由于doom3是开源的,另外网页上有大量研究doom3的文章,所以分析起来难度比较小,值得注意的是Valve公司旗下的source引擎的基础也是类似这套体系,所以也可以从中得到不少东西,包括他在source sdk上的一些介绍文章,目前的文章还在草稿期,比较乱,后期我再进行整理。

简单的一句话介绍doom3的网络体系就是基于客户端预测及快照的网络同步方式,c/s网络拓扑结构,udp作为底层网络协议。

基本代码分析

相关文件

AsyncClient.*

AsyncServer.*

AsyncNetwork.*

每帧逻辑位于Common::RunFrame

如果是多人游戏,则代码走

idAsyncNetwork::RunFrame();

if (idAsyncNetwork::isActive()) {

session->GuiFrameEvents();

session->UpdateScreen(false);

}

单机游戏的话 ,代码走

session->Frame(); // 最终也是调用Game::RunFrame

session->UpdateScreen(false)

idAsyncNetwork::RunFrame()代码中,如果是服务端,则走AsyncServer::RunFrame逻辑,如果是客户端则走AsyncClient::RunFrame()逻辑

AsyncServer::RunFrame最终调用的逻辑是执行Game::RunFrame,另外在代码最后发送了当前的系统快照。系统快照的发送时间限制了最大比特率为 16000 bytes / s 即大概16k每秒,如果上次发送时间与本次发送时间超过了1秒或者小于每秒的比特率,则发送快照及本帧的指令信息(注意这里的指令信息会额外多发送之前的一些指令,因为udp不可靠,另外如果进行重发及验证,还不如每次都额外多发一些)。

AsyncClient::RunFrame的逻辑最重要的是客户端预测的逻辑,客户端预测的在本玩家PVS下的所有实体(玩家,怪物,投射物灯),不仅仅是本地玩家,另外预测是有一个最大预测时长的,默认的预测时间为c->s的延迟时间,预测的本地玩家直接使用的就是当前的操作指令,而其他玩家,使用的是之前的指令,这样的话本地玩家是完全没有延迟反应的。

关于客户端预测的细节,客户端预测的初始值是往返的响应时间,假设为100ms,首次执行的时候,ClientPredictTIme为100ms,gameTimeResidual为16.67ms,假设客户端机器性能非常好,没有任何掉帧,则本次RunFrame会一次性执行 (100 + 16.67) / 16.67 次模拟,及玩家位置会在这帧内多次执行到7帧后的位置(即客户端预测服务端收到包并返回给自己时 ,自己应该处于的位置) ,

下一帧,如果还是没有收到服务器返回,则本次只执行一次,而非多次,因为由于上一帧的预测影响,导致gameTimeResidual已经是负数了。所以本次只累加了16.67ms。

由于出现延迟的下一帧就会移动预测到延迟时间后的游戏时间,之后每帧实际上都也是步进一次,所以实际上并不会出现预测自己的情况出现,只有在第一次会出现一次跳跃。

注意,客户端预测不仅仅是基于之前指令进行计算,同时也发送了本机之后的指令给服务端,否则服务端会一直接受不到最新的指令,毕竟两者之间是有延迟存在的。

攻击相关的本机预测

查看Weapon::Event_LaunchProjectiles可知,目前联机的时候,甚至连投射物都没有做预测播放,投射物的产生以服务端的快照为准,由于doom3是fps游戏,所以攻击流程为player::Weapon_Combat -> weapon::BeginAttack(); ->weapon脚本调用Event_LaunchProjectiles状态,注意整个过程中,人物,武器的动画状态都是预测的,即立即显示出开火的相关状态,但是伤害的投射物及最终伤害都未预测

投射物由服务端快照创建,当投射物遇到障碍物时,预测爆破及特效。

最终的伤害及效果都有player::readFromSnapshot处理,比如当前血量比原来血量少,则播放受击动画,反馈等,如果死亡,则播放死亡。

关于指令及快照的发送及接收

玩家指令,还有快照信息都是通过不可靠的消息传递的

指令包是不管是否有无收到的,默认doom会复制之前的5条指令,因为就算你保证发送成功了,服务端时间也未必恰好用你发的指令进行计算。当有延迟的情况出现时,延迟的客户端会体验发送预测延迟时间内的指令,为了后期恰好给服务端使用。

服务端的指令包及snapshot也不是保证一定收到的,反正如果没收到,收到最新的时候都会从lastsnapshot从头开始计算

所以在doom中不会出现等待的情况,只会回拉

关于平滑拉扯

由于客户端预测的存在,另外客户端并不会等待服务端指令,服务端也不会等待客户端指令,所以整个客户端及服务端都是异步的,这样肯定会出现不一致的情况,这种情况的处理需要做一些平滑,否则感受会比较差,doom3是这么做平滑的。

其他玩家的渲染位置与当前真实渲染位置是会做平滑的,即当前逻辑位置虽然是直接拉到了理想的服务器位置,但是渲染位置用了smooth做了慢慢平滑

代码在 idPlayer::GetPhysicsToVisualTransform

及 idPlayer::ClientPredictionThink均有涉及,这里的平滑只涉及到了其他玩家。

注意渲染位置并不会立即改变,只会每帧改变一次(smoothedOriginUpdated),免得出现位置跳跃。

因为玩家本身的指令应该与服务端的指令几乎是完全一致的,不应该出现偏差,仅当服务端没有收到指令包(因为这里是UDP)超过两次时,才会进行一次平滑到真实位置(因为udp并不可靠)。

另外还有一个平滑是关于玩家的视角,即玩家的那个晃动视角

其他玩家的攻击指令没有做预测

真实主客机的测试

主机无延迟

客机有200ms单向延迟,400ms往返延迟

现象为

在客机屏幕,由于有400ms延迟及客户端预测的存在,所以主机的玩家一旦移动就会出现尽量同步到400ms后的位置,所以一旦开始移动就有个快速拉扯,停止的时候也会回拉。

在主机屏幕,由于延迟为0,所以客户端移动虽然有延迟,但是不会出现加速及回拉的情况。

比较出乎我意料的是,在客机屏幕的客户端预测,居然不会影响到本地玩家,我一直以为本地玩家也会有快速拉扯,实际上完全没有。不管在主机还是客机屏幕,本地玩家的行为都完全正常。

与之前的代码分析完全一致。

使用该做法的游戏

http://dev.dota2.com/showthread.php?t=527&page=7&p=4253&viewfull=1#post4253

dota2这篇论坛上的资料透露, Counterstrike, Left 4 Dead, Team Fortress 2等游戏都采用了该技术,而dota2在此技术的基础上,去掉了客户端预测,减少了拉扯现象,不过玩家的控制信息得不到立即反馈,该文章也是为了解答玩家关于控制延迟的疑问。

updatedupdated2021-01-202021-01-20