这篇文章转载自 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 同步一次当前位置,这样相应的发包也会减少。
条件的改变频率
此时增加【条件】的冗余可以减少当前发包的数量,如增加 角度/距离 变更的判定范围。实际上这样做的效果并不好,原因是【条件】太容易满足。
基于误差累计替换【条件】
(航位推算法DR)
客户端代码:
服务器的预测点 = 上次同步给服务器的坐标 + 运动方向 * 同步结束后累计的时间(当前时间 - 上次发包时间)
需要同步 = Vector3.Distance(服务器的预测点,当前的实时位置) > 0.2m
上面的做法,好处是误差可以控制在一定的范围内并且尽量少发送同步包;比如误差控制在 0.2m 范围,如果发送的包超量再适当增加此范围。
弱网多人联机表现优化
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]);
}
}
}
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 中文开发者一同学习。
方式二:联系邮箱 learn-cn@unity.cn,投稿技术内容。
点击“阅读原文”,访问博主更多技术分享