Client/Server Sync tested for 1 player, lerp smoothing, player face to target

This commit is contained in:
torsten 2026-02-15 12:42:08 +01:00
parent fbb880acd6
commit 5fbdc61bec
4 changed files with 65 additions and 13 deletions

View file

@ -103,3 +103,32 @@ Dieser Abschnitt beschreibt die Verantwortlichkeiten der zentralen Skripte im Pr
- **Rolle:** Hauptmenü-UI.
- **Verantwortlichkeit:** Enthält die UI-Elemente wie "Host" und "Join" Buttons. Es wird angenommen, dass die `pressed()` Signale dieser Buttons direkt im Godot-Editor mit den entsprechenden Signalen im `GameEvents`-Singleton (`host_requested`, `join_requested`) verbunden sind.
## 8. Wichtige Erkenntnisse & Best Practices
Dieser Abschnitt dokumentiert wichtige technische Lektionen, die während der Entwicklung gelernt wurden.
### Dynamische Konfiguration des MultiplayerSynchronizer
Die Konfiguration des `MultiplayerSynchronizer` für wiederverwendbare Szenen (wie den Spieler) ist kritisch und muss korrekt gehandhabt werden, um Timing- und Pfad-Probleme zu vermeiden. Die folgenden vier Regeln sind entscheidend:
1. **Konfiguration nur per Code:** Die Konfiguration muss zur Laufzeit im Code erfolgen, nicht im Editor. Der Editor speichert absolute Pfade, die in einem dynamischen Szenen-Setup (mehrere Welten/Viewports) unbrauchbar sind.
2. **Ort der Konfiguration: `_ready()`:** Die Logik zur Erstellung der `SceneReplicationConfig` gehört in die `_ready()`-funktion des Skripts, dessen Eigenschaften synchronisiert werden sollen (z.B. `player_controller.gd`).
3. **Korrekte Eigenschafts-Syntax:** Der Pfad zu einer Eigenschaft, die sich direkt auf dem `root_path`-Node des Synchronizers befindet (standardmäßig sein Parent), **muss mit einem Doppelpunkt beginnen**.
- **Korrekt:** `config.add_property(":sync_position")`
- **Falsch:** `config.add_property("sync_position")` oder `config.add_property("..:sync_position")`
4. **Timing im Spawner sicherstellen (`async`/`await`):** Der Code, der den Node instanziiert und dem Baum hinzufügt (z.B. `_spawn_player_in_world` im `NetworkManager`), muss nach dem `add_child()`-Aufruf explizit auf den nächsten Frame warten, bevor er den Synchronizer des neuen Nodes benutzt. Dies stellt sicher, dass dessen `_ready()`-Funktion bereits ausgeführt wurde.
```gdscript
# Im NetworkManager
async func _spawn_player_in_world(id, viewport_node):
# ...
viewport_node.add_child(player)
await get_tree().process_frame # Garantiert, dass _ready() auf player läuft
# Jetzt ist die Benutzung des Synchronizers sicher
var sync = player.get_node("MultiplayerSynchronizer")
sync.set_visibility_for(id, true)
# ...
```

View file

@ -162,19 +162,22 @@ func _spawn_player_in_world(id, viewport_node):
#player.set_multiplayer_authority(id)
player.set_multiplayer_authority(1)
viewport_node.add_child(player, true)
# Visibility Filtering: Nur Spieler im gleichen Viewport sehen sich
var sync = player.get_node("MultiplayerSynchronizer")
# SCHRITT 2: Explizit auf den nächsten Frame warten.
# Dies gibt Godot die Zeit, die _ready() Funktion des Spielers auszuführen.
await get_tree().process_frame
# SCHRITT 3: Jetzt ist der Spieler garantiert bereit und konfiguriert.
# Alle folgenden Operationen sind sicher.
var sync = player.get_node("MultiplayerSynchronizer")
# Visibility Filtering: Nur Spieler im gleichen Viewport sehen sich
for other in viewport_node.get_children():
if other.is_in_group("players"):
var other_id = other.name.to_int()
var other_id = other.name.to_int()
# Der neue Spieler sieht den alten
other.get_node("MultiplayerSynchronizer").set_visibility_for(id, true)
# Der alte Spieler sieht den neuen
sync.set_visibility_for(other_id, true)

View file

@ -8,13 +8,16 @@
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_fqfg4"]
[node name="CharacterBody3D" type="CharacterBody3D" unique_id=682006733]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0.26067275)
script = ExtResource("1_ktl23")
interpolation_speed = 5.0
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=159496934]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.0185384, 0)
shape = SubResource("CylinderShape3D_ktl23")
[node name="Visuals" type="Node3D" parent="." unique_id=1840405751]
transform = Transform3D(-0.99988204, 0, 0.015358234, 0, 1, 0, -0.015358234, 0, -0.99988204, 0, 0, 0)
[node name="Dude" parent="Visuals" unique_id=1185543431 instance=ExtResource("1_2anqx")]

View file

@ -6,6 +6,8 @@ extends CharacterBody3D
@export var sync_position: Vector3
@export var sync_rotation: Vector3
@export var interpolation_speed = 15.0 # Geschwindigkeit für die Glättung
var path: PackedVector3Array = []
var speed = 5.0
@ -17,13 +19,25 @@ func _enter_tree():
#set_multiplayer_authority(name.to_int())
set_multiplayer_authority(1)
func _ready():
target_position = global_position
# --- START: Dynamische Replikations-Konfiguration ---
# Eine neue Konfigurations-Ressource im Code erstellen.
var config = SceneReplicationConfig.new()
# Die zu synchronisierenden Eigenschaften hinzufügen. Der Pfad '..:sync_position' bedeutet:
# '..': Gehe zum Eltern-Node (dem CharacterBody3D).
# ':': Greife auf eine Eigenschaft dieses Nodes zu.
# 'sync_position': Der Name der Eigenschaft.
config.add_property(NodePath(":sync_position"))
config.add_property(NodePath(":sync_rotation"))
# Die generierte Konfiguration dem Synchronizer zuweisen. Dies überschreibt alle Editor-Einstellungen.
synchronizer.replication_config = config
# --- ENDE: Dynamische Replikations-Konfiguration ---
target_position = global_position
# Wir schalten die automatische Physik-Synchronisation aus
set_physics_process(multiplayer.is_server())
func _physics_process(delta):
# Dieser Teil läuft NUR auf dem Server
if path.size() > 0:
@ -32,6 +46,9 @@ func _physics_process(delta):
var next_point = path[0]
var direction = (next_point - global_position).normalized()
# Richte den Avatar in die Bewegungsrichtung aus.
look_at(next_point, Vector3.UP)
# Wir nutzen move_and_slide, damit Kollisionen (Wände) funktionieren
velocity = direction * speed
move_and_slide()
@ -115,8 +132,8 @@ func _get_click_floor_pos():
func _process(_delta):
# Dieser Teil läuft auf dem Client (besonders dem, der die Authority hat)
if not multiplayer.is_server():
if global_position != sync_position:
print("Client: _process() => Setting client position")
# Wir erzwingen die Position, die der Server berechnet hat
global_position = sync_position
rotation = sync_rotation
# Bewege die lokale Position sanft in Richtung der vom Server vorgegebenen Position.
global_position = global_position.lerp(sync_position, _delta * interpolation_speed)
# Rotiere sanft in Richtung der vom Server vorgegebenen Rotation.
rotation = rotation.lerp(sync_rotation, _delta * interpolation_speed)