diff --git a/weltenauge/PROJEKT_UEBERSICHT.md b/weltenauge/PROJEKT_UEBERSICHT.md index 6367e1a..9213070 100644 --- a/weltenauge/PROJEKT_UEBERSICHT.md +++ b/weltenauge/PROJEKT_UEBERSICHT.md @@ -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) + # ... + ``` + diff --git a/weltenauge/systems/network/network_manager.gd b/weltenauge/systems/network/network_manager.gd index dbe36c9..e835880 100644 --- a/weltenauge/systems/network/network_manager.gd +++ b/weltenauge/systems/network/network_manager.gd @@ -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) diff --git a/weltenauge/systems/player/player.tscn b/weltenauge/systems/player/player.tscn index af52e9c..6a21653 100644 --- a/weltenauge/systems/player/player.tscn +++ b/weltenauge/systems/player/player.tscn @@ -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")] diff --git a/weltenauge/systems/player/player_controller.gd b/weltenauge/systems/player/player_controller.gd index ac4109b..a7ff801 100644 --- a/weltenauge/systems/player/player_controller.gd +++ b/weltenauge/systems/player/player_controller.gd @@ -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)