Unity3D中实现帧同步 (二):跟踪平均数


概览

在上次实现的帧同步模型当中,游戏帧率和通信频率(也就是帧同步长度)长度是固定间隔的。但实际上,每个玩家的延迟和性能都不同的。在update中会跟踪两个变量。第一个是玩家通信的时长。第二个则是游戏的性能时长。

移动平均数

为了处理延迟上的波动,我们想快速增加帧同步回合的时长,同时也想在低延迟的时候减少。如果游戏更新的节奏能够根据延迟的测量结果自动调节,而不是固定值的话,会使得游戏玩起来更加顺畅。我们可以累加所有的过去信息得到”移动平均数”,然后根据它作为调节的权重。

每当一个新值大于平均数,我们会设置平均数为新值。这会得到快速增加延迟的行为。当值小于当前平均值,我们会通过权重处理该值,我们有以下公式:

newAverage=currentAverage∗(1–w)+newValue∗(w)

其中0<w<1

在我的实现中,我设置w=0.1。而且还会跟踪每个玩家的平均数,而且总是使用所有玩家当中的最大值。这里是增加新值的方法:
 

  1. public void Add(int newValue, int playerID) {
  2.     if(newValue > playerAverages[playerID]) {
  3.         //rise quickly
  4.         playerAverages[playerID] = newValue;
  5.     } else {
  6.         //slowly fall down
  7.         playerAverages[playerID] = (playerAverages[playerID] * (9) + newValue * (1)) / 10;
  8.     }
  9. }
复制代码


为了保证计算结果的确定性,计算只使用整数。因此公式调整如下:

newAverage=(currentAverage∗(10–w)+newValue∗(w))/10

其中0<w<1

而在我的例子中,w=1。

运行时间平均数

每次游戏帧更新的时间是由运行时间平均数决定的。如果游戏帧要变得更长,那么我们需要降低每次帧同步回合更新游戏帧的次数。另一方面,如果游戏帧执行得更快了,每次帧同步回合可以更新游戏帧的次数也多了。对于每次帧同步回合,最长的游戏帧会被添加到平均数中。每次帧同步回合的第一个游戏帧都包含了处理动作的时间。这里使用Stopwatch来计算流逝的时间。
 

  1. private void ProcessActions() {
  2.     //process action should be considered in runtime performance
  3.     gameTurnSW.Start ();
  4.     ...
  5.     //finished processing actions for this turn, stop the stopwatch
  6.     gameTurnSW.Stop ();
  7. }
  8. private void GameFrameTurn() {
  9.    ...
  10.          
  11.     //start the stop watch to determine game frame runtime performance
  12.     gameTurnSW.Start();
  13.     //update game
  14.     ...
  15.     GameFrame++;
  16.     if(GameFrame == GameFramesPerLockstepTurn) {
  17.         GameFrame = 0;
  18.     }
  19.     //stop the stop watch, the gameframe turn is over
  20.     gameTurnSW.Stop ();
  21.     //update only if it's larger - we will use the game frame that took the longest in this lockstep turn
  22.     long runtime = Convert.ToInt32 ((Time.deltaTime * 1000))/*deltaTime is in secounds, convert to milliseconds*/ + gameTurnSW.ElapsedMilliseconds;
  23.     if(runtime > currentGameFrameRuntime) {
  24.         currentGameFrameRuntime = runtime;
  25.     }
  26.     //clear for the next frame
  27.     gameTurnSW.Reset();
  28. }
复制代码


注意到我们也用到了Time.deltaTime。使用这个可能会在游戏以固定帧率执行的情况下与上一帧时间重叠。但是,我们需要用到它,这使得Unity为我们所做的渲染以及其他事情都是可测量的。这个重叠是可接受的,因为只是需要更大的缓冲区而已。

网络平均数

拿什么作为网络平均数在这里不太明确。我最终使用了Stopwatch计算从玩家发送数据包到玩家确认动作的时间。这个帧同步模型发送的动作会在未来两个回合中执行。为了结束帧同步回合,我们需要所有玩家都确认了这个动作。手机号码拍卖平台在这之后,我们可能会有两个动作等待对方确认。为了解决这个问题,用到了两个Stopwatch。一个用于当前动作,另一个用于上一个动作。这被封装在ConfirmActions类当中。当帧同步回合往下走,上一个动作的Stopwatch会成为这一个动作的Stopwatch,而旧的”当前动作Stopwatch”会被复用作为新的”上一个动作Stopwatch”。

  1. public class ConfirmedActions
  2. {
  3. ...
  4.     public void NextTurn() {
  5.         ...
  6.         Stopwatch swapSW = priorSW;
  7.             
  8.         //last turns actions is now this turns prior actions
  9.         ...
  10.         priorSW = currentSW;
  11.          
  12.         //set this turns confirmation actions to the empty array
  13.         ...
  14.         currentSW = swapSW;
  15.         currentSW.Reset ();
  16.     }
  17. }
复制代码


每当有确认进来,我们会确认我们接收了所有的确认,如果接收到了,那么就暂停Stopwatch。
 

  1. public void ConfirmAction(int confirmingPlayerID, int currentLockStepTurn, int confirmedActionLockStepTurn) {
  2.     if(confirmedActionLockStepTurn == currentLockStepTurn) {
  3.         //if current turn, add to the current Turn Confirmation
  4.         confirmedCurrent[confirmingPlayerID] = true;
  5.         confirmedCurrentCount++;
  6.         //if we recieved the last confirmation, stop timer
  7.         //this gives us the length of the longest roundtrip message
  8.         if(confirmedCurrentCount == lsm.numberOfPlayers) {
  9.             currentSW.Stop ();
  10.         }
  11.     } else if(confirmedActionLockStepTurn == currentLockStepTurn -1) {
  12.         //if confirmation for prior turn, add to the prior turn confirmation
  13.         confirmedPrior[confirmingPlayerID] = true;
  14.         confirmedPriorCount++;
  15.         //if we recieved the last confirmation, stop timer
  16.         //this gives us the length of the longest roundtrip message
  17.         if(confirmedPriorCount == lsm.numberOfPlayers) {
  18.             priorSW.Stop ();
  19.         }
  20.     } else {
  21.         //TODO: Error Handling
  22.         log.Debug ("WARNING!!!! Unexpected lockstepID Confirmed : " + confirmedActionLockStepTurn + " from player: " + confirmingPlayerID);
  23.     }
  24. }
复制代码


发送平均数

为了让一个客户端向其他客户端发送平均数,Action接口修改为一个有两个字段的抽象类。
 

  1. [Serializable]
  2. public abstract class Action
  3. {
  4.     public int NetworkAverage { get; set; }
  5.     public int RuntimeAverage { get; set; }
  6.     public virtual void ProcessAction() {}
  7. }
复制代码


每当处理动作,这些数字会加到运行平均数。然后帧同步回合以及游戏帧回合开始更新
 

  1. private void UpdateGameFrameRate() {
  2.     //log.Debug ("Runtime Average is " + runtimeAverage.GetMax ());
  3.     //log.Debug ("Network Average is " + networkAverage.GetMax ());
  4.     LockstepTurnLength = (networkAverage.GetMax () * 2/*two round trips*/) + 1/*minimum of 1 ms*/;
  5.     GameFrameTurnLength = runtimeAverage.GetMax ();
  6.     //lockstep turn has to be at least as long as one game frame
  7.     if(GameFrameTurnLength > LockstepTurnLength) {
  8.         LockstepTurnLength = GameFrameTurnLength;
  9.     }
  10.     GameFramesPerLockstepTurn = LockstepTurnLength / GameFrameTurnLength;
  11.     //if gameframe turn length does not evenly divide the lockstep turn, there is extra time left after the last
  12.     //game frame. Add one to the game frame turn length so it will consume it and recalculate the Lockstep turn length
  13.     if(LockstepTurnLength % GameFrameTurnLength > 0) {
  14.         GameFrameTurnLength++;
  15.         LockstepTurnLength = GameFramesPerLockstepTurn * GameFrameTurnLength;
  16.     }
  17.     LockstepsPerSecond = (1000 / LockstepTurnLength);
  18.     if(LockstepsPerSecond == 0) { LockstepsPerSecond = 1; } //minimum per second
  19.     GameFramesPerSecond = LockstepsPerSecond * GameFramesPerLockstepTurn;
  20.     PerformanceLog.LogGameFrameRate(LockStepTurnID, networkAverage, runtimeAverage, GameFramesPerSecond, LockstepsPerSecond, GameFramesPerLockstepTurn);
  21. }

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值