diff --git a/weltenauge/PROJEKT_UEBERSICHT.md b/weltenauge/PROJEKT_UEBERSICHT.md index 9213070..8ca84a6 100644 --- a/weltenauge/PROJEKT_UEBERSICHT.md +++ b/weltenauge/PROJEKT_UEBERSICHT.md @@ -1,7 +1,3 @@ -# Projektübersicht: weltenauge - -Dieses Dokument dient als zentrale Wissensbasis für das Godot-Projekt "weltenauge". Es soll dem KI-Assistenten und den Entwicklern helfen, den Überblick über die Architektur und die Ziele des Projekts zu behalten. - ## 1. Projektziel & Vision "weltenauge" ist ein von Neverwinter Nights (NWN) inspiriertes Multiplayer-Rollenspiel. Das Kernstück ist eine robuste Client/Server-Architektur, die in einer einzigen Godot-Codebase umgesetzt wird. @@ -12,8 +8,8 @@ Dieses Dokument dient als zentrale Wissensbasis für das Godot-Projekt "weltenau - **Eine Codebase:** Client und Server sind Teil desselben Godot-Projekts. - **Start-Logik:** - - Im normalen Modus startet das Spiel mit einem Menü, das "Host" und "Beitreten" anbietet. - - Im Headless-Modus (`--headless`) startet das Programm automatisch als dedizierter Server (Host). + - Im normalen Modus startet das Spiel mit einem Menü, das "Host" und "Beitreten" anbietet. + - Im Headless-Modus (`--headless`) startet das Programm automatisch als dedizierter Server (Host). - **Verantwortlichkeiten:** Der Server (Host) ist die Autorität für den Spielzustand. Clients empfangen und visualisieren den Zustand. ### Dynamisches Area-Management @@ -83,17 +79,17 @@ Dieser Abschnitt beschreibt die Verantwortlichkeiten der zentralen Skripte im Pr ### `/systems/network/network_manager.gd` (Autoload: `NetworkManager`) - **Rolle:** Das Herzstück der gesamten Anwendung. - **Verantwortlichkeit:** - - **Startlogik:** Prüft in `_ready()` selbst, ob das Spiel headless gestartet wurde und ruft dann `host_game()` auf. - - **Verbindungs-Management:** Startet den Server (`host_game`) oder verbindet als Client (`join_game`). - - **Area-Management:** Lädt und verwaltet die Spielwelten (Areas). Erstellt für jede Area auf dem Server einen eigenen `SubViewport` mit `own_world_3d = true`, um die Physikwelten zu trennen. - - **Spieler-Spawning:** Koordiniert den komplexen Prozess des Spieler-Spawns. Weist den Client an, eine Szene zu laden (`client_load_area`), wartet auf dessen Bereitschaftsmeldung (`client_ready_to_spawn`) und instanziiert dann den Spieler-Avatar serverseitig in der korrekten Sub-Welt. + - **Startlogik:** Prüft in `_ready()` selbst, ob das Spiel headless gestartet wurde und ruft dann `host_game()` auf. + - **Verbindungs-Management:** Startet den Server (`host_game`) oder verbindet als Client (`join_game`). + - **Area-Management:** Lädt und verwaltet die Spielwelten (Areas). Erstellt für jede Area auf dem Server einen eigenen `SubViewport` mit `own_world_3d = true`, um die Physikwelten zu trennen. + - **Spieler-Spawning:** Koordiniert den komplexen Prozess des Spieler-Spawns. Weist den Client an, eine Szene zu laden (`client_load_area`), wartet auf dessen Bereitschaftsmeldung (`client_ready_to_spawn`) und instanziiert dann den Spieler-Avatar serverseitig in der korrekten Sub-Welt. ### `/systems/player/player_controller.gd` - **Rolle:** Steuerungslogik für den Spieler-Avatar. - **Verantwortlichkeit:** Dieses Skript läuft auf dem Server und allen Clients, verhält sich aber je nach Autorität unterschiedlich: - - **Client (mit Autorität):** Fängt Mauseklicks ab (`_input`), berechnet die Zielposition auf dem Boden und sendet einen `request_move` RPC an den Server. - - **Server:** Empfängt `request_move`, berechnet den Pfad (aktuell nur direktes Ziel) und bewegt den `CharacterBody3D` mittels `move_and_slide()` im `_physics_process`. Die resultierende Position und Rotation werden in `sync_position` und `sync_rotation` gespeichert. - - **Client (ohne Autorität):** Liest im `_process` kontinuierlich `sync_position` und `sync_rotation` aus und setzt die `global_position` und `rotation` des Avatars hart auf diese Werte, um ihn mit dem Server synchron zu halten. + - **Client (mit Autorität):** Fängt Mauseklicks ab (`_input`), berechnet die Zielposition auf dem Boden und sendet einen `request_move` RPC an den Server. + - **Server:** Empfängt `request_move`, berechnet den Pfad (aktuell nur direktes Ziel) und bewegt den `CharacterBody3D` mittels `move_and_slide()` im `_physics_process`. Die resultierende Position und Rotation werden in `sync_position` und `sync_rotation` gespeichert. + - **Client (ohne Autorität):** Liest im `_process` kontinuierlich `sync_position` und `sync_rotation` aus und setzt die `global_position` und `rotation` des Avatars hart auf diese Werte, um ihn mit dem Server synchron zu halten. ### `/systems/player/camera_logic.gd` - **Rolle:** Kamera-Steuerung. @@ -116,19 +112,18 @@ Die Konfiguration des `MultiplayerSynchronizer` für wiederverwendbare Szenen (w 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")` + - **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) - # ... - ``` - + ```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/maps/outdoor/grass/keuwin_grasland.tscn b/weltenauge/maps/outdoor/grass/keuwin_grasland.tscn index 100017f..e32c5bc 100644 --- a/weltenauge/maps/outdoor/grass/keuwin_grasland.tscn +++ b/weltenauge/maps/outdoor/grass/keuwin_grasland.tscn @@ -18,6 +18,9 @@ albedo_color = Color(9.3854965e-07, 0.4579985, 0.32070312, 1) [sub_resource type="PlaneMesh" id="PlaneMesh_0iae2"] material = SubResource("StandardMaterial3D_30klc") +[sub_resource type="ConcavePolygonShape3D" id="ConcavePolygonShape3D_1yg6u"] +data = PackedVector3Array(1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, 1, -1, 0, -1, 1, 0, -1) + [node name="keuwin_grasland" type="Node3D" unique_id=1953380732] [node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1262732162] @@ -35,6 +38,11 @@ environment = SubResource("Environment_8r0t0") transform = Transform3D(10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0) mesh = SubResource("PlaneMesh_0iae2") +[node name="StaticBody3D" type="StaticBody3D" parent="Architecture/MeshInstance3D" unique_id=209028061] + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Architecture/MeshInstance3D/StaticBody3D" unique_id=1119853048] +shape = SubResource("ConcavePolygonShape3D_1yg6u") + [node name="Navigation" type="Node3D" parent="." unique_id=1472582667] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.01873529, 0) diff --git a/weltenauge/maps/planes/fugue/halle_der_ahnen.tscn b/weltenauge/maps/planes/fugue/halle_der_ahnen.tscn index 8542db9..f40ceb4 100644 --- a/weltenauge/maps/planes/fugue/halle_der_ahnen.tscn +++ b/weltenauge/maps/planes/fugue/halle_der_ahnen.tscn @@ -21,6 +21,9 @@ albedo_color = Color(0.1800012, 0.004964735, 0.2305904, 1) [sub_resource type="PlaneMesh" id="PlaneMesh_0iae2"] material = SubResource("StandardMaterial3D_30klc") +[sub_resource type="ConcavePolygonShape3D" id="ConcavePolygonShape3D_0hy1c"] +data = PackedVector3Array(1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, 1, -1, 0, -1, 1, 0, -1) + [node name="halle_der_ahnen" type="Node3D" unique_id=1953380732] [node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1262732162] @@ -38,6 +41,11 @@ environment = SubResource("Environment_8r0t0") transform = Transform3D(10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0) mesh = SubResource("PlaneMesh_0iae2") +[node name="StaticBody3D" type="StaticBody3D" parent="Architecture/MeshInstance3D" unique_id=1395982254] + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Architecture/MeshInstance3D/StaticBody3D" unique_id=521000493] +shape = SubResource("ConcavePolygonShape3D_0hy1c") + [node name="Navigation" type="Node3D" parent="." unique_id=1472582667] [node name="SpawnPoints" type="Node3D" parent="." unique_id=438053421] diff --git a/weltenauge/systems/network/network_manager.gd b/weltenauge/systems/network/network_manager.gd index c51914c..1218215 100644 --- a/weltenauge/systems/network/network_manager.gd +++ b/weltenauge/systems/network/network_manager.gd @@ -54,7 +54,7 @@ func _on_player_disconnected(id): func change_player_area(id: int, area_key: String): if not multiplayer.is_server(): return print("Server is loading ", area_key) - var path = area_paths[area_key + var path = area_paths[area_key] var container_name = area_key + "_container" var viewport_name = area_key + "_viewport" @@ -111,7 +111,7 @@ func client_ready_to_spawn(): if not multiplayer.is_server(): return if player_worlds.has(id): - var viewport_node = player_worlds[id + var viewport_node = player_worlds[id] print("Client ", id, " ist bereit. Spawne jetzt.") _spawn_player_in_world(id, viewport_node) @@ -152,7 +152,8 @@ func client_load_area(area_key): # Kamera-Setup nur beim ersten Laden oder falls sie gelöscht wurde - if not multiplayer.is_server() and local_camera == null: + 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 @@ -225,11 +226,3 @@ func handle_area_change_request(area_key: String): # 2. Bestehende Funktion für den Wechsel aufrufen change_player_area(id, area_key) - -## The following script is for a Portal node. -## File: portal.gd -extends Area3D - -## Definiert, wohin dieses Portal führt. -## Dieser Wert muss einem Schlüssel in NetworkManager.area_paths entsprechen. -@export var target_area_key: String = "" diff --git a/weltenauge/systems/player/player_controller.gd b/weltenauge/systems/player/player_controller.gd index 3e06b38..bae3ec7 100644 --- a/weltenauge/systems/player/player_controller.gd +++ b/weltenauge/systems/player/player_controller.gd @@ -76,53 +76,53 @@ func request_move(target: Vector3): path = [target] target_position = target +# ERSETZT die alte _input(event)-Funktion func _input(event): # Ist dieser Avatar nicht der des lokalen Spielers? Wenn ja, sofort abbrechen. if name.to_int() != multiplayer.get_unique_id(): return - - # NUR der Client, der diesen Spieler kontrolliert, darf klicken - #if not is_multiplayer_authority(): return if DisplayServer.get_name() == "headless": return # Server ignoriert Klicks - if multiplayer.is_server(): return # Kein Input vom Server - if event is InputEventMouseButton and event.pressed: - print("Click") - var target = _get_click_floor_pos() - if target != Vector3.INF: - print("Request move to target {0} by user {1}".format([target, name])) - # RPC an den Server (ID 1) - request_move.rpc_id(1, target) + # Wir reagieren nur auf den Klick der linken Maustaste + if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: + var click_result = _get_click_result() + + if click_result: # Prüfen, ob der Strahl überhaupt etwas getroffen hat + var collider = click_result.collider + + # PRIORITÄT 1: Haben wir ein Portal angeklickt? + # Wir prüfen, ob das getroffene Objekt gültig ist und zur Gruppe "portals" gehört. + if is_instance_valid(collider) and collider.is_in_group("portals"): + print("Portal angeklickt. Fordere Area-Wechsel an zu: ", collider.target_area_key) + # Sende RPC direkt an den NetworkManager mit dem Ziel des Portals + NetworkManager.handle_area_change_request.rpc_id(1, collider.target_area_key) + return # Wichtig: Aktion ist beendet, nichts weiter tun. + + # PRIORITÄT 2 (Fallback): Wir haben etwas anderes getroffen (vermutlich den Boden), also bewegen. + var target_position = click_result.position + request_move.rpc_id(1, target_position) -func _get_click_floor_pos(): - # Wir brauchen die Kamera, die tatsächlich für diesen Viewport zuständig ist + +# ERSETZT die alte _get_click_floor_pos()-Funktion +func _get_click_result(): var camera = get_viewport().get_camera_3d() - - # DEBUG: Falls keine Kamera gefunden wird, wissen wir woran es liegt - if not camera: - print("Keine Kamera im Viewport von ", name, " gefunden!") - return Vector3.INF + if not camera: return null var mouse_pos = get_viewport().get_mouse_position() + var space_state = get_world_3d().direct_space_state var ray_origin = camera.project_ray_origin(mouse_pos) var ray_dir = camera.project_ray_normal(mouse_pos) - - # Da wir in einer isolierten World3D sind, müssen wir den Physics-State - # des Viewports direkt abfragen, statt nur gegen eine Plane zu prüfen: - var space_state = get_world_3d().direct_space_state + var query = PhysicsRayQueryParameters3D.create(ray_origin, ray_origin + ray_dir * 1000) - - # Falls du Layer nutzt: query.collision_mask = 1 + # WICHTIG: Schließe den eigenen Körper von der Abfrage aus, um nicht auf sich selbst zu klicken. + query.exclude = [self] + + # NEU: Sage dem Raycast, dass er auch mit Area3D-Nodes kollidieren soll. + query.collide_with_areas = true + var result = space_state.intersect_ray(query) - - if result: - return result.position - - # Fallback auf Plane, falls kein Mesh getroffen wurde - var world_plane = Plane(Vector3.UP, 0) - var intersection = world_plane.intersects_ray(ray_origin, ray_dir) - return intersection if intersection else Vector3.INF + return result # Gibt das komplette Ergebnis-Dictionary zurück func _process(_delta):