April 17, 2020 (edited June 7, 2020)

Networked multiplayer games. This is a huge, not straightforward, thing to be done. That's true if we want to close some obvious doors for cheaters while also still keeping the game at a reasonable playable state because of inherent latency that can also be added on top of the physical lag caused by the network infrastructure.

I have published two tutorials (here and here) on this web site regarding how to setup networked multiplayer games with Godot. The overall necessary steps to achieve a synchronized multiplayer game through the network with snapshot interpolation is shown throughout both tutorials. Yet, just a little bit of network latency results in a somewhat unplayable experience because the shown method makes the client wait for the response from the server in order to update its state.

But there is also one rather problematic thing with that entire system. Adapting to a custom project may not be that difficult, however small changes in the game requires a bunch of updates in pretty much the entire networking system which obviously will lead to not so fun to hunt bugs. If one were to add client side prediction to hide the latency, it also brings another thing that must be verified whenever the game is updated.

This addon was born from the desire to automate most of the process to achieve a synchronized networked multiplayer game. Most of the design decisions behind it are meant to result in a less intrusive as possible system, that should not interfere (too much) with the game logic and, if possible, don't force any node hierarchy whatsoever.

When you see the code samples and/or the how to use tab, you will probably notice the fact that there isn't a node meant to be attached into your game object which will perform the synchronization. While this kind of node would be really nice, reality is that it would be really difficult to implement something generic enough to be useful. The problem is that often it's not enough to replicate only the "physical states" of the game objects (orientation, position and velocities). Anything that results in a visual display (player health for GUI, color of a banner and so on) will require replication, at least at some point in time.

The system consists in basically performing some initialization, creating a few short scripts meant to work as a bridge between the game logic and the snapshot system and perform some per game loop calls that are mostly telling which objects are part of the snapshots. In this most basic usage the result will be a client/server game that is in a similar state to the one that is achieved by following my other tutorials. In other words, absolutely no mechanism to hide latency.

Client side prediction is a technique used to try to hide network latency and consists in running the same game logic no only on the server, but also on the client machine. One may think that game logic should not be run on the client, but that's not really the case, provided the client does not directly tell the server anything about the game state. With this technique the client tries to stay ahead of the last received snapshot data, which represents a past state of the server (remember there is latency). When a new snapshot is received, the client must then compare with a corresponding internal prediction state, which is also client's past. If there are differences, then the client must perform the corrections accordingly. Because each project has to deal with this mechanism based on its code, the entire prediction and correction system cannot be part of the addon. Still, this tutorial describes how to achieve this technique using this addon. Also check the network and the complete examples in the demo project.

Two things to remember about this addon:

  1. It requires activation in the project settings, plugins tab.
  2. There is an internal dependency, more specifically the script.

There are a few limitations with this addon:

  • The input system works in a way that becomes rather difficult (if not impossible) to mix local multiplayer with networked multiplayer in a single game instance.
  • The input system packs boolean actions into a single 8, 16 or 32 bit integer, meaning that only a maximum of 32 of those types of actions can be used with this system.
  • Signatures of input and snapshot data are using 32 bit, meaning that if the game is at 60 frames per second then after slightly more than 4 years and a half of continuously running it will break, forcing a restart to occur periodically.
  • The replicated event system requires each event type to have an identification code (number), which will be encoded using 16 bits to be sent through the network. Because of that there is a limitation of only 65536 event types that can be used with this system.
How to Use
How it Works

Once this addon is activated, it will not only create a few entries in the project settings main tab, it will also automatically add the script into the project's auto-load (singletons) list. If you have any other singleton script and intend to access the network addon from it, ensure your script is loaded after the network.

The additional project settings are shown in the next image:

Additional Project Settings

Those settings will be described throughout this tutorial's tab, so don't worry too much about them at this moment.

Server Creation and Joining

The very first thing that we must deal with is, obviously, the structure to open the server as well as means to connect to them as clients. Those operations can fail and the result is rarely immediate. Because of that, the operations that must be performed based on success (change scene) or failure (display error message) must be done through signal handling. For that, in the main menu script we have to listen to a few signals defined in the script.

SignalUsually For
server_createdSince this will be given when the server is created, this should be a pretty good place to transition into the game scene
server_creation_failedThis is given when the creation fails. Because it may take some time before this signal is given, it would be a good idea to temporarily disable the create and join buttons to avoid further requests while this signal is not given.
join_failThis is given when joining a server has failed. Maybe the connection is not correctly working, or the server does not exist or even the port is not correct. This behaves much like the server_creation_failed, so maybe some buttons should be temporarily disabled until the signal is dealt with.
join_acceptedJoining the server has been successful. Perhaps this is a good place to transition into the game scene
join_rejectedJoining the server has actually happened but the system allows for some other kinds of checking which may allow manual rejections. In that case this signal will be received. Again, it may take some time so further UI code may be desired.

Note that if you don't intend to use the connection rejection (typically for a banning system or wrong credentials), then you can safely ignore the join_rejected signal since it will only be given if you explicitly deal with this. Shortly I will show how.

In any way, once the desired signals are properly handled, it's a matter of calling, from button clicks, the functions that may trigger those events. Namely, in order to create a server it's simply calling the function create_server(), which requires the connection port, the name of the server and then the maximum number of connections:

func _on_create_server_clicked() -> void:
    # Try to create the server on port 1223, named "TestServer" and with max of 5 connections
    network.create_server(1223, "TestServer", 5)
    # ... perhaps some UI buttons can be disabled in order to avoid multiple clicks while
    # the network system is performing the necessary tasks, which may even fail
    # Another option is to display a modal window with a message like "Creating server..."

Once that is called, the addon will try to create the server. In case it's successful, then the signal server_created will be given and provided there is a proper handler, the game may change into the game world scene. Obviously, if the creation has failed (server_creation_failed), then it's a good idea to show a message box and, if there was any UI disabling, re-enable them after the message box is dealt with.

Now to join a server. This is done by calling the join_server() function, which requires the IP address and the connection port. In the following snippet we try to connect to the local machine (which is represented by the special IP, which should have a server window running:

func _on_join_server_clicked() -> void:
    # Try to join the server on port 1223, on the local machine
    network.join_server("", 1223)
    # ... perhaps some UI buttons ca be disabled in order to avoid multiple clicks while
    # the network system is performing the necessary tasks, which may even fail
    # Another option is to display a modal window with a message like "Joining server..."

Again, provided there are proper signal handlers, it may be a good place to change into the game world when the join_accepted signal is given. Obviously, in the same fashion that was done with the server creation, deal with the UI if the join_fail has been given.

Connection Rejection

Perhaps you may want to create login or a ban system. For that, there is a way to reject connections. Using this system consists in handling a signal named credentials_requested and setting a function reference property named credential_checker. The signal handling is something that is run on the client, while the checking is done on the server. Note that if the checker function reference is not set the entire system will be ignored and new connections will be automatically accepted by the addon, provided the ENet system didn't reject because of maximum amount of connections.

Basically, if the credential checker function reference is set then the server will request credentials to the client trying to connect. On the client this will result in the credentials_requested signal being given. Since this is part of the attempt to connect to the server, the proper location to deal with this is still in the main menu script. So let's first deal with the request to give credentials. Suppose we have the following function connected to the credentials_requested signal. It will create a credential dictionary that will be sent to the server:

func on_credentials_requested() -> void:
    # This dictionary can surely change from project to project
    var credential: Dictionary = {
        "user_name": get_user_name(),
        "password": get_salted_password(),
   # Dispatch the credentials to the server

Now that the credentials have been sent to the server, the authority must deal with it. We need a function that receives a dictionary as argument (the credentials dispatched by the client) and return an string, empty to accept the connection or a text that will be given with the rejection, which may give the reason for the fact.

func on_credential_received(cred: Dictionary) -> String:
    # Assume the function check_db_for_user checks a database and returns true if the
    # provided user name exists and the password matches
    if (check_db_for_user(cred.user_name, cred.password)):
        if (!is_user_banned(cred.user_name)):
            # The user exists and is not banned. So accept the connection by returning empty string
            return ""
            # Reject the connection because user is banned. It would be a good idea to also check the
            # ban time and attach that into the returned string
            return "You have been banned"
        # The user name does not exist in the data base or the password does not match
        # So, reject the connection
        return "Invalid user name and/or password"

As you can see, the credentials system gives space for any kind of checking, being open for the requirements of pretty much most projects because the actual credential contents can be stuffed into a dictionary, which will be sent to the server and dealt accordingly.

Input Setup

Part of the game synchronization mechanism when dealing with client/server architecture consists in the client sending input data to the server, which will then compute the state based on that. The networking addon pretty much automates the process of sending input data to the server, but it must "know" what data is meant to be gathered. If the requirements are relatively simple like any movement that directly responds to the input keys, then the process consists of registering within the network object all of the relevant input mappings while also specifying if that map should be dealt as boolean or analog.

So, suppose we have in the input map (project settings window) the following actions (the actual buttons are irrelevant here):

| Action | Analog? | | jump | No | | sprint | No | | shoot | No | | move_forward | Yes | | move_backward | Yes | | move_left | Yes | | move_right | Yes |

During the initialization of the game we then have to tell the network system that we want to deal with these action keys, through the register_action() function. The first argument should be the name of the input map, while the second specifies if the value should be gathered as analog or not:

func _setup_network_input() -> void:
    network.register_action("jump", false)
    network.register_action("sprint", false)
    network.register_action("shoot", false)

    network.register_action("move_forward", true)
    network.register_action("move_backward", true)
    network.register_action("move_left", true)
    network.register_action("move_right", true)

At a later moment, when the input is necessary, regardless of server or client, it should be retrieved through the network system. This will be shown later in this tutorial, but for now, focus on the initialization. With those lines we are essentially telling the network system that we have those input mappings to be encoded/decoded when sending data through the network.


The system has a limitation of only 32 booleans that can be sent through the encoding because it packs all of the boolean input actions into a single integer (either 8, 16 or 32 bit). In this regard, each analog (floating point) input action will take 32 bit of data when being sent through the network.

Ok, but, what if we need the mouse relative (or even mouse speed) as part of the input data? In a first person shooter, for instance, the mouse relative is rather necessary! Obviously we have to tell the network system that we need this information when gathering input data. There are two options here to enable this to be part of encoded/decoded data. The first method has already been indirectly shown, in the additional project settings image. The relevant properties in there are Use Input Mouse Relative and Use Input Mouse Speed which, if enabled, will attach the mouse relative and mouse speed, respectively, into the encoded/decoded input data. The other option is to do that from code, with the rest of the input setup:

func _setup_network_input() -> void:
    # Enable mouse relative data in the input objects
    # Rest of the setup


Mouse relative and mouse speed are vectors, containing 2 floating points each. This means that each one of those will add 64 bit to the input objects. The shown initialization, which contains 3 booleans (fit in a single byte), 4 analog and just the mouse relative results in 200 bits (25 bytes) per input object.

Advanced Input Setup

How about dealing with input that is based on clicking the game world. As an example, suppose you have multiple units that can be selected by dragging the mouse, or even single selections by just clicking in the unit. Right clicking would then move the selected units into the clicked location. The way this is typically done is by first obtaining the world coordinates of the clicked locations and performing the relevant actions based on that information. Obviously this can't exactly be done with the shown action registrations because, generally speaking, converting from screen space into world space requires the camera which most likely will be in different states for each player.

Because of that, we have to deal with input in a completely different way for this use case. With this addon, this basically consists in using custom input data. First it's necessary to "register" this custom data just so the encoding and decoding can retrieve that information from the InputData objects. Custom input data can be boolean/analog actions (but wont be automatically retrieved through device polling) or vectors (Vector2 or Vector3). Regardless of the type, at some point those custom data must be filled within the obtained InputData object.

To register the custom input data:

# Register a custom boolean input data
network.register_custom_action("custom_boolean", false)
# Register a custom analog input data
network.register_custom_action("custom_analog", true)
# Register a custom vector2 input data
# Register a custom vector3 input data

Following the example of selecting units, we would then register a few custom vectors to represent the world space coordinates of the clicked locations (check the network example in the demo project, does exactly that). Shortly in this tutorial I will show how to fill the input objects with this custom data.

Client Is Ready

This is a rather short topic but must be mentioned either way. In order for the server to start sending snapshot data to the client, a "ready" signal must be given by the client. This is done by calling the notify_ready() function. A good place to perform this is at the end of the _ready() function of the game world script. Granted, at this point the game world should be initialized and ready to begin the simulation:

func _ready() -> void:
    # ... some code
    # If this is client, notify the server that snapshot data can come this way
    if (!network.has_authority()):

Snapshot Cycle

At each game loop iteration the snapshot must be created and, obviously, finished. To start the snapshot creation, it's a matter of calling the init_snapshot() function. This will create all of the internal state in order to fill it with the necessary data. This call will also setup an internal state which will automatically finish the snapshot once entering the first idle process call (IE.: the finishing is deferred).

So, in the game world, the usual code becomes something like this:

func _physics_process(_dt: float) -> void:
    # Start building the new snapshot
    # Perform any necessary update in the game code

If you check the demos in the repository project, right after initializing the snapshot, each connected player is iterated through and the corresponding characters are spawned, provided the code is running on the server. Something like this:

func _physics_process(_dt: float) -> void:
    # Start building the new snapshot

    if (network.has_authority()):
        # Create character(s) for the local player

        # Now for each connected player
        for pid in network.player_data.remote_player:

Obviously this assumes the spawn_player_avatar() function exists somewhere in the script and receives an instance of the PlayerNode, which is defined in the file.

Bridging Snapshot and Game Logic

As mentioned early in this tutorial, a few short scripts are necessary in order to bridge data between game logic and snapshots. Don't worry too much about this because, really, those are very simple. The most important thing to note is that they must follow four "rules":

  1. Derive from the class SnapEntityBase.
  2. Implement the _init() function in order to relay data to the parent class, which does have an init.
  3. Implement the apply_state() function, which is used by the snapshot system to take data and apply into the specified node.
  4. Declared properties must be static typed.

During initialization the network addon will scan all scripts derived from SnapEntityBase and automatically perform the registration of the classes. Think about them as representations of your game world objects within the snapshots. I strongly recommend that you add a class_name SomeUsefulName line just bellow the extends SnapEntityBase line because this "name" will be used as identification when referencing objects in the snapshot.

By default, each object of this class will contain two properties, id and class_hash. The first property is used to identify the game object within the game world, specially when sending data through the network. This will be used to match things on all peers. Understand the class_hash as the class identification which can be used to specify which node path should be used. Both of those properties are integers and, when encoding/decoding, are considered as unsigned 32 bit integers.

The class_hash property can be disabled, which in turn will result int 32 bit less data encoded per snapshot object represented by the class. To do that just add a meta named class_hash and set its value to 0 within the _init() function.

Each additional property added to the script will be automatically "registered" as data to be encoded/decoded into/from snapshots. During the scanning process the code requires information regarding the type of each property, so it becomes imperative that they are static typed in the script. Note that not every type can be used here, just a subset of them, which are:

  • bool
  • integer
  • float
  • Vector2
  • Rect2
  • Quat
  • Color
  • Vector3
  • 16 Bit Unsigned Integer
  • 32 Bit Unsigned Integer
  • Byte

But, wait, Godot does not have unsigned integer (16 and 32 bit) types nor specifically byte (8 bits) type. Indeed, they do not exist, but it's possible to tell the network system that an integer property is meant to be interpreted as an unsigned integer of either 16 or 32 bits or even a single byte. To do that, in the _init() function just add a meta with the name of the integer property while setting its value to one of the following constants (defined in the EntityInfo class):

Meta valueResult
CTYPE_UINTThe property will be considered as a 32 bit unsigned integer
CTYPE_USHORTThe property will be considered as a 16 bit unsigned integer
CTYPE_BYTEThe property will be considered as a single byte

As for the _init() function itself, it must call the parent's initialization by giving both unique id and class hash, even if the second property has been disabled. The thing is, disabling it only prevents the data from being encoded in the snapshots but the property is still there.

Finally, there is the apply() function that requires one argument of type Node. The idea of this function is to relay the state in the snap entity base object into the game node.

Now for a simple example here. Let's suppose a 3D game where we have kinematic body objects. Minimally we need position (vector3) and orientation (quaternion). However, in order to make it easier to perform corrections if we deal with client side prediction, it's a good idea to also have the accumulated velocity (vector3). With that in mind, the script would become something like this:

extends SnapEntityBase
class_name SnapPlayerCharacter

var position: Vector3 = Vector3()
var orientation: Quat = Quat()
var velocity: Vector3 = Vector3()

func _init(uid: int, h: int).(uid, h) -> void:
    # No extra initialization needed.

func apply_state(to_node: Node) -> void:
    # Ensure the node is actually a KinematicBody - we could, if desired, take
    # an specific class corresponding to the player character if so desired
    assert(node is KinematicBody)

    # Assumes the player character script contains a function named apply_state()
    # that receives a dictionary with the fields corresponding to the properties
        "position": position,
        "orientation": orientation,
        "velocity": velocity,

With this script we now have means to represent player characters within the snapshot objects. At a later moment in this tutorial there will be some details related to the player character itself. That is, how to deal with the received state data and also how to generate it in order for the snapshot to be sent to the clients.

Before moving to the next topic, the next snippet is given to exemplify how to use the custom types:

extends SnapEntityBase
class_name SomeExample

var some_uint: int
var some_byte: int
var some_ushort: int

func _init(uid: int, h: int).(uid, h) -> void:
    # some_uint is meant to be unsigned int
    set_meta("some_uint", EntityInfo.CTYPE_UINT)
    # some_byte is meant to be a single byte
    set_meta("some_byte", EntityInfo.CTYPE_BYTE)
    # some_ushort is meant to be unsigned int of 16 bits
    set_meta("some_ushort", EntityInfo.CTYPE_USHORT)

... rest of the code

Node Spawners

While game nodes can be described by classes derived from SnapEntityBase and represented in the snapshots by those objects, spawning the game nodes must be done through the network system. The reason for this is so the addon can properly associate the nodes with the snapshot objects and then perform the synchronization.

In order to do so (and somewhat automate part of the process), the network system requires objects named node spawners, which are basically classes derived from NetNodeSpawner (defined in the file). Note, however, that there is a default node spawner that should be enough for the vast majority of cases, so usually there is no need to create a new one. Spawners are used in order to "bridge" the network system internal data and the game world node hierarchy. With that said, the spawners must be registered before being able to create any game node that will be synchronized through the network addon.

Spawner registration is done through the snapshot data function named register_spawner(). This function requires the reference to the snapshot object type, a class hash (again, think about it as a class identification), the spawner object, the node that will be used as a parent when the new object is spawned and an optional function reference that will be called when the node is spawned. In here, basically each object type (those that are derived from SnapEntityBase) can have multiple spawners associated with. The spawners are found by the class hash. The spawner is the object that performs the instantiation of the game node.

The default spawner that is provided with the addon, NetDefaultSpawner (defined in the file) holds a single property, which is the packed scene of the node that must be instantiated. So, suppose we have 3 scenes for player characters and we want spawners for all of them, associated with the SnapPlayerCharacter that has been shown early. In the game world there is also one spatial node named player_characters meant to hold all spawned characters. Also suppose we want to perform some extra setup whenever the nodes are spawned. The registration (in the game world script) would look something like this:

func setup_network_spawners() -> void:
    # Function to be called whenever the nodes are spawned
    var extra_setup: FuncRef = funcref(self, "on_extra_pchar_setup")

    # Gather data for the barbarian player character
    var pack_barbarian: PackedScene = load("res://scenes/pchar_barbarian.tscn")
    var barb_class_hash: int = pack_barbarian.resource_path.hash()
    # Perform the actual registration, using the default spawner
    network.snapshot_data.register_spawner(SnapPlayerCharacter, barb_class_hash,, $player_characters, extra_setup)

    # Gather data for the wizard player character
    var pack_wizard: PackedScene = load("res://scenes/pchar_wizard.tscn")
    var wiz_class_hash: int = pack_wizard.resource_path.hash()
    # Perform the actual registration, using the default spawner
    network.snapshot_data.register_spawner(SnapPlayerCharacter, wiz_class_hash,, $player_characters, extra_setup)

    # Gather data for the rogue player character
    var pack_rogue: PackedScene = load("res://scenes/pchar_rogue.tscn")
    var rogue_class_hash: int = pack_rogue.resource_path.hash()
    # Perform the actual registration, using the default spawner
    network.snapshot_data.register_spawner(SnapPlayerCharacter, rogue_class_hash,, $player_characters, extra_setup)

In this script note something very important. The class hash has been calculated by taking the resource path and hashing the string. You can use whatever identification you want, it's just that by using this hashing the ID becomes automatically calculated.

Some things to note regarding the class hash:

  • Using the resource path string hash as the spawner ID (class_hash) allows for easier additional classes/node types to the game. Maybe even through mods as the necessary identifications happen through internal calculations.
  • Manually defining the ID brings more control over what is happening, at the cost of making new additional node types slightly harder to be incorporated into the project, specially if mods are considered.
  • Hashed strings can clash and bugs in this case can become rather difficult to hunt and be found.
  • The string hash method can still be used, but from manually typed strings rather than the resource path.

Just to complete the example, let's see the on_extra_pchar_setup() function, which will be called whenever one of the player characters is spawned in the game world:

func on_extra_pchar_setup(new_pchar: Node) -> void:
    # And any other additional initialization that you may need.

Spawning Nodes

Now that node spawners have been registered, spawning then becomes possible. Note that ideally "manual" node spawning should happen only on the server, which will then "tell the clients" about this through the snapshots. To spawn new nodes in the game there is the snapshot data function named spawn_node(), which requires reference to the snapshot object type, the unique id and the class hash.

When the spawn node function is called, the network system will use the registered spawner to perform the scene instantiation, attaching it into the parent node specified when registering the spawner itself. Additionally to this, two meta data entries are also created in the spawned node, id and class_hash. Later you will see how that can be extremely useful, specially the unique id.

Nevertheless, if you go back a little bit, into the Snapshot Cycle topic, the second snippet calls the function spawn_player_avatar(). Shortly we will see a little bit of this function. There are a few things to consider first, though. In this snippet the player character unique ID will be the exact same network ID. This can be done only if each player owns a single character. In a game where each player controls multiple characters (think a real time strategy game, for example) the unique ID must be something else.

Then, before spawning the game node we have to check if it already exists. For that, we can first obtain the node using get_game_node() function and checking if the returned value is valid or not. The inputs to this function are the node unique ID and the associated snapshot object type reference.

The following snippet is hard coding the barbarian class and assumes its class hash is cached in a variable named cached_barb_chash.

func spawn_player_avatar(pnode: NetPlayerNode) -> void:
    var char_node: KinematicBody = network.snapshot_data.get_game_node(pnode.net_id, SnapPlayerCharacter)
    if (!char_node):
        char_node = network.snapshot_data.spawn_node(SnapPlayerCharacter, pnode.net_id, cached_barb_chash)
        char_node.global_transform.origin = get_character_initial_location()

Later in this tutorial I will show how to deal with player selection through the network. Indeed, when a client selects the desired class, it must be given to the server so the correct node is instantiated.

State to Snapshot

Up to this point, the server will indeed create player character nodes. But that is it. If a client connects, nothing will happen on its end. That's because the snapshots must be filled with data, which I haven't shown how to do so yet.

Any object that must be part of the snapshot must take its state, set that into a new instance of the corresponding class derived from SnapEntityBase and add that object into the snapshot. So, suppose there is a base player character scene with an script attached into it. In there a function can be defined in order to generate the snapshot object:

func create_snapshot_object() -> SnapPlayerCharacter
    var uid: int = get_meta("uid")
    var chash: int = get_meta("chash")

    var ret: SnapPlayerCharacter =, chash)
    ret.position = global_transform.origin
    ret.orientation = Quat(global_transform.basis)
    ret.velocity = velocity

    return ret

Now, in the _physics_process() function, near its end, we call this function and add the return value to the snapshot, using the function snapshot_entity():

func _physics_process(dt: float) -> void;
    # ... some code - the usual character logic
    # Now add the state to the snapshot if running on the server
    if (network.has_authority()):

The previous snippet adds the state into the snapshot only if running on the server. This is perfectly valid for the case when there is no desire to incorporate client side prediction in order to hide the latency. However, in the case of client side prediction, the client do need to fill local snapshots (the reason for this is explained in the how it works tab). The function in this case would look like this:

func _physics_process(dt: float) -> void;
    # ... some code - the usual character logic
    # Now add the state to the snapshot

State From Snapshot

The data is being added to the snapshot. Now, when the client receives it, the system will compare with local data. When necessary, it will call the apply_state() function within the snapshot object. Just as a quick reminder, this is the function defined in the class derived from SnapEntityBase in order to take the snapshot state and transfer it into the actual game. Going back to that topic (Bridging Snapshot and Game Logic) the shown snippet assumes the corresponding game node implements a function also named apply_node() that uses a Dictionary as input. So, continuing with the example up to this point, let's see how it would look like in the player character script:

func apply_state(state: Dictionary) -> void:
    global_transform.origin = state.position
    global_transform.basis = Basis(state.orientation)
    velocity = state.velocity

With this code, the state is immediately applied into the game node. This is valid if there is no client side prediction method, however it will result in some repeated movements if prediction is applied. The problem is that the state may be applied in between physics updates, where the actual prediction may happen. In order to perform client side prediction, it becomes necessary to cache the received state, which should be applied at a later moment. So, assuming the player character script declares a property named correction_data, meant to be a dictionary, this function can be changed to:

func apply_state(state: Dictionary) -> void:
    correction_data.has_correction = true
    correction_data.position = state.position
    correction_data.orientation = state.orientation
    correction_data.velocity = state.velocity

Notice in this script that a flag is part of the local dictionary (and set in the function). This will be used to determine if the stored data must be applied or not during the physics updates. The next snippet exemplifies this:

func _physics_process(dt: float) -> void:
    if (correction_data.has_correction):
        # Reset the flag so it doesn't trigger the correction multiple times
        correction_data.has_correction = false

        # Apply to the game node
        global_transform.origin = correction_data.position
        global_transform.basis = Basis(correction_data.orientation)
        velocity = correction_data.velocity

At this point correction data is applied to the game node but there is still some work that must be done if this is an entity directly controlled by player input. The reason follows:

  • When client receives the very first snapshot, it can start predicting what comes next by running the exact same update rules.
  • Depending on the latency, it may run several frames until the next data from the server arrives.
  • At each frame that was run on the client, some input data was consumed (and sent to the server).
  • When the next snapshot arrives from the server (indicating that it used input), it corresponds to a previous snapshot in the local storage.
  • If client code predicted the result incorrectly, the server state must be applied. But because it is "older" (in the client), the local input data must be reused.

The necessary steps to perform the correction in a client will be mentioned in the next topic.

Gathering Input - The Simple Case

For now let's assume that our input directly affects the character. That is, pressing the "move_forward" button will move the character forward and so on. Normally in this case, the relevant piece of code does something like Input.is_action_pressed("some_mapping") and perform something in case it's true. Instead of doing that we must request one InputData object from the network system, which is done by calling the get_input() function. This function requires the network player ID as argument and will return null if the player is not supposed to deal with the input. As an example said case, suppose the code is running on the client and the specific instance of the player character corresponds to the server's player.

This InputData object contains a few functions that can be easily used to determine the state of the relevant buttons/keys:

  • get_mouse_relative() → Returns a Vector2 with the mouse relative data if mouse relative is enabled in the network system.
  • get_mouse_speed() → Returns a Vector2 with the mouse speed data if mouse speed is enabled in the network system.
  • get_analog("some_mapping") → Returns a floating point number indicating the state of the specified mapped input key/button.
  • is_pressed("some_mapping") → Returns true if the specified mapped input key/button is pressed, false otherwise.

As you can see, the game code to deal with input doesn't change that much, just how you obtain the state! When the get_input() function is called, the network system automatically deal with the generation of the data to be sent to the server when running on a client. And, if running on the server, it will get data that was received from the corresponding client.

With all that in mind, a function can be created in order to calculate the simulation based on the input:

func handle_input(input: InputData) -> void:
    # If the input object is not valid, bail as this machine is not meant to do anything
    if (!input):

    # ... some code
    if (input.is_pressed("jump")):
        # Perform the jump operations
    if (input.is_pressed("sprint")):
        # Perform the sprinting operations
    # And so on...

Obviously this is a rather simplistic example, but this is mostly to show the overall pattern! Nevertheless, I have mentioned previously that if client side prediction is desired, input data must be replayed when running in the client and corrections are applied. This means the client must store the input objects in a local cache and use those when correcting the state. Once input is confirmed, clean up only the relevant data. The network addon already deals with this cache management so the code will mostly have to request an array of this non confirmed data!

With the list of non acknowledged input data in one array, when correction is applied they can be replayed. With this in mind, the physics update function can be updated. Note in the code that we only replay the input data if the character belongs to the local player, otherwise remote players will incorrectly replay their own input data:

func _physics_process(dt: float) -> void:
    # Obtain the unique ID of this character
    var uid: int = get_meta("uid")

    if (correction_data.has_correction):
        # ... apply the state code
        # Replay the stored input objects

        if (network.is_id_local(uid)):
            var inputlist: Array = network.player_data.local_player.get_cached_input_list()
            for idata in inputlist:

    # Because the unique ID corresponds to the network unique ID, this can be used
    var input: InputData = network.get_input(uid)
    # Handle the input

This code assumes that all of the simulation code for the node object is done within the handle_input() function, including gravity and so on.

Correcting The Snapshot

The code shown in the previous topic does indeed allow the client to predict and correct its state. However it still does one problem. The local snapshots are still incorrect and this fact will result in the correction code to be triggered multiple times. Depending on the nature of the correction this can result in some visual glitches that obviously are not desired. To fix that the re-simulated entities must corrected within the corresponding snapshots. To do that there is the network.correct_in_snapshot() function, which requires two arguments, the snapshot entity object with the correct state and the input object.

The two arguments are already easily obtained. In a previous topic, State to Snapshot, I have shown the create_snapshot_object(), which will provide the snapshot entity object. The input data is then the one we used to re-simulate the entity. That said, we can update the previous snippet to something like this:

func _physics_process(dt: float) -> void:
    # Obtain the unique ID of this character
    var uid: int = get_meta("uid")

    if (correction_data.has_correction):
        # ... apply the state code
        # Replay the stored input objects

        if (network.is_id_local(uid)):
            var inputlist: Array = network.player_data.local_player.get_cached_input_list()
            for idata in inputlist:
                network.correct_in_snapshot(create_snapshot_object(), idata)

    # Because the unique ID corresponds to the network unique ID, this can be used
    var input: InputData = network.get_input(uid)
    # Handle the input

Gathering Input - The Advanced Case

Early in this tutorial I mentioned about custom input data, which can be created in order to send additional custom data to the server. When that data is registered, it becomes responsibility of the game logic to fill in when relevant. Basically, in the client code this data must be set while in the server it is retrieved. The relevant functions are:

  • get_custom_vec2(name)
  • set_custom_vec2(name, value)
  • get_custom_vec3(name)
  • set_custom_vec3(name, value)
  • get_custom_bool(name)
  • set_custom_bool(name, value)
  • get_analog(name)
  • set_analog(name, value)

These names should be self explanatory so I won't get into any explanation about them, only that the name argument on those functions must match the registered custom input data, including the corresponding types, otherwise the encoding/decoding will fail or not do anything at all. Nevertheless, when input is required, the object can be retrieved exactly like shown in the previous topic, with network.get_input(id). Based on wether the code is running on server or client, the custom data can be "get" or "set". One possible way here is to perform this from within the handle_input() function:

func handle_input(idata: InputData) -> void:
    # ... some code - also assume this declares some variables:
    # custom_analog, custom_bool, custom_vec2 and custom_vec3
    if (network.has_authority()):
        # This is server code
        custom_bool = input.get_custom_bool("custom_boolean")
        custom_analog = input.get_analog("custom_analog")
        custom_vec2 = input.get_custom_vec2("custom_vector2")
        custom_vec3 = input.get_custom_vec3("custom_vector3")
        # This is client code
        custom_bool = calculate_custom_bool...
        custom_analog = calculate_custom_analog...
        custom_vec2 = calculate_custom_vector2...
        custom_vec3 = calculate_custom_vector3...
        input.set_custom_bool("custom_boolean", custom_bool)
        input.set_custom_analog("custom_analog", custom_analog)
        input.set_custom_vec2("custom_vector2", custom_vec2)
        input.set_custom_vec3("custom_vector3", custom_vec3)
    # ... some more code

Note that the names of the custom data match those registered early in this tutorial! Also note one specific pattern here. In the server section the custom values are retrieved from the input object. In the client section those custom values are first calculated (and set within the variables) then assigned to the input object. Later in the code those variables containing the custom data are used to perform whatever is necessary.

While this example is very simplistic, this is, in a way, what is done within the network example in the demo project (look into the file).

Prediction Without Input Data

In the Gathering Input - The Simple Case topic it's mentioned how client side prediction is performed. In that case, the amount of input data objects stored in the container is used to perform multiple simulations of the game node in order to "catch up" with the most recent local snapshot. What if there is any desire to perform this kind of re-simulation on entities that don't need input data, like a projectile for instance? This can still be done, through the network.snapshot_data.get_prediction_count() function. It requires two arguments, the entity unique ID and the snapshot entity class that represents the entity itself. It returns a number, which should be how many times the simulation of the entity should run.

So, suppose we have a projectile class, represented in snapshots by SnapEntityProjectile. In the projectile's script there is a function called _simulate(), which performs the movement simulation. There's also a property named _uid, caching the unique ID of the projectile. From the _physics_process(), among other possible code, the _simulate() is called. We can incorporate prediction code, similar to what has been done with the character script, by storing the correct data somewhere and, during the physics update, check if there is any correction. If there is any, then the prediction counter can be used:

func _physics_process(dt: float) -> void:
    # ... some code
    if (correction_data.has_correction):
        # ... Apply the correct state code
        # Then re-simulate the movement
        for _i in network.snapshot_data.get_prediction_count(_uid, _SnapEntityProjectile):
    # ... some other code
    # Simulate current frame
    # ... even more code

Floating Point Comparison

When comparing server and client data, by default this addon uses "ordinary" comparison, that is, if value1 == value2 returns false, then the objects are considered in different state and will trigger the apply_state() function. Unfortunately comparing floating point numbers can be rather finicky. The most important thing to know about all this is the fact that operations with floats are not guaranteed to yield the same result when performed on different machines. This becomes a problem because the client side prediction works best if the simulation gives the same result. Triggering the apply_state() way too often on predicted objects may lead to some visual glitches.

Because of that, often it's recommended to perform floating point comparisons with a certain tolerance. And Godot does offer that, through the is_equal_approx() function. It receives only two arguments (the two values to be compared) and a tolerance will be automatically calculated. Under the hood, the core of the comparison is performed by abs(value1 - value2) < tolerance. If this expression is true, then the two value are considered "equal".

With this in mind, this addon gives the possibility to perform floating point comparisons through the is_equal_approx() or even by using a custom fixed tolerance value. To do that, when defining the snapshot entity class (those derived from SnapEntityBase), add a meta with a name matching that of the floating point property, including compound ones like Vector2, Vector3 and so on. Assign the tolerance to this meta or, if you prefer the is_equal_approx() assign 0 or any negative number.

If you decide to use a custom fixed tolerance value, the only thing to keep in mind is that it will use the same expression abs(value1 - value2) < tolerance to test for "equality".

The snippet bellow showcases how to declare 3 properties to use tolerance in their comparisons:

extends SnapEntityBase
class_name SomeSnapshotEntity

var prop1: Vector3
var prop2: Quat
var prop3: float

func _init(uid: int, hash: int).(uid, hash) -> void:
    # Configure prop1 and prop2 to use is_equal_approx()
    set_meta("prop1", 0)
    set_meta("prop2", 0)
    # Configure prop3 to use 0.001 as tolerance
    set_meta("prop3", 0.001)

Things to keep in mind:

  • Booleans and integers don't require any kind of specialized comparison method. Setting a meta to integers is meant to change how they will be interpreted by the system during encoding and decoding process. Setting a meta to a boolean will do absolutely nothing.
  • On certain cases it's perfectly acceptable to allow the "ordinary" comparison to take place. The thing is, using a tolerance can negatively impact the performance. But please, before just never using them, or always using them, profile the code before taking a decision!

Custom Player Properties

Early I have shown how to spawn player character nodes from within the game world script. In there, the character class is hard coded. I have also mentioned that I would show how to deal with player choice. The thing is, whenever a player chooses a character class, most likely the game is not even connected yet, still somewhere in the main menu.

When the character selection is done, it will obviously change some internal variable and most likely some visual feedback within the UI. All of that still in the main menu script. With all that in mind, it would be a good idea for this script (or even a singleton script) to have all of the class hash values stored somewhere, preferably associated with the UI option in order to make things simpler.

Yet, this information must be given to the server. Enter custom player properties. This is a feature that is part of this network addon in order to allow some additional values to be automatically synchronized through the network. If a custom property is changed while still not connected, after connecting that property will be sent to the server. Besides that, if the value is changed during the game, it will be updated. The most important thing to note is that custom properties are synchronized using the reliable method, which can be slower than the rest of the system. This fact should not keep you from using this system, just that you should avoid constantly updating values in it.

A custom property must be registered within the network system. This is done by calling the add_custom_property() function, which is part of the player_data. The function needs 3 parameters. The first one is the name of the custom property, which is how the value can be obtained at a later moment. The second argument is the default value of this property. Lastly, is how this property should be replicated, which is basically chosen from the enum ReplicationMode that is defined in the NetCustomProperty class. The options are:

NoneThe property will not be replicated at all.
ServerOnlyThe value will be sent only to the server.
ServerBroadcastThe value will be sent to the server, which then will broadcast to the other clients.

In the main menu script we can register a custom property that will hold the class hash of the selected player character. There is no need for this property to be broadcast to all clients, since the snapshot system will take care of spreading the correct class hash value through the snapshot data. In the following snippet it's assumed the barb_hash holds a cached value of the class hash corresponding to the barbarian scene, which was described early in this ongoing series of examples:

network.player_data.add_custom_property("character_class", barb_hash, NetCustomProperty.ReplicationMode.ServerOnly)

The property is now registered and can be used! When the player selects a character class in the menu, this property can be changed, with the set_custom_property() function. Note that to update the property the specific player node must be accessed and obviously ideally this should be done only for the local player. With all that in mind, suppose there is a function meant to handle the player character selection. During the signal connection, a payload has been added into it, which is the class hash corresponding to the selected character class:

func on_character_selected(class_hash: int) -> void:
    network.player_data.local_player.set_custom_property("character_class", class_hash)

Now, to get this property the function is get_custom_property() which is also part of the player node. This function requires the name of the custom property and a default value in case this property is not found.

If you recall that spawn_player_avatar() function, which was previously shown, it does receive an instance of the player node as argument. So let's update that function to use this custom property instead of the hard coded cached hash:

func spawn_player_avatar(pnode: NetPlayerNode) -> void:
    var char_node: KinematicBody = network.snapshot_data.get_game_node(pnode.net_id, SnapPlayerCharacter)
    if (!char_node):
        var chash: int = pnode.get_custom_property("character_class", cached_barb_chash)
        char_node = network.snapshot_data.spawn_node(SnapPlayerCharacter, pnode.net_id, chash)
        char_node.global_transform.origin = get_character_initial_location()

And that's it! The custom property is registered, assigned and used to indicate which player character scene to be used by an specific player!

Now there is one important thing to note here. This spawning function is defined in the game world script. The custom property is probably being registered in the main menu script. In this case, directly testing the scene (by default, F6 Key), means the property does not exist when get_custom_property() is called. That's the reason the cached_barb_chash is added as default value.

An alternative to the default value is to perform the custom property registration from a singleton (auto-load) script, as long as the script is loaded after the network addon.

Because the demo project is meant to give several examples and at least two of them are using the networking system, you will probably notice the fact that there is no extra auto-load script performing any kind of operation. Indeed, this is meant to avoid "polluting" the overall game code environment so everything in the example code is done to be "as confined as possible".

Replicated Events

On certain cases, synchronizing the game state becomes a lot easier through events that are sent through the network. Think, for example, a player picking up an item from the game world. While it's possible to synchronize the action purely through snapshots (specially when the pickup just changes the character state), using events in this case is still a lot easier. To that end, this addon offers means to dispatch events, with a few things that must be kept in mind:

  1. Only the server will send events.
  2. Events will be accumulated during the game update and sent right after the snapshot data in a single byte array.
  3. Snapshots use the unreliable channel while events use the reliable channel.

As many of the other features of the addon, event types must be registered before being used, which consists of specifying the event type ID and a list of parameter types (in an array).

Regarding the event type ID, it must be a number, smaller or equal to 65535 (that is, 16 bits). In other words, do not consider the possibility of creating named events and hashing those strings as those will result in 32 bit numbers.

As for the parameter types, those must be supported by the encoding system (the list is in the Bridging Snapshot and Game Logic topic).

The registration itself can be done a single time in the game so a good place for this is either in the main menu script or a singleton (auto-load) script. The following snippet registers 2 event types. One that doesn't receive any parameter and the other that uses two (a floating point and one 16 bit integer):

# Register an event (code = 0) that doesn't receive any parameter
network.register_event_type(0, [])
# Register an event (code = 1) that receives two parameters (float and 16 bit integer)
network.register_event_type(1, [TYPE_REAL, NetEventInfo.CTYPE_USHORT])

Obviously event handlers are necessary in order to perform anything useful when the event does happen. To that end, those must be attached to the event type. Note that multiple handlers can be added into a single event type. Because events are (frequently) more useful when the game is already running, often those will be part of the game world script. The function to attach event handlers (attach_event_handler()) requires 3 arguments, which are the event type code, a valid object and the name of the function that must be part of that object. This specified function will be called in order to handle the event when the event is decoded on clients and when processed on the server.

The next snippet attaches one event handler for each of the previously registered events. The event type = 0 will be handled by a function called on_simple_net_event while the event type = 1 will be handled by a function called on_another_event, both being part of the same script performing the attachment:

# Attach an event handler for the event code = 0
network.attach_event_handler(0, self, "on_simple_net_event")
# Attach an event handler for the event code = 1
network.attach_event_handler(1, self, "on_another_event")

Those event handlers must implemented. Those are simple functions, which should have parameters matching the requested ones when registering:

func on_simple_net_event() -> void:
    print("Received a network event without arguments")

func on_another_event(p1: float, p2: int) -> void:
    print("Received a network event with two arguments: ", p1, " and ", p2)

The last thing about this system is in how to actually send one event. This is done through the function send_event(), which requires two arguments. The first one is the event type ID code, while the second is the list of parameters stuffed into an array. Notice that this function can be called regardless of being on client or server as the check will be performed from within. In other words, if calling on a client, this function will do nothing. With this in mind, the game logic code can become slightly simpler when it becomes necessary to send an event through the network. The following snippet sends an event of each of the previously shown event types:

# Send an event of type = 0
network.send_event(0, [])
# Send an event of type = 1
network.send_event(1, [3.14, 314])

Right after sending snapshot data, those two events will be encoded and sent to all connected clients with a single remote call. During the event processing on the server, it will locally call the event handlers just so the server can use the same code to handle the events that clients do.


This addon provides means to exchange chat messages. To send a message, there is send_chat_message() function. It requires two arguments, the message itself and the network ID of the destination. If set to 0, then the message will first be sent to the server which will then broadcast to every connected player, obviously skipping the sender. So, supposing there is an event that once handled, takes the text from a LineEdit control stored in a variable named txt_chat_input to be sent as a chat message. The following snippet performs the operation while also clearing that control for further chat message inputs:

# Broadcast this message to everyone connected
network.send_message(txt_chat_input.text, 0)
txt_chat_input.text = ""

When a chat message is received, the signal chat_message_received will be given by the network system. The signal contains two arguments, the message itself and the network ID of the person who sent the message, in this order. With that in mind, a possible event handler can look something like this:

func on_chat_message_received(msg: String, sender: int) -> void:
    var output: String = format_chat_message(msg, sender)

And that's it for the chat system!

One thing to note: for the moment there is no (easy) way to whisper a message to the specified player in a way that the message passes through the server first. This use case may be desired and this feature is in the TODO list (once incorporated this topic will be updated).

Latency Measurement

This addon automatically measures latency (ping) of connected players. By default the server will take the measured value and broadcast to all connected players. This can be disabled and only the relevant player will get the measured value through the project settings. The specific property is the Broadcast Measured Ping, which should be set to false if you don't want the ping value to be given to every connected player.

In any way, from wherever you want to deal with this value, all that must be done is connect a function to the network ping_updated signal, which receives two arguments, the ID of the player and the measured ping, in milliseconds. So, an example of event handler would look something like this:

func on_ping_updated(player: int, value: float) -> void:
    var hud: Control = get_hud_for_player(player)

Obviously this is a rather simplistic example and assumes a bunch of things. But the gist of how to deal with the ping values measured by the addon is in there.

Incrementing IDs

As mentioned early, the most important property to this entire snapshot system is the unique ID of the game objects. It's the value that is used to identify and perform the "connection" between the game logic and snapshot objects. Without it, sending data to a remote machine would have no easy means to know what the data corresponds to. Because of that, dealing with the unique ID for the various object types becomes necessary. Still, often this ID can be simply an incrementing integer.

Thinking about this, one really good place to hold this kind of incrementing value is a singleton (auto-load) script. Taking advantage of the fact that the is set as one, there is one incrementing id system that has been added into it. Obviously this must be registered first, with the register_incrementing_id() function:

# Register incrementing ID for projectiles
# Register incrementing ID for every object that will be represented by SnapMonster

As you can see, the ID must be unique per object type, not project wide! In other words, if you have 4 classes derived from the SnapEntityBase, you may have four incrementing IDs, one for each of those classes.

Then, at a later moment, when dynamically spawning a projectile, you may want to take the ID:

# ... some code
var uid: int = network.get_incrementing_id("projectiles")
# ... perform the rest of spawning code

Please note that this incrementing ID thing is provided mostly for convenience and in many cases will not be enough for the correct identification, which will often require other means to generate the number.

Extra Network Signals

Up to this point the core of the network addon is in use and provided everything was done right, the synchronization through the network should be working. Yet, there are a few signals that can be used in order to bring some extra features/polishing to the overall behavior of the game. As an example, when a new player joins (or leaves) the game, we can update the HUD. With that in mind, the following table shows the signals that can be used to perform some operation. Events that have been previously discussed are not shown in this table.

player_added(id)This is given when a new player is registered within the network system. The argument is a simple integer corresponding to the network id of the new player
player_removed(id)This is given when a player is removed from the internal network system (normally due to disconnection). The argument is a simple integer corresponding to the network id of the player.
disconnectedA client will get this signal whenever disconnected from the server. No argument is given
kicked(reason)When a client gets kicked by the server, this signal is given. The argument is an string with the reason for the kick.

Level Designed Synchronization

If a level is designed in a way that contains movable objects, chances are high that one would like those to be synchronized. To that end, there is the add_pre_spawned_node() function, which is used to perform the registration of a node that is part of the tree but was not dynamically spawned through the network system (with network.snapshot_data.spawn_node()). This functions takes, as arguments, the snapshot entity type, the unique ID of the entity and the reference to the node itself. If you take a look into the script (in the demo project), near the end of the _ready() function this "registration" function is called in order to associate itself (the "clutter" object) within the network system. In that demo project the snapshot entity class is named MegaSnapClutter. With that in mind, the relevant code is performed like this:

network.snapshot_data.add_pre_spawned_node(MegaSnapClutter, uid, self)

The rest of the synchronization happens like normal, that is, during the _physics_process() an object of the MegaSnapClutter for each instance of the clutter base is created, holding current state, then added into the snapshot with the network.snapshot_entity() function.

Some Special Synchronization Cases

Before wrapping this how to use, I would like to take some time to discuss some special synchronization use cases, that may require some different way of thinking.

  • Synchronize Weather Conditions: Suppose the game contains dynamic weather system and it's desired to synchronize the conditions with all clients. Because the environment does not exactly contain entities it may not be clear on how to perform replication using this snapshot system. However a possible solution here would be to create one entity class derived from SnapEntityBase containing the environment variables that control the weather state. An spawner is not necessary in this case so there is no need to register one. Then use the game world script as the node to take the environment state.

To create one (very simple) example, the snapshot representation of the environment can be called SnapEnvironment (remember, this is derived from SnapEntityBase). In it two variables are added, time_of_day (as a float) and weather (as a short integer). The first variable will be used to tell if the game is in daytime or night time (or something in between). And the second variable will tell, through an enumeration, the weather state. Those could be clear, cloudy, raining and so on. The script would look like this:

extends SnapEntityBase
class_name SnapEnvironment

var time_of_day: float
var weather: int

func _init(uid: int, chash: int).(uid, chash) -> void:
    set_meta("weather", EntityInfo.CTYPE_USHORT)

func apply_state(node: Node) -> void:
    var state: Dictionary = {
        "time": time_of_day,
        "weather": weather

    # Assume the game world script contains the apply_state function

Next, the game world needs to "register" itself as the node associated with the environment entity type. This is done by using the add_pre_spawned_node() function, which was explained in the Level Designed Synchronization topic (the previous one). Because there will be only one game world, we can hard code its unique ID. The snippet performing this registration:

func _ready() -> void:
    #... some code
    network.snapshot_data.add_pre_spawned_node(SnapEnvironment, 1, self)

Then, there are two final tasks to be done. The first one involves implementing the apply_state() in the game world script, which will take the given dictionary and perform the necessary internal code to apply the state. Then, the other task is to create the SnapEnvironment object within the _physics_process() function.

  • Item Pickup Synchronization: In this case, there are two problems to be solved. The first one is the actual pickup action. The second relates to the item itself. Both aspects will be discussed bellow.

When the item is picked from the game world, the action in itself must be replicated through the network just so every connected player can update the visual state and remove the "item on the ground" node. The easiest way here is to emit a network event telling that an specific item was removed from the ground, picked by the player. Still, depending on what the pickup actually is, events may not be necessary.

Now regarding the item itself. There are still a few cases which will determine the complexity of the problem. The easiest one is related to simple pickups, which just change the character state like healing or, in other words, pickups that are not added into an inventory. In this case, the changes given by the pickup can be incorporated into the snapshot entity, which will then replicate the effect to all other players.

When the pickup requires inventory, things can become a bit complex, specially if the items are dynamically generated through random rolls determining the properties of those items. Let's step back a little bit into items with fixed properties. Those can be part of a data base and identified by some unique ID, which should be the replicated value. On the client side, the same database must be queried in order to obtain the correct item.

If the item is fully randomized, then the easiest here is to have each of those properties be identified by a unique property id. Some kind of bit packing could be used in order to encode collections of property id's into a little bits as possible. This "collection" should then be replicated and on client side, use to rebuild the item.

Up to this point things should be pretty straightforward, mostly because it's all based in dealing with unique ID to query (static) data bases. However dealing with the inventory itself can become quite problematic. The easy implementation would be to allow each client to know whatever is in its inventory and be done with that. But when the player decides to drop the item on the ground, it means telling the server "hey, I'm dropping this item on the ground". This is a problem because in this case client is dictating the result of an action, meaning that a cheater can tamper with the data and tell about a different (maybe rarer) item and even the quantity if the item can be stacked.

For certain kinds of games this should be entirely acceptable, however when there is competition the server must handle everything, including the inventory management (and the stored persistent data - IE.: the save files). In this case, interacting with the inventory should be entirely replicated so the server can know about everything, including item positions within the inventory and obviously the properties of the items. This will potentially greatly increase the bandwidth requirements of the game, specially in the fact that this kind of replication requires knowledge about every single slot of the inventory.

But how to replicate the inventory becomes the next question. The inventory itself will probably be a node and most likely have an script attached to it. With this in mind, one possibility is to create a new snapshot entity class (something derived from SnapEntityBase) to represent the inventory state. In this case, this new class would create an association with the inventory itself. Normally speaking, inventories are implemented using arrays to hold the slots, which is a type not supported by this network addon. Obviously manually adding a property for each slot is not an option, specially because it does not scale well, not to mention the limit of 30 additional properties that can be added into the class as it's handled by the network addon (please note here, a maximum of 30 replicable properties is a limitation in how the addon was implemented, not a Godot one).

The solution here becomes to create one snapshot entity class for each slot in the inventory rather than the inventory itself. This will result in one object of the new class for each slot within the snapshot data. This somewhat forces that each slot becomes a node. An script for this node is optional since the apply_state() can directly deal with the slot rather than calling an extra function like I have been showing throughout this tutorial. In case this extra script is not implemented, the inventory itself can iterate through the slots in order to generate the snapshot entity object.

Closing and Resetting

Finally, some cleanup steps. Suppose the player is going back to the main menu, maybe to restart the server, or connection was lost or... There is one function that can be used to reset most of the internal network system, the reset_system().

If the player did actively click a button to go back to the main menu, it's a good idea to forcefully close the server (if server) or disconnect from it (if client). Those can be done with close_server() and disconnect_from_server() functions. Note that there is no harm in calling both, regardless if the code is running as server or client or none! So, with that in mind, it would be a good idea to incorporate the following in the game world script:

func _exit_tree() -> void:

One very important thing here: the reset_system() call will also cleanup any event handler that has been attached to the various event types.


And that is it! Yes, I know, this addon is pretty big and maybe the snippets were not enough to explain how to completely use this system. Indeed, again, these tutorials are primarily meant to give a grasp to understand the code examples that can be found in the project repository. If you are still in doubt, feel free to contact me!