These are some ways that can help us optimize our multiplayer games. Ways in which we can reduce the CPU and Bandwidth load of our hardware and/or cloud instances.
Some reasons you would want to improve the performance of your servers are: to greatly reduce costs, to have faster servers that will allow you to provide a much better experience, or to simply have a much deeper understanding of the technology you’re using, whether you launch your game or not.
A multiplayer game, while similar in some ways to a normal single-player game, is very different in its very core. A well designed multiplayer game will have a totally different system architecture than its single-player counterpart.
We need to be very aware that a single clock-cycle can mean the difference between a good experience and a lag-spike. A little dramatic? Not really, while technology is getting better and better by the second, we still can’t control how our packets travel through the internet. Since we can’t control the packet loss, all the different routing tables, the performance in each node, and many other things, this is why we have to care so much about the performance of our applications. The few things we can actually control, should be optimized for performance as much as possible.
Server and client are different things
If you read my previous article about Costs in Multiplayer Games, you already know this. You need to separate your server’s code from your client’s code.
This comes with many perks, you will have a more organized and dedicated code. Making it easier to maintain, fix, update, and distribute. Think about it, if you only update your client code, you do not have to also update your servers. Same way on the other side, if you update your server’s code, you will not have to update all the users with the new “Compound Code”.
On the same note, you can strip down a lot of unnecessary logic that is only used on “the other side (server or client)”. You can also specialize logic that was too general to be used on both sides.
This will help us by not having that extra logic to take care of, things that do not pertain to our direct application. Validating all this unnecessary logic, just to make sure if certain code should be allowed to run in the server or the client, could leech some precious clock-cycles from our applications.
Send Fewer Messages
While you want to keep your game state as updated as possible, it makes a very big difference to send continuous messages compared to sending messages just on events. i.e. Sending the full state of your character (Position, health, mana, the state of very single key, etc..) every frame will have a much higher impact on both your computer and network performance than if you were to send only the required information when it’s needed.
Having smaller specific messages that tell the story of how things are changing is much better than having the whole snapshot of your game for every frame per user.
Having events in your code that run specific messages will help you achieve this. For instance, if you press a key, validate on the client side the action and then send a message specific to this action to the server. You can also do this on the server side, i.e. When validating if an ability hits someone, you can call an “Ability Hit Event” that will take care of doing damage, user notifications which can include both the aggressor and the victim, and so on. I’m sure you can get creative with these events.
Send Smaller Messages
This point goes hand in hand with the previous one. If you already thought about braking down those massive continuous messages into smaller eventual messages, you’re naturally sending less information per message. But there are some very useful ways to make things even smaller.
Why do we want to make our messages so small? Well as stated before, we cant control the internet. We can safely assume that the time it takes a message to be delivered is much bigger than the time it takes our computer to process the same message in many ways. By having fewer and smaller messages, we save ourselves a big percentage of our communications issues.
A great way to make our messages smaller is to see our attributes in terms of bits, yes 1’s and 0’s, I like to call this – Bitwise Optimization. For instance, a Boolean is equivalent to a single bit, we can group up several states in our game that are represented in bools (for example stun, blinded, silenced, …) and pack them into a single Byte. Then send a message with a single byte rather than sending a message with several bools. The reason these two ways are so different is because of serialization. If you’re using a built in communication library like UNET, this serializes the attributes and then sends this serialized byte array. This serialized information of multiple bools ends up being bigger than even a serialized byte.
You can do the same with small numbers that never go above a certain value. Lets take life for example, the life value will always be between [0 – 100]. This value will have a maximum of 7 bits and if you do the same with 3 other values, you now will have four 7bit values that you can easily encapsulate into a UInt32 and still have room for 4 bools. This UInt32 serialized will be smaller than the serialized array of 4 bytes and 4 bools containing exactly the same information.
No State Synchronization
State synchronization is getting better and better each day, so much that is getting difficult to distinguish the performance in small applications. Still not the cream of the crop when talking about performance in game communication.
Don’t get me wrong, state sync is great for prototypes, and it is certainly easier to learn how to use than to use a specialized or custom network communications library.
State synchronization has a lot of overhead in many ways, serializing and de-serializing messages, using reflection (that could be Unbuffered). The time it takes to process attributes into and from a message is just a little too big for my taste, not to mention the size in bytes of the serialized messages. Having bigger messages means more/bigger packets, and more/bigger packets lead to… Packet Loss.
Maybe I’m just biased because I like having everything as “Crisp” as possible. My “assembly mindset” gets in the way a little too much. Either a good or a bad thing in your eyes, it has allowed me to see things in different light, and made me want to improve many things that someone would normally just let go. Yes it takes more effort, time, and the drive to want to improve such “little things”, but trust me, being this meticulous pays off big time.
You will have much more control over the content and frequency/eventuality of your messages. This goes hand in hand also with the two previous points of optimization – Send fewer and smaller messages.
Custom Network Library
While many optimizations can be done with a built-in network communications library, having a custom networking library will allow you, by far, the greatest amount of room to make improved optimizations in many ways.
You will be able to control how you encapsulate and deliver your messages. This will allow you to take full advantage of the previously mentioned optimizations and will allow these to have great synergy. Moreover, it will help you deeply understand how the whole communication process works inside-out, and if done well, it will never make you want to look back.
I will not go into detail on how to code your own sockets communication library, there are several tutorials, videos, and resources out there that explain it very well. I will however encourage you to at least code your own, even if you’re never going to use it in your game. It will open your eyes and allow you to see things in a different light when developing multiplayer games.
I had to include this whole section just to mention some of the more obvious ways to optimize code in general, and things that apply to general coding and not only to Multiplayer Networked Games. i.e. Object Pooling and Design Patterns (as long as you don’t abuse them). All those things that help make your code more efficient will also help make a better networked game.
There are some design patterns like Command Design Pattern that are useful in multiplayer games for specific reasons like recording a whole multiplayer session so you can then save it and replay it in an In-Game-Replay kinda way.
I hope you found this article useful and clear. I know I’m missing some optimization techniques, I was aiming to explain a few of the most useful but lesser known approaches. Please feel free to add questions or comments below.