Home Blog Photography Games



Advice for Writing Multiplayer Code

GodotSteam is a C++ module that can be added to the Godot Engine to enable the programmer to add universal peer to peer multiplayer functionality to their game in a (relatively) simple fassion. I also chose to use this for my MultiGodot project, which is itself a C++ module.

This blog post will use C++ code. It will be less of a tutorial of setting up GodotSteam (for that you can read the docs), and more of a method to easily communicate between two or more peers through packets.

Initial Functions

After following the GodotSteam tutorials you should end up with this set of functions:

// CONSTANTS

static const int PACKET_READ_LIMIT = 32;
static const int MAX_MEMBERS = 4; // Possibly increase this in the future if there is a need.
static const int DEFAULT_CHANNEL = 0;
static const int PACKET_SIZE_LIMIT = 1200; // bytes

// NODES

Steam *steam = nullptr;

// PROPERTIES

uint64_t lobby_id = 0;
uint64_t steam_id = 0;
bool is_lobby_owner = false;

// METHODS

void _create_lobby();
void _join_lobby(uint64_t this_lobby_id);
void _get_lobby_members();
void _make_p2p_handshake();
void _send_p2p_packet(uint64_t this_target, Dictionary packet_data, P2PSend custom_send_type, int custom_channel);
void _read_all_p2p_packets(int read_count);
void _read_p2p_packet();
void _leave_lobby();

// SIGNALS

void _on_lobby_created(int connect, uint64_t this_lobby_id);
void _on_lobby_match_list(Array these_lobbies);
void _on_lobby_joined(uint64_t this_lobby_id, int _permissions, bool _locked, int response);
void _on_lobby_chat_update(uint64_t this_lobby_id, uint64_t change_id, uint64_t making_change_id, int chat_state);
void _on_p2p_session_request(uint64_t remote_id);
void _on_p2p_session_connect_fail(uint64_t this_steam_id, int session_error);

These will enable you to create, join, and send packets (represented by Godot's Dictionary types and then converted to bytecode), which can hold pretty much any information except for pointer types. Pointer types are any type that inherits from Object, because all Objects in Godot are heap allocated, and sending a pointer over the internet isn't very useful.

Remote Messages

All GDScript functions are registered through the ClassDB. This class binds a method pointer to a String, enabling the programmer to call any registered function using just a String.

If we want to call a function remotely on another (or all other) peers, we need to send a message devoid of pointers, including function pointers. Thus, we can utilize the ClassDB to send a String with the name of the function and values of arguments over the internet, and then the client can call that function with those arguments on the other side. In practice:

void _call_func(Node *node, String function_name, Array args, uint64_t custom_target) {
   NodePath path = editor_node_singleton->get_path_to(node);
   auto send_dictionary = Dictionary({
      KeyValue<Variant, Variant>("message", "call_func"),
      KeyValue<Variant, Variant>("path", path),
      KeyValue<Variant, Variant>("function_name", function_name),
      KeyValue<Variant, Variant>("args", args),
   });
   _send_p2p_packet(custom_target, send_dictionary);
}

And on the receiving end, inside the _read_p2p_packet function:

if (message == "call_func") {
   NodePath path = readable_data["path"];
   String function_name = readable_data["function_name"];
   Array args = readable_data["args"];
   Node *node = editor_node_singleton->get_node_or_null(path);
   if (!node) {
      print_error("Remote function call on null node at path " + String(path));
   }
   node->callv(function_name, args);
}

With just this simple addition we can now send messages back and forth to any peer!

In theory you could extend this to implement the concept of "returning" variables from functions. This would be very easily done in GDScript because the _call_func function could return a Signal that would be triggered when the value of the function was returned. The user code could then await for that signal to be called. The concept is slightly harder in C++ because you would have to implement the whole async await hastle which probably isn't worth it, at least not without an external library.

Tips

Writing multiplayer code can seem hard to wrap your mind around at first, similar in caliber to recursive functions or multithreading. Really, it's just a matter of abstracting lower level features like packets in ways that are more similar to the conventional style of programming. Remotely calling methods on peers is similar to calling methods on objects. This abstracts the hastle of "sending messages" into a more familiar style.

Similarities between multiplayer and multithreading can also be drawn, especially in the context of the race condition. When multiple processes are running, and one process writies to a data while another process reads or writes that data, undefined behavior occurs. In the context of threading, this problem is mostly solved with the Mutex, which makes sure a thread waits for the other to finish writing/reading to the data before doing the same.

The key difference between multiplayer and multithreading is that each context has its own individual variables, and so the risk is more that these variables can become out of sync. When this happens to key parts of the application (say, a Player's position), you'll have a problem.

One of the solutions to this problem is the concept of "ownership". Each client owns a set of specific values, and those are the values they can set. All other values can be read of course, but should not be set by that client. This concept is great for games, but starts to break down for more complex apps where ownership may not be so clear, such as a document editing app. To solve problems like this you could:

Although some of this might be helpful, chances are you're still going to run into trouble doing whatever it is you're doing. When you do run into trouble, stay resilient! That is how you gain valuable experience, in a way that no LLM could gain experience without spending millions of dollars of water and electricity on a retrain.

It would be hard to put into words the knowledge I have gained (and still am gaining, you never stop learning) from my current project, MultiGodot which you can read more about in this blog post, and maybe in the future a more technical post about its inner workings. But until then ...

Happy Tuesday, August 22nd, 2025, 18:47 PDT!















































































© 2025 Carson Bates. Some people are surprised that the above animation doesn't use JS, others aren't.