Using API Keys

GD-Sync makes use of API keys to authenticate developers and their players. API Keys can be generated on our website. Once logged in, you can see an API Keys section on the navigation menu.

We highly recommend using a separate API key for each of your games. Lobbies are separated for each API key, using the same key for two different games means you will mix their lobbies together. Using separate keys also makes viewing statistics on the dashboard easier, since data is logged for each key.

In order to use an API Key, you can navigate to the GD-Sync configuration menu. The configuration menu can be found under Project > Tools > GD-Sync. The configuration menu contains two fields where you can enter both your public and private key. Once you have entered these correctly, you will be able to connect to our servers.

Basics

Once you have enabled the plugin you will get access to the GDSync class. This global class contains all the functions you will need to interact with our server network.

The GDSync class contains in-engine documentation which has descriptions for all signals, functions and their parameters. Extended documentation is available right here including code samples.

When testing your multiplayer game, Godot supports launching multiple instances of your game at once right in the editor. This feature can be extremely useful to test if your multiplayer game is working as expected. To launch multiple instances, go to Debug > Run Multiple Instances and select how many instances you want to launch.

Demo Project

In order to demonstrate our plugin and its capabilities, we have developed a demo project which makes full potential of all its features.

The demo project can be downloaded or cloned directly from Github (repo link). Make sure to enter your own public and private key in the configuration menu.

In-Engine Documentation

GD-Sync has in-engine documentation for every signal and function. This can be found by pressing the “Search Help” button on the top right of the Godot script editor. Once inside the search menu, search for “MultiplayerClient” and you’ll be able to view all functionalities.

Due to a Godot 4.x bug, the documentation might not show up. To fix this, please edit Addons > GD-Sync > MultiplayerClient.gd in any way and save it. Once you save it, the documentation shows up in the help menu.

Connecting

Connecting and going online using GD-Sync can be done with only a single function,  GDSync.start_multiplayer() . Make sure to connect relevant signals so you will be notified if the connection handshake has succeeded or not.

You can check whether you are connected or not using GDSync.is_active(). If the client disconnects for any reason (server outage, poor internet connection, etc), the GDSync.disconnected() signal is emitted. The plugin does not automatically try to reconnect if the connection fails completely.

func _ready():
	GDSync.connected.connect(connected)
	GDSync.connection_failed.connect(connection_failed)
	
	GDSync.start_multiplayer()

func connected():
	print("You are now connected!")

func connection_failed(error : int):
	match(error):
		ENUMS.CONNECTION_FAILED.INVALID_PUBLIC_KEY:
			push_error("The public or private key you entered were invalid.")
		ENUMS.CONNECTION_FAILED.TIMEOUT:
			push_error("Unable to connect, please check your internet connection.")
      

Clients

Players that connect to our servers are called Clients. Clients are identified by their ID, which is an integer. Each client has its own unique ID. If you want to call a function on or get specific data from a specific client you will need to use their ID. When connected you can retrieve your own client ID using GDSync.get_client_id().

Player Data

Each client has their own collection of data. This data is automatically synchronized across all clients in the same lobby. It may take up to 1 second before the synchronization occurs on all clients.

Beware when updating the player data frequently as it can be a relatively expensive operation, using up quite a bit of transfer.

Each client can only modify their own player data. To set player data you can use GDSync.set_player_data(key : String, value : Variant). The key is the name of the variable you want to set. The value can be any Godot Variant. This function can be used to create a new entry or to overwrite existing data.

To erase data you can use GDSync.erase_player_data(key : String). This will erase existing data if the key exists.

Player data can be retrieved using GDSync.get_player_data(client_id : int, key : String, default : Variant). This function requires a client ID, as you can also use it to retrieve data from other clients in the same lobby. To retrieve your own data you can input your own client ID. Default is the value it will return in case the key does not exist, by default this is null. You can also check if an entry exists using GDSync.has_player_data(client_id : int, key : String). You can also retrieve all data from a player as a dictionary using GDSync.get_all_player_data(client_id : int).

When player data is changed it emits a signal on all clients, GDSync.player_data_changed(client_id : int, key : String, value). This signal will also emit when data is erased, in that case the value will be null. The example showcases a bit of code in a player script that updates their color on all clients if it changes.

Player data has a maximum storage of 2048 bytes. If you exceed it a critical error will be emitted, see Critical Errors for more info.

#Player script
func _ready():
	GDSync.player_data_changed.connect(player_data_changed)

func player_data_changed(client_id : int, key : String, value):
	if client_id != int(name): return #Data changed for a different player
	
	#Data changed for this player!
	if key == "Color" and value is Color: modulate = value
      

Usernames

As an optional feature, GD-Sync has a built-in username system. You can set your username using GDSync.set_username(username : String). Setting a username will emit the GDSync.player_data_changed(client_id : int, key : String, value) signal with the key “Username”.

You can retrieve other usernames including yourself using GDSync.get_player_data(client_id : int, key : String, default : Variant), with "Username" as the key.

In the configuration menu you can also enforce unique usernames. This rule will be enforced for each lobby separately. Enabling this feature will block two clients with the same username from joining, ensuring that each username in the lobby is unique

Lobbies

GD-Sync makes use of lobbies in which players can play together. Lobbies have a bunch of built-in functionalities that can aid you in the development of your game and networking.

Our service simplifies matchmaking by allowing you to retrieve all lobbies that are publicly visible. Lobbies can either be private or public, only public lobbies will show up when browsing. See Lobby Visibility And Browsing for more info.

Our lobby system has the addition of a tag system. Tags are publicly available data that can be viewed from the outside. Tags are useful to display information like the selected map or gamemode. See Tags And Data for more info.

Lobbies offer automatic host selection which simplifies authoritative logic when developing peer-to-peer games, see Host Selection for more info.

Creation

In order to create a lobby, you can use GDSync.create_lobby(name : String, password : String, public : bool, playerlimit : int, tags : Dictionary). Lobbies are identified using their name, thus it’s the only required parameter. All other parameters (password, public visibility, maximum player limit and tags) are optional. If no player limit is provided the limit will automatically be set to the maximum your plan allows.

Creating a lobby does not automatically join it, for joining a lobby see Joining And Leaving. After creating a lobby you have 5 seconds to join it. A lobby will get automatically deleted after 5 seconds if no players join within this timeframe.

The lobby name must be a minimum of 3 characters and a maximum of 32 characters. The password can be a maximum of 16 characters. If the password is left empty, joining a lobby will always succeed if you have not yet hit the lobby player limit.

func _ready():
	GDSync.lobby_created.connect(lobby_created)
	GDSync.lobby_creation_failed.connect(lobby_creation_failed)
	
	GDSync.create_lobby(
		"Lobby Name",
		"Password123",
		true,
		10,
		{
			"Map" : "Desert",
			"Game Mode" : "Free For All"
		}
	)

func lobby_created(lobby_name : String):
	print("Succesfully created lobby "+lobby_name)
#	Now you can join the lobby!

func lobby_creation_failed(lobby_name : String, error : int):
	match(error):
		ENUMS.LOBBY_CREATION_ERROR.LOBBY_ALREADY_EXISTS:
			push_error("A lobby with the name "+lobby_name+" already exists.")
		ENUMS.LOBBY_CREATION_ERROR.NAME_TOO_SHORT:
			push_error(lobby_name+" is too short.")
		ENUMS.LOBBY_CREATION_ERROR.NAME_TOO_LONG:
			push_error(lobby_name+" is too long.")
		ENUMS.LOBBY_CREATION_ERROR.PASSWORD_TOO_LONG:
			push_error("The password for "+lobby_name+" is too long.")
		ENUMS.LOBBY_CREATION_ERROR.TAGS_TOO_LARGE:
			push_error("The tags have exceeded the 2048 byte limit.")
		ENUMS.LOBBY_CREATION_ERROR.DATA_TOO_LARGE:
			push_error("The data have exceeded the 2048 byte limit.")
		ENUMS.LOBBY_CREATION_ERROR.ON_COOLDOWN:
			push_error("Please wait a few seconds before creating another lobby.")
      

Joining

After a lobby is created you will be able to join it. After creation you have to join within 5 seconds, otherwise it will get automatically deleted.

Joining a lobby can be done using GDSync.join_lobby(name : String, password : String). This function requires you to enter the name of the lobby you would like to join and the password if it has one. Make sure to connect relevant signals to be notified whether the join attempt succeeded or not.

func _ready():
	GDSync.lobby_joined.connect(lobby_joined)
	GDSync.lobby_join_failed.connect(lobby_join_failed)
	
	GDSync.join_lobby("Lobby Name", "Password")

func lobby_joined(lobby_name : String):
	print("Succesfully joined lobby "+lobby_name)

func lobby_join_failed(lobby_name : String, error : int):
	match(error):
		ENUMS.LOBBY_JOIN_ERROR.LOBBY_DOES_NOT_EXIST:
			push_error("The lobby "+lobby_name+" does not exist.")
		ENUMS.LOBBY_JOIN_ERROR.LOBBY_IS_CLOSED:
			push_error("The lobby "+lobby_name+" is closed.")
		ENUMS.LOBBY_JOIN_ERROR.LOBBY_IS_FULL:
			push_error("The lobby "+lobby_name+" is full.")
		ENUMS.LOBBY_JOIN_ERROR.INCORRECT_PASSWORD:
			push_error("The password for lobby "+lobby_name+" was incorrect.")
		ENUMS.LOBBY_JOIN_ERROR.DUPLICATE_USERNAME:
			push_error("The lobby "+lobby_name+" already contains your username.")
      

Leaving

Leaving a lobby is quite simple, once you are inside a lobby you can use GDSync.leave_lobby() to exit it. Leaving a lobby does not emit any signals and always succeeds.

If all players inside the lobby have left, it will automatically get deleted.

Visibility & Browsing

In GD-Sync, lobbies can be either public or private. If the lobby is public, it can be retrieved using GDSync.get_public_lobbies(). Using this function will in turn emit the signal GDSync.lobbies_received(lobbies : Array). The signal returns an array of all public lobbies in the format displayed on the right.

[
  {
    "Name": "Super Cool Lobby",
    "PlayerCount": 2,
    "PlayerLimit": 4,
    "Public": true,
    "Closed": false,
    "Tags": {
      "Map": "Desert",
      "Gamemode": "Free For All"
    }
  },
  {
    "Name": "Another Cool Lobby",
    "PlayerCount": 4,
    "PlayerLimit": 4,
    "Public": true,
    "Closed": true,
    "Tags": {
      "Map": "City",
      "Gamemode": "Team Deathmatch"
    }
  }
]

      

Opening & Closing

Lobbies can be either open or closed. If a lobby is closed it no longer allows new players to join. This can be used to prevent players from joining while a game is in-progress. Lobbies can be opened using GDSync.open_lobby() and closed using GDSync.close_lobby().

Joining & Leaving Signals

If a client joins or leaves a lobby a signal is emitted. If a client joins a lobby, the signal GDSync.client_joined(client_id : int). The ID is the id of the client that joined the lobby. This signal is emitted for all clients that join including yourself.

If a client leaves the signal GDSync.client_left(client_id : int) is emitted. This signal is emitted for all clients excluding you. The reason for this is that when you leave a lobby you no longer receive messages from it, thus the signal is not emitted for yourself.

var clients : Dictionary = {}

func _init():
	GDSync.client_joined.connect(client_joined)
	GDSync.client_left.connect(client_left)

func client_joined(client_id : int):
	print("A new client has joined!")
	var client : Node = player_controller.instantiate()
	client.name = str(client_id)
	
	clients[client_id] = client
	add_child(client)

func client_left(client_id : int):
	print("A client has left")
	
	clients[client_id].queue_free()
	clients.erase(client_id)
      

Tags & Data

Lobbies can store temporary variables using tags and lobby data. Both are functionally the same with one key difference, tags are publicly visible and data is not.

Lobby tags and data are automatically synchronized across all clients in the same lobby. It may take up to 1 second before the synchronization occurs on all clients.

Beware when updating the tags or data frequently as it can be a relatively expensive operation, using up quite a bit of transfer.

A client can only modify the tags or data of the lobby they are in. To set lobby tags you can use GDSync.set_lobby_tags(key : String, value : Variant). The key is the name of the variable you want to set. The value can be any Godot Variant. This function can be used to create a new entry or to overwrite existing data.

To erase tags you can use GDSync.erase_lobby_tags(key : String). This will erase the existing tag if the key exists.

Lobby tags can be retrieved using GDSync.get_lobby_tag(key : String, default : Variant). Key is the variable you want to retrieve. Default is the value it will return in case the key does not exist, by default this is null. You can also check if an entry exists using GDSync.has_lobby_tag(key : String). You can also retrieve all lobby tags as a dictionary using GDSync.get_all_lobby_tags().

When lobby tags are changed it emits a signal on all clients in the lobby, GDSync.lobby_tags_changed(key : String, value : Variant).

To set lobby data you can use GDSync.set_lobby_data(key : String, value : Variant). The key is the name of the variable you want to set. The value can be any Godot Variant. This function can be used to create a new entry or to overwrite existing data.

To erase data you can use GDSync.erase_lobby_data(key : String). This will erase existing data if the key exists.

Lobby data can be retrieved using GDSync.get_lobby_data(key : String, default : Variant). Key is the variable you want to retrieve. Default is the value it will return in case the key does not exist, by default this is null. You can also check if an entry exists using GDSync.has_lobby_data(key : String). You can also retrieve all lobby data as a dictionary using GDSync.get_all_lobby_data().

When lobby data is changed it emits a signal on all clients in the lobby, GDSync.lobby_data_changed(key : String, value : Variant).

Host Selection

When developing a peer-to-peer game, generally one client is always picked as the host. The host client will perform most authoritative logic such as spawning enemies, starting and ending matches, etc.

GD-Sync has built-in host selection which gives you an easy way to know on which client to perform important tasks. This is an essential part of the plugin that is used often. By default, the first player that joins a lobby is picked as the host. If the current host leaves, a new one is automatically selected.

Checking if you are the host can be done in two ways. The first method is using GDSync.is_host(), which will return whether you are the host of the current lobby or not. The second method is using the signal GDSync.host_changed(is_host : bool). This signal is automatically emitted when the host changes (if the current host disconnects or leaves).

func _ready():
	GDSync.host_changed.connect(host_changed)
	
	if GDSync.is_host():
		print("I am the host!")
	else:
		print("I am not the host.")

func host_changed(is_host : bool, new_host_id : int):
	print("Client "+str(new_host_id)+" is the new host")
	if is_host:
		print("I am the new host!")
	else:
		print("I am not new the host.")
      

Accessing & Synchronizing Remote Nodes

GD-Sync is entirely dependent on Node paths. When synchronizing variables or calling remote functions, it is required that the path of the Node you are trying to access is the same across all clients. This also means a Node must be present in the scene tree in order for GDSync to access it.

When spawning in Nodes such as enemies or bullets, make sure that the instantiated Node has the same name across all clients if you want to synchronize it.

Remote Function Calls

GD-Sync allows you to call functions on other clients that are in the same lobby as you. As a safety feature, GD-Sync automatically blocks all remote function calls. In order to allow remote calls, you can use GDSync.expose_func(callable : Callable) or GDSync.expose_node(node : Node) to grant full access to an entire Node.

To call a function on another client you can use GDSync.call_func(callable : Callable, parameter_array : Array). This function requires parameters to be passed inside an array. Using this function will perform a remote call on all clients except yourself. The example code shows a script that spawns an enemy every 10 seconds.

In this example a single client is authoritative (the host) which remotely spawns in the enemies on other clients. This script also ensures that the NodePath of the instantiated Node is the same across all clients, so it can be properly accessed later on.

Instead of manually instantiating nodes, we highly recommend using the custom NodeInstantiator.



GD-Sync also allows you to see which client a remote function call came from. GDSync.get_sender_id() returns the client ID of the last client that performed a remote function call. By default this feature is disabled, as it slightly increases data usage. It can be enabled through the configuration menu.

func _ready():
	GDSync.expose_func(instantiate_enemy)

var t : float = 0.0
var enemy_index : int = 0
func _process(delta):
	t += delta
	if t >= 10:
		t -= 10
		spawn_enemy()

func spawn_enemy():
	if !GDSync.is_host(): return
	
	var enemy_name : String = "Enemy"+str(enemy_index)
	instantiate_enemy(enemy_name)
	GDSync.call_func(instantiate_enemy, [enemy_name])

func instantiate_enemy(name : String):
	var enemy : Node = enemy_scene.instantiate()
	enemy.name = name
	add_child(enemy)

	enemy_index += 1
      

Remote Variable Syncing

GD-Sync allows you to synchronize Node variables on clients that are in the same lobby as you. As a safety feature, GD-Sync automatically blocks all sync requests. In order to allow variable synchronization, you can use GDSync.expose_var(variable_name : String) or GDSync.expose_node(node : Node) to grant full access to an entire Node.

To synchronize a variable to all other clients you can use GDSync.sync_var(node : Node, variable_name). Using this function will sync the value of the variable to all other clients.

Instead of synchronizing variables manually, why highly recommend using the custom PropertySynchronizer. See PropertySynchronizer Class for more info.

var i : int = 0
func _ready():
	GDSync.expose_var("i")
	
	i = 10
	GDSync.sync_var(self, "i")
#	The value of i will now be 10 on all clients

NodeInstantiator Class

GD-Sync offers a simple solution to spawning in Nodes across multiple clients. Spawning in Nodes can be done using the custom NodeInstantiator class.

An example is spawning in bullets that come out of a gun. The screenshot shows a NodeInstantiator that spawns bullets inside the current scene. If “Sync Starting Changes” is enabled, any changes made to the instantiated Node within the same frame will automatically be synchronized across all clients. This can be useful for when setting the position, direction and speed of the bullet, doing this will set those values on all clients. This setting will only synchronize properties on the top-level Node of the Instantiated scene.

To instantiate a Node using the NodeInstantiator you can use instantiate_node(), which will return the instantiated Node. This Node will automatically be instantiated on all other clients as well. The NodeInstantiator automatically ensures the NodePath matches up on all clients so that GD-Sync may properly use it.

Any scripts that are instantiated using the NodeInstantiator may override the function _mutliplayer_ready(). This is called after _ready(), when the starting synchronisations have been applied. When a Node is instantiated by a NodeInstantiator, it will emit the node_instantiated(node : Node) signal.

The NodeInstantiator also supports scene replication.

PropertySynchronizer Class

GD-Sync comes with a custom PropertySynchronizer. The PropertySynchronizer ensures that a value of a variable is synced across all clients. Simply point it to a variable and it will do the rest. The PropertySynchronizer is optimized for data usage and will only synchronize if the property it is linked to changes.

When using the PropertySynchronizer, make sure its NodePath matches up on all clients. If this is not the case it won’t be able to find itself and perform the sync. When spawning in Nodes using the custom NodeInstanciator Class, this is automatically done for you.

The PropertySynchronizer also offers built-in interpolation. Interpolation only works with certain Variants (int, float, vector2 ,vector3, vector4, color, quaternion, basis), if one of these is selected the interpolation settings will automatically appear in the inspector.

The frequency of the sync requests is entirely dependent on the refresh rate. We recommend keeping this relatively low (< 30) in combination with interpolation, to keep the bandwidth as low as possible.If you want to synchronize the rotation/orientation of a Node3D, make sure to fill in the property “basis” instead of “rotation”.

SynchronizedAnimationPlayer Class

GD-Sync comes with an extended version of the default AnimationPlayer. The SynchronizedAnimationPlayer adds full multiplayer support to AnimationPlayers without any extra steps.

All functions related to animation playback will automatically be called on all other clients:
- play()
- play_backwards()
- pause()
- stop()
- queue()
- seek()
- advance()

The SynchronizedAnimationPlayer does NOT synchronize the playback of AnimationTrees. It only works for AnimationPlayers who play animations directly.

Scene Replication

GD-Sync offers built-in scene replication with NodeInstantiators. Once enabled, it will replicate spawned Nodes on new clients that join the lobby.

Node replication keeps functioning, even if the NodeInstantiator itself has been destroyed.

Once a spawned Node is removed from the scene tree, it is considered deleted and will no longer be replicated for now clients.

Node Ownership

GD-Sync has a built-in method to assign node ownership. Assigning an owner to a Node does not do anything by itself, but it is an extremely useful feature when writing your code. Node ownership is global across all clients, if you assign ownership to a Node it will do it on all clients.

A good example of this is when writing player scripts. When re-using the same scene for all players, you only want movement inputs to function on YOUR player. An easy way to do this is when spawning the client in, to assign it to yourself. When a client joins the lobby you can make them the owner of their own player. This can later be used inside the player script to handle the inputs to only allow you to move your player instance.

Ownership can be assigned using GDSync.set_gdsync_owner(node : Node, owner : int). The owner can be the client ID of any client in your lobby. To remove ownership, use GDSync.clear_gdsync_owner(node : Node).

You can check whether you are the owner of a Node with GDSync.is_gdsync_owner(node : Node). You can also retrieve the owner of a node with GDSync.get_gdsync_owner(node : Node).

#Player Spawner Script
func _ready():
	GDSync.client_joined.connect(client_joined)

func client_joined(client_id : int):
	print("A new client has joined!")
	var client : Node = player_controller.instantiate()
	client.name = str(client_id)
	
	clients[client_id] = client
	add_child(client)
	GDSync.set_gdsync_owner(client, client_id)
#Player Script
func _process(delta):
	if !GDSync.is_gdsync_owner(self): return
	var direction : Vector2 = Vector2.ZERO
	
	if Input.is_action_pressed("Up"):
		direction.y -= 1
	if Input.is_action_pressed("Down"):
		direction.y += 1
	if Input.is_action_pressed("Left"):
		direction.x -= 1
	if Input.is_action_pressed("Right"):
		direction.x += 1
	
	direction = direction.normalized()
	position += direction * speed * delta

Critical errors

The GD-Sync plugin has some limitations which are in place to protect the servers from malicious attacks. If these limits are exceeded a critical error will be returned and printed in the error logs of the console.

These limits include a 2048 byte limit on lobby data and tags and player data. It also limits the maximum packet size to 20480 bytes. This means that if you exceed this limit per packet you need to adjust your code. This can either be caused by too many PropertySynchronisers or remote calls that are too large.

Have a question for us?

Send us a message and we'll be in touch.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.