去年unity就发布了一个客户端+服务器的FPS游戏实例,并不是一个简单的DEMO而是一个完整可玩的游戏项目,今天抽空学习一波记录点心得。这哥们从头到尾也没有以“真面目示人”我们就姑且叫他“无面哥"吧(起名是这套教程的传统,哈哈哈)!

网络传输中我们有两种协议UDP和TCP,每个终端都有自己的IP,通过网络传输协议与对应的服务端进行交互。Unity inc使用的就是TCP协议,而Google使用的是UDP协议。

客户端和服务端都是电脑主机,只是服务端不需要渲染、用户输入等操作。不同的是客户端只用处理自己本机的数据,而服务端需要处理多台客户端的数据。它们进行网络交互,红色的方块表示客户端向服务器发送的数据,蓝色的方块是服务端回复客户端的数据。

我们使用的是UDP网络协议,UDP并不是可靠协议,那么就有3个问题需要我们克服
- 数据包重复:数据包从客户端发到服务器之间会经历N个路由器,很有可能中间某个路由器对数据包进行了复制,这样服务端可能会收到2个相同的数据包。
- 数据包错序:客户端依次发送了数据包A和B,但是服务器先收到了数据包B后收到了数据包A。
- 数据包丢失:客户端依次发送了数据包A和B,但是服务器只收到了A并没有收到数据包B。
为什么不使用TCP呢?TCP虽然能提供数据可靠性,但是它会带来更大的延迟,因为当出现数据包的丢失、错序、复制的时候TCP的Stream就需要停止等待了。

为什么FPS要使用UDP?我觉得有个重要的原因,UDP虽然会出现丢包和错序问题,但是客户端可以通过前后两个包插值出中间包的数据,也就是数据包预测机制。还有就是自己维护一套可信赖的UDP可以根据项目的情况定制性,效率肯定比TCP要高。
红色方块表示客户端发给服务器的包,蓝色方块表示服务器回给客户端的包。首先解决重复包的问题,每个数据包带一个自增并且唯一的序列id,收消息时进行比较如果之前有收到过相同的数据包,则无视这个新收到的数据包。

每次收消息都能收到对方上一次发送的序列id,下次自己向对方发消息的时候把这个序列id也带上发给对方,这样对方就知道消息是否出现错序问题。

实际中可能会出现多个消息漏掉,每个消息还会发送一个16位的掩码,用于记录之前的16个消息是否已经完全收到。比如之前的16个消息全都收到了,那么对应的掩码就是 1111111111111111,如果前5个消息都没有收到反而收到了最新的消息,那么对应的掩码就应该是1111111111000001 ,其中0表示没有收到,1表示收到了。

数据包在传输工程中是有延迟的,处理数据包如果以收到的时间点来处理结果就不对了,所以数据包需要做延迟补偿,客户端在1000毫秒的时候像服务器发送了一个数据包。

服务器在1050毫秒的时候收到了这个数据包,接着服务器在本地花了30毫秒处理这个数据包,最终在1080毫秒时向客户端发送数据包,并且带上了Tim:30ms 表示这个数据包在服务器处理了30毫秒。

最终客户端在1130毫秒时收到了服务器发的数据包,所以这个数据包的RTT应该是1130-1000-30=100毫秒

接着我们来看看完整的数据包传输,客户端为了保证流畅度,每秒60帧率在运行游戏。这样玩家每秒就会产生60次操作数据(注意这里用的是状态同步并不是帧同步)每个数据包中记录的就是User Command的信息,每秒会产生60个数据包发送给服务器。服务器也需要考虑性能并且要留给充足的时间等到多个客户端发来消息,它是以每秒20个包的速度向客户端发消息,也就是说服务器是20帧率在运行。每个包就是世界快照,包含世界中每个玩家的信息。客户端每秒只能收到20个包但是却要刷新60次,为了让同步玩家信息更加流畅必然需要使用到数据包的预测以及插值。

比如现在有2个客户端都在向服务器发送数据包91和92,第一个客户端网络比较好顺利的将数据包91和92发给服务器,然而第二个客户端网络比较差,只是将数据包92数据发给了服务器。刚巧现在到了服务器更新的时间,如果服务器在继续等待那么客户端就会出现卡顿了。

因为客户端是一秒60个包发给服务器,服务器返回给客户端只是每秒20个包,所以服务器返回的每个快照包会可能包含玩家3帧的信息, 我们通过前后两个快照就可以比较出之间的插值,这里可以看到玩家的坐标发生了变化,客户端根据它来做差值平滑的过度到每一帧中。

这里还有个优化协议包大小的技巧,首先服务端和客户端本地都有一套相同预测包的代码,服务端现在有数据包82 85 88 91,此时如果直接将91发给客户端包体是比较大的,所以服务端先根据82 85 88预测出91的数据包,然后将预测的91数据包减去实际计算的91数据包得到一个91数据包的变化值,只将这个91数据包的变化值发给客户端,这样包体就小很多了。

客户端这边就比较有意思了,客户端本地有数据包82 85 88 和服务器一样先预测出91数据包,由于这时候还收到了服务端发来的91变化值数据包,使用预测91数据包减去变化值91数据包就得到了最终准确的91数据包,通过这样预测减变化值的办法大幅度减少传输的数据包体大小。

最后客户端根据还原出的数据包进行每帧的插值还原游戏画面

更多详情: https://unity.com/fps-sample
本文深入探讨了Unity FPS游戏中的网络优化技术,包括使用UDP协议的原因、如何克服数据包重复、错序和丢失等问题,以及如何实现数据包预测、插值和延迟补偿,确保游戏的流畅性和实时性。
642

被折叠的 条评论
为什么被折叠?



