社区分享 | 在网游中进行位置同步的那些事儿

文摘   科技   2024-10-12 20:00   上海  

这篇文章转载自 Unity 社区开发者 Keyle,整理了在网游中进行位置同步的思路及 tips,希望为准备做多端同步的小伙伴以及正在做的小伙伴提供一点启发。点击阅读原文,前往 Keyle 的 Unity 中国开发者社区主页,阅读更多干货文章。

基于条件进行位置同步

在网游的移动同步中,我们需要思考同步的频率,是固定周期好还是根据条件判定好?如果只是开发一个 demo,这当然是固定频率去同步比较简单。但是在正式的产品开发中,面对大用户的接入,就需要严格控制乃至减少现在数据包的大小以及频率。这时候就要结合周期同步以及条件同步进行优化,让客户端的发包量进一步的降低。

假设我们现在就在开发一款 MMO,我们先列一下几种位置同步的【条件】:

角度是否变更位置是否变更

在固定的周期内(如 15hz)会检测两个条件是否超出一定的阈值,如果超过定量则在该周期内同步一次。服务器则根据当前同步的角度预测计算帧当前角色可能的位置,通常的计算方式如下:

预测玩家当前位置 = 上个包的位置 + (服务器当前时间 - 上个包的时间) * 上个包的移速 * 上个包移动朝向

如果是这种做法,当前移动速度为 6m/s :

客户端刷新周期250ms,延迟为200ms,服务器得到当位置的最大误差为:(0.25s + 0.2s) * 6m = 2.7m   每秒4个包客户端刷新周期100ms,延迟为200ms,服务器得到当位置的最大误差为:(0.1s + 0.2s) * 6m  = 1.8m   每秒10个包客户端刷新周期50ms,延迟为200ms,服务器得到当位置的最大误差为:(0.05s + 0.2s) * 6m  = 1.5m   每秒20个包...

以此类推,稍微优化一下也用不了那么多包,如果【条件】没有变更的话不需要持续在周期内同步。也就变成了每帧判定【条件】是否变更,如果没有变更则无需同步。否则走默认的周期检测,检测周期也就可以改为更长时间,如 1s 同步一次当前位置,这样相应的发包也会减少。

条件的改变频率

在此基础上可以知道延迟固定的最大误差为 0.2s * 6m = 1.2m ,那么现在基于此应该去尽量少地发送数据包,也尽量少降低误差。如果【条件】改变得频繁,会大幅增加发包的数量,甚至可能每帧都产生一个包,如玩家在原地绕圈的时候会更多。就比如我,玩游戏就有闲着没事手原地搓摇杆的习惯,顺便一问你也有这种习惯吗?还有就是原地切个枪什么的。

此时增加【条件】的冗余可以减少当前发包的数量,如增加 角度/距离 变更的判定范围。实际上这样做的效果并不好,原因是【条件】太容易满足。

基于误差累计替换【条件】

(航位推算法DR)

前面有说到服务器预测当前物体,在计算帧的坐标是基于 运动朝向 + 物体坐标,那么在我们的检测代码中可以做两次计算,在客户端先预测服务器使用的当前物体预测位置。
客户端代码:服务器的预测点 = 上次同步给服务器的坐标 + 运动方向 * 同步结束后累计的时间(当前时间 - 上次发包时间)
然后计算当前物体实际距离与 预测服务器得到的当前物体的位置 之间的距离大于一定的值,如同步的精度为 0.2m,那就是:
需要同步 = Vector3.Distance(服务器的预测点,当前的实时位置) > 0.2m
如果当前位置与服务器预测的位置误差控制在一定的范围内则不需要同步,否则的话就立即同步一次。这里需要注意一下,如果玩家从静止状态第一次进入运动状态是需要立即告知的,反之亦然。没有立即同步的话,你的好兄弟与你联机时候只会看到你全屏漂移,然后问你一句,你飞够了吗?

上面的做法,好处是误差可以控制在一定的范围内并且尽量少发送同步包;比如误差控制在 0.2m 范围,如果发送的包超量再适当增加此范围。

弱网多人联机表现优化

这里讲一下我的思路,收到服务器同步包的时候,用包里的位置与现在的实时位置做一个向量的减法,这样就能得到一个误差值。这个误差值,我们在后续的 x帧内给它缓慢补上,让画面减少抖动。 
除此之外,还可以用一个同步的误差值进行容错,后续的 x 帧内并没有完全消除误差的情况下,等下一次的移动中继续补上它的误差。当然如果这个误差值过大,则还需要立即将其拉到消除误差的点位。毕竟我们不能因为要画面而去牺牲手感。可能你也遇到过在射击游戏中明明看到敌人了,却射不中,大概就是此类问题的表现了。
言归正传,下面用代码做个演示。玩家每次收到的 Pack 结构如下:position 是此刻当前物体处在位置,angle 为运动方向,speed 为 0 则物体进入 idle。
public class PlayerStatePack{    public float speed;    public Vector3 position;    public int angle_deg;}

下面是模拟服务端延迟的实现,fluctuation 用来模拟网络抖动:

currentTime = Time.time + Ping / 1000f _2 + Random.Range(-fluctuation_ 2/1000f , fluctuation \* 2/1000f );
void Update(){    for (int i = 0; i < packLst.Count; i++)    {        if (packLst[i].BeSend)            continue;
if (packLst[i].currentTime <= Time.time) { packLst[i].BeSend = true; ReceivePack?.Invoke(packLst[i]); } }}
客户端接收消息实现如下:通过 ReceivePack 函数订阅模拟服务器下发的 Pack,在该函数内做一个向量的减法,获取当前客户端所处与服务器同步过来的位置之间的误差,有了此向量,那后面就可以通过插值逐渐修正到一个趋近正确的位置,一定程度上抹平网络波动引起的误差。在 ApplyPoint 函数我做了一个当前误差的差值与角度差值,这样效果会好一些。
public void OnMove(float passTime,float step,Vector3 pos,float angle){    ...}

private void ReceivePack(PlayerStatePack item){ Debug.Log("其他玩家收到同步包");
modifyOffset = item.position - transform.position; localPositon = transform.position; localAnlge = transform.eulerAngles.y; ...}
public float timeTakenDuringLerp = .2f;
private void Update(){ .... var passTime = Time.time - currentPack.CurrentTime;
OnMove(passTime,step,pos,angle);
ApplyPoint(passTime);}
private void ApplyPoint(float passTime){ float percentageComplete = passTime / timeTakenDuringLerp;
if(percentageComplete >= 1.0f) { transform.position = RuntimePos + modifyOffset; return; }
var offset = Vector3.Lerp(Vector3.zero, modifyOffset,percentageComplete); transform.position = RuntimePos + offset;
if(percentageComplete*2 >= 1.0f) { return; } var yoffset = Mathf.LerpAngle(localAnlge, currentPack.angle_deg,percentageComplete*2); transform.eulerAngles = new Vector3(transform.transform.eulerAngles.x,yoffset,transform.transform.eulerAngles.z);}

最终效果如下:

移动同步过程中载具平台旋转问题

假设你已经做好的上述的全部工作,现在要追加一个新的功能,让玩家可以在移动的船上游戏,其他的玩家也在不同的船上。关键是船是有自身旋转的,这时候你会发现前面我们做的功能出现了新的问题。由于船或者移动的平台会产生自身旋转,也就带着玩家一起移动,玩家的坐标在不断变更,这时候同步的位置也不断的出现变化。对于这种状态该如何比较好的检出玩家的位移变化呢?
刚开始我们可能会认为这个检出的过程应该这样:需要综合 上一次同步点 与 上一次同步的载具旋转角度与当前载具的旋转角度差值,然后三角函数计算预测下一个落点。再将这个落点与当前要同步的点进行匹配。如果是一个点就不进行同步。

这个判定的计算非常繁琐,还需要额外记录载具的角度变化,那么有什么办法不看载具信息吗 ?

我们再分析下这个需求,载具平台旋转,子物体跟转,相对静止。有句话是这样说的,当你不知道解题思路,你把题干抄下来,也是能得分的。没错,答案就是 相对静止。

由于是相对静止(船心坐标O),向量 OA蓝色 与 OA红色 他们的长度是没有变化的。也就意味着,只需要计算上一次同步的相对位置与当前位置的长度差值,就可以判断玩家是否在载具平台上有位移。

bool isMove = Mathf.Abs( lastSyncRelPosition.magnitude - currentRelPosition.magnitude) > 0.01f

当然也可以优化成下面这样,不开方更省一些性能:

bool isMove = Mathf.Abs( lastSyncRelPosition.sqrMagnitude - currentRelPosition.sqrMagnitude) > 0.01f

本期的分享就到这里,欢迎朋友们在评论区留言,我们下期再会。

Unity 中文社区持续征集内容投稿,欢迎与 Unity 官方分享你的技术笔记、项目 demo、行业经验、有趣案例,加入社区建设,繁荣内容生态,带领百万 Unity 中文开发者一同学习。

投稿方式:
方式一:在 Unity 中文社区首页(https://developer.unity.cn/)创建个人账号,点击【写文章】,发表文章;

方式二:联系邮箱 learn-cn@unity.cn投稿技术内容


长按关注
Unity 官方开发者服务平台
第一时间了解 Unity 社区动向,学习开发技巧

 点击“阅读原文”,访问博主更多技术分享 


Unity官方开发者服务平台
Unity引擎官方开发者服务平台,分享技术干货、学习课程、产品信息、前沿案例、活动资讯、直播信息等内容。
 最新文章