Vor einigen Tagen wurde im offiziellen Godot Discord-Server darüber diskutiert, wie man es schaffen kann, Materialien über das Netzwerk zu synchronisieren. Um das Problem etwas besser nachvollziehen zu können, sollte ich vielleicht zuerst einmal erklären, dass man nicht jeden Datentyp per RPC oder MultiplayerSynchronizer
über das Netzwerk transferien kann, sondern nur skalare Datentypen. Zu den skalaren Datentypen zählen generell alle Datentypen, die nur einen einzelnen Wert beinhalten können, wie int
, float
, Bool
, String
oder StringName
.
Generell nennt man das ganze Themengebiet Serialisierung und dabei geht es darum, die Variablen oder teilweise auch Objekte aus dem Arbeitsspeicher auszulesen und so zu transformieren, dass man daraus erneut Variablen / Objekte des ursprünglichen Typs mit identischem Inhalt erzeugen kann. Daher ist Serialisierung auch nicht nur für den Netzwerktransfer relevant, sondern wird auch benötigt, wenn man bspw. den Spielstand speichern möchte.
Die Idee
Direkt nach dem Lesen der Frage hatte ich auch bereits eine Idee, wie man das Problem lösen kann. Aber um wirklich sicherzugehen, dass diese Idee funktioniert, habe ich das altbekannte Tutorialprojekt so erweitert, dass die Clients mit der Tabulator-Taste zwischen drei verschiedenen Materialien wechseln können und alle anderen Clients diesen Materialienwechsel ebenfalls synchronisiert bekommen.
Ganz genau genommen funktioniert es in der Praxis dann doch ein klein wenig anders:
- Es gibt ein Enum
SupportedColors
, welches für alle Spielermaterialien einen Wert bereithält. (player.gd
) - Es gibt ein Dictionary
colorToMaterial
, welches alle Enum-Werte auf die zugehörigen gespeicherten Materialien abbildet. (player.gd
) - Die Spielfigur erhält ein Property
currentColor
vom TypSupportedColors
,- welches immer den Enum-Wert des aktuell angezeigten Materials enthält. (
player.gd
) - welches im Setter, also wenn die Farbe geändert wird, das Material des Spielfigur-Meshs durch das Material aus
colorToMaterial
austauscht. (player.gd
) - welches mit Hilfe von unserem MultiplayerSynchronizer
ServerSynchronizer
synchronisiert wird (Spawn, Watch). (player.tscn
)
- welches immer den Enum-Wert des aktuell angezeigten Materials enthält. (
- Wenn in einem der Clients die Tabulator-Taste betätigt wird, wird dies mit Hilfe eines RPCs an den Server signalisiert. (
player_input.gd
) - Sobald der Server den Materialänderungswunsch empfängt, ändert er den Wert von
currentColor
. Dies wird an alle Clients in Reichweite synchronisiert, wodurch die jeweiligen Setter aufgerufen werden, was in der Materialänderung des Spielfigur-Meshs resuliert. (player.gd
)
Die Praxis
Nun aber genug der Theorie, so gerne ich sie auch mag, aber manchmal ist Quellcode dann doch überzeugender. Bevor wir starten können, müssen ein paar Materialien angelegt werden. Ich habe für dieses Beispiel drei Standardmaterialien (StandardMaterial3D
) angelegt, bei denen ich jeweils die Albedo-Farbe (albedo_color
) auf rot, grün bzw. blau gestellt und abgespeichert habe. Mit dieser Vorbereitung können wir nun mit der Erweiterung des Players (player.gd
) starten:
ToDo 1 und 2
Die Punkte 1 und 2 sind relativ schnell umgesetzt und sollten keiner weiteren Worte bedürfen:
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
Anschließend kümmern wir uns um die Punkte 3.1 und 3.2, indem wir unser Property currentColor
anlegen und den zugehörigen Setter schreiben:
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
Das Property is_ready
soll nur verhindern, dass wir das Material setzen, bevor wir Zugriff auf das Mesh haben. Damit dies funktioniert, benötigen wir noch zwei weitere Zeilen:
32var is_ready := false
33
34func _ready() -> void:
35 is_ready = true
36 if player == multiplayer.get_unique_id():
37 $Camera3D.current = true
Nachdem wir das Property hinzugefügt haben, können wir auch die Replikationskonfiguration unseres ServerSynchronizers
passend erweitern:

Replikation-Tab von ServerSynchronizer
ToDo 4
Bei der Umsetzung des RPCs für Punkt 4, habe ich den gleichen Mechanismus genutzt wie im früheren Artikel für das Springen (player_input.gd
). Zuerst fügen wir ein Property hinzu, welches den Materialänderungswunsch des Clients repräsentiert:
4@export var switch_color := false
Anschließend erzeugen wir die Funktion, die wir per RPC aufrufen:
16@rpc("call_local")
17func color_switch() -> void:
18 switch_color = true
Und zum Schluss erweitern wir unsere _process()
Funktion, um die Tabulator-Taste mit dem RPC zu verbinden:
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
Zuguterletzt fehlt noch die Reaktion auf den Materialänderungswunsch, der alle unsere Puzzleteile miteinander verbinden wird (player.gd
):
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
Der letzte Schritt

MeshInstance3D
Wenn man an dieser Stelle unsere bisherige Erweiterung ausprobieren möchte, fällt auf, dass bereits vieles funktioniert. Durch Betätigung der Tabulator-Taste wird sich das Material und somit die Farbe der Spielfigur ändern, aber gleichzeitig ändert sich auch das Material jeder anderen Spielfigur.
Das liegt daran, dass sich alle Szenen standardmäßig die jeweiligen Ressourcen teilen. Wenn durch einen Client das Mesh-Material verändert wird, geschieht dies automatisch ebenfalls für alle anderen Spielfiguren, da alle dasselbe Mesh verwenden. Um dies zu ändern, müssen wir noch einen kleinen, aber entscheidenden, Haken in der Konfiguration der Mesh-Ressource setzen, nämlich den hinter “Local to Scene”, wie im nebenstehenden Bild ziemlich weit unten gezeigt. Dies bewirkt, dass alle Instanzen der Szene, also alle vorhandenen Spielfiguren, eigene Meshs benutzen.
Im Allgemeinen ist es eine gute Idee, dass die Engine versucht, möglichst viele Ressourcen einzusparen. Nur habe ich bei meinen Versuchen leider selbst nicht sofort daran gedacht und befürchtete schon, ich hätte Fehler bei der Programmierung gemacht. Durch ein wenig Debugging habe ich dann festgestellt, dass der Quellcode alles macht, wie es angedacht war und dann ist mir glücklicherweise auch wieder eingefallen, dass sich die Spielfiguren das Mesh teilen 😓.
Fazit
Das hier gezeigte ist natürlich sehr rudimentär, sollte aber die generelle Methodik verdeutlichen. Natürlich kann man das beliebig weiter ausbauen oder auch auf andere Anwendungsfälle umbauen. Um dem Client bspw. einen ColorPicker anzubieten, könnte man einfach die RGBA-Werte übertragen und darauf basierend die Albedo-Farbe des aktuellen Mesh-Materials anpassen. Wie schon August Heinrich Hoffmann von Fallersleben sagte: “Die Gedanken sind frei, …”.
Natürlich findet ihr wie gewohnt den kompletten Quellcode in meinem Tutorial Repository.