帧同步含义
游戏客户端接受来自网络的多个客户端的操作,如果这些操作在各个客户端是一样的,那么多个客户端的显示也就一样了,这就带来了“同步”的效果。所以在这种情况下,各个客户端的运算要绝对一致,不能依赖诸如本地时间、本地随机数等等“输入”,而要一切以网络来的操作数据为主。
一般来说,大多数的游戏客户端引擎,都会定时调用一个接口函数,这个函数由用户填写内容,用来修改和控制游戏中各种需要显示的内容。比如在在Unity里面叫Update(),这类函数通常会在每帧画面渲染前调用,当用户修改了游戏中的各个角色的位置、大小后,就在下一帧画面中显示出来。而在帧同步的游戏中,这个Update()函数依然是存在,只不过里面大部分的内容,需要挪到另外一个类似的函数中,我们可以称之为UpdateNet()函数——由网络层不断的接收服务器发来的“网络帧”数据包,每收到一个这样的数据包,就调用一次这个UpdateNet()函数,这样游戏就从通过本地CPU的Update()函数的驱动,改为根据网络来的UpdateNet()函数驱动了。显然,网络发过来的同步帧速度会明显比本地CPU要慢的多,这里就对我们的游戏逻辑开发提出了更高的要求——如何同步的同时,还能保证流畅?
实现UpdateNet函数内容,其实就是定义一个堆栈用于存放网络发过来的消息,通过帧监测将其数据拿出来使用,因为Update函数明显比UpdateNet快的多,这就需要我们定义一个时间间隔用于消息的发送,比如50毫秒或者100毫米等。
private void GameFrameTurn() {
//first frame is used to process actions
if(GameFrame == 0) {
if(LockStepTurn()) {
GameFrame++;
}
} else {
//update game
SceneManager.Manager.TwoDPhysics.Update (GameFramesPerSecond);
List<IHasGameFrame> finished = new List<IHasGameFrame>();
foreach(IHasGameFrame obj in SceneManager.Manager.GameFrameObjects) {
obj.GameFrameTurn(GameFramesPerSecond);
if(obj.Finished) {
finished.Add (obj);
}
}
foreach(IHasGameFrame obj in finished) {
SceneManager.Manager.GameFrameObjects.Remove (obj);
}
GameFrame++;
if(GameFrame == GameFramesPerLocksetpTurn) {
GameFrame = 0;
}
}
}
private void GameFrameTurn() {
//first frame is used to process actions
if(GameFrame == 0) {
if(LockStepTurn()) {
GameFrame++;
}
} else {
//update game
SceneManager.Manager.TwoDPhysics.Update (GameFramesPerSecond);
List<IHasGameFrame> finished = new List<IHasGameFrame>();
foreach(IHasGameFrame obj in SceneManager.Manager.GameFrameObjects) {
obj.GameFrameTurn(GameFramesPerSecond);
if(obj.Finished) {
finished.Add (obj);
}
}
foreach(IHasGameFrame obj in finished) {
SceneManager.Manager.GameFrameObjects.Remove (obj);
}
GameFrame++;
if(GameFrame == GameFramesPerLocksetpTurn) {
GameFrame = 0;
}
}
}
帧同步游戏中,由于需要“每一帧”都要广播数据,所以广播的频率非常高,这就要求每次广播的数据要足够的小。最好每一个网络帧,能在一个最大传输单元(Maximum Transmission Unit,MTU)以下,这样才能有效降低底层网络的延迟。同样的理由,我们为了提高实时性,一般也倾向于使用UDP而不是TCP协议,这样底层的处理会更高效。但是,这样也会带来了丢包、乱序的可能性。因此我们常常会以冗余的方式——比如每个帧数据包,实际上是包含了过去2帧的数据,也就是每次发3帧的数据,来对抗丢包。也就是说三个包里面只要有一个包没丢,就不影响游戏。
帧同步实现的过程有个很重要的地方就是逻辑层和表现层一定要分开,表现层先行,逻辑层等发到服务端的指令再处理。帧与帧之间的播放频率,则由服务器统一控制,但由于网络抖动等影响,帧的频率并不是太稳定,为避免播放抖动,帧数控制器需要进行一定的平滑处理。

网络抖动的产生原因:在网络游戏中,各个客户端的运行条件和环境往往千差万别,有的硬件好一些,有的差一些,各方的网络情况也不一致;时不时玩家的网络还会在游戏过程中,发生临时的拥堵,我们称之为“网络抖动”。可能导致客户端收到“过去时间”里的一堆网络帧,客户端需要拿出一定的时间去处理这些堆积的网络帧,因此,客户端必须要有处理这些堆积起来的网络数据的能力。
实时同步游戏最重要的是流畅,然而影响游戏流畅的因素很多,网络带宽的限制,CPU运算和渲染效率的限制。一般玩家控制的角色的动作,包括当前客户端控制的角色,还是应该从网络帧里面获得行为数据,因为如果玩家爱控制角色不一致的太多,整个游戏场面就会差更多。很多游戏中的怪物AI都是根据玩家角色来设定的,所以一旦玩家角色的行为是同步的,那么大多数的怪物的表现还是一致的。
帧同步游戏技术,并不存在一种可以让游戏流畅的通用做法,而是需要和游戏具体做很多结合,在减少数据包,优化游戏快进体验,控制发包速度上尽量调优。同时还需要和游戏产品策划一起,平衡一致性、实时性、公平性的策略,才能真正达到流畅游戏的目的。
优化方案:
1:减少数据包;
帧同步游戏中,由于需要“每一帧”都要广播数据,所以广播的频率非常高,这就要求每次广播的数据要足够的小。最好每一个网络帧,能在一个最大传输单元(Maximum Transmission Unit,MTU)以下,这样才能有效降低底层网络的延迟。
2:使用UDP冗余的方式
比如每个帧数据包,实际上是包含了过去2帧的数据,也就是每次发3帧的数据,来对抗丢包。也就是说三个包里面只要有一个包没丢,就不影响游戏。这样,即便丢包,也能保证大概率客户端稳定。
3:逻辑层和表现层分离
表现层先行,逻辑层等发到服务端的指令再处理。帧与帧之间的播放频率,则由服务器统一控制,但由于网络抖动等影响,帧的频率并不是太稳定,为避免播放抖动,帧数控制器需要进行一定的平滑处理。
4:优化游戏快进体验,控制发包速度上尽量调优
参考:

本文介绍了Unity中基于帧同步的网络优化方案,强调帧同步原理与实施要点,如UpdateNet函数、数据包最小化、UDP冗余策略、逻辑层与表现层分离等。并探讨了如何处理网络抖动、数据堆积以及优化游戏流畅性的问题。
2715

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



