- BP_DungeonGenerator: FindCompatibleRoom mit Data Table Filterung, gewichteter Zufallswahl und Socket-Matching - DT_RoomPool Data Table erstellt (General, Corridor, DeadEnd) - Raum-Blueprints aktualisiert (alle Raumtypen) - plan_v1.md: DT_RoomPool Anleitung ergänzt, UE5-spezifische Korrekturen (Get Data Table Row, Return Node Pins) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1253 lines
No EOL
63 KiB
Markdown
1253 lines
No EOL
63 KiB
Markdown
# 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<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.
|
||
|
||
**GetSocketWorldTransformByIndex** (existiert bereits, behalten als Fallback):
|
||
- Input: SocketIndex (Int)
|
||
- Output: Location (Vector), Rotation (Rotator)
|
||
- Implementierung: Get Components by Class → Get (Index) → GetWorldLocation + GetWorldRotation
|
||
|
||
### 2.5 BPI_DungeonRoom Interface aktualisieren
|
||
|
||
| Funktion | Inputs | Outputs |
|
||
|---|---|---|
|
||
| GetSocketDefinitions | – | SocketDefinitions (Array S_SocketDefinition) |
|
||
| GetSocketWorldTransformById | SocketId (Name) | Location (Vector), Rotation (Rotator), Success (Bool) |
|
||
| GetSocketWorldTransformByIndex | SocketIndex (Int) | Location (Vector), Rotation (Rotator) |
|
||
| 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 |
|
||
|
||
---
|
||
|
||
## 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** (Set Random Stream Seed oder Random Integer in Range falls Seed == 0)
|
||
3. → **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. → (While Loop nächste Iteration – der nächste Socket in MainPathFrontier wird versucht)
|
||
|
||
**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. → (While Loop nächste Iteration)
|
||
|
||
**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)
|
||
|
||
**Schritt 1: Socket-Weltposition holen (via Interface)**
|
||
|
||
1. → OpenSocket → **Break S_OpenSocketData** → OwnerRoom, SocketId, SocketIndex
|
||
2. → OwnerRoom → **GetSocketWorldTransformById (Message)** (SocketId) → Location, Rotation, Success
|
||
3. → **Branch** (Success)
|
||
- False ▶: OwnerRoom → **GetSocketWorldTransformByIndex (Message)** (SocketIndex) → Location, Rotation (Fallback)
|
||
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)** (MatchingSocketId) → TempSocketLocation, TempSocketRotation, Success
|
||
- **Bevorzugt ById** statt ByIndex, weil GetSocketWorldTransformById auf die SocketDefinitions-Variable zugreift (im Construction Script befüllt), während ByIndex über Get Components by Class geht, was auf frisch gespawnten Actors fehlschlagen kann (siehe 7.6)
|
||
- Falls ById fehlschlägt (Success == false): Fallback auf **GetSocketWorldTransformByIndex (Message)** (NewSocketIndex)
|
||
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**
|
||
|
||
12. → TempRoom → **Get Overlapping Actors** (kein Class Filter) → OverlappingActors Array
|
||
13. → OverlappingActors → **Remove** (Item: TempRoom) ← **MUSS in der Exec-Kette liegen!**
|
||
14. → OverlappingActors → **Length** → **> 0** → **Branch**
|
||
|
||
**True ▶ (Kollision mit anderem Raum):**
|
||
15. → **Print String** "Collision detected, destroying room" (Debug)
|
||
16. → TempRoom → **Destroy Actor**
|
||
17. → **Return** (SpawnedRoom: None, Success: **false**)
|
||
|
||
**False ▶ (Keine Kollision – Erfolg):**
|
||
15. → **Return** (SpawnedRoom: TempRoom, Success: **true**)
|
||
|
||
**Exec-Kette (KRITISCH – alle müssen verbunden sein):**
|
||
```
|
||
SpawnActor ▶ → GetSocketWorldTransformById ▶ → SetActorLocationAndRotation ▶
|
||
→ Get Overlapping Actors ▶ → Remove ▶ → 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
|
||
- **GetSocketWorldTransformByIndex**: Get Components by Class (Arrow) → Get (Index) → GetWorldLocation + GetWorldRotation → Return
|
||
- **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<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
|
||
``` |