Clientside prediction of weapon switching and reloading
I ran into an interesting problem while building my most recent 2D shooter game. This game exists for two purposes: 1) refining nengi.js, my networking library (aiming for over 100 simultaneous players) and 2) capturing my insatiable need for actiony multiplayer gameplay. As such, I need everything to ‘feel’ a certain way, fast and responsive... and I need it all multiplayer.
Enter the classic FPS-style switching between weapons. You’ve got a few ‘slots’ with a weapon in each, and you can switch between them all. The weapons don’t come out instantly (or else you would just mash 1 2 3 4 5 while clicking your mouse as fast as you can and every weapon would fire in a split second). The timings of everything matter. Pull out your <whatever> at the wrong time as someone peeks the corner? Well that 1/4 of a second it takes to switch back is starting to feel real long.
To implement this system, I began by making weapons that had ‘drawTime’ and ‘reloadTime’ referring respectively to how long it takes to pull out the weapon and how long it takes to reload the weapon. Maybe the pistol comes out inhumanely quick in 50 milliseconds, but the sniper rifle takes 800 milliseconds making you wonder why you even bother. I then wrote a fairly robust ‘WeaponController’ that handled switching between weapons and reloading them, basing the timing math off of javascript’s setTimeout and closures that would finish the weapon draw or finish the weapon reload.
As I finished the WeaponController, I wondered to myself if today was the day that I would get away with leaving setTimeout in one of my apps. I mean for once, it wasn’t just a hack.. I really did have code whose job was to wait awhile and then call a function. I even wrote some clever ‘cancelReload’ functionality which used clearTimeout to stop the reload when the player switched weapons or started to reload something else. It was like async programming... in a program with a game loop that had no need for such a thing.. but alas i was hopeful anyways.
Let’s talk about networked games, and prediction, so that the mistake may unfold.
Here’s a naive networked game:
every frame on your client, collect input (keystrokes, commands, etc) and send them to the server
every frame on your server, process input, and send the game state (positions of entities, etc) to the clients
Here’s a networked game with the possibility of prediction:
every frame on your client increment a clientFrameNumber variable, collect input (keystrokes, commands, etc) and send them to the server stamping them with the clientFrameNumber
every frame on your server, process input, and send the game state (positions of entities, etc) to the clients noting the last processed clientFrameNumber
Now when the client receives the game state from the server, it also knows how many frames a head of the server it is. If the server just responded saying the last processed clientFrameNumber is 52, but the client just finished sending 59, then the client knows it has 7 frames worth of inputs sent to the server that are not yet represented in the most recently received game state. Now its time for prediction! Staying very general, prediction code looks like this:
1) set the state of things on the client to the last confirmed server state
2) all at once, apply all the inputs that have yet to be confirmed by the server
Do this every frame of the game, and the end result is the game responds to you instantaneously, as if you’re playing a single player game. Also, if ever there is a disagreement between the server and the client, the client will be corrected (harshly), thus preserving the server’s authority over the game.
Back to my issues with drawing weapons and reloading. Prediction involves applying numerous past frames worth of inputs to your game every new frame. These inputs are being repeated every frame! If you’re 7 frames behind on frame 52, then you have multiple frames of input that need applied right away to catch your client up to what the player has already done. Getting your game into the right state always consists of the same process: setting the state to the last known state from the server, and then advancing that state by 7 frames of yet-to-be-confirmed inputs. And how long do you have to run these 7 frames? No time really. It all has to happen instantly. You have to write the type of game code that can handle being stepped forward 7 frames all at once. That means you gotta have a ‘deltaTime’ on your update functions, and the code must be relatively frame-rate independent. Running someComponent.update(10) ten times in a row and running someComponent.update(100) should produce the same state in someComponent. And that’s prerequisite to a lot of multiplayer functionality (well, if it is going to be nicely networked).
Figuring that out initially might be tricky, but making it actually happen isn’t too bad at all. How does setTimeout(action, 800) convert to something compatible with prediction? Like this:
var timeElapsed = 0 function update(deltaTime) { timeElapsed += deltaTime if (timeElapsed >= 800) { action() } }
This deltaTime idea doesn’t even originate from multiplayer programming... this idea originates from the desire to have games play similarly on different speed CPUs.
Code like this allows you to advance the simulation of the game by any amount of time. Advance it by 500 ms? Well then action won’t have fired yet. Advance it 7 times by 200 ms? Then action is going to fire (on call 4 out of 7). In my case, action() is the code which decides whether that sniper rifle is in your hand, or still in your bag. It decides if your ammo is 0/30, or a freshly reloaded 30/30... and the best part is that combined with prediction it allows most of the game interface respond to you as if your ping is zero.






