The game loop

As you know, the game loop is the very core of any real-time game. Although the game loop serves a fairly simple function, let us now consider some of the implications of having a game server and client running together.

Frame rate independence

The purpose of the game loop is nothing more than to ensure that the game runs in a consistent, ordered manner. For example, if we draw the current game state before we update it, the player might find the game to be slightly out of sync when they interact with it since the current display would be at least one frame behind what the player would expect it to be.

In addition, and this is particularly so in JavaScript's event-based input system, if we update the game every time we receive input from the user, we might have different parts of the game updating at different times, making the experience even more out of sync.

Thus, we put a game loop in place to ensure that, after any input is handled and cached and until the next tick of the game loop, we can apply the input during the update phase of the game step and then render the outcome of the update:

Frame rate independence

The most obvious solution to this problem is to model the input space within your game; then, query this during the update phase and respond accordingly. In other programming environments, we can simply query the input devices directly. Since JavaScript exposes events instead, we can't ask the runtime whether the left key is currently pressed.

Next, we need to update the game, which in most cases means that we'll move something just a tiny bit. After a few frames have been updated, these small movements that we've updated in each iteration will combine to create a smooth motion. In practical terms, what we need to do once the game loop has completed a cycle is to call the game loop again for the next cycle:

while (true) {
   update();
   render();
}

While a traditional game loop in most other programming languages might look something like the preceding code snippet, we can't do this in JavaScript because the while loop would block JavaScript's single thread, causing the browser to lock up:

function tick() {
   setTimeout(tick, 0.016);
   update();
   render();
}

A more appropriate approach in JavaScript is to use one of the timer functions (either setTimeout or setInterval) to call the game step method. While this solution actually works, unlike the while loop idea, we can run into issues such as the game consuming too much CPU (as well as the battery life of a mobile device), particularly when the loop continues to execute when the game is not running. We can also run into issues with the timer approach if JavaScript is busy with other things, and the tick function can't be called as often as we'd like.

Note

You may wonder why we make the call to setTimeout and requestAnimationFrame at the beginning of the tick method, instead of at the end, after we have actually executed the code inside the method.

The reason for this is that calling either of these two functions simply schedules the callback function to run at the next event loop cycle. Calling setTimeout or requestAnimationFrame returns execution to the next command in the function calling it immediately, then the rest of the function executes to completion.

Once the function returns, JavaScript will execute the next piece of code in the event loop, which was added to the loop some time in the past. In other words, if JavaScript detects user input while we're executing our game tick method or some other event takes place, these events will be added to the queue and will be handled after our tick method returns. Thus, if we wait until the end of the tick method to schedule it again with the event loop, we may find the tick method waiting in line (so that it can have a turn at the CPU again) before other callbacks are handled.

By scheduling the tick method early on, we can be sure that it will be called again as soon as it can after it completes its current execution, even if other events are triggered during the current execution, and other code is placed on the event loop.

Finally, the most appropriate way to write a game loop in JavaScript is to use the more recent window.requireAnimationFrame function:

function tick(timestamp) {
   var rafId = requestAnimationFrame(tick);
   update();
   render();
}

RequestAnimationFrame is a handy function implemented in browsers that we can use to ask the browser to invoke our callback function right before the browser performs its next repaint. Since the inner workings of the browser are outside the scope of JavaScript, the refreshing rate is now at the operating system level, which is much more precise. In addition, since the browser knows when it needs to repaint and is much closer to the display device than JavaScript can possibly be, it can make many optimizations that we couldn't do on our own.

Calling requestAnimationFrame will return an integer value that will map to the provided function in the callback list. We can use this ID number to cancel our callback from being triggered when the browser determines that it should have. This is a handy way to pause execution of the game loop without using a conditional statement at the beginning of the callback, which would normally evaluate to false most of the time (or so we hope).

Finally, the callback function that we supply to RequestAnimationFrame will be passed a timestamp value in the form of a DOMHighResTimeStamp type. This timestamp represents the time when the callbacks registered with RequestAnimationFrame get triggered in a given cycle. We can use this value to calculate the delta time since the previous frame, thus breaking our game loop out of the time-space continuum, which we'll discuss next.

Time-based game loop

Now that we have an effective way to update our game as fast as the underlying hardware is able to, we just need to control the rate at which the update happens. One option would be to ensure that the game loop doesn't execute again until at least some time has elapsed. This way we will not update more often than we know we have to. The other option is to calculate how long the previous update took and send that number into the update function so that it can move everything relative to that time difference:

Time-based game loop

As illustrated in the preceding figure, if we update the game twice as fast in one browser or device, then the time taken to update a single frame (also known as the delta time) will be half as well. Using this delta as a factor in the physics update, we can make each update relative to how long it'll take to update a single frame. In other words, in the course of a whole second, we can either update the game a few times where each time the update is larger or we update the game many times during the same second, but each update would be much smaller. At the end of the second, we would have still moved the same distance.

Multiple game loops

Running a game smoothly and consistently across different CPUs is a victory on its own. Now that we're past that, let's think about how we can actually achieve it across the client and the server.

On the browser, we can run the game for the user using requestAnimationFrame, as demonstrated earlier. On the server, however, there is no requestAnimationFrame. Worse yet, we can't quite send updates across the network to all participants at a full 60 updates per second. In theory, we could very well do that—maybe for a few seconds before the server heats up and melts down. In other words, running 60 updates per second for every game in the same server would cause tremendous load on the server. Thus, we will need to slow down the update's pace on the server.

First things first, though. Since there is no requestAnimationFrame in Node.js, we know that we can't use it. However, since the concrete implementation of the game loop for the game server is separate from the game client's, we can just choose another timer mechanism that Node offers.

Secondly, we need to have a second timer running in the server so that it can send updates to the clients at a much slower pace. If we actually try to send updates to every single client at 60 frames per second, we will likely overload the server very quickly and performance will decrease.

The solution to the client update problem is to send updates at a slower but consistent rate, allowing the server to be the ultimate authority on game state in a way that we can scale. Between updates from the server, if the game requires quicker updates, we can make the game client update itself in the best way it can; then, once it receives information from the server, we can fix the client state if needed.

There are two timer functions that are commonly used in Node.js as higher resolution replacements for setTimeout(). These are setImmediate() and process.nextTick(). The reason you will want to use one of these two functions instead of setTimeout() is because setTimeout() doesn't guarantee the delay you specify nor does it guarantee the order in which the events will be executed.

For a better alternative, we can use setImmediate to schedule a callback to run after every event that is currently sitting on the event queue. We could also use process.nextTick, which will schedule the callback to run right after the current block of code finishes its execution.

While process.nextTick might seem like the better option between the two, keep in mind that it will not give the CPU a chance to execute other code in the event queue (or allow the CPU to rest), causing execution to consume 100 percent of the CPU. Thus, for the particular use case of a game loop in your Node.js game simulation, you might be better off using setImmediate.

As mentioned before, there will be two timers or loops running in the game server. The first is the physics update loop, which will use setImmediate in an attempt to efficiently run at a full 60 fps. The second will be the client sync loop, which doesn't need to run as fast.

The purpose of the client sync loop is to authoritatively tell the clients what the real state of the game is so that each client can update itself. If we try to let the server adjust each client at every frame, we would have a very slow game and a very slow server. A simple, widely used solution is to only synchronize the clients a couple of times per second. In the mean time, each client can play the game locally and then make any necessary corrections when the server updates its state.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.140.188.244