weltenauge/weltenauge/PROJEKT_UEBERSICHT.md

9.4 KiB

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.

2. Kernkonzepte der Architektur

Client/Server-Modell

  • 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).
  • Verantwortlichkeiten: Der Server (Host) ist die Autorität für den Spielzustand. Clients empfangen und visualisieren den Zustand.

Dynamisches Area-Management

  • Serverseitig: Der Server lädt alle von Spielern bewohnten Areas (Szenen) gleichzeitig.
  • Clientseitig: Ein Client lädt nur die eine Area, in der sich sein Charakter gerade befindet. Dies spart Ressourcen auf den Client-Rechnern.
  • Area-Wechsel: Ein Wechsel wird immer vom Server initiiert. Der Server lädt die neue Szene für den Spieler, platziert dessen Avatar und sendet ein Signal an den Client. Der Client lädt daraufhin ebenfalls die neue Szene.

Isolierte Welten auf dem Server

  • Um Physik und Szenen-Bäume sauber zu trennen, wird jede Area auf dem Server in einen eigenen SubViewport geladen.
  • Dies ermöglicht "Multi-World Physics", sodass sich Objekte aus verschiedenen Areas auf dem Server nicht gegenseitig beeinflussen können.

Netzwerk-Synchronisation

  • Visibility Filtering: MultiplayerSynchronizer und MultiplayerSpawner werden mit Visibility Filtern versehen. Dadurch wird sichergestellt, dass ein Client nur Updates für die Objekte in seiner eigenen Area erhält.
  • Positions-Synchronisation: Es wird explizit die position-Eigenschaft von Nodes synchronisiert, nicht global_position, um Konflikte mit der SubViewport-Struktur zu vermeiden.

3. Geplante Ordnerstruktur

Alle Skripte und die dazugehörige Logik sollen thematisch in Unterordnern unter res://systems/ abgelegt werden:

  • /core/: Haupt-Game-Loop, State-Machine (Laden, Menü, Spiel).
  • /network/: Peer-Verbindung, Headless-Logik, Synchronisation, RPCs.
  • /player/: Input-Handling, Kamera-Steuerung, lokale Charakter-Steuerung.
  • /ai/: NPC-Verhaltenslogik (z.B. Behavior Trees).
  • /combat/: Kampfsystem, Trefferberechnung, Würfelwürfe.
  • /dialogue/: Gesprächs-System.
  • /inventory/: Item-Management, Ausrüstung, Truhen.
  • /interaction/: Interaktionslogik für Türen, Hebel, Portale.
  • /navigation/: A-Stern (AStar) Pfadfindungs-Logik.
  • /data/: Speicher-Logik, Daten-Wrapper (z.B. für JSON, SQLite).
  • /ui/: (Existierender Ordner) UI-Szenen und Skripte.

4. Wichtige Dateien & Szenen

  • Spieler-Modell: res://systems/player/player.tres
  • Area 1: res://maps/outdoor/grass/keuwin_grasland.tscn
  • Area 2: res://maps/planes/fugue/halle_der_ahnen.tscn
  • Startpunkt: main.tscn (vermutlich für das Hauptmenü)
  • Haupt-Logik: game_root.gd / game_root.tscn (vermutlich die Wurzel des aktiven Spiels)

5. Implementierungsdetails & Konventionen

  • Click-to-Move: Die Spielerbewegung soll aus Performancegründen über eine AStar-Implementierung (systems/navigation/) gesteuert werden.

6. Globale Skripte (Autoloads)

Die folgenden Skripte sind als Singletons (Autoloads in Godot) konfiguriert und global verfügbar:

  • GameEvents (res://systems/core/game_events.gd): Zentraler Event-Bus für die Kommunikation zwischen entkoppelten Systemen.
  • NetworkManager (res://systems/network/network_manager.gd): Verwaltet die gesamte Netzwerklogik, wie das Hosten, Beitreten und die RPC-Kommunikation.
  • ChatManager (res://systems/network/chat_manager.gd): Kümmert sich um die Logik des In-Game-Chats.

7. Analyse der existierenden Skripte (Stand: Commit 30b6ade)

Dieser Abschnitt beschreibt die Verantwortlichkeiten der zentralen Skripte im Projekt.

/game_root.gd

  • Rolle: Absoluter Startpunkt des Spiels.
  • Verantwortlichkeit: Hält die Instanz des Hauptmenüs (main.tscn). Seine einzige Aufgabe ist es, zu existieren und die initiale Szene zu laden. Die eigentliche Logik wird sofort an die globalen Manager und das UI delegiert.

/systems/core/game_events.gd (Autoload: GameEvents)

  • Rolle: Globaler Event-Bus.
  • Verantwortlichkeit: Dient als zentrale Sammelstelle für Signale, um verschiedene Systeme voneinander zu entkoppeln. Zum Beispiel sendet das UI ein host_requested Signal, auf das der NetworkManager hört, ohne dass die beiden sich direkt kennen müssen.

/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.

/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.

/systems/player/camera_logic.gd

  • Rolle: Kamera-Steuerung.
  • Verantwortlichkeit: Ein einfaches Skript, das an einer Camera3D hängt. Es verfolgt sanft (lerp) ein target (den Spieler-Avatar) und behält dabei einen festen offset bei, um eine klassische Top-Down-Perspektive zu erzeugen. Die Zuweisung des Ziels erfolgt durch den NetworkManager über den assign_camera_target RPC.

/main.tscn

  • Rolle: Hauptmenü-UI.
  • Verantwortlichkeit: Enthält die UI-Elemente wie "Host" und "Join" Buttons. Es wird angenommen, dass die pressed() Signale dieser Buttons direkt im Godot-Editor mit den entsprechenden Signalen im GameEvents-Singleton (host_requested, join_requested) verbunden sind.

8. Wichtige Erkenntnisse & Best Practices

Dieser Abschnitt dokumentiert wichtige technische Lektionen, die während der Entwicklung gelernt wurden.

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:

  1. Konfiguration nur per Code: Die Konfiguration muss zur Laufzeit im Code erfolgen, nicht im Editor. Der Editor speichert absolute Pfade, die in einem dynamischen Szenen-Setup (mehrere Welten/Viewports) unbrauchbar sind.

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

    # 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)
        # ...