# Prozeduraler Dungeon Generator – Komplette Neuanleitung v4.2 ## Überblick Snap-Together Dungeon Generator für Unreal Engine 5.7 mit Blueprints. Vorgefertigte Räume docken an Arrow-Component-Sockets aneinander an. 5–100 Level pro Dungeon, Hauptpfad + optionale Abzweigungen (Branches). **Ablauf:** Generate Dungeon → Level für Level bauen (Entrance/PortalIn → MainPath → PortalOut/Exit) → nach Exit: Branches auf allen Leveln bauen → offene Sockets versiegeln → Spieler platzieren → Fertig. ### Änderungen gegenüber v4 (final) 1. **Runtime-State zentral im Generator** – unverändert übernommen. 2. **Stabile Socket-IDs** – unverändert übernommen. 3. **LevelVerticalOffset als Variable** – unverändert übernommen. 4. **NEU: Detaillierte Update-Logik für AllTrackedSockets** – Jede Stelle, an der ein Socket-State geändert wird, hat jetzt eine Schritt-für-Schritt Anleitung mit Find-by-Predicate + Set Array Elem. 5. **NEU: BranchFrontier-Kopie** – BuildBranches iteriert über eine Kopie, nicht das Original, um Array-Mutation während Iteration zu vermeiden. 6. **NEU: Socket-Auswahl beim Startraum** – Konkrete Logik, welcher Socket auf MainPath geht. 7. **NEU: Retry-Logik komplett definiert** – RetryLevel als eigenes Custom Event. 8. **NEU: FindCompatibleRoom Socket-Matching** – Konkrete Kriterien für "nutzbarer Eingangs-Socket". 9. **NEU: RegisterRoomSockets ohne TargetArray-Parameter** – Arbeitet direkt auf den Generator-Variablen. 10. **NEU: SnapRoomToSocket 180°-Rotation erklärt** – Warum Combine Rotators mit 180° nötig ist. ### Bewusst nicht übernommene Vorschläge | Vorschlag | Entscheidung | Begründung | |---|---|---| | BP_DungeonRoomBase als gemeinsame Elternklasse | Nicht übernommen | UE erlaubt keine Viewport-Änderungen in Child Blueprints. Wir nutzen eigenständige Duplikate + Interface, was flexibler ist für unterschiedliche Raum-Geometrien. | | Kollision vor Spawn prüfen (CanPlaceRoom) | Nicht übernommen (vorerst) | Spawn-then-Destroy funktioniert für den Prototyp. Optimierung auf Box Trace kommt später wenn die Grundlogik steht. | | Branches während des MainPath-Baus | Nicht übernommen | Branches nach dem Exit über alle Level ist einfacher zu implementieren und zu debuggen. | | Erweiterte Data Table (Tags, FootprintExtent, CanBeMainPath etc.) | Nicht übernommen (vorerst) | Die einfache Data Table reicht für den Prototyp. | | FindCompatibleRoom mit Score-System | Nicht übernommen (vorerst) | Gewichtete Zufallswahl reicht erstmal. | | E_SocketType (Standard, Large, Stair, Portal) | Nicht übernommen (vorerst) | Alle Räume haben aktuell die gleiche Durchgangsgröße. | | SealOpenSockets als eigener Schritt | Übernommen | DeadEnds schließen Branches ab. Übrige offene Sockets werden mit WallBlockern verschlossen. | --- ## Teil 1: Datenstrukturen ### 1.1 Enums **E_RoomType** (existiert bereits, ggf. erweitern): - Entrance, General, Corridor, PortalIn, PortalOut, Exit, DeadEnd **E_SocketStatus** (existiert bereits): - Open, Occupied, Closed **E_PathType** (NEU erstellen): - MainPath, Branch, None ### 1.2 Structs #### S_SocketDefinition (NEU – ersetzt das alte S_SocketData in den Räumen) Beschreibt einen Socket am Raum. Wird im Construction Script befüllt. Enthält **keinen** Laufzeit-Zustand. | Feld | Typ | Beschreibung | |---|---|---| | SocketId | Name | Eindeutiger Name z.B. "Socket_North", "Socket_South" | | SocketComponent | Arrow Component Ref | Referenz auf die Arrow Component | | LocalTransform | Transform | Lokale Position/Rotation relativ zum Actor | #### S_OpenSocketData (NEU – Runtime-State im Generator) Das ist der wichtigste Struct. Der Generator verwaltet damit alle offenen Verbindungspunkte. Ersetzt das alte S_SocketData für Laufzeit-Zwecke. | Feld | Typ | Beschreibung | |---|---|---| | OwnerRoom | Actor Object Ref | Der Raum dem dieser Socket gehört | | SocketId | Name | Eindeutige Socket-ID (z.B. "Socket_North") | | SocketIndex | Integer | Index in der Arrow-Liste (Fallback) | | PathType | E_PathType | MainPath, Branch oder None | | LevelIndex | Integer | Auf welchem Level liegt dieser Socket | | BranchDepth | Integer | Tiefe der Abzweigung (0 = am MainPath) | | State | E_SocketStatus | Open, Occupied, Closed | **WICHTIG:** WorldTransform wird NICHT im Struct gespeichert! Die Position wird immer live über GetSocketWorldTransformById berechnet. #### S_PlacedRoomData (NEU – Raum-Tracking im Generator) Speichert Informationen über jeden platzierten Raum zentral im Generator. | Feld | Typ | Beschreibung | |---|---|---| | RoomActor | Actor Object Ref | Referenz auf den gespawnten Actor | | RoomType | E_RoomType | Typ des Raums | | PathType | E_PathType | MainPath oder Branch | | LevelIndex | Integer | Auf welchem Level | | BranchDepth | Integer | Abzweigungstiefe (0 = MainPath) | #### S_RoomPoolEntry (existiert bereits, unverändert) | Feld | Typ | Beschreibung | |---|---|---| | RoomClass | Actor Class Ref | Blueprint-Klasse des Raums | | RoomType | E_RoomType | Typ | | Weight | Float | Gewichtung für Zufallswahl | | MinLevel | Integer | Ab welchem Level verfügbar | | MaxLevel | Integer | Bis welchem Level (0 = unbegrenzt) | #### DT_RoomPool erstellen (Data Table im Editor) Die Data Table `DT_RoomPool` ist die zentrale Tabelle, aus der der Generator zur Laufzeit Räume auswählt. Sie basiert auf dem Struct `S_RoomPoolEntry`. **Voraussetzung:** Der Struct `S_RoomPoolEntry` muss bereits existieren (siehe oben). **Schritt 1 – Data Table Asset anlegen:** 1. Im Content Browser: Rechtsklick → **Miscellaneous** → **Data Table** 2. Im Popup **"Pick Row Structure"**: Wähle **S_RoomPoolEntry** als Row Structure 3. Bestätige mit **OK** 4. Benenne das neue Asset: **DT_RoomPool** 5. Speichere es z.B. unter `Content/Data/DT_RoomPool` (oder einem Ordner deiner Wahl) **Schritt 2 – Zeilen hinzufügen:** Öffne DT_RoomPool per Doppelklick. Die Tabelle ist zunächst leer. 1. Klicke oben links auf **Add** (+ Symbol), um eine neue Zeile hinzuzufügen 2. Vergib einen eindeutigen **Row Name** (z.B. "General_01", "Corridor_01", "DeadEnd_01") 3. Befülle die Felder der Zeile: | Feld | Was eintragen | Hinweis | |---|---|---| | RoomClass | Blueprint-Klasse auswählen (z.B. BP_DungeonRoom_General) | Klick auf das Dropdown → Blueprint-Class wählen | | RoomType | Passenden Enum-Wert wählen (z.B. General) | Muss zum Blueprint passen | | Weight | Gewichtung als Float (z.B. 1.0) | Höher = häufiger gewählt | | MinLevel | Ab welchem Level verfügbar (z.B. 1) | 1 = ab dem ersten Level | | MaxLevel | Bis welchem Level (z.B. 0) | 0 = unbegrenzt / auf allen Levels | **Schritt 3 – Empfohlene Einträge für den Prototyp:** | Row Name | RoomClass | RoomType | Weight | MinLevel | MaxLevel | |---|---|---|---|---|---| | General_01 | BP_DungeonRoom_General | General | 1.0 | 1 | 0 | | Corridor_01 | BP_DungeonRoom_Corridor | Corridor | 1.0 | 1 | 0 | | DeadEnd_01 | BP_DungeonRoom_DeadEnd | DeadEnd | 1.0 | 1 | 0 | > **Hinweis:** Entrance, PortalIn, PortalOut und Exit werden **nicht** in die Data Table eingetragen – diese Räume spawnt der Generator direkt per Klasse (hartcodiert in BuildLevel / PlaceEndRoom). Nur Räume, die per Zufallswahl platziert werden (General, Corridor, DeadEnd), gehören hier rein. > **Tipp:** Du kannst später weitere Zeilen hinzufügen (z.B. General_02, General_03 mit verschiedenen Gewichtungen oder Level-Bereichen), um mehr Vielfalt zu erzeugen. **Schritt 4 – Im Generator referenzieren:** Im BP_DungeonGenerator: Die Variable `RoomPoolTable` (Typ: Data Table Reference) auf **DT_RoomPool** setzen. Das geschieht im Details Panel unter dem Default Value: 1. Wähle die Variable `RoomPoolTable` aus 2. Im Details Panel → Default Value → Dropdown → wähle **DT_RoomPool** ### 1.3 Was wird gelöscht/ersetzt - **S_SocketData** bleibt in den Räumen, wird aber auf **S_SocketDefinition** reduziert (nur noch statische Daten) - Felder die aus S_SocketData entfernt werden: Status, IsOnMainPath, OwnerRoom, WorldLocation, WorldRotation - Der gesamte Laufzeit-Zustand wandert in **S_OpenSocketData** und **S_PlacedRoomData** im Generator --- ## Teil 2: Stabile Socket-IDs einführen ### 2.1 Arrow Components benennen In **jedem** Raum-Blueprint: Benenne die Arrow Components eindeutig. Beispiel für einen 4-Socket-Raum: - Arrow Component 1 → Name: **Socket_North** - Arrow Component 2 → Name: **Socket_East** - Arrow Component 3 → Name: **Socket_South** - Arrow Component 4 → Name: **Socket_West** Beispiel für DeadEnd (1 Socket): - Arrow Component 1 → Name: **Socket_Entry** Beispiel für Corridor (2 Sockets): - Arrow Component 1 → Name: **Socket_Entry** - Arrow Component 2 → Name: **Socket_Exit** Die Namen müssen innerhalb eines Raums eindeutig sein, dürfen aber zwischen verschiedenen Raumtypen gleich heißen. ### 2.2 Variablen pro Raum-Blueprint Jeder Raum-Blueprint braucht nur die wirklich nötigen Variablen. Der Laufzeit-State liegt **nicht** im Raum, sondern im Generator. **Pflicht-Variablen in jedem Raum:** | Variable | Typ | Default | Zweck | |---|---|---|---| | RoomType | E_RoomType | je nach Raum | Gibt den Typ des Raums zurück | | SocketDefinitions | Array of S_SocketDefinition | leer | Wird im Construction Script aus den Arrow Components befüllt | **Optional, nur wenn du sie wirklich brauchst:** | Variable | Typ | Default | Zweck | |---|---|---|---| | RoomMesh / zusätzliche Mesh-Referenzen | Static Mesh Component Ref | – | Nur falls du im Graph direkt darauf zugreifen willst | | BoundingBox | Box Collision Ref | – | Für Overlap/Kollisionsprüfung | | DebugName | Name oder String | leer | Optional für Prints und Debugging | **Nicht mehr im Raum speichern:** - ~~Sockets~~ als `Array` - ~~ConnectedRooms~~ - ~~IsOnMainPath~~ - ~~BranchDepth~~ - ~~SocketStatus / Occupied / Closed~~ - ~~OwnerRoom~~ - ~~WorldLocation / WorldRotation als gespeicherte Runtime-Werte~~ **RoomType-Defaults je Raum:** | Blueprint | RoomType Default | |---|---| | BP_DungeonRoom_Entrance | Entrance | | BP_DungeonRoom_General | General | | BP_DungeonRoom_Corridor | Corridor | | BP_DungeonRoom_PortalIn | PortalIn | | BP_DungeonRoom_PortalOut | PortalOut | | BP_DungeonRoom_Exit | Exit | | BP_DungeonRoom_DeadEnd | DeadEnd | ### 2.3 Construction Script anpassen (alle Räume) Das Construction Script befüllt jetzt **S_SocketDefinition** statt S_SocketData: 1. **Construction Script ▶** → **Clear** `SocketDefinitions` 2. → **Get Components by Class** (Arrow Component, Target: self) 3. → **ForEachLoop** (Array: Return Value) 4. **Loop Body ▶** → **Make S_SocketDefinition**: - SocketId: Array Element → **Get Display Name** (oder **Get Object Name**) → **Convert to Name** (z.B. "Socket_North") - SocketComponent: Array Element (die Arrow Component Referenz selbst) - LocalTransform: Array Element → **Get Relative Transform** 5. → **Add** zu SocketDefinitions Array (Variable im Raum, Typ: Array of S_SocketDefinition) 6. ForEachLoop **Completed ▶** → (Ende) **Blueprint-Nodes Schritt für Schritt:** ``` Construction Script ▶ → Clear (SocketDefinitions) → Get Components by Class (Arrow Component) → ForEachLoop Loop Body ▶ → Make S_SocketDefinition ├─ SocketId: Array Element → Get Object Name → To Name ├─ SocketComponent: Array Element └─ LocalTransform: Array Element → Get Relative Transform → Add (SocketDefinitions, S_SocketDefinition) Completed ▶ → (Ende) ``` ### 2.4 GetSocketWorldTransformById implementieren Die Interface-Funktion sucht einen Socket anhand seiner ID und gibt die aktuelle Weltposition zurück: **GetSocketWorldTransformById** (NEU im Interface): - Input: SocketId (Name) - Output: Location (Vector), Rotation (Rotator), Success (Bool) Implementierung in jedem Raum: 1. **SocketDefinitions** Array → **ForEachLoop** 2. → **Branch**: Array Element → Break S_SocketDefinition → SocketId → **Equal (Name)** → Input SocketId 3. → True ▶: Array Element → Break S_SocketDefinition → SocketComponent → **GetWorldLocation** → Set Location 4. → Array Element → Break S_SocketDefinition → SocketComponent → **GetWorldRotation** → Set Rotation 5. → **Return** (Location, Rotation, Success: true) 6. → ForEachLoop Completed ▶: **Return** (Location: 0,0,0, Rotation: 0,0,0, Success: **false**) **WICHTIG:** Wir holen Location/Rotation direkt von der **Arrow Component Referenz** (SocketComponent), nicht aus dem gespeicherten LocalTransform. So bekommen wir immer die aktuelle Weltposition, auch wenn der Actor bewegt wurde. ### 2.5 BPI_DungeonRoom Interface aktualisieren | Funktion | Inputs | Outputs | |---|---|---| | GetSocketDefinitions | – | SocketDefinitions (Array S_SocketDefinition) | | GetSocketWorldTransformById | SocketId (Name) | Location (Vector), Rotation (Rotator), Success (Bool) | | GetRoomType | – | RoomType (E_RoomType) | **Entfernte Funktionen** (Laufzeit-State liegt jetzt im Generator): - ~~SetIsOnMainPath~~ → wird über S_PlacedRoomData im Generator gesetzt - ~~GetIsOnMainPath~~ → wird über S_PlacedRoomData im Generator gelesen - ~~SetBranchDepth~~ → wird über S_PlacedRoomData im Generator gesetzt - ~~GetBranchDepth~~ → wird über S_PlacedRoomData im Generator gelesen - ~~GetSockets~~ → ersetzt durch GetSocketDefinitions --- ## Teil 3: BP_DungeonGenerator Variablen ### 3.1 Dungeon-Parameter (Instance Editable) | Variable | Typ | Default | Beschreibung | |---|---|---|---| | Difficulty | Float | 0.5 | 0.0–1.0, bestimmt Größe | | RandomSeed | Integer | 0 | Seed für Reproduzierbarkeit | | RoomPoolTable | Data Table Ref | DT_RoomPool | Referenz auf die Raum-Tabelle | | MinRoomsPerLevel | Integer | 5 | Minimum Räume pro Level | | MaxRoomsPerLevel | Integer | 15 | Maximum Räume pro Level | | MinLevels | Integer | 5 | Minimum Dungeon-Level | | MaxLevels | Integer | 20 | Maximum Dungeon-Level | | MaxRetries | Integer | 5 | Retry-Limit pro Level | | LevelVerticalOffset | Float | 1000.0 | Z-Abstand zwischen Level-Ebenen | | MaxBranchDepthMin | Integer | 1 | Minimale Branch-Tiefe (für Random Range) | | MaxBranchDepthMax | Integer | 5 | Maximale Branch-Tiefe (für Random Range) | ### 3.2 Laufzeit-Variablen (nicht Instance Editable) | Variable | Typ | Default | Beschreibung | |---|---|---|---| | TotalLevels | Integer | 0 | Berechnete Level-Anzahl | | CurrentLevel | Integer | 0 | Aktuelles Level | | MaxRoomsOfLevel | Integer | 0 | Berechnete Räume pro Level | | CurrentRoomsOnLevel | Integer | 0 | Zähler platzierte Räume | | PlacedRooms | Array (S_PlacedRoomData) | leer | Alle platzierten Räume mit Metadaten | | PlacedRoomActors | Array (Actor Obj Ref) | leer | Alle Raum-Actors (für Destroy bei Retry) | | AllTrackedSockets | Array (S_OpenSocketData) | leer | **Zentrale Wahrheitsquelle** für alle Sockets aller platzierten Räume | | MainPathFrontier | Array (S_OpenSocketData) | leer | Offene Sockets am MainPath (Arbeitsliste) | | BranchFrontier | Array (S_OpenSocketData) | leer | Alle Branch-Socket-Kandidaten (Arbeitsliste) | | StartRoom | Actor Object Ref | None | Aktueller Startraum | | EndRoom | Actor Object Ref | None | Aktueller Endraum | | LastPortalOut | Actor Object Ref | None | Letzter PortalOut | | RetryCount | Integer | 0 | Aktueller Retry-Zähler | | SpawnedBlockers | Array (Actor Obj Ref) | leer | Alle WallBlocker | | CurrentSocket | S_OpenSocketData | – | Temporär: aktuell verarbeiteter Socket | | OpenSocketsBuffer | Array (S_OpenSocketData) | leer | Wiederverwendbarer Puffer für Socket-Verteilung – immer clearen vor Verwendung | | RandomPickedIndex | Integer | 0 | Wiederverwendbar für zufälligen Socket-Pick | | RandomStream | Random Stream | – | Initialisiert mit RandomSeed in GenerateDungeon – überall für Zufallszahlen verwenden | --- ## Teil 4: BP_DungeonGenerator – Event Graph ### 4.1 Custom Events erstellen 1. **GenerateDungeon** – Keine Parameter 2. **BuildLevel** – Keine Parameter 3. **BuildMainPath** – Keine Parameter 4. **PlaceEndRoom** – Keine Parameter 5. **BuildBranches** – Keine Parameter 6. **SealOpenSockets** – Keine Parameter 7. **PlacePlayer** – Keine Parameter 8. **RetryLevel** – Keine Parameter (NEU) ### 4.2 BeginPlay 1. **Event BeginPlay ▶** → **GenerateDungeon** ### 4.3 GenerateDungeon 1. **GenerateDungeon ▶** 2. → **Set RandomSeed** (falls Seed == 0: vorher `Random Integer in Range` (1, 999999) → Set RandomSeed) 3. → **Make Random Stream** (RandomSeed) → **SET RandomStream** 4. → **Print String**: "Seed: " + RandomSeed (für Reproduzierbarkeit) 4. → **Lerp (Float)**: A = MinLevels, B = MaxLevels, Alpha = Difficulty → **Round to Int** → **Set TotalLevels** 5. → **Lerp (Float)**: A = MinRoomsPerLevel, B = MaxRoomsPerLevel, Alpha = Difficulty → **Round to Int** → **Set MaxRoomsOfLevel** 6. → **Set CurrentLevel** = 1 7. → **Set CurrentRoomsOnLevel** = 0 8. → **Set RetryCount** = 0 9. → **Clear** PlacedRooms, PlacedRoomActors, AllTrackedSockets, MainPathFrontier, BranchFrontier, SpawnedBlockers 10. → **BuildLevel** **Exec-Kette:** Alle Setter und Clears müssen in der Exec-Kette verbunden sein! ``` GenerateDungeon ▶ → Set RandomSeed ▶ → Print String ▶ → Lerp+Set TotalLevels ▶ → Lerp+Set MaxRoomsOfLevel ▶ → Set CurrentLevel ▶ → Set CurrentRoomsOnLevel ▶ → Set RetryCount ▶ → Clear (alle Arrays) ▶ → BuildLevel ``` ### 4.4 BuildLevel 1. **BuildLevel ▶** 2. → **Set CurrentRoomsOnLevel** = 0 3. → **Clear** MainPathFrontier 4. → **Branch**: CurrentLevel == 1? **True ▶ (Erstes Level – Entrance):** 5. → **SpawnActor** BP_DungeonRoom_Entrance, Location: (0, 0, 0), Rotation: (0, 0, 0) 6. → **Set StartRoom** = Return Value 7. → **Add** Return Value zu PlacedRoomActors 8. → **RegisterRoomSockets** aufrufen (Funktion, Room = Return Value, BranchDepth = 0) - Füllt OpenSocketsBuffer mit allen Sockets (PathType: Branch, State: Open) - AllTrackedSockets wird noch NICHT befüllt – das passiert nach dem Random Pick **Socket-Verteilung für den Startraum (Schritt 9):** 9. → OpenSocketsBuffer verteilen (kein Filter nötig – Buffer enthält nur Sockets dieses Raums): - **Schritt 9a:** Zufälligen MainPath-Socket aus OpenSocketsBuffer wählen - **Schritt 9b:** Gewählten Socket mit PathType: **MainPath** in AllTrackedSockets eintragen → **Add** zu **MainPathFrontier** - **Schritt 9c:** Alle übrigen mit PathType: **Branch** in AllTrackedSockets eintragen → **Add** zu **BranchFrontier** > **Warum zufällig?** "Ersten nehmen" wäre immer derselbe Socket (abhängig von der Reihenfolge der Arrow Components im Blueprint-Viewport), nicht vom Seed gesteuert. Der Zufalls-Pick sorgt dafür, dass der MainPath-Ausgang bei jedem Seed anders liegen kann. **Konkret in Blueprints:** ``` // Schritt 9a – Zufälligen MainPath-Socket wählen Branch: OpenSocketsBuffer Length > 0? True ▶ → Random Integer in Range from Stream (Stream: RandomStream, Min: 0, Max: OpenSocketsBuffer Length - 1) → Set RandomPickedIndex // Schritt 9b + 9c – Alle Buffer-Einträge in AllTrackedSockets eintragen und verteilen OpenSocketsBuffer → ForEachLoop Loop Body ▶ → Branch: Array Index == RandomPickedIndex? True ▶ → Array Element → Break S_OpenSocketData → Make S_OpenSocketData ├─ OwnerRoom, SocketId, SocketIndex, LevelIndex, BranchDepth, State: jeweiliger Pin von Break └─ PathType: MainPath ← einzige Änderung → Add zu AllTrackedSockets → Add zu MainPathFrontier False ▶ → Array Element → Add zu AllTrackedSockets (PathType bereits Branch, kein Break/Make nötig) → Array Element → Add zu BranchFrontier ``` **Gemeinsame Variablen (Generator-Ebene, überall wiederverwenden):** - `OpenSocketsBuffer` – Array of S_OpenSocketData (wird von RegisterRoomSockets geleert und befüllt) - `RandomPickedIndex` – Integer 10. → **Make S_PlacedRoomData** (RoomActor: StartRoom, RoomType: Entrance, PathType: MainPath, LevelIndex: CurrentLevel, BranchDepth: 0) → **Add** zu PlacedRooms 11. → **BuildMainPath** **False ▶ (Level 2+ – PortalIn):** 5. → LastPortalOut → **GetActorLocation** → **Subtract** Vector(0, 0, **LevelVerticalOffset**) → **Set** PortalInLocation 6. → **SpawnActor** BP_DungeonRoom_PortalIn, Location: PortalInLocation, Rotation: (0, 0, 0) 7. → **Set StartRoom** = Return Value 8. → **Add** Return Value zu PlacedRoomActors 9. → **RegisterRoomSockets** aufrufen (Funktion, Room = Return Value, BranchDepth = 0) - Füllt OpenSocketsBuffer mit allen Sockets (PathType: Branch, State: Open) **Socket-Verteilung (identisch zu oben):** 10. → Random Pick aus OpenSocketsBuffer → gewählter Socket mit PathType: MainPath in AllTrackedSockets + MainPathFrontier → Rest mit PathType: Branch in AllTrackedSockets + BranchFrontier (siehe Schritt 9 oben für vollständige Blueprint-Logik) 11. → **Make S_PlacedRoomData** (RoomActor: StartRoom, RoomType: PortalIn, PathType: MainPath, LevelIndex: CurrentLevel, BranchDepth: 0) → **Add** zu PlacedRooms 12. → **BuildMainPath** ### 4.5 RegisterRoomSockets (Hilfsfunktion, NEU) Diese Funktion liest die Socket-Definitionen eines Raums und füllt **OpenSocketsBuffer** mit S_OpenSocketData-Einträgen. Sie schreibt **nicht** direkt in AllTrackedSockets – das übernimmt der Aufrufer nach dem Random Pick, damit PathType für jeden Socket korrekt gesetzt wird. **Umsetzung:** Erstelle eine neue Funktion (nicht Custom Event) im BP_DungeonGenerator: **Inputs:** - Room (Actor Object Ref) - BranchDepth (Integer, Default: 0) **Keine Outputs** (schreibt in OpenSocketsBuffer, clearet ihn vorher). **Graph:** 1. → **Clear** OpenSocketsBuffer 2. → Room → **GetSocketDefinitions (Message)** → SocketDefinitions Array 3. → **Branch**: SocketDefinitions → **Length** > 0? 4. → False ▶: **Print String** "WARNING: Room has no sockets!" → **Return** 5. → True ▶: **ForEachLoop** über SocketDefinitions 6. **Loop Body ▶** → **Make S_OpenSocketData**: - OwnerRoom: Room (Input) - SocketId: Array Element → Break S_SocketDefinition → SocketId - SocketIndex: ForEachLoop → **Array Index** - PathType: **Branch** (immer – wird nach dem Random Pick für den gewählten Socket auf MainPath korrigiert) - LevelIndex: CurrentLevel (Generator-Variable) - BranchDepth: BranchDepth (Input) - State: **Open** 7. → **Add** zu **OpenSocketsBuffer** 8. ForEachLoop **Completed ▶** → **Return** ``` RegisterRoomSockets (Room, BranchDepth) → Clear OpenSocketsBuffer → Room → GetSocketDefinitions (Message) → SocketDefinitions → Branch: Length > 0? False ▶ → Print "WARNING: Room has no sockets!" → Return True ▶ → ForEachLoop (SocketDefinitions) Loop Body ▶ → Make S_OpenSocketData ├─ OwnerRoom: Room ├─ SocketId: Element.SocketId ├─ SocketIndex: Array Index ├─ PathType: Branch ├─ LevelIndex: CurrentLevel ├─ BranchDepth: BranchDepth └─ State: Open → Add zu OpenSocketsBuffer Completed ▶ → Return ``` **WICHTIG:** `AllTrackedSockets` ist die einzige vollständige Socket-Datenbank. `OpenSocketsBuffer` ist nur ein temporärer Zwischenpuffer – nach dem Random Pick werden alle Einträge aus dem Buffer mit korrekt gesetztem PathType in AllTrackedSockets eingetragen. `MainPathFrontier` und `BranchFrontier` sind Arbeits-Arrays für aktuell offene Kandidaten. ### 4.5.1 UpdateSocketState (Hilfsfunktion, NEU) Diese Funktion wird an vielen Stellen benötigt. Sie findet einen bestimmten Socket in AllTrackedSockets und setzt seinen State. **Umsetzung:** Erstelle eine neue Funktion im BP_DungeonGenerator: **Inputs:** - TargetRoom (Actor Object Ref) – Der Raum dessen Socket geändert wird - TargetSocketId (Name) – Die Socket-ID die geändert wird - NewState (E_SocketStatus) – Der neue State (Occupied, Closed, etc.) **Keine Outputs.** **Graph:** 1. → **AllTrackedSockets** → **ForEachLoop with Break** 2. **Loop Body ▶** → Array Element → **Break S_OpenSocketData** 3. → **Branch**: OwnerRoom == TargetRoom **AND** SocketId == TargetSocketId? 4. → **True ▶:** - Array Element → **Break S_OpenSocketData** → alle Felder einzeln rausholen - **Make S_OpenSocketData** mit allen gleichen Werten, aber **State = NewState** - **Set Array Elem** (Target: AllTrackedSockets, Index: ForEachLoop Array Index, Item: der neue S_OpenSocketData) - → **Break** (ForEachLoop beenden) 5. → **False ▶:** → weiter (nächste Iteration) ``` UpdateSocketState (TargetRoom, TargetSocketId, NewState) → AllTrackedSockets → ForEachLoop with Break Loop Body ▶ → Break S_OpenSocketData → Branch: OwnerRoom == TargetRoom AND SocketId == TargetSocketId? True ▶ → Make S_OpenSocketData (alle Felder kopieren, State = NewState) → Set Array Elem (AllTrackedSockets, Array Index, neuer Eintrag) → Break False ▶ → (weiter) ``` **UE Blueprint Details für Set Array Elem:** - Rechtsklick in den Graph → "Set Array Elem" - Target: AllTrackedSockets (Variable-Getter, als Referenz) - Index: ForEachLoop → Array Index - Item: Der neue S_OpenSocketData Struct - **Size to Fit: NEIN** (der Index existiert bereits) ### 4.6 BuildMainPath **Umsetzung als While-Loop:** **While Loop Condition:** - **AND**: MainPathFrontier **Length > 0** UND CurrentRoomsOnLevel **< MaxRoomsOfLevel** **Loop Body ▶:** 1. → MainPathFrontier **Get (Index 0)** → **Set CurrentSocket** 2. → MainPathFrontier **Remove Index 0** 3. → **FindCompatibleRoom** (OpenSocket: CurrentSocket, ExcludeTypes: [Entrance, Exit, PortalIn, PortalOut, DeadEnd]) - Details siehe **Teil 5.1** 4. → **Branch** (Success) **False ▶ (kein kompatibler Raum gefunden):** 5. → **Print String** "No compatible room found for socket" (Debug) 6. → Exec-Pin zurück zum **Loop-Eingang** des While-Loop-Macros (nächste Iteration) **True ▶ (Raum gefunden):** 5. → **SnapRoomToSocket** (OpenSocket: CurrentSocket, NewRoomClass: FoundRoomClass, NewSocketIndex: MatchingSocketIndex, NewSocketId: MatchingSocketId) - Details siehe **Teil 5.2** 6. → **Branch** (Success) **False ▶ (Snap fehlgeschlagen, z.B. Kollision):** 7. → **Print String** "Snap failed (collision)" (Debug) 8. → Exec-Pin zurück zum **Loop-Eingang** des While-Loop-Macros (nächste Iteration) > **WICHTIG:** Beide Fehlerpfade müssen ihren Exec-Output-Pin zurück an den **Loop-Eingang** des While-Loop-Macros anschließen. Ohne diese Verbindung bricht die Schleife beim ersten Fehler ab anstatt mit dem nächsten Socket weiterzumachen. **True ▶ (Snap erfolgreich):** **Schritt 7 – Raum registrieren:** 7. → **Add** SpawnedRoom zu PlacedRoomActors 8. → **CurrentRoomsOnLevel + 1** → **Set CurrentRoomsOnLevel** 9. → **Make S_PlacedRoomData** (RoomActor: SpawnedRoom, RoomType: aus FindCompatibleRoom, PathType: MainPath, LevelIndex: CurrentLevel, BranchDepth: 0) → **Add** zu PlacedRooms **Schritt 10 – Benutzten Socket auf Occupied setzen:** 10. → **UpdateSocketState** (TargetRoom: CurrentSocket.OwnerRoom, TargetSocketId: CurrentSocket.SocketId, NewState: **Occupied**) **Schritt 11 – Sockets des neuen Raums registrieren und verteilen (KRITISCH):** Ohne die folgende Regel verzweigt der MainPath unkontrolliert und wird zu einem Netz statt einem klaren Pfad. Die Regel ist: **Genau 1 Socket geht auf MainPath, der Rest geht auf Branch.** > **Warum zufällig?** "Ersten nehmen" wäre immer derselbe Socket (abhängig von der Reihenfolge der Arrow Components im Blueprint-Viewport). Der Zufalls-Pick sorgt dafür, dass der MainPath-Ausgang bei jedem Seed anders liegen kann. Das gilt für alle Räume gleichermaßen – beim Startraum gibt es keinen Eingangs-Socket, bei allen anderen ist genau einer belegt. **MatchingSocketId kennen:** MatchingSocketId ist die SocketId des Sockets am neuen Raum, der für das Andocken benutzt wurde. Du bekommst sie aus FindCompatibleRoom: - In FindCompatibleRoom hast du den SocketDefinitions-Array des Kandidaten durchsucht - MatchingSocketIndex ist der Array-Index des gewählten Sockets - MatchingSocketId = SocketDefinitions[MatchingSocketIndex].SocketId - **Erweitere FindCompatibleRoom so, dass es auch MatchingSocketId (Name) zurückgibt** (zusätzlich zu MatchingSocketIndex) **Umsetzung (gleiches Muster wie Schritt 9, nur mit Eingangs-Socket-Behandlung davor):** ``` // Schritt 11a – Buffer füllen (RegisterRoomSockets cleared OpenSocketsBuffer selbst) RegisterRoomSockets (Room: SpawnedRoom, BranchDepth: 0) → OpenSocketsBuffer enthält alle Sockets mit PathType: Branch, State: Open // Schritt 11b – Eingangs-Socket aus Buffer herausnehmen und als Occupied in AllTrackedSockets eintragen OpenSocketsBuffer → ForEachLoop with Break Loop Body ▶ → Branch: Element.SocketId == MatchingSocketId? True ▶ → Make S_OpenSocketData (alle Felder von Element, State: Occupied, PathType: Branch) → Add zu AllTrackedSockets → Remove (OpenSocketsBuffer, Array Index) → Break // Schritt 11c – Zufälligen MainPath-Socket aus verbliebenem Buffer wählen Branch: OpenSocketsBuffer Length > 0? False ▶ → Print "WARNING: New room has no free sockets!" → (weiter) True ▶ → Random Integer in Range from Stream (0, OpenSocketsBuffer Length - 1) → Set RandomPickedIndex // Schritt 11d – Restlichen Buffer in AllTrackedSockets eintragen und verteilen OpenSocketsBuffer → ForEachLoop Loop Body ▶ → Branch: Array Index == RandomPickedIndex? True ▶ → Array Element → Break S_OpenSocketData → Make S_OpenSocketData ├─ OwnerRoom, SocketId, SocketIndex, LevelIndex, BranchDepth, State: jeweiliger Pin von Break └─ PathType: MainPath ← einzige Änderung → Add zu AllTrackedSockets → Add zu MainPathFrontier False ▶ → Array Element → Add zu AllTrackedSockets (PathType bereits Branch, kein Break/Make nötig) → Array Element → Add zu BranchFrontier ``` **Ergebnis pro Raum:** Genau 1 Socket → Occupied (Eingang), genau 1 zufälliger Socket → MainPathFrontier (Ausgang), Rest → BranchFrontier. ``` Zusammenfassung Socket-Verteilung (gilt für ALLE Räume): Schritt a – RegisterRoomSockets → OpenSocketsBuffer gefüllt (alle Branch, Open), AllTrackedSockets noch leer für diesen Raum Schritt b – Eingangs-Socket aus Buffer → AllTrackedSockets (Occupied) [entfällt beim Startraum] Schritt c – Random Pick Index aus verbliebenem Buffer bestimmen Schritt d – Buffer → AllTrackedSockets + Frontiers: ├─ Zufälliger Index → PathType: MainPath, MainPathFrontier └─ Alle anderen → PathType: Branch, BranchFrontier ``` **While Loop Completed ▶** → **PlaceEndRoom** ### 4.7 PlaceEndRoom 1. **PlaceEndRoom ▶** 2. → **Branch**: MainPathFrontier **Length > 0**? **False ▶ (Kein offener MainPath-Socket mehr):** 3. → **Print String** "No open MainPath socket for end room!" 4. → **RetryLevel** (siehe Teil 4.11) **True ▶:** 3. → MainPathFrontier **Get (Index 0)** → **Set CurrentSocket** 4. → **Branch**: CurrentLevel == TotalLevels? **True ▶ (Letztes Level – Exit):** 5. → **FindCompatibleRoom** (OpenSocket: CurrentSocket, ExcludeTypes: [Entrance, PortalIn, PortalOut, General, Corridor, DeadEnd]) - Damit bleibt nur **Exit** übrig als gültiger Typ 6. → **Branch** (Success) - False ▶: **Print String** "No Exit room found!" → **RetryLevel** 7. → True ▶: **SnapRoomToSocket** (OpenSocket: CurrentSocket, NewRoomClass: FoundRoomClass, NewSocketIndex: MatchingSocketIndex, NewSocketId: MatchingSocketId) 8. → **Branch** (Success) - False ▶: **Print String** "Exit snap failed!" → **RetryLevel** 9. → True ▶: 10. → **Set EndRoom** = SpawnedRoom 11. → **Add** SpawnedRoom zu PlacedRoomActors 12. → **Make S_PlacedRoomData** (PathType: MainPath, RoomType: Exit, LevelIndex: CurrentLevel, BranchDepth: 0) → **Add** zu PlacedRooms 13. → **UpdateSocketState** (CurrentSocket.OwnerRoom, CurrentSocket.SocketId, **Occupied**) **Schritt 14 – Alle Sockets des Exit-Raums registrieren:** 14. → **RegisterRoomSockets** (Room: SpawnedRoom, PathType: MainPath, BranchDepth: 0) - Registriert alle Sockets des Exit-Raums in AllTrackedSockets mit State: Open 15. → Den Eingangs-Socket des Exit-Raums auf Occupied setzen: → **UpdateSocketState** (TargetRoom: SpawnedRoom, TargetSocketId: **MatchingSocketId**, NewState: **Occupied**) - MatchingSocketId kommt aus FindCompatibleRoom (Schritt 5) - Übrige Sockets des Exit-Raums bleiben Open und werden später von SealOpenSockets mit WallBlockern verschlossen 16. → **BuildBranches** **False ▶ (Nicht letztes Level – PortalOut):** 5. → **FindCompatibleRoom** (OpenSocket: CurrentSocket, ExcludeTypes: [Entrance, PortalIn, Exit, General, Corridor, DeadEnd]) - Damit bleibt nur **PortalOut** übrig als gültiger Typ 6. → **Branch** (Success) - False ▶: **Print String** "No PortalOut room found!" → **RetryLevel** 7. → True ▶: **SnapRoomToSocket** (OpenSocket: CurrentSocket, NewRoomClass: FoundRoomClass, NewSocketIndex: MatchingSocketIndex, NewSocketId: MatchingSocketId) → **Branch** (Success) - False ▶: → **RetryLevel** 8. → True ▶: 9. → **Set EndRoom** = SpawnedRoom 10. → **Set LastPortalOut** = SpawnedRoom 11. → **Add** SpawnedRoom zu PlacedRoomActors 12. → **Make S_PlacedRoomData** (PathType: MainPath, RoomType: PortalOut, LevelIndex: CurrentLevel, BranchDepth: 0) → **Add** zu PlacedRooms 13. → **UpdateSocketState** (CurrentSocket.OwnerRoom, CurrentSocket.SocketId, **Occupied**) **Schritt 14 – Alle Sockets des PortalOut-Raums registrieren:** 14. → **RegisterRoomSockets** (Room: SpawnedRoom, PathType: MainPath, BranchDepth: 0) - Registriert alle Sockets des PortalOut-Raums in AllTrackedSockets mit State: Open 15. → Den Eingangs-Socket des PortalOut-Raums auf Occupied setzen: → **UpdateSocketState** (TargetRoom: SpawnedRoom, TargetSocketId: **MatchingSocketId**, NewState: **Occupied**) - MatchingSocketId kommt aus FindCompatibleRoom (Schritt 5) - Übrige Sockets des PortalOut-Raums bleiben Open → werden von SealOpenSockets verschlossen 16. → **Set CurrentLevel** = CurrentLevel + 1 17. → **BuildLevel** (nächstes Level starten) **WICHTIG:** Branches werden NICHT nach PortalOut gebaut! Nur nach Exit (letztes Level). Dadurch sammelt BranchFrontier Sockets über ALLE Level hinweg, und BuildBranches baut sie am Ende alle auf einmal. ### 4.8 BuildBranches Wird NUR nach dem Exit-Raum aufgerufen. Baut Abzweigungen über alle Level hinweg. 1. **BuildBranches ▶** 2. → **Branch**: BranchFrontier **Length > 0**? - False ▶: **Print String** "No branch sockets available" → **SealOpenSockets** **WICHTIG: Array-Kopie erstellen!** BranchFrontier wird während der Iteration verändert (neue Sockets werden hinzugefügt). In UE Blueprints darf man ein Array nicht verändern während man darüber iteriert. 3. → True ▶: BranchFrontier → **Copy** in lokale Variable **BranchWorkList** (Rechtsklick auf BranchFrontier → Get → in neue lokale Variable ziehen, oder: **Set BranchWorkList** = BranchFrontier, dann **Clear** BranchFrontier) **Konkret:** ``` Set BranchWorkList = BranchFrontier (kopiert das Array) Clear BranchFrontier (leert das Original – neue Sockets aus Branch-Räumen kommen hier rein) ``` 4. → **ForEachLoop** über **BranchWorkList** (NICHT über BranchFrontier!) **Loop Body ▶ (für jeden Branch-Startpunkt):** 5. → **Random Integer in Range** (MaxBranchDepthMin, MaxBranchDepthMax) → **Set** MaxBranchDepth (lokale Variable) 6. → **Set** CurrentBranchDepth = 0 (lokale Variable) 7. → Array Element → **Set** CurrentBranchSocket (lokale Variable, Typ: S_OpenSocketData) **Innere While-Loop** (Condition: CurrentBranchDepth < MaxBranchDepth): 8. → **Branch**: CurrentBranchDepth == MaxBranchDepth - 1? **True ▶ (Letzter Raum der Branch – DeadEnd platzieren):** 9. → **SnapRoomToSocket** (OpenSocket: CurrentBranchSocket, NewRoomClass: BP_DungeonRoom_DeadEnd, NewSocketIndex: 0, NewSocketId: "Socket_Entry") - DeadEnd hat immer genau 1 Socket namens "Socket_Entry", daher Index 0 und Id fest 10. → **Branch** (Success) **False ▶ (DeadEnd Snap fehlgeschlagen):** 11. → **Print String** "DeadEnd snap failed" → **Break** innere While-Loop (Branch abbrechen) **True ▶ (DeadEnd erfolgreich):** 11. → **Add** SpawnedRoom zu PlacedRoomActors 12. → **Make S_PlacedRoomData** (PathType: Branch, RoomType: DeadEnd, LevelIndex: CurrentBranchSocket.LevelIndex, BranchDepth: CurrentBranchDepth + 1) → **Add** zu PlacedRooms 13. → **UpdateSocketState** (CurrentBranchSocket.OwnerRoom, CurrentBranchSocket.SocketId, **Occupied**) 14. → DeadEnd hat nur 1 Socket (Socket_Entry) → Eingangs-Socket registrieren: - **Make S_OpenSocketData** (OwnerRoom: SpawnedRoom, SocketId: "Socket_Entry", SocketIndex: 0, PathType: Branch, LevelIndex: CurrentBranchSocket.LevelIndex, BranchDepth: CurrentBranchDepth + 1, State: **Occupied**) → **Add** zu AllTrackedSockets 15. → **Break** innere While-Loop (Branch fertig) **False ▶ (Nicht letzter Raum – normaler Branch-Raum):** 9. → **FindCompatibleRoom** (OpenSocket: CurrentBranchSocket, ExcludeTypes: [Entrance, Exit, PortalIn, PortalOut, DeadEnd]) - Liefert: FoundRoomClass, MatchingSocketIndex, **MatchingSocketId**, FoundRoomType, Success 10. → **Branch** (Success) - False ▶: **Print String** "No room for branch" → **Break** innere While-Loop 11. → True ▶: **SnapRoomToSocket** (OpenSocket: CurrentBranchSocket, NewRoomClass: FoundRoomClass, NewSocketIndex: MatchingSocketIndex, NewSocketId: **MatchingSocketId**) → **Branch** (Success) - False ▶: **Print String** "Branch snap failed" → **Break** innere While-Loop 12. → True ▶: 13. → **Add** SpawnedRoom zu PlacedRoomActors 14. → **Make S_PlacedRoomData** (PathType: Branch, RoomType: aus FindCompatibleRoom, LevelIndex: CurrentBranchSocket.LevelIndex, BranchDepth: CurrentBranchDepth + 1) → **Add** zu PlacedRooms 15. → **UpdateSocketState** (CurrentBranchSocket.OwnerRoom, CurrentBranchSocket.SocketId, **Occupied**) 16. → Sockets des neuen Branch-Raums registrieren und verteilen: - SpawnedRoom → **GetSocketDefinitions (Message)** → SocketDefinitions - Lokaler Bool **BranchContinued** = false - **ForEachLoop** über SocketDefinitions: - **Branch**: SocketId == MatchingSocketId? (Eingangs-Socket, MatchingSocketId kommt aus FindCompatibleRoom in Schritt 9) - True ▶: **Make S_OpenSocketData** (State: **Occupied**) → Add zu AllTrackedSockets - False ▶: **Branch**: BranchContinued == false? - True ▶ (erster freier Socket → Branch weiterführen): - **Make S_OpenSocketData** (PathType: Branch, State: Open, BranchDepth: CurrentBranchDepth + 1) → Add zu AllTrackedSockets - → **Set CurrentBranchSocket** = dieser neue S_OpenSocketData - → **Set BranchContinued** = true - False ▶ (weitere Sockets → zur späteren Verarbeitung): - **Make S_OpenSocketData** (PathType: Branch, State: Open, BranchDepth: CurrentBranchDepth + 1) → Add zu AllTrackedSockets - → **Add** zu **BranchFrontier** (wird in einer späteren Iteration verarbeitet, falls gewünscht) 17. → **CurrentBranchDepth + 1** → Set CurrentBranchDepth 18. → (Innere While-Loop nächste Iteration) **ForEachLoop Completed ▶:** 19. → **Branch**: BranchFrontier **Length > 0**? (Wurden während des Branch-Baus neue Sockets gesammelt?) - True ▶: Optional: Nochmal eine Runde Branch-Bau starten (Rekursion/Loop), ODER: ignorieren und mit SealOpenSockets verschließen - False ▶: → **SealOpenSockets** **Empfehlung für den Prototyp:** Neue BranchFrontier-Einträge aus dem Branch-Bau NICHT weiter verfolgen, sondern direkt zu SealOpenSockets gehen. Das verhindert unkontrolliertes Wachstum. Also nach ForEachLoop Completed immer → **SealOpenSockets**. ### 4.9 SealOpenSockets Alle übrig gebliebenen offenen Sockets mit WallBlockern verschließen. Die Funktion nutzt **AllTrackedSockets** als einzige Wahrheitsquelle. - **Occupied** = Socket wurde als Verbindung zwischen zwei Räumen benutzt - **Open** = Socket wurde nie verbunden (braucht WallBlocker) - **Closed** = Socket wurde bereits mit einem WallBlocker verschlossen 1. **SealOpenSockets ▶** **Schritt 2 – Offene Sockets sammeln:** 2. → Lokales Array **OpenSocketsToSeal** (Typ: Array of S_OpenSocketData) erstellen 3. → **AllTrackedSockets** → **ForEachLoop** 4. → Array Element → Break S_OpenSocketData → State 5. → **Branch**: State == **Open**? 6. → True ▶: **Add** Array Element zu OpenSocketsToSeal 7. → ForEachLoop Completed ▶: weiter mit Schritt 8 ``` AllTrackedSockets → ForEachLoop Loop Body ▶ → Break S_OpenSocketData → Branch: State == Open? True ▶ → Add zu OpenSocketsToSeal Completed ▶ → (weiter) ``` **Schritt 8 – WallBlocker spawnen:** 8. → **ForEachLoop** über OpenSocketsToSeal 9. → Array Element → Break S_OpenSocketData → OwnerRoom, SocketId 10. → OwnerRoom → **GetSocketWorldTransformById (Message)** (SocketId) → Location, Rotation, Success 11. → **Branch** (Success) - False ▶: **Print String** "WARNING: Socket not found for sealing" → weiter 12. → True ▶: **SpawnActor** BP_WallBlocker - Location: Location vom Socket - Rotation: Rotation vom Socket 13. → **Add** WallBlocker zu SpawnedBlockers 14. → **UpdateSocketState** (OwnerRoom, SocketId, **Closed**) **ForEachLoop Completed ▶** → **PlacePlayer** ### 4.10 PlacePlayer 1. **PlacePlayer ▶** 2. → **Get Player Character** (Player Index: 0) 3. → **Branch**: IsValid (Player Character)? - False ▶: **Print String** "No player character found!" 4. → True ▶: StartRoom → **GetActorLocation** → **Set** StartLocation 5. → **SetActorLocation** (Target: Player Character, New Location: StartLocation) 6. → **Print String** "Dungeon Generation Complete! Levels: " + TotalLevels + " Rooms: " + PlacedRooms Length ### 4.11 RetryLevel (NEU) Wird aufgerufen wenn BuildMainPath oder PlaceEndRoom fehlschlägt. 1. **RetryLevel ▶** 2. → **RetryCount + 1** → **Set RetryCount** 3. → **Branch**: RetryCount > MaxRetries? **True ▶ (Zu viele Retries):** 4. → **Print String** "FATAL: Level generation failed after " + MaxRetries + " retries!" 5. → (Hier enden – optional: gesamten Dungeon neu starten mit GenerateDungeon) **False ▶ (Retry möglich):** 4. → **Print String** "Retrying level " + CurrentLevel + " (Attempt " + RetryCount + ")" **Schritt 5 – Räume des aktuellen Levels aufräumen:** Iteriere direkt über **PlacedRooms** (nicht PlacedRoomActors), weil PlacedRooms den LevelIndex enthält: 5. → PlacedRooms → **Rückwärts-ForLoop** (Length - 1 → 0, step -1) 6. → PlacedRooms **Get (Index)** → Break S_PlacedRoomData → LevelIndex, RoomActor 7. → **Branch**: LevelIndex == CurrentLevel? 8. → True ▶: RoomActor → **Destroy Actor** → **Remove Index** (PlacedRooms, Index) 9. → ForLoop Completed ▶: **Schritt 10 – Weitere Arrays bereinigen:** 10. → PlacedRoomActors: Alle ungültigen Actors entfernen (Rückwärts-ForLoop → IsValid? → False → Remove Index) 11. → AllTrackedSockets: Alle Einträge mit LevelIndex == CurrentLevel entfernen (Rückwärts-ForLoop) 12. → MainPathFrontier → **Clear** 13. → BranchFrontier: Alle Einträge mit LevelIndex == CurrentLevel entfernen (Rückwärts-ForLoop) **Schritt 14 – WallBlocker aufräumen (falls bereits Branches gebaut wurden):** 14. → SpawnedBlockers → **ForEachLoop** → **Destroy Actor** → **Clear** SpawnedBlockers **Schritt 15 – Level neu starten:** 15. → **BuildLevel** **Hinweis zur Array-Bereinigung:** "Alle Einträge mit LevelIndex == X entfernen" ist in Blueprints am einfachsten mit einer **Rückwärts-ForLoop**: ``` PlacedRooms → Length - 1 → ForLoop (LastIndex to 0, step -1) Loop Body ▶ → PlacedRooms Get (Index) → Break → LevelIndex → Branch: LevelIndex == CurrentLevel? True ▶ → Remove Index (PlacedRooms, Index) ``` Rückwärts iterieren verhindert Index-Verschiebung beim Entfernen! --- ## Teil 5: Funktionen ### 5.1 FindCompatibleRoom **Umsetzung:** Erstelle eine neue Funktion im BP_DungeonGenerator. **Inputs:** - OpenSocket (S_OpenSocketData) – Der offene Socket an den angedockt werden soll - ExcludeTypes (Array of E_RoomType) – Raumtypen die ausgeschlossen werden **Outputs:** - FoundRoomClass (Actor Class Ref) – Die Blueprint-Klasse des gewählten Raums - MatchingSocketIndex (Integer) – Index des Sockets im neuen Raum der als Eingang dient - MatchingSocketId (Name) – **NEU** – Socket-ID des Eingangs-Sockets (für spätere Zuordnung) - FoundRoomType (E_RoomType) – **NEU** – Typ des gewählten Raums (für S_PlacedRoomData) - Success (Bool) **Schritt 1 – Gültige Kandidaten aus der Data Table filtern:** **Lokale Variablen (in der Funktion anlegen):** - **RowNames** (Array of Name) – Zeilennamen aus der Data Table - **ValidCandidates** (Array of S_RoomPoolEntry) – gefilterte Kandidaten - **TotalWeight** (Float) – Summe aller Weights - **RandomPick** (Float) – zufälliger Wert zwischen 0 und TotalWeight - **RunningWeight** (Float) – laufende Summe beim Iterieren - **ChosenEntry** (S_RoomPoolEntry) – der gewählte Kandidat - **TempCandidate** (Actor Object Ref) – temporär gespawnter Raum für Socket-Matching > **Wichtig:** In UE5 gibt es keinen "Get Data Table Rows"-Node (Plural). Stattdessen holst du zuerst alle Zeilennamen mit **Get Data Table Row Names**, dann iterierst du darüber und holst jede Zeile einzeln mit **Get Data Table Row**. 1. → **Get Data Table Row Names** (Table: RoomPoolTable) → Set **RowNames** 2. → **ForEachLoop** über RowNames 3. → **Get Data Table Row** (Table: RoomPoolTable, Row Name: Array Element) → hat zwei Exec-Pins und einen Data-Pin: - **Row Found ▶** (Exec) – Zeile existiert - **Row Not Found ▶** (Exec) – Zeile nicht gefunden (einfach ignorieren, weiter zur nächsten Iteration) - **Out Row** (S_RoomPoolEntry) – die Zeilendaten 4. → **Row Found ▶**: Out Row → **Break S_RoomPoolEntry** → RoomType, MinLevel, MaxLevel 5. → **Branch**: Drei Bedingungen müssen ALLE true sein (AND): - RoomType → **Contains** in ExcludeTypes Array → **NOT** (= RoomType ist NICHT ausgeschlossen) - MinLevel **<=** CurrentLevel - MaxLevel **>=** CurrentLevel **OR** MaxLevel == 0 (0 = unbegrenzt) 6. → True ▶: Out Row → **Add** zu ValidCandidates 7. → ForEachLoop Completed ▶: **Branch** (ValidCandidates **Length > 0**) 8. → False ▶: **Return** (Success = false) ``` Get Data Table Row Names (RoomPoolTable) → RowNames ForEachLoop (RowNames) Loop Body ▶ → Get Data Table Row (RoomPoolTable, Array Element) Row Found ▶ → Break S_RoomPoolEntry → RoomType, MinLevel, MaxLevel → Branch: NOT Contains(ExcludeTypes, RoomType) AND MinLevel <= CurrentLevel AND (MaxLevel >= CurrentLevel OR MaxLevel == 0) True ▶ → Add zu ValidCandidates Row Not Found ▶ → (nichts, nächste Iteration) Completed ▶ → Branch: ValidCandidates Length > 0? False ▶ → Return (Success = false) True ▶ → (weiter mit Schritt 9) ``` **Schritt 9 – Gewichtete Zufallswahl:** 9. → **Set TotalWeight** = 0.0 10. → **ForEachLoop** über ValidCandidates → TotalWeight += Array Element → Weight 11. → **Random Float in Range** (0.0, TotalWeight) → **Set** RandomPick 12. → **Set** RunningWeight = 0.0 13. → **ForEachLoop** über ValidCandidates 14. → RunningWeight += Array Element → Weight 15. → **Branch**: RunningWeight >= RandomPick? 16. → True ▶: Array Element ist der gewählte Kandidat → **Set** ChosenEntry = Array Element → **Break** ``` Gewichtete Zufallswahl: TotalWeight = Sum aller Weights RandomPick = Random Float (0, TotalWeight) RunningWeight = 0 ForEachLoop: RunningWeight += Weight Branch: RunningWeight >= RandomPick? True ▶ → ChosenEntry = Element → Break ``` **Schritt 17 – Socket-Matching (den passenden Eingangs-Socket am Kandidaten finden):** Wir müssen herausfinden, welcher Socket des neuen Raums als "Eingang" benutzt wird – also an den offenen Socket des bestehenden Raums andockt. **Für den Prototyp** gilt folgende einfache Regel: **Nimm den ersten Socket des Kandidaten.** Für den Prototyp reicht das, weil alle Räume gleich große Durchgänge haben und Richtungs-Matching noch nicht implementiert ist. 17. → ChosenEntry → Break S_RoomPoolEntry → RoomClass → **SpawnActor** temporär bei (0, 0, 10000) → **Set** TempCandidate - Position weit oben, damit er nicht mit dem Dungeon kollidiert 18. → TempCandidate → **GetSocketDefinitions (Message)** → SocketDefinitions 19. → **Branch**: SocketDefinitions **Length > 0**? - False ▶: TempCandidate → **Destroy Actor** → **Return** (Success = false) 20. → True ▶: SocketDefinitions **Get (Index 0)** → Break S_SocketDefinition 21. → TempCandidate → **Destroy Actor** 22. → **Return Node** – die Output-Pins direkt am Return Node verdrahten: - FoundRoomClass ← ChosenEntry → Break S_RoomPoolEntry → RoomClass - MatchingSocketIndex ← 0 (Integer Literal) - MatchingSocketId ← Break S_SocketDefinition → SocketId (aus Schritt 20) - FoundRoomType ← ChosenEntry → Break S_RoomPoolEntry → RoomType - Success ← true (Boolean Literal) > **Hinweis:** In UE5 Funktionen haben die Outputs keine separaten Set-Nodes — sie sind direkt als Data-Pins am Return Node. Du ziehst die Daten-Verbindungen (grüne/blaue Drähte) direkt in die Pins des Return Nodes. Die Exec-Kette ist: SpawnActor ▶ → GetSocketDefinitions ▶ → Branch → True ▶ → Destroy Actor ▶ → Return. **Spätere Erweiterung – Richtungs-Matching:** Statt pauschal Socket 0 zu nehmen, könnte man die Richtung des offenen Sockets prüfen und den Socket des Kandidaten wählen, der in die entgegengesetzte Richtung zeigt (Nord → Süd, Ost → West). Das ist aber für den Prototyp nicht nötig. ### 5.2 SnapRoomToSocket Diese Funktion dockt einen neuen Raum an einen offenen Socket eines bestehenden Raums an. **Umsetzung:** Erstelle eine neue Funktion im BP_DungeonGenerator. **Inputs:** - OpenSocket (S_OpenSocketData) – Der offene Socket am bestehenden Raum - NewRoomClass (Actor Class Ref) – Blueprint-Klasse des neuen Raums - NewSocketIndex (Integer) – Index des Sockets am neuen Raum der als Eingang dient - NewSocketId (Name) – **NEU** – Socket-ID des Eingangs-Sockets am neuen Raum (für GetSocketWorldTransformById) **Outputs:** - SpawnedRoom (Actor Object Ref) - Success (Bool) **Lokale Variablen (in der Funktion anlegen):** - **SocketWorldPos** (Vector) – Weltposition des offenen Sockets am bestehenden Raum - **SocketWorldRot** (Rotator) – Weltrotation des offenen Sockets - **TargetRotation** (Rotator) – Ziel-Rotation für den neuen Raum (mit 180°-Drehung) - **TempRoom** (Actor Object Ref) – der temporär gespawnte neue Raum - **TempSocketLocation** (Vector) – Weltposition des Eingangs-Sockets am neuen Raum - **TempSocketOffset** (Vector) – Offset vom Actor-Origin zum Socket - **RotatedOffset** (Vector) – TempSocketOffset rotiert um TargetRotation - **FinalPosition** (Vector) – finale Spawn-Position des neuen Raums **Schritt 1: Socket-Weltposition holen (via Interface)** 1. → OpenSocket → **Break S_OpenSocketData** → OwnerRoom, SocketId 2. → OwnerRoom → **GetSocketWorldTransformById (Message)** (SocketId) → Location, Rotation, Success 3. → **Branch** (Success) - False ▶: **Print String** "ERROR: Socket not found!" → **Return** (SpawnedRoom: None, Success: false) 4. → Location → **Set SocketWorldPos** **Schritt 2: Ziel-Rotation berechnen (WARUM 180°?)** Wenn zwei Räume aneinander andocken, müssen ihre Sockets **einander zugewandt** sein. Ein Socket zeigt "nach außen" aus seinem Raum heraus. Der neue Raum muss also so rotiert werden, dass sein Eingangs-Socket in die **entgegengesetzte** Richtung des Dock-Sockets zeigt. Darum drehen wir um 180° auf der Z-Achse (Yaw). 5. → Rotation → **Combine Rotators** (A: Rotation, B: (Pitch: 0, Yaw: 180, Roll: 0)) → **Set TargetRotation** **Schritt 3: Neuen Raum spawnen** 6. → **SpawnActor** (Class: NewRoomClass, Location: (0, 0, 0), Rotation: (0, 0, 0)) → **Set TempRoom** - Wir spawnen bei 0,0,0 und bewegen danach – so können wir den Socket-Offset korrekt berechnen **Schritt 4: Offset des neuen Sockets berechnen** Der neue Raum wurde bei (0,0,0) ohne Rotation gespawnt. Jetzt berechnen wir, wo sein Eingangs-Socket relativ zum Actor-Origin liegt: 7. → TempRoom → **GetSocketWorldTransformById (Message)** (NewSocketId) → TempSocketLocation, TempSocketRotation, Success - Falls ById fehlschlägt (Success == false): **Print String** "ERROR: NewSocket not found!" → TempRoom → **Destroy Actor** → **Return** (SpawnedRoom: None, Success: false) 8. → TempSocketLocation **- (Subtract Vector)** TempRoom → **GetActorLocation** → **Set TempSocketOffset** - Das ist der Vektor vom Actor-Origin zum Socket, in Weltkoordinaten (bei Rotation 0) **Schritt 5: Offset rotieren** Da wir den Raum gleich um TargetRotation drehen werden, muss der Offset ebenfalls rotiert werden: 9. → TempSocketOffset → **Rotate Vector** (Rotator: TargetRotation) → **Set RotatedOffset** **Schritt 6: Finale Position berechnen** Der Socket des neuen Raums soll genau auf SocketWorldPos liegen. Also: Finale Position = SocketWorldPos - RotatedOffset 10. → SocketWorldPos **- (Subtract Vector)** RotatedOffset → **Set FinalPosition** **Schritt 7: Raum positionieren** 11. → TempRoom → **SetActorLocationAndRotation** (Location: FinalPosition, Rotation: TargetRotation, Teleport: **true**) **Schritt 8: Kollisionsprüfung** > **WICHTIG:** `Get Overlapping Actors` gibt ein read-only Array zurück – du kannst nicht direkt darauf entfernen. Erst in eine lokale Variable kopieren, dann entfernen. **Lokale Variable anlegen:** In SnapRoomToSocket → My Blueprint → Local Variables → `OverlapBuffer` (Actor Object Ref, Array) 12. → TempRoom → **Get Overlapping Actors** (kein Class Filter) → **SET OverlapBuffer** 13. → OverlapBuffer → **Remove** (Item: OpenSocket → Break S_OpenSocketData → OwnerRoom) ← Donor-Raum entfernen, er berührt den neuen Raum immer (false positive) 14. → OverlapBuffer → **Length** → **> 0** → **Branch** > **Hinweis:** TempRoom selbst ist nie in der OverlappingActors-Liste – `Get Overlapping Actors` gibt nur andere Actors zurück die mit dem Target überlappen, nicht das Target selbst. Ein Remove auf TempRoom ist daher unnötig. **True ▶ (Kollision mit anderem Raum):** 16. → **Print String** "Collision detected, destroying room" (Debug) 17. → TempRoom → **Destroy Actor** 18. → **Return** (SpawnedRoom: None, Success: **false**) **False ▶ (Keine Kollision – Erfolg):** 16. → **Return** (SpawnedRoom: TempRoom, Success: **true**) **Exec-Kette (KRITISCH – alle müssen verbunden sein):** ``` SpawnActor ▶ → GetSocketWorldTransformById ▶ → SetActorLocationAndRotation ▶ → Get Overlapping Actors ▶ → SET OverlapBuffer ▶ → Remove (OwnerRoom) ▶ → Branch ▶ → (Destroy oder Return) ``` --- ## Teil 6: Raum-Blueprints ### 6.1 Vorhandene Räume anpassen Für jeden Raum-Blueprint (Entrance, General, Corridor, PortalIn, PortalOut, Exit): 1. Arrow Components umbenennen (Socket_North, Socket_South, Socket_East, Socket_West, oder Socket_Entry/Socket_Exit für Corridors) 2. Variable `SocketDefinitions` hinzufügen (Typ: Array of S_SocketDefinition) 3. Variable `RoomType` hinzufügen (Typ: E_RoomType, Default: je nach Raum) 4. Construction Script umbauen (siehe Teil 2.3) 5. Interface-Funktionen implementieren: - **GetSocketDefinitions**: SocketDefinitions Variable → Return Node - **GetSocketWorldTransformById**: Implementierung siehe Teil 2.4 - **GetRoomType**: RoomType Variable → Return Node 6. **Entfernen** (falls vorhanden): - ~~SetIsOnMainPath~~, ~~GetIsOnMainPath~~ - ~~SetBranchDepth~~, ~~GetBranchDepth~~ - ~~GetSockets~~ (ersetzt durch GetSocketDefinitions) - Variablen ~~IsOnMainPath~~, ~~BranchDepth~~, ~~ConnectedRooms~~ 7. BoundingBox: Etwas kleiner als Boden (10-20 Units eingerückt), flach (nur Bodenhöhe), Collision Preset: **OverlapAllDynamic** ### 6.2 BP_DungeonRoom_DeadEnd erstellen - Neuer Actor Blueprint erstellen - **Components:** - DefaultSceneRoot - RoomMesh (Static Mesh Component – ein Raum mit nur einem Eingang) - BoundingBox (Box Collision, kleiner als Boden, flach, OverlapAllDynamic) - **Socket_Entry** (Arrow Component – zeigt zum Eingang hinaus) - **Variablen:** RoomType = DeadEnd, SocketDefinitions (Array of S_SocketDefinition) - **Construction Script:** Wie alle anderen Räume (Teil 2.3) - **Interface implementieren:** Wie alle anderen Räume (Teil 6.1 Schritt 5) - **In DT_RoomPool eintragen:** RoomClass: BP_DungeonRoom_DeadEnd, RoomType: DeadEnd, Weight: 1.0, MinLevel: 1, MaxLevel: 0 ### 6.3 BP_WallBlocker erstellen (falls noch nicht vorhanden) - Neuer Actor Blueprint - **Components:** - DefaultSceneRoot - WallMesh (Static Mesh Component – eine Wand die den Durchgang blockiert) - Kein Interface nötig, keine Variablen nötig - Wird nur von SealOpenSockets gespawnt ### 6.4 BoundingBox-Regeln - Etwas kleiner als der Raumboden (10–20 Units eingerückt auf jeder Seite) - Nur Bodenhöhe (flach) – Wände sollen NICHT kollidieren, sonst scheitert jeder benachbarte Raum - Collision Preset: **OverlapAllDynamic** - Collision Enabled: **Query Only** (keine Physik-Simulation) - Generate Overlap Events: **Ja** --- ## Teil 7: Wichtige Details und Fallstricke ### 7.1 Append-Reihenfolge - **Target (oberer Pin):** Das Array das befüllt wird - **Source (unterer Pin):** Die neuen Daten - Vertauschung überschreibt statt anzuhängen! ### 7.2 Socket-Weltposition live berechnen **NIEMALS** gespeicherte Positionen verwenden. **IMMER** GetSocketWorldTransformById/ByIndex auf dem OwnerRoom aufrufen. Grund: Wenn ein Actor bewegt wird (z.B. bei SetActorLocationAndRotation), werden gespeicherte Positionen ungültig. ### 7.3 Destroy Actor Target Beim Retry: Target = **Array Element** aus PlacedRoomActors, **NICHT** self! "self" würde den Generator selbst zerstören. ### 7.4 WallBlocker beim Retry aufräumen SpawnedBlockers Array durchgehen → Destroy Actor → Array Clear. ### 7.5 PortalIn Positionierung PortalIn Position = LastPortalOut.GetActorLocation() minus Vector(0, 0, **LevelVerticalOffset**). Dadurch liegt jedes Level um LevelVerticalOffset tiefer als das vorherige. ### 7.6 Get Components by Class auf frischen Actors Kann Length 0 zurückgeben direkt nach SpawnActor, besonders wenn das Construction Script noch nicht gelaufen ist. Deshalb immer Interface-Funktionen verwenden (GetSocketWorldTransformById), die auf die SocketDefinitions-Variable zugreifen. ### 7.7 Remove in Exec-Kette Der Remove-Node bei Get Overlapping Actors muss in der Exec-Kette liegen: ``` SetActorLocationAndRotation ▶ → Get Overlapping Actors ▶ → Remove ▶ → Branch ▶ ``` Ohne Exec-Verbindung wird Remove möglicherweise nicht ausgeführt bevor die Branch-Bedingung geprüft wird. ### 7.8 Return Node mit SpawnedRoom verbinden Im SnapRoomToSocket: Der Success-Return-Node muss **TempRoom** auf dem **SpawnedRoom** Output-Pin haben! Sonst gibt die Funktion None zurück obwohl der Raum erfolgreich gespawnt wurde. ### 7.9 ForEachLoop mit Break vs. ohne Break - **ForEachLoop** (Standard): Iteriert über ALLE Elemente, kann nicht vorzeitig beendet werden - **ForEachLoop with Break**: Hat einen zusätzlichen "Break" Exec-Pin um die Schleife vorzeitig zu beenden - Verwende **with Break** in: UpdateSocketState, FindCompatibleRoom (gewichtete Wahl), GetSocketWorldTransformById - Verwende Standard in: RegisterRoomSockets, SealOpenSockets, Socket-Verteilung ### 7.10 Struct-Vergleiche in Blueprints UE Blueprints können Structs nicht direkt mit == vergleichen. Um einen bestimmten Socket in AllTrackedSockets zu finden, vergleiche die **einzelnen Felder** (OwnerRoom == X AND SocketId == Y), nicht den gesamten Struct. ### 7.11 Array-Mutation während Iteration **NIEMALS** ein Array verändern (Add, Remove, Clear) während du mit ForEachLoop darüber iterierst. Das führt zu undefiniertem Verhalten. Lösung: Vor der Iteration eine **Kopie** des Arrays erstellen und über die Kopie iterieren. --- ## Teil 8: Test-Reihenfolge ### Phase 1: Grundgerüst 1. GenerateDungeon aufrufen → Print: Seed, TotalLevels, MaxRoomsOfLevel 2. BuildLevel → Entrance spawnt bei (0,0,0) → Print "Entrance spawned" 3. Print SocketDefinitions Length → muss > 0 sein 4. Print MainPathFrontier Length → muss genau 1 sein 5. Print AllTrackedSockets Length → muss == Anzahl Arrow Components am Entrance sein ### Phase 2: Positionierung 6. BuildMainPath → Erster Raum wird gespawnt → Print FinalPosition 7. Positionen müssen verschieden sein (nicht alle bei 0,0,0) 8. F8 im Play-Mode → Kamera freigeben → Räume visuell prüfen: Docken sie korrekt aneinander? 9. Print CurrentRoomsOnLevel nach jedem Raum → muss hochzählen ### Phase 3: Level-Übergang 10. PlaceEndRoom → PortalOut spawnt → Print "PortalOut placed" 11. CurrentLevel wird auf 2 erhöht 12. PortalIn spawnt unter PortalOut → Print PortalIn Location (Z muss um LevelVerticalOffset niedriger sein) 13. Zweites Level baut sich → Print Raumanzahl ### Phase 4: Exit und Branches 14. Letztes Level → Exit spawnt → Print "Exit placed" 15. BuildBranches wird aufgerufen → Print BranchFrontier Length vor dem Bau 16. Branches bauen sich → Print je Branch: Tiefe, Raumtyp 17. DeadEnd am Ende jeder Branch → Print "DeadEnd placed" ### Phase 5: Abschluss 18. SealOpenSockets → Print Anzahl versiegelter Sockets 19. WallBlocker an offenen Sockets sichtbar 20. PlacePlayer → Spieler steht im Entrance-Raum → Print "Complete!" ### Phase 6: Retry-Test 21. Künstlich einen Fehler provozieren (z.B. MaxRoomsPerLevel = 1) → RetryLevel muss feuern 22. Print Retry-Zähler → muss hochzählen 23. Nach MaxRetries → Abbruch-Meldung ### Debug-Empfehlungen - **Print String** mit RandomSeed ganz am Anfang (für Reproduzierbarkeit) - **Print String** mit FinalPosition in SnapRoomToSocket (sind die Positionen plausibel?) - **Print String** mit OwnerRoom → IsValid vor GetSocketWorldTransform (Raum noch vorhanden?) - **Print String** mit Arrow Count nach Get Components by Class (Sockets vorhanden?) - **DrawDebugBox** für jeden platzierten Raum an GetActorLocation (optional, zeigt Raumgrenzen) - **Print String** mit AllTrackedSockets Length nach jedem RegisterRoomSockets (wächst korrekt?) --- ## Anhang: Ablauf-Zusammenfassung ``` BeginPlay └→ GenerateDungeon ├─ Seed, Levels, Rooms berechnen └→ BuildLevel (Level 1) ├─ Spawn Entrance ├─ RegisterRoomSockets → AllTrackedSockets ├─ Socket-Verteilung: 1→MainPathFrontier, Rest→BranchFrontier └→ BuildMainPath ├─ While (Frontier>0 AND Rooms