The workbook for this chapter is here: Chapter 2 Workbook
In the genealogy of procedural generation, Rogue (1980)[1] stands as the mitochondrial Eve. While modern titles like No Man's Sky or Minecraft utilize continuous noise functions—Perlin, Simplex, or Voronoi—to generate infinite terrain, Rogue operated on a fundamentally different, discrete architectural philosophy. It did not seek to simulate a world; it sought to simulate a dungeon. This distinction is critical. A world is open, continuous, and biomic. A dungeon is closed, discrete, and topological.
To understand how to implement Rogue[1:1] in Unreal Engine 5.7, we must first deconstruct the limitations of the PDP-11 and VAX-11/780 systems for which it was originally written. The developers, Michael Toy and Ken Arnold, were constrained by the curses library and a 24x80 character terminal. They could not afford the memory for a cellular automata simulation, nor the cycles for complex binary space partitioning (BSP). Their solution was elegant in its simplicity and profound in its gameplay implications: the 3x3 Grid.
This chapter is a comprehensive technical treatise on porting this 45-year-old algorithm into the cutting-edge ecosystem of Unreal Engine 5.7. We will not merely replicate the logic; we will re-engineer it using Data-Oriented Design (DOD) principles native to the Procedural Content Generation (PCG) Framework. We will explore the transition from C-style structs to Unreal's reflected USTRUCT, the shift from UPCGPointData to the high-performance UPCGPointArrayData introduced in UE 5.6, and the vector mathematics required to generate "dogleg" corridors in 3D space.
The resulting system will be a hybrid: possessing the structural predictability of Rogue (guaranteeing a playable, connected level) with the visual fidelity of Nanite and Lumen.
The fundamental unit of space in Rogue is not the tile, but the Sector. The screen (or map) is subdivided into nine equal sectors, arranged in a 3-column by 3-row lattice.
Sector Connectivity
| Sector Index | Grid Coordinate | Connectivity Potential |
|---|---|---|
| 0 | (0, 0) | Right (1), Down (3) |
| 1 | (1, 0) | Left (0), Right (2), Down (4) |
| 2 | (2, 0) | Left (1), Down (5) |
| 3 | (0, 1) | Up (0), Right (4), Down (6) |
| 4 | (1, 1) | Up (1), Left (3), Right (5), Down (7) |
| 5 | (2, 1) | Up (2), Left (4), Down (8) |
| 6 | (0, 2) | Up (3), Right (7) |
| 7 | (1, 2) | Up (4), Left (6), Right (8) |
| 8 | (2, 2) | Up (5), Left (7) |
Table 2.1: The connectivity matrix of the Rogue 3x3 grid. Note that Sector 4 (the center) has the highest degree of connectivity (4 neighbors), making it a critical hub in the dungeon's graph.
This structure solves the "Overlap Problem" instantly. In free-form placement algorithms (like the "Drunkard's Walk" or "Random Rectangles"), the generator must constantly check for collisions between new rooms and existing ones—an operation. In the 3x3 grid, a room generated in Sector 0 is mathematically guaranteed never to overlap with a room in Sector 1, provided the room's maximum dimensions are smaller than the sector's dimensions.
In Unreal Engine, we define these sectors in World Space coordinates. If our dungeon is units, each sector is .
A defining characteristic of Rogue is its sparseness. Not every sector contains a room. The rooms.c source code from the 5.4.4 distribution reveals a flag—often referred to as IS_GONE.
When the generator iterates through the 9 sectors, it rolls a probability check (typically ~10–15%). If the check fails, the room is marked "Gone."
However, a "Gone" room presents a topological challenge. If Sector 4 is "Gone," how does the player travel from Sector 1 to Sector 7?
Corridors in Rogue are rarely straight lines. A straight line implies that the door of Room A is perfectly aligned on the Cartesian axis with the door of Room B. Given the random internal placement of rooms within their sectors, this alignment is statistically impossible.
To resolve this, Rogue utilizes the Dogleg Corridor (also known as an L-shaped connector). A dogleg consists of two segments connected by an "Elbow" (turn).
Let be the exit point of the Source Room. Let be the entry point of the Target Room.
There are two possible geometric solutions for a Manhattan-distance path (ignoring obstacles):
The choice between these two solutions creates the variability in the dungeon's layout. In the C++ implementation, a FRandomStream is used to deterministically select between these two paths, ensuring that sharing a "Level Seed" produces identical corridor geometry on different machines.
The implementation of this system in UE 5.7 requires a nuanced understanding of the PCG Framework's evolution. In versions 5.0 through 5.5, PCG data was primarily handled via UPCGPointData, which stored points as a TArray. This was functional but memory-inefficient for massive point clouds due to padding and alignment overheads in the struct.
UPCGPointArrayDataAs of Unreal Engine 5.6 and solidified in 5.7, Epic Games has transitioned to UPCGPointArrayData. This class utilizes a Structure-of-Arrays (SoA) memory layout under the hood for certain attributes, or at least optimized contiguous memory blocks that are friendlier to the CPU cache and SIMD (Single Instruction, Multiple Data) operations.
Implication for C++ Developers:
We can no longer simply Cast<UPCGPointData>(Data). We must handle the UPCGPointArrayData type, particularly when writing custom PCG elements. Attempting to cast to the older type will result in nullptr and a failed generation graph. The implementation will explicitly handle this modern data type to ensure future-proofing.
The system is composed of three distinct C++ classes:
URogueDungeonConfig (Data Asset): A strictly typed configuration object holding the "Rules" of the dungeon (sizes, seeds, spawn rates).FPCGRogueDungeonElement (Custom PCG Node): The execution worker that runs on a background thread, performing the grid calculations and generating the point cloud.UPCGRogueDungeonSettings (PCG Settings): The editor-facing node that exposes the parameters to the PCG Graph.This separation follows the Model-View-Controller (MVC) pattern:
URogueDungeonConfigFPCGRogueDungeonElementWe begin by defining the parameters. We use a UDataAsset or a USTRUCT embedded in the settings. For flexibility in the PCG graph, a USTRUCT is preferred as it allows pin overrides (e.g., driving the "Room Size" logic based on a difficulty curve).
// RogueDungeonConfig.h
#pragma once
#include "CoreMinimal.h"
#include "RogueDungeonConfig.generated.h"
/**
* Configuration structure for the Rogue Dungeon Generator.
* Passed into the PCG node to control the 3x3 grid logic.
*/
USTRUCT(BlueprintType)
struct FRogueDungeonConfig
{
GENERATED_BODY()
// The total size of the dungeon in Unreal Units (e.g., 9000x9000).
// In Rogue, this maps to the 80x24 terminal grid.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Rogue|Dimensions")
FVector2D DungeonSize = FVector2D(9000.0f, 6000.0f);
// Minimum dimensions for a single room.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Rogue|Dimensions")
FVector2D MinRoomSize = FVector2D(600.0f, 600.0f);
// Maximum dimensions for a single room.
// Must be smaller than (DungeonSize / 3) to prevent sector overlap.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Rogue|Dimensions")
FVector2D MaxRoomSize = FVector2D(1200.0f, 1200.0f);
// The probability (0.0 - 1.0) that a room is "Gone" (skipped).
// Rogue 5.4.4 used a complex check, roughly approximating 10–15%.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Rogue|Generation", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float RoomSkipChance = 0.15f;
// The size of a single tile in Unreal Units. Used for rasterization.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Rogue|Rasterization")
float TileSize = 100.0f;
// Deterministic seed. If -1, a random seed is generated.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Rogue|Seed")
int32 Seed = 42;
};
We abstract the logic into a helper class, RogueGenHelpers. This ensures that the algorithmic code is not polluted by Unreal's UObject overhead, keeping it pure and testable.
These are transient C++ structs used only during the generation phase. They are not UObjects.
// RogueGenHelpers.h
#pragma once
#include "CoreMinimal.h"
struct FRogueRoom
{
int32 ID; // 0–8
int32 GridX; // 0–2
int32 GridY; // 0–2
FBox2D Bounds; // World Space Bounds
bool bIsGone; // If true, this room is skipped
bool bIsDark; // Fog of War logic (future use)
// Connectivity
TArray<int32> ConnectedRoomIDs;
FRogueRoom()
: ID(-1), GridX(0), GridY(0), bIsGone(false), bIsDark(false)
{}
};
struct FRogueCorridor
{
FVector StartPoint;
FVector ElbowPoint;
FVector EndPoint;
bool bIsVerticalFirst;
int32 FromRoomID;
int32 ToRoomID;
};
This class performs the 3x3 calculation. It replaces the rooms.c logic from the original BSD source.
Algorithm Deep Dive: The Random Number Generator (RNG)
Rogue used a Linear Congruential Generator (LCG) which is notoriously poor by modern standards. UE 5.7 uses FRandomStream (typically based on PCG-XSH-RR or similar robust algorithms). FRandomStream is used to ensure that dungeon generation is stable across different platforms (Windows vs Console), which is crucial for features like "Daily Runs" where all players must see the same dungeon.
The dogleg logic requires careful vector arithmetic. We must select a point on the Source room's wall and a point on the Target room's wall.
Insight: The randomization of the Elbow point gives Rogue its distinct "maze-like" feel even in non-maze levels. It prevents the player from predicting exactly where the door to the next room lies, maintaining the cognitive load of exploration.
We now wrap this logic in a UPCGSettings and FSimplePCGElement class. This allows use of the node in the PCG Graph Editor.
UPCGRogueGenSettings)This class defines the input pins, output pins, and editable properties.
FPCGRogueGenElement)This is the implementation that runs on the worker thread. This is where UE 5.7-specific UPCGPointArrayData handling becomes important.
Crucial Update for UE 5.6/5.7: In previous versions, we would NewObject<UPCGPointData>(). In 5.7, while UPCGPointData still exists, the engine defaults to UPCGPointArrayData for performance. However, for creating new data from scratch (as we are doing), UPCGPointData is still the standard API surface for C++ creation unless we manually manipulate the underlying FPCGPoint buffers of an array data.
Since we are generating, we will create UPCGPointData and the PCG framework will automatically optimize it into an Array Data structure if required during the graph compilation or data passing phases.
In the code above, we rasterize the points directly into the buffer. In a production scenario involving thousands of rooms (e.g., a Rogue "Megadungeon"), creating individual FPCGPoints for every tile is memory intensive.
Unreal Engine 5.7 offers an alternative: Spline Generation. Instead of points, UPCGSplineData could be output. The FRogueCorridor struct is perfectly suited for this—Start, Elbow, and End define a polyline. We could then use the new Spline Sampler node in the graph to generate the floor tiles only when needed (e.g., near the player), leveraging the Nanite streaming system implicitly.
However, for a faithful Rogue recreation (where the map is static and fully known), the point cloud approach is robust and easiest to debug.
Visualizing procedural generation is notoriously difficult because the data exists only in memory until the very end of the pipeline. To mitigate this, DrawDebugBox and related helpers are used.
ExecuteInternal can be modified to draw debug shapes when in the Editor. This allows the "Skeleton" of the dungeon to be seen before any meshes are spawned.
By visualizing the bounds immediately, the "Gone" logic and "Dogleg" connections can be verified without waiting for the StaticMeshSpawner to load assets. This rapid iteration loop is essential for tuning parameters like RoomSkipChance.
By implementing the Rogue dungeon generation algorithm in C++ and exposing it to the Unreal Engine 5.7 PCG framework, the system becomes both historically authentic and technologically modern. The constraints of the 3x3 grid are respected to guarantee connectivity and navigable topology, while UPCGPointArrayData (implicitly via UPCGPointData) ensures efficient memory usage.
The result is a reusable PCG node that generates a dungeon "skeleton." In the next chapter, this skeleton will be draped in the flesh of high-fidelity assets, using the Rooms and Corridors output pins to drive distinct material and mesh selection strategies—creating a dungeon that plays like 1980 but looks like 2025.
RogueGenHelpers) from the engine (FPCGRogueGenElement) allows for thread-safe, performant generation in UE 5.7.UPCGPointArrayData vs UPCGPointData is critical for casting and data manipulation in modern Unreal Engine versions.This implementation provides the foundation upon which complex gameplay systems—monster spawning, treasure placement, and traps—can be built, just as Toy and Arnold intended forty-five years ago.
Rogue (video game) https://en.wikipedia.org/wiki/Rogue_(video_game) – The original 1980 procedural dungeon generation game for the PDP-11. ↩︎ ↩︎