Client-side scripting and weapons (continued)
I ended the last blog entry hopeful that the remaining work for scripted weapons – the transmission of variables between the server and the client – would proceed smoothly. It seems that my optimism was well placed, since instead of the week I originally scheduled, it only took a day to get working!
Our updated pistol behaves a lot more like a pistol should -- it has limited ammunition and when you exhaust all of the bullets in a clip you can reload it. Here's the updated pistol source code with the interesting bits highlighted.
Since both the server and the client are running the same script code, they both keep track of how much ammo is left and remove a bullet after each shot. Normally you might that that's enough to keep everything working perfectly, but it's possible for the ammo value on the server and the client to get out of sync with each other. This occurs if the client incorrectly executes a weapon firing event which never occurs on the server.
Here's a scenario where that can happen: say you're getting charged by an Onos. Naturally you pull out the most powerful weapon in the TSA arsenal – the pistol – and draw a bead on his left nostril. But a millisecond before you shoot, the Onos does a stomp attack that stuns you. It takes a little while for the message saying you're stunned to arrive from the server, so your client thinks you should be able to fire when you pull the trigger. It happily runs the pistol attack code which plays the attack animation and removes a bullet from your clip. When the server gets the message that you pulled the trigger, it just ignores it since it knows that you're supposed to be immobile. Now the pistol on the client has one fewer bullets than the pistol on the server.
This is where variable transmission comes to the rescue; at least to the rescue of our out of sync problem, it won't help one bit with your Onos problem. Since our pistol script code informed the engine that the ammo variable should be networked, the networking system will periodically transmit its current value from the server to the client. So even though the variable is incorrect on the client, it'll be fixed up soon enough (although in your current predicament with the Onos you may not live to see it).
Keep in mind that this type of scenario is a corner case and doesn't happen too often. Normally the client prediction matches what's happening on the server and a fix up like this isn't necessary. But when you have a high ping to the server, your client tends to be more out-of-date with the server and the situation is more prevalent. That's why when you're lagging in most shooters, you sometimes see things happen on your machine that a few seconds later become undone. This however is a small price to pay for the responsiveness that client-side prediction affords.
The last piece I'm going to add to the pistol before moving on is a secondary fire mode. We're planning on including alternate fire for all of the weapons in Natural Selection 2, although the list currently isn't finalized. Feel free to post your suggestions in the comments for this blog.
One other thing worth mentioning in the pistol code is the use of the balance variable system. Balance variables store important constants that can be changed via an in-game debug interface while the game is running. The balance variables in the pistol code are things like kPistolClip and kPistolDamage, which store the maximum clip size and the damage done by each bullet. As the name suggests, the balance variable system is there so that we can easily adjust the game balance without interrupting a play test session. Just like the scripting approach, balance variables are another facet of our initiative to help us rapidly iterate and fine tune the game.
The balance variable system is an enhancement of a similar system that was in Natural Selection 1, and one of the great new features for Natural Selection 2 is how it integrates with our scripts. In Lua the balance variables automatically appear as global variables and you access their values just like you would any other variables. This isn't directly possible in C++ since all variables must be declared at compile time and the balance variables are read from a text file when the game is started. That's just another way that scripting is making things a little bit easier.