From e7ebb2042885c5bc934a83b4c4ae53ab54e1a674 Mon Sep 17 00:00:00 2001 From: torsten Date: Sun, 15 Feb 2026 18:09:48 +0100 Subject: [PATCH] First working area switch --- weltenauge/PROJEKT_UEBERSICHT.md | 25 +++---- .../maps/outdoor/grass/keuwin_grasland.tscn | 4 ++ .../maps/planes/fugue/halle_der_ahnen.tscn | 4 ++ weltenauge/systems/network/network_manager.gd | 66 +++++++++---------- 4 files changed, 53 insertions(+), 46 deletions(-) diff --git a/weltenauge/PROJEKT_UEBERSICHT.md b/weltenauge/PROJEKT_UEBERSICHT.md index 8ca84a6..8c6daea 100644 --- a/weltenauge/PROJEKT_UEBERSICHT.md +++ b/weltenauge/PROJEKT_UEBERSICHT.md @@ -55,6 +55,11 @@ Alle Skripte und die dazugehörige Logik sollen thematisch in Unterordnern unter ## 5. Implementierungsdetails & Konventionen - **Click-to-Move:** Die Spielerbewegung soll aus Performancegründen über eine AStar-Implementierung (`systems/navigation/`) gesteuert werden. +- **GDScript-Konvention: `async`-Schlüsselwort vermeiden:** In GDScript 4 wird jede Funktion, die `await` enthält, automatisch von der Engine als asynchron behandelt. Das `async`-Schlüsselwort in der Funktionssignatur (`async func...`) ist optional und hat in diesem Projekt wiederholt zu schwer diagnostizierbaren Syntaxfehlern geführt. + - **Regel:** Das `async`-Schlüsselwort **darf nicht** verwendet werden. + - **Korrekt:** `func my_function(): await get_tree().process_frame` + - **Falsch:** `async func my_function(): await get_tree().process_frame` + ## 6. Globale Skripte (Autoloads) @@ -103,6 +108,10 @@ Dieser Abschnitt beschreibt die Verantwortlichkeiten der zentralen Skripte im Pr Dieser Abschnitt dokumentiert wichtige technische Lektionen, die während der Entwicklung gelernt wurden. +### Technische Detail + +Es gibt kein async Keyword, aber await funktioniert trotzdem. => await get_tree().process_frame + ### 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: @@ -115,15 +124,7 @@ Die Konfiguration des `MultiplayerSynchronizer` für wiederverwendbare Szenen (w - **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) - # ... - ``` +### Dynamische Konfiguration des MultiplayerSpawner + +- Handhabt nicht nur originäre Objekterzeugung in den Levels, sondern auch Objektaktualisierungen für "Late Joiner" +- Generischer MultiplayerSpawner in Basis Level, von dem alle Level erben, so dass die Spawnliste nur 1x bearbeitet werden muss \ No newline at end of file diff --git a/weltenauge/maps/outdoor/grass/keuwin_grasland.tscn b/weltenauge/maps/outdoor/grass/keuwin_grasland.tscn index e32c5bc..0e51265 100644 --- a/weltenauge/maps/outdoor/grass/keuwin_grasland.tscn +++ b/weltenauge/maps/outdoor/grass/keuwin_grasland.tscn @@ -55,3 +55,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.01873529, 0) [node name="Portal" parent="." unique_id=988801188 instance=ExtResource("1_1yg6u")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 0, -6) target_area_key = "ahnenhalle" + +[node name="MultiplayerSpawner" type="MultiplayerSpawner" parent="." unique_id=58641401] +_spawnable_scenes = PackedStringArray("uid://bjurf8vfd1fg8") +spawn_path = NodePath("../SpawnPoints") diff --git a/weltenauge/maps/planes/fugue/halle_der_ahnen.tscn b/weltenauge/maps/planes/fugue/halle_der_ahnen.tscn index f40ceb4..7ae1095 100644 --- a/weltenauge/maps/planes/fugue/halle_der_ahnen.tscn +++ b/weltenauge/maps/planes/fugue/halle_der_ahnen.tscn @@ -53,3 +53,7 @@ shape = SubResource("ConcavePolygonShape3D_0hy1c") [node name="Interaction" type="Node3D" parent="." unique_id=1592705952] [node name="Triggers" type="Node3D" parent="." unique_id=1507691477] + +[node name="MultiplayerSpawner" type="MultiplayerSpawner" parent="." unique_id=1610787579] +_spawnable_scenes = PackedStringArray("uid://bjurf8vfd1fg8") +spawn_path = NodePath("../SpawnPoints") diff --git a/weltenauge/systems/network/network_manager.gd b/weltenauge/systems/network/network_manager.gd index 1218215..486f486 100644 --- a/weltenauge/systems/network/network_manager.gd +++ b/weltenauge/systems/network/network_manager.gd @@ -12,7 +12,7 @@ var area_paths = { "ahnenhalle": "res://maps/planes/fugue/halle_der_ahnen.tscn" } -# Server-seitiges Tracking: peer_id -> SubViewport (World) +# Server-seitiges Tracking: peer_id -> SubViewportContainer var player_worlds = {} func _ready(): @@ -77,17 +77,6 @@ func change_player_area(id: int, area_key: String): else: map_instance = viewport_node.get_node_or_null(area_key) - # Spawner muss vor laden der map hinzugefügt werden - var spawner = viewport_node.get_node_or_null("AreaSpawner") - if not spawner: - spawner = MultiplayerSpawner.new() - spawner.name = "AreaSpawner" - print("Viewport Name: " + str(viewport_node.name)) - # "." bedeutet: Er spawnt Objekte als direkte Kinder des Viewports - spawner.add_spawnable_scene("res://systems/player/player.tscn") - viewport_node.add_child(spawner) - spawner.spawn_path = viewport_node.get_path() - map_instance = load(path).instantiate() map_instance.name = area_key viewport_node.add_child(map_instance) @@ -97,8 +86,7 @@ func change_player_area(id: int, area_key: String): player_worlds.erase(id) # --- NEUEN ZUSTAND REGISTRIEREN --- - player_worlds[id] = viewport_node - + player_worlds[id] = container_node # Client anweisen, Szene zu laden rpc_id(id, "client_load_area", area_key) @@ -106,14 +94,21 @@ func change_player_area(id: int, area_key: String): # Avatar auf Server erstellen erfolgt erst wenn Client Vollzug meldet (client_ready_to_spawn) @rpc("any_peer", "call_local", "reliable") -func client_ready_to_spawn(): +func client_ready_to_spawn(area_key:String): var id = multiplayer.get_remote_sender_id() if not multiplayer.is_server(): return if player_worlds.has(id): - var viewport_node = player_worlds[id] - print("Client ", id, " ist bereit. Spawne jetzt.") - _spawn_player_in_world(id, viewport_node) + var container_node = player_worlds[id] + var viewport_node = container_node.get_node(area_key+"_viewport") + var spawn_target_node = viewport_node.get_node(area_key).get_node("SpawnPoints") + + print("Container Node:", container_node) + print("Viewport Node:", viewport_node) + print("spawn target Node:", spawn_target_node) + + print("Client ", id, " bereits für Spawning.") + _spawn_player_in_world(id, spawn_target_node) @rpc("authority", "call_remote", "reliable") @@ -139,28 +134,22 @@ func client_load_area(area_key): viewport_node.size = Vector2i(1280,720) container_node.add_child(viewport_node) - # Spawner muss existieren bevor die map zum Viewport hinzugefügt wird - var spawner = MultiplayerSpawner.new() - spawner.name = "AreaSpawner" - spawner.add_spawnable_scene("res://systems/player/player.tscn") - viewport_node.add_child(spawner) - spawner.spawn_path = viewport_node.get_path() - var map = load(area_paths[area_key]).instantiate() map.name = area_key viewport_node.add_child(map) - # Kamera-Setup nur beim ersten Laden oder falls sie gelöscht wurde - if not multiplayer.is_server() and not is_instance_valid(local_camera): + + if not multiplayer.is_server(): #and not is_instance_valid(local_camera): print("Setting up camera") setup_client_camera(viewport_node) # Jetzt können wir dem Server ok fürs Spawnen geben - client_ready_to_spawn.rpc_id(1) # Server ist immer id=1 + client_ready_to_spawn.rpc_id(1, area_key) # Server ist immer id=1 func setup_client_camera(node): local_camera = Camera3D.new() + local_camera.name = "Camera3D" local_camera.set_script(load("res://systems/player/camera_logic.gd")) local_camera.current = true node.add_child(local_camera) @@ -168,14 +157,14 @@ func setup_client_camera(node): local_camera.add_child(AudioListener3D.new()) -func _spawn_player_in_world(id, viewport_node): +func _spawn_player_in_world(id, spawn_target_node): var player_scene = load("res://systems/player/player.tscn") var player = player_scene.instantiate() player.name = str(id) #player.set_multiplayer_authority(id) player.set_multiplayer_authority(1) - viewport_node.add_child(player, true) + spawn_target_node.add_child(player, true) # SCHRITT 2: Explizit auf den nächsten Frame warten. # Dies gibt Godot die Zeit, die _ready() Funktion des Spielers auszuführen. @@ -186,7 +175,7 @@ func _spawn_player_in_world(id, viewport_node): var sync = player.get_node("MultiplayerSynchronizer") # Visibility Filtering: Nur Spieler im gleichen Viewport sehen sich - for other in viewport_node.get_children(): + for other in spawn_target_node.get_children(): if other.is_in_group("players"): var other_id = other.name.to_int() # Der neue Spieler sieht den alten @@ -218,11 +207,20 @@ func handle_area_change_request(area_key: String): print("Server: Player ", id, " requests area change to ", area_key) - # 1. Alten Avatar finden und entfernen - var old_viewport = player_worlds[id] - var player_node = old_viewport.find_child(str(id), true) + # 1. Alten Spieler finden und entfernen + var old_container = player_worlds[id] + print("container name=", old_container) + var old_viewport = old_container.get_child(0) + print("Viewport name=", old_viewport) + var player_node = old_viewport.get_child(0).get_node("SpawnPoints").get_node(str(id)) + if player_node: player_node.queue_free() + else: + print("Old viewport:", old_viewport) + + print("Player for erasing not found:", id) + # 2. Bestehende Funktion für den Wechsel aufrufen change_player_area(id, area_key)