Making a game server can be a hard task. Multiplayer games require the server to respond as quickly as possible and sometimes even predict what’s going to happen to avoid issues caused by players having different latencies. In this article, I’ll explain how I made my game (RPGMod) have a server script capable of handling up to 1000 simultaneous players per instance without issues, on Node.js.
Game Server VS Web Server
First, what is the difference between a game server and other types of servers (such as the one for this website, called a web server)? Well, pretty much everything.
A good response time for a website is about 200ms, for a game it’s about 20ms. Websites can respond to requests asynchronously(which in this case means, whatever response can be processed faster, goes out first) to improve performance, while game servers need to respond to everything in a specific, synchronous order to not cause problems. A web server has a lot more time to send and process information than a game server, and can even take advantage of Content Distribution Networks (CDNs) to improve the user experience, caching common responses and heavy content, while game servers have to do everything on their own and are a lot more constrained in regards to time.
Another basic difference is: a web server uses mostly HTTP, whereas a Node.js game server will likely use WebSockets (much faster for continuous communication). Since dealing with WebSockets isn’t that easy server-wise, I’d recommend you use “Socket.io” for it.
The basics
First thing to go over for optimization are the basics: Cache everything, Multithread when you can, Optimize Loops. Caching means if you did a calculation that will be done again in the future, save the result in a variable and re-use it. If you access an object’s value that you’ll need again in the future (which is costly on Node.js) put it in a variable. If you’re going to read a file, put it in a variable and next time you need it, read the variable instead (RAM will always be faster than the disk/SSD).
Loops can be done in a variety of ways in JavaScript: you can use forEach, for of, while, do while, for, etc. They all work in very similar ways and can achieve basically the same results, but if you want the BEST possible performance, use a for loop. More specifically, like this:
for(var i=value;i--;){
//do work here
}
This is the most optimal way you can write a loop in JavaScript. It eliminates the constant check for i = 0 by decreasing its value instead of increasing (for loops stop automatically when the counter variable reaches 0) and you cache the value you want to iterate (say, an array.length) which reduces the amount of checkups for an object’s value. While, do while and for of have similar performances, but in my tests for loops are still slightly faster overall (and for Node.js, in the case of a game server, you’ll need to squeeze as much performance as possible).
ForEach loops are an absolute no-go, as they call a function for each and every value of the array (and function calls have an inherent performance cost, as small as it may be). If the function is anonymous(which means, isn’t named and built outside of the foreach), it can be even worse, as each iteration will create the function in memory, execute then repeat.
Another thing about loops is, you can skip an iteration and even break out of them using “continue” and “break“, and that can also save processing time. Example:
for(var i=massive_array.length;i--;){
if(massive_array[i]!="something I want") continue; //skips to the next value
else{
//do something
break; //ends loop, instead of iterating through the remaining values
}
}
Multithreading, it’s a little more complicated.
Javascript can use threads in the form of “workers” (web workers for JavaScript web and worker threads for Node.js) and both of those possess a small amount of overhead when communicating with the main thread(due to serializing and deserializing when sending data to/from a thread to another).
Overhead means you’ll need to weigh whether or not giving separate threads data to process instead of the main thread is worth the cost. For small messages, usually the overhead will be about 0.25ms for each one sent sent/received, but bigger amounts of data will mean bigger overhead. On my server script, I’ve only two threads: main thread doing most things (server logic, connections, etc) and secondary thread having the AI logic (A* algorithm) + initial part of encryption (all communication starts encrypted using RSA then upgrades to AES-128 bits).
Advanced bits
Now a more interesting part comes into play, the advanced optimization. Node.js uses v8 from Google as their JavaScript engine, and JavaScript engines tend to be a bit of a black box under the hood, but there’s ways to make them work for you.
First tip is, use typed arrays whenever applicable. Typed arrays are a more primitive kind of array, they’re used to create a specific view of a data buffer. Each entry is a raw binary value in one of a number of supported formats, from 8-bit integers to 64-bit floating-point numbers. Sorting through them is quite faster than normal arrays.
Another tip: avoid functions. As much as it sounds counter-intuitive, and as useful as they are to not repeat code, Function calls in JavaScript come with a small overhead. Now, normally that isn’t a big deal, but consider the following scenario: Your server has 1000 players, each of them receiving and sending 10 messages per second (10,000 messages in 1 second). If you always call a certain function for all of those messages, if said function takes 0.002ms to be called, that means that 20ms out of every second are spent just calling it.
It might not seem like much, but for game servers every ms counts . You’ve a budget of 20-30ms to send messages to the client without them considering it “laggy”.
On the topic of functions, avoid anonymous functions at all costs. Anonymous functions are functions without name, usually used for callbacks. Those functions never get optimized by the v8, so declare your functions beforehand then use them as callback.
Going back to encryption that was mentioned before, encrypting every single message before sending to the player might be expensive (on my machine, encryption takes 0.02ms which when applied to the scenario above may mean 200ms out of every second spent just encrypting/decrypting) so the less you do it, the better. That’s why I recommend two things:
- A Ticks system . Ticks, in the case of a game server, means that messages are only sent to the client once every X milliseconds. This is a common design implemented in many shooters (Battlefield, CS:GO, etc), that help keep all players a little more synchronized.
- Grouping messages before encrypting. Make sure that all messages that each specific player would receive in each tick are grouped (maybe add a “&&” between each to separate them for the client) before encrypting and sending, thus reducing CPU and bandwidth usage.
Other minor things
To wrap up, here’s a list of minor things you can do that also help to improve performance and to monitor it:
- Keep node.js up to date(every now and then, things get optimized in new versions)
- Run nothing else in the background
- Use node.js’ built-in profiler