Introduction
Networking has proved to be a challenge. For starters, Godot’s networking API is fairly new. It was introduced into Godot by Fabio Alessandrelli and others from the Godot development team. Before 4.0, you needed to network your game using only RPCs. As of Godot 4, you have a few objects/nodes to help you out. TeamSpec uses a combination of all of these. In this article, I will explain what I learned and the problems I worked around when writing net code.
Here's some quick vocabulary to better understand this article if you don't use Godot:
The Godot engine is built upon these two concepts.
Multiplayer Synchronizer
Godot 4 added the Multiplayer Synchronizer node. This node has one job and it is to synchronize a list of attributes from a node in its tree across all instances of that node (given they have the same path) across different clients.
When using this node, I had a massive problem. It had to do with the server_relay boolean. When set to true, a client would take the data it needs to send, multiply it by the number of other peers it will need to reach, and send all that data to the server. What this meant is that, if n players were connected on a server, each player had to bear the responsibility of sending n times their upload bandwidth. This essentially made the game unplayable; Most clients typically don’t pay for internet speeds that have a high upload bandwidth. This is the responsibility of the server in normal use. Suddenly forcing each client to act as a server in terms of bandwidth just does not work, especially if I want lobbies with 24 total players. Each player would be forced to send 4608 bits instead of 192 (32-bit floats in a 3D vector for 96 bits, times two for 192).
The only reason I was using the server_relay = true setting was because I wanted to save time and not make a fully authoritative server just yet. This issue forced me to go ahead and make an authoritative server model instead. I couldn’t just have clients send their inputs to the server and then have the player move. No action game does things this way. It would mean that everyone would have input lag equal to 2x their ping. Simply unacceptable. What I instead decided to do was have two synchronizers that essentially synchronize the same thing. The first is called ClientMovement, and it synchronizes to the server with the authority of that client. It keeps telling the server where it is and where it is looking. The second is also located in that player, but it is called the ServerMovement. This synchronizer has server authority, and its job is to constantly broadcast what the ClientMovement says to everyone who isn’t the person who sent it. I don’t have a way to tell the server to not waste bandwidth sending duplicate info back to the sender, but for now, I just tell the client to ignore the data when it receives it.
This new system was tested with 5 players and worked flawlessly. It means that clients are required to have high download bandwidth, but incredibly small upload bandwidth as they only need to upload two 3D Vectors 100 times per second.
Multiplayer Spawner
Godot 4 also added the Multiplayer Spawner node. The job of this node is a wrapper for Godot’s scene replication and removal interface. It basically will duplicate certain declared nodes across all peers when they are spawned as a child of a certain reference node declared in the spawner.
This node has two modes:
The first is the auto spawner mode. When using this, you must declare an array of objects the node can spawn (presumably for security). Once done, the spawner will automatically replicate nodes spawned by the authority (typically the server) across all peers. When the node is removed on the authority, it will remove it on all peers too.
The second mode is the manual spawning mode. This is what I have started to use for character instances. What this does is, instead of adding a node to the reference node, you use the spawn() function. This will invoke the custom spawn function you implemented and essentially allows you to pass additional data across to the peers before the node is created. I used this to pass the data that tells a character if it’s a bot or a player.
RPCs
RPCs (Remote Procedure Calls) are used in TeamSpec to broadcast one-time events to fellow peers. I can choose to broadcast to everyone or just the server. Some actions are sent directly to the server while others are sent only to the client whom it would concern. These actions can vary from applying an impulse to the player, setting a player’s position, changing a player's health, switching weapons, and much more. These require quite a bit more code to write so I try to only use them when I have to. Currently, a player’s object name needs to match the multiplayer ID assigned to them upon their connection, otherwise, my RPC system doesn’t work. For this reason, I don’t know how I’m going to cleanly handle bots. Bots would all have the same multiplayer ID (the server), but the ID alone would not be enough to distinguish who they are. This is a problem I need to try and solve cleanly; I don’t want to bloat every RPC with an extra integer every time, but I might have to.
Conclusion
The purpose of this blog post was to expose something most people don’t even think about. Networking has been the second most complicated aspect of TeamSpec thus far, right behind physics. The only reason physics was so complicated was because I was incredibly picky and wanted it to feel perfect. The physics and the net code still aren’t perfect, but I’m working hard to make them. I thank you for taking the time to read this, and I hope to see you on launch day!