somethinglikegames.de

My personal Blog about Game Development as a single person in part time


Categories

A few days ago there was a discussion in the official Godot Discord server about how to manage to synchronize materials over the network. To understand the problem a bit better, I should perhaps first explain that you cannot transfer every datatype via RPC or MultiplayerSynchronizer over the network, but only scalar datatypes. Scalar datatypes generally include all datatypes that can contain only a single value, such as int, float, Bool, String or StringName.

Generally the whole topic is called serialization and it is about reading the variables or partly also objects from the memory and transforming them in such a way that variables / objects of the original type can be created again with identical contents. Serialization is therefore not only relevant for network transfers, but is also required when you want to save the game state, for example.

My statement about the “allowed” data types is not quite correct. There are the functions PackedByteArray var_to_bytes_with_objects(Variant variable) and Variant bytes_to_var_with_objects(PackedByteArray bytes) which can be used to serialize and deserialize any object.
⚠️ In practice, however, you should be extremely careful with this, as it can also be used to transfer executable code, which is an extremely high security risk and should therefore not be used in practice, even if you really know what you are doing.

The Idea

Immediately after reading the question I also had already an idea how to solve the problem. But to really make sure that this idea works, I extended the old known tutorial project so that the clients can switch between three different materials with the tab key and all other clients get this material switch synchronized as well.

Strictly speaking it works a little bit different in practice:

  1. There is an enum SupportedColors which holds a value for all player materials. (player.gd)
  2. There is a dictionary colorToMaterial, which maps all enum values to the corresponding stored materials. (player.gd)
  3. The player gets a property currentColor of type SupportedColors,
    1. which always contains the enum value of the currently displayed material. (player.gd)
    2. which in the setter, i.e. when the color is changed, replaces the material of the player mesh with the material from colorToMaterial. (player.gd)
    3. which is synchronized by our MultiplayerSynchronizer ServerSynchronizer (Spawn, Watch). (player.tscn)
  4. When the tab key is pressed in one of the clients, this is signaled to the server using an RPC. (player_input.gd)
  5. As soon as the server receives the material change request, it changes the value of currentColor. This is synchronized to all clients in range, calling their respective setters, which resulates in the material change of the game character mesh. (player.gd)

The Practice

But enough of theory, as much as I like it, sometimes source code is more convincing. Before we can start, a few materials have to be created. For this example I have created three standard materials (StandardMaterial3D), for each of which I have set the albedo color (albedo_color) to red, green and blue respectively and saved them. With this preparation we can now start with the extension of the player (player.gd):

ToDo 1 and 2

Items 1 and 2 are implemented relatively quickly and should need no further words:

 6const colorToMaterial: Dictionary = { SupportedColors.GREEN: preload("res://materials/green.tres"),
 7										SupportedColors.BLUE: preload("res://materials/blue.tres"),
 8										SupportedColors.RED: preload("res://materials/red.tres"),
 9										}
10
11enum SupportedColors { GREEN, BLUE, RED}

ToDo 3

Then we take care of items 3.1 and 3.2 by creating our property currentColor and writing the corresponding setter:

21@export var currentColor : SupportedColors = SupportedColors.GREEN:
22	set(color):
23		currentColor = color
24		if is_ready:
25			mesh.surface_set_material(0, colorToMaterial[currentColor])
26
27@onready var mesh: Mesh = $MeshInstance3D.mesh

The is_ready property is just to prevent us from setting the material before we have access to the mesh. For this to work, we need two more lines:

32var is_ready := false
33
34func _ready() -> void:
35	is_ready = true
36	if player == multiplayer.get_unique_id():
37		$Camera3D.current = true

After adding the property, we can also extend the replication configuration of our ServerSynchronizer appropriately:

Replication tab of ServerSynchronizer

Replication tab of ServerSynchronizer

ToDo 4

In implementing the RPC for item 4, I used the same mechanism as in the previous article for jumping (player_input.gd). First, we add a property representing the client’s material change request:

4@export var switch_color := false

Then we create the function that we call via RPC:

16@rpc("call_local")
17func color_switch() -> void:
18	switch_color = true

And finally, we extend our _process() function to connect the tab key to the RPC:

21func _process(delta: float) -> void:
22	direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
23	if Input.is_action_just_pressed("ui_accept"):
24		jump.rpc_id(MultiplayerPeer.TARGET_PEER_SERVER)
25	if Input.is_action_just_pressed("ui_focus_next"):
26		color_switch.rpc_id(MultiplayerPeer.TARGET_PEER_SERVER)

ToDo 5

Last but not least, the response to the material change request that will connect all our puzzle pieces (player.gd) is still missing:

40func _process(_delta: float) -> void:
41	if input.switch_color:
42		input.switch_color = false
43		match currentColor:
44			SupportedColors.GREEN:
45				currentColor = SupportedColors.BLUE
46			SupportedColors.BLUE:
47				currentColor = SupportedColors.RED
48			SupportedColors.RED:
49				currentColor = SupportedColors.GREEN

The final step

MeshInstance3D

MeshInstance3D

If you want to try our previous expansion at this point, you will notice that a lot of things already work. Pressing the tab key will change the material and thus the color of the character, but at the same time the material of every other character will change as well.

This is because all scenes share the respective resources by default. If by a client the mesh material is changed, this will automatically happen for all other game characters as well, since they all use the same mesh. To change this, we still need to set a small, but crucial, hook in the mesh resource configuration, namely the one behind “Local to Scene”, as shown quite far down in the accompanying image. This causes all instances of the scene, i.e. all existing game characters, to use their own meshes.

In general, it’s a good idea for the engine to try to save as many resources as possible. Only, unfortunately, I didn’t think of this myself right away during my experiments and was already afraid that I had made programming mistakes. Through a little debugging I then realized that the source code does everything as it was intended and then fortunately I remembered that the game characters share the mesh 😓.

Conclusion

What is shown here is of course very rudimentary, but should clarify the general methodology. Of course, you can extend this as much as you like or even modify it for other use cases. For example, to offer a ColorPicker to the client, one could simply transfer the RGBA values and adjust the albedo color of the current mesh material based on them. As August Heinrich Hoffmann von Fallersleben said: “Thoughts are free, …”.

Of course you can find the complete source code in my Tutorial Repository.