Articles and experiences in game development

Menu

Month: April 2014

In the previous implementation of the lockstep model the game frame rate and the length of the communication turn (referred to as the lockstep turn here) were set at a fixed interval. In real scenarios latency and performance will vary. This update to the original model will keep track of two metrics. The first is the amount of time it takes to communicate with the other players. The second is run-time performance of the game frame method.

Rolling Averages

To handle the fluctuations in latency we want to quickly increase the amount of time of a lockstep turn when the latency increases, while we want to slowly adjust for better latency. This will make the game play feel smoother as the pace at which the game updates will be more stable than constantly adjusting. We also do not want to have to keep track of all of the past measurements of latency for the past lockstep turns. We can sum up all of the past information in a “rolling average” and adjust it by some weight for future values.

When ever a new value is larger than the current average, we will set the average to the new value. This will give use the behavior of quickly raising to the increase in latency. When a value is smaller than the current average, we will trust it’s new value by some weight w, so we have the formula:
newAverage = currentAverage * (1 – w) + newValue * ( w)
Where 0 < w < 1
In my implementation I set w = 0.1. I also keep track of the average per player, and always use the maximum average among the players. Here is the method for adding a new value:
[code lang="csharp"]
public void Add(int newValue, int playerID) {
if(newValue > playerAverages[playerID]) {
//rise quickly
playerAverages[playerID] = newValue;
} else {
//slowly fall down
playerAverages[playerID] = (playerAverages[playerID] * (9) + newValue * (1)) / 10;
}
}
[/code]
In order to maintain determinism the calculation is done using only integers. So the formula is adjusted like the following:
newAverage = (currentAverage * (10 - w) + newValue * ( w)) / 10
Where 0 < w < 10
And in my case, w = 1.
Runtime Average

The time it takes to execute each game frame is tracked to be used for the runtime average. If the game frames start taking longer, then we need to decrease the number of game frames per lockstep turn. On the other hand if the game frames start executing faster, we can have more game frames per lockstep. For each lock step turn, the longest running game frame is used to be added to the average. The first game frame of each lockstep turn also includes the amount of time it takes to process the action for that lockstep turn. A Stopwatch is used to take the measure of lapsed time.

Note that we also include Time.deltaTime. Including this may have some overlap with the last frame if gameframe is running at the same rate as the Update() method. However we need to include it so that rendering and other things that Unity does for us is considered in the measurement. The potential overlap is acceptable as it would just give us a bigger buffer.

Network Average

What to use as the network average was not as clear to me. I ended up using a Stopwatch that ran from the time a player sends an action to the time they receive the final confirmation of the action. This lockstep model sends an action to be processed for two turns in the future. To increment the lockstep turn we ensure that all players have confirmed the action we are about to process. Due to this setup, we could potentially have two actions that are waiting for all of their confirmations. To deal with this, two Stopwatches are used. One for the current action and one for the prior action. This is encapsulated in the ConfirmActions class. When the lockstep turn is incremented, the prior Stopwatch becomes the current stop watch, and the old “current stop watch” is cleared and reused as the new “prior stop watch”.

Since the posting of this article a small update has been made to support single player mode. Special thanks to Dan at redstinggames.com for figuring this out. You can see the modifications here:Single Player Update diff