unity响应服务器消息,[从零开始的Unity网络同步] 5.服务器将状态同步给客户端(状态缓存,状态插值,估算帧)...

在上一篇文章中,已经可以在服务器上直接根据服务器自己的操作指令,模拟得出结果,修改球的位置了,接下来,将要考虑如何将服务器模拟的位置如何同步到客户端.

1.服务器向客户端发送单位实体(Entity)状态

首先需要设定一个发包的频率(SendRate),目前设置的是每10个模拟帧发送一次,对于60模拟帧每秒的游戏世界来说,这也相当于6个包每秒.这个包的数据应该是描述Entity在当前模拟帧的状态.

public class State

{

public int frame; //模拟的帧号

public Entity entity; //所属的Entity

public List properties; //需要描述的属性

public int Pack(Packet packet)

{

packet.Write(frame);

//将属性数据写入消息包packet

}

public int Read(Packet packet)

{

frame = packet.ReadInt();

//从消息包中取出属性数据

}

}

发送的方法:

public void FixedUpdate()

{

if (Core.frame % SendRate == 0) //每隔10帧发送一次

{

foreach (var conn in connections)

{

conn.Send();

}

}

}

//connection中发送的方法

public void Send()

{

Packet packet = PacketPool.Get();

foreach(Entity entity in entities)

{

entity.currentState.Pack(packet); //将当前状态数据写入消息包

}

_connection.Send(CustomMsgTypes.InGameMsg, packet.msg_untiy); //通过UnityEngine.Networking组件的Connection发送数据

}

这样就把Entity的状态打包发向所有的客户端了.

2.客户端接收到服务端的状态包

客户端接收到服务端的数据包,然后从数据包中拿到描述Entity状态的数据后,需要考虑的是,如果是第一个状态,可以直接拿来应用到Entity上,如果不是第一个状态的话,那就不能直接应用,因为网络传输抖动的因素,服务端虽然是每隔10帧发一个包,但是客户端收包频率不一定是每隔10帧就收到的,如果直接应用的话,必然会导致抖动.这个时候,我们就需要在客户端对服务器端进行状态缓存(StateBuffer)和状态插值(StateInterpolation).

1.为什么需要状态缓存和状态插值

客户端收到的状态包都是带帧号(Frame),帧号表示了这个状态是服务器在那帧模拟得到的状态,客户端想要,去除抖动,平滑的渡过的状态之间的时间的话.就需要在State_A与State_B进行插值计算.插值计算的公式应该是这样

Current = MathUtils.Interpolate(State_A, State_B, ???? / (State_B.frame -State_A.frame ))

在公式右侧,除了????,其他都是已知的,想要得到插值结果,那么????应该是什么呢?

因为分母的两个状态的帧号差,所以分子应该也是帧号才对,客户端的帧号跟服务端帧号不一致(因为服务器肯定早就启动了,客户端是后来才连接服务器的),这个时候就要新增一个变量用来表示客户端估算出来的服务器帧(RemoteEstimatedFrame).

这个估算帧用来表示客户端在本地估测服务器模拟的帧号,它的第一次赋值应该是客户端收到服务器的帧号时,

// 调整远程估算帧

public void AdjustRemoteEstimatedFrame()

{

if (packetsReceived == 1)

remoteEstimatedFrame = remoteActualFrame; //当收到第一个包时,将包的帧号赋值给估算帧

}

估算帧也是按照模拟频率一直累加的,但是估算不一定总是准的,有时提前收到包,有时延迟收到包,甚至丢包.所以如果收到的包帧号跟估算帧相差太大的时候,就需要对估算帧重新调整

public void AdjustRemoteEstimatedFrame()

{

if (packetsReceived == 1)

remoteEstimatedFrame = remoteActualFrame; //当收到第一个包时,将包的帧号赋值给估算帧

else

{

remoteDiffFrame = remoteActualFrame - remoteEstimatedFrame;// 差异=实际收到的帧号-估算帧

if (remoteDiffFrame < minDiff || (remoteDiffFrame > maxDiff) //如果差异太大的话,估算帧就要重新赋值

{

remoteEstimatedFrame = remoteActualFrame;

}

}

}

效果如下:

6c1b37735c85

server 1.gif

从这个图可以看出,服务器移动很平滑,但是客户端移动可以明显看出抖动的情况,问题在哪呢?其实问题是出在估算帧的设置问题,从状态A插值到状态B的过程,由于估算帧等于(或者接近)状态A的帧号,而状态B的包客户端还没有收到,这就造成了在状态B到来之前,客户端没办法插值,只好原地等待,当状态B的包到来的时候,立即设置了位置,所以造成了抖动,那么如何解决这个问题呢?

做法是故意让估算帧的帧号在实际的状态包帧号之前,让客户端滞后:

public void AdjustRemoteEstimatedFrame()

{

if (packetsReceived == 1)

remoteEstimatedFrame = remoteActualFrame - delay; //当收到第一个包时,估算帧 = 包帧号 - 延迟

else

{

remoteDiffFrame = remoteActualFrame - remoteEstimatedFrame;// 差异=实际收到的帧号-估算帧

if (remoteDiffFrame < minDiff || (remoteDiffFrame > maxDiff) //如果差异太大的话,估算帧就要重新赋值

{

remoteEstimatedFrame = remoteActualFrame - delay;

}

}

}

将delay = 10(因为服务器每10帧发个包)这样尽可能的预留出一个状态包用来做插值计算了,看看效果:

6c1b37735c85

server 2.gif

可以看到客户端的抖动几乎看不出来了,但是代价是延迟比较大了(为了更好的表现,这个牺牲是必要的)

3.小结

服务端模拟结果,下发状态给客户端基本就完成了,需要补充的是,在估算帧的计算中,可以根据估算帧和实际帧的差距动态的调整本地模拟的频率,比如:

如果估算帧滞后太多了,那客户端就每帧加2,甚至加3(默认是每个模拟帧加1)来追赶.

如果估算帧超前很多,那客户端就估算帧的累加可以暂停来等待,通过这样的方式来缓和.

现在客户端通过插值,实现了比较平滑的表现,但是有比较明显的延迟,这个可以通过加大发包的频率来缓解这个问题.

后续实现了客户端的预表现后,这个问题也就不那么重要了.

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值