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.
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).
//do work here
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:
if(massive_array[i]!="something I want") continue; //skips to the next value
break; //ends loop, instead of iterating through the remaining values
Multithreading, it’s a little more complicated.
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).
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.
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