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
- **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

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")]
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")

View file

@ -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")

View file

@ -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)