DungeonCrawler-UE-C/Planung/plan_v1.md
sandr 5cbc932187 Update plan: RandomStream, retry limit, collision fix, loop exec fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 18:27:32 +02:00

1266 lines
No EOL
64 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.
5100 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<S_SocketData>`
- ~~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.01.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 (1020 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<Max):
│ ├─ FindCompatibleRoom
│ ├─ SnapRoomToSocket
│ ├─ UpdateSocketState (Occupied)
│ └─ Socket-Verteilung: 1→Occupied, 1→MainPath, Rest→Branch
└→ PlaceEndRoom
├─ Nicht letztes Level → PortalOut
│ ├─ CurrentLevel + 1
│ └→ BuildLevel (nächstes Level)
│ ├─ Spawn PortalIn (unter LastPortalOut)
│ └→ BuildMainPath → PlaceEndRoom → ...
└─ Letztes Level → Exit
└→ BuildBranches
├─ Kopie von BranchFrontier
├─ ForEach Branch-Socket:
│ ├─ While (Depth < MaxDepth):
│ │ ├─ Letzter Raum → DeadEnd
│ │ └─ Sonst → FindCompatibleRoom + Snap
│ └─ Break bei Fehler
└→ SealOpenSockets
├─ Alle Open Sockets → WallBlocker
└→ PlacePlayer
└─ Spieler bei Entrance platzieren
```