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

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

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

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

public class State
{
    public int frame;                                               //模拟的帧号
    public Entity entity;                                          //所属的Entity
    public List<Property> 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;
        }
    }
}

效果如下:

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帧发个包)这样尽可能的预留出一个状态包用来做插值计算了,看看效果:

server 2.gif

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

3.小结

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

如果估算帧滞后太多了,那客户端就每帧加2,甚至加3(默认是每个模拟帧加1)来追赶.
如果估算帧超前很多,那客户端就估算帧的累加可以暂停来等待,通过这样的方式来缓和.

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • 1.啃玉米棒子,小宝用手掰了粒玉米,说:“我掉了一颗牙。” 2.给小宝穿袜子时,她说:“我说停就停。”穿到一半,喊...
    Jamesyanyb阅读 151评论 0 0
  • 何谓死亡?” “枯藤,老树,昏鸦。” “可否具体?” “古道,西风,瘦马。” “可否再具体?” “万物皆永恒,卿不在”
    燕小乙乙阅读 336评论 0 0
  • 所需工具:windows电脑就足够了。当前使用的移动盒子型号:中兴B860AV2.1。目前来说盒子的破解大致分为2...
    iosRn阅读 82,277评论 1 1
  • 你来时,像一颗花生。光秃秃的身体,蜷缩着,挣扎着,不知是欣喜还是控诉。那一天,2010年4月22日。 你成长的时候...
    懿冉臻阅读 397评论 0 1