Collisions for Area Change Trigger
This commit is contained in:
parent
721f005ff3
commit
1eeca653f5
5 changed files with 74 additions and 70 deletions
|
|
@ -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)
|
||||
# ...
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue