First working area switch

This commit is contained in:
torsten 2026-02-15 18:09:48 +01:00
parent 1eeca653f5
commit e7ebb20428
4 changed files with 53 additions and 46 deletions

View file

@ -55,6 +55,11 @@ Alle Skripte und die dazugehörige Logik sollen thematisch in Unterordnern unter
## 5. Implementierungsdetails & Konventionen ## 5. Implementierungsdetails & Konventionen
- **Click-to-Move:** Die Spielerbewegung soll aus Performancegründen über eine AStar-Implementierung (`systems/navigation/`) gesteuert werden. - **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) ## 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. 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 ### 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: 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")` - **Korrekt:** `config.add_property(":sync_position")`
- **Falsch:** `config.add_property("sync_position")` oder `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. ### Dynamische Konfiguration des MultiplayerSpawner
```gdscript
# Im NetworkManager - Handhabt nicht nur originäre Objekterzeugung in den Levels, sondern auch Objektaktualisierungen für "Late Joiner"
async func _spawn_player_in_world(id, viewport_node): - Generischer MultiplayerSpawner in Basis Level, von dem alle Level erben, so dass die Spawnliste nur 1x bearbeitet werden muss
# ...
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

@ -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")] [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) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 0, -6)
target_area_key = "ahnenhalle" target_area_key = "ahnenhalle"
[node name="MultiplayerSpawner" type="MultiplayerSpawner" parent="." unique_id=58641401]
_spawnable_scenes = PackedStringArray("uid://bjurf8vfd1fg8")
spawn_path = NodePath("../SpawnPoints")

View file

@ -53,3 +53,7 @@ shape = SubResource("ConcavePolygonShape3D_0hy1c")
[node name="Interaction" type="Node3D" parent="." unique_id=1592705952] [node name="Interaction" type="Node3D" parent="." unique_id=1592705952]
[node name="Triggers" type="Node3D" parent="." unique_id=1507691477] [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")

View file

@ -12,7 +12,7 @@ var area_paths = {
"ahnenhalle": "res://maps/planes/fugue/halle_der_ahnen.tscn" "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 = {} var player_worlds = {}
func _ready(): func _ready():
@ -77,17 +77,6 @@ func change_player_area(id: int, area_key: String):
else: else:
map_instance = viewport_node.get_node_or_null(area_key) 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 = load(path).instantiate()
map_instance.name = area_key map_instance.name = area_key
viewport_node.add_child(map_instance) viewport_node.add_child(map_instance)
@ -97,8 +86,7 @@ func change_player_area(id: int, area_key: String):
player_worlds.erase(id) player_worlds.erase(id)
# --- NEUEN ZUSTAND REGISTRIEREN --- # --- NEUEN ZUSTAND REGISTRIEREN ---
player_worlds[id] = viewport_node player_worlds[id] = container_node
# Client anweisen, Szene zu laden # Client anweisen, Szene zu laden
rpc_id(id, "client_load_area", area_key) 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) # Avatar auf Server erstellen erfolgt erst wenn Client Vollzug meldet (client_ready_to_spawn)
@rpc("any_peer", "call_local", "reliable") @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() var id = multiplayer.get_remote_sender_id()
if not multiplayer.is_server(): return if not multiplayer.is_server(): return
if player_worlds.has(id): if player_worlds.has(id):
var viewport_node = player_worlds[id] var container_node = player_worlds[id]
print("Client ", id, " ist bereit. Spawne jetzt.") var viewport_node = container_node.get_node(area_key+"_viewport")
_spawn_player_in_world(id, viewport_node) 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") @rpc("authority", "call_remote", "reliable")
@ -139,28 +134,22 @@ func client_load_area(area_key):
viewport_node.size = Vector2i(1280,720) viewport_node.size = Vector2i(1280,720)
container_node.add_child(viewport_node) 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() var map = load(area_paths[area_key]).instantiate()
map.name = area_key map.name = area_key
viewport_node.add_child(map) viewport_node.add_child(map)
# Kamera-Setup nur beim ersten Laden oder falls sie gelöscht wurde # 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") print("Setting up camera")
setup_client_camera(viewport_node) setup_client_camera(viewport_node)
# Jetzt können wir dem Server ok fürs Spawnen geben # 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): func setup_client_camera(node):
local_camera = Camera3D.new() local_camera = Camera3D.new()
local_camera.name = "Camera3D"
local_camera.set_script(load("res://systems/player/camera_logic.gd")) local_camera.set_script(load("res://systems/player/camera_logic.gd"))
local_camera.current = true local_camera.current = true
node.add_child(local_camera) node.add_child(local_camera)
@ -168,14 +157,14 @@ func setup_client_camera(node):
local_camera.add_child(AudioListener3D.new()) 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_scene = load("res://systems/player/player.tscn")
var player = player_scene.instantiate() var player = player_scene.instantiate()
player.name = str(id) player.name = str(id)
#player.set_multiplayer_authority(id) #player.set_multiplayer_authority(id)
player.set_multiplayer_authority(1) 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. # SCHRITT 2: Explizit auf den nächsten Frame warten.
# Dies gibt Godot die Zeit, die _ready() Funktion des Spielers auszuführen. # 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") var sync = player.get_node("MultiplayerSynchronizer")
# Visibility Filtering: Nur Spieler im gleichen Viewport sehen sich # 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"): 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 # 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) print("Server: Player ", id, " requests area change to ", area_key)
# 1. Alten Avatar finden und entfernen # 1. Alten Spieler finden und entfernen
var old_viewport = player_worlds[id] var old_container = player_worlds[id]
var player_node = old_viewport.find_child(str(id), true) 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: if player_node:
player_node.queue_free() 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 # 2. Bestehende Funktion für den Wechsel aufrufen
change_player_area(id, area_key) change_player_area(id, area_key)