Spring Sale: 30% off bundles with SPRINGBUNDLE or 15% off individual products with SPRING15 — ends Apr 15

StraySparkStraySpark
ProductsFree AssetsDocsBlogGamesAbout
StraySparkStraySpark

Game Studio & UE5 Tool Developers. Building professional-grade tools for the Unreal Engine community.

Products

  • Complete Toolkit (Bundle)
  • Procedural Placement Tool
  • Cinematic Spline Tool
  • Blueprint Template Library
  • DetailForge
  • UltraWire
  • Unreal MCP Server
  • Blender MCP Server
  • Godot MCP Server

Resources

  • Free Assets
  • Documentation
  • Blog
  • Changelog
  • Roadmap
  • FAQ
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 StraySpark. All rights reserved.

Back to Blog
tutorial
StraySparkMarch 23, 20265 min read
Building a Roguelike in UE5: Procedural Generation, Run Structure, and Meta-Progression 
Unreal EngineRoguelikeProcedural GenerationGame DesignTutorial

Why Roguelikes Keep Growing

Roguelikes are one of the most commercially successful indie genres. Hades, Slay the Spire, Balatro, Vampire Survivors — the genre produces breakout hits consistently. The reason: roguelikes have infinite replayability built into their structure, and they're scope-manageable for small teams.

A roguelike's content comes from combinations, not quantity. 20 weapons × 30 upgrades × 15 room layouts = thousands of unique runs. One level designer's work creates months of playtime.

Core Architecture

The Run Loop

Every roguelike follows this structure:

[Meta Hub] → [Start Run] → [Room/Encounter] → [Reward Choice] →
[Room/Encounter] → [Reward Choice] → ... → [Boss] →
[Next Floor] → ... → [Death or Victory] → [Meta Progression] → [Meta Hub]

System Component Architecture

// Key systems for a roguelike
UCLASS()
class URunManager : public UGameInstanceSubsystem
{
    GENERATED_BODY()

public:
    void StartNewRun(int32 Seed = 0);
    void CompleteRoom(ERoomResult Result);
    void EndRun(bool bVictory);

    UPROPERTY()
    FRunState CurrentRun;  // Current run data

    UPROPERTY()
    FMetaProgressionData MetaData;  // Persistent across runs
};

USTRUCT()
struct FRunState
{
    GENERATED_BODY()

    int32 Seed;
    int32 CurrentFloor;
    int32 CurrentRoom;
    TArray<FName> CollectedItems;
    TArray<FName> ActiveUpgrades;
    int32 Gold;
    float RunTime;
    int32 EnemiesKilled;
};

Procedural Level Generation

Room-Based Generation

The most common roguelike approach: pre-designed rooms connected procedurally.

Step 1: Define room templates

Create 50-100+ room layouts in the editor:

  • 10+ combat rooms (varying difficulty and enemy composition)
  • 5+ reward rooms (item shops, upgrade altars)
  • 5+ event rooms (story encounters, NPCs, challenges)
  • 5+ rest rooms (healing, item management)
  • 3+ boss rooms
  • 5+ corridor/transition rooms

Step 2: Tag rooms with metadata

USTRUCT()
struct FRoomTemplate
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere)
    TSoftObjectPtr<UWorld> RoomLevel;

    UPROPERTY(EditAnywhere)
    ERoomType Type; // Combat, Reward, Event, Rest, Boss

    UPROPERTY(EditAnywhere)
    int32 DifficultyTier; // 1-5

    UPROPERTY(EditAnywhere)
    int32 MinFloor; // Don't appear before this floor

    UPROPERTY(EditAnywhere)
    TArray<FName> RequiredDoors; // North, South, East, West
};

Step 3: Generate the floor layout

void UFloorGenerator::GenerateFloor(int32 FloorIndex, FRandomStream& RNG)
{
    // Create a grid-based map
    TArray<FIntPoint> RoomPositions;
    FIntPoint CurrentPos(0, 0);
    RoomPositions.Add(CurrentPos);

    // Random walk to create connected room positions
    for (int32 i = 0; i < RoomsPerFloor; i++)
    {
        // Pick random adjacent cell that isn't occupied
        TArray<FIntPoint> ValidNeighbors = GetUnoccupiedNeighbors(CurrentPos, RoomPositions);
        if (ValidNeighbors.Num() > 0)
        {
            CurrentPos = ValidNeighbors[RNG.RandHelper(ValidNeighbors.Num())];
            RoomPositions.Add(CurrentPos);
        }
    }

    // Assign room types based on position
    // First room: starting room
    // Last room: boss
    // Others: weighted random based on floor difficulty
    AssignRoomTypes(RoomPositions, FloorIndex, RNG);
}

Map Shapes

Different generation algorithms create different map feels:

Linear path: Room → Room → Room → Boss. Simple, fast-paced.

Branching tree: Fork at junctions, player chooses path. Strategic.

Grid/dungeon: Connected rooms on a grid. Exploratory.

Graph-based: Rooms connected by edges with complex topology. Slay the Spire style.

Seed System

Seeds make runs reproducible:

void URunManager::StartNewRun(int32 Seed)
{
    if (Seed == 0)
        Seed = FMath::Rand(); // Random seed if none provided

    CurrentRun.Seed = Seed;
    FRandomStream RNG(Seed);

    // All procedural decisions use this stream
    FloorGenerator->GenerateFloor(1, RNG);
    ItemPool->ShufflePool(RNG);
    EnemySpawner->ConfigureSpawns(RNG);
}

Share seeds between players for competitive runs ("Try seed 42069 — it's brutal") or use them for daily challenges (everyone plays the same seed).

Item and Upgrade Design

The Power Fantasy Curve

Roguelike items should make the player feel increasingly powerful throughout a run:

Power
  ▲
  │                      ╱
  │                    ╱
  │                 ╱
  │              ╱
  │           ╱
  │        ╱
  │     ╱
  │  ╱
  │╱
  └──────────────────────────▶ Run Progress
  Start    Early    Mid    Late    Boss

By the end of a successful run, the player should feel dramatically more powerful than when they started.

Item Design Principles

Synergies create depth: Items should interact with each other in interesting ways.

Item: "Burning Blade" — Attacks inflict Burn
Item: "Explosion on Burn" — Burning enemies explode when killed
Item: "Chain Reaction" — Explosions spread Burn to nearby enemies

Combined: Every kill starts a chain reaction of burning explosions.

Categories for balance: Organize items into categories:

  • Offensive: Damage, attack speed, crit chance
  • Defensive: Health, armor, dodge, lifesteal
  • Utility: Movement speed, pickup radius, XP gain
  • Synergy: Items that modify how other items work

Rarity and weighting: Control power creep with rarity tiers:

  • Common (60% of pool): Small, reliable buffs
  • Uncommon (25%): Stronger, more specific bonuses
  • Rare (12%): Build-defining items with strong effects
  • Legendary (3%): Game-changing items that reshape playstyle

Item Pool Management

UCLASS()
class UItemPool : public UObject
{
public:
    FName DrawItem(ERarity MaxRarity, FRandomStream& RNG);

    void RemoveItemFromPool(FName ItemId); // Items already found don't reappear
    void AddItemToPool(FName ItemId); // Unlocked items enter the pool

private:
    TArray<FWeightedItem> AvailableItems;
    TArray<FName> DrawnThisRun; // Prevent duplicates
};

Meta-Progression

What Persists Between Runs

Meta-progression keeps players coming back after death:

  • Permanent unlocks: New characters, weapons, items added to the pool
  • Upgrades: Small permanent stat bonuses (health +5%, damage +3%)
  • Knowledge: Bestiary entries, lore fragments, map reveals
  • Cosmetics: Character skins, visual effects, victory screens
  • Currency: Meta-currency earned per run, spent at the hub

Designing Meta-Progression

Horizontal, not vertical: Unlock variety (new items, new characters), not power. A player on their 100th run should have more options than a new player, but not be 10x stronger. The skill improvement IS the progression.

Breadth early, depth later: First unlocks should expand the item/character pool dramatically (lots of new options quickly). Later unlocks are more niche or cosmetic.

Run-earned currency: Currency earned scales with run performance, not just victories. Dying on floor 3 should still earn something. This prevents frustration loops where weak players can't earn enough to improve.

Hub Design

The meta hub is where players spend between runs:

  • Persistent NPC: Vendor/mentor who accepts meta-currency for upgrades
  • Trophy room: Visual representation of achievements and progress
  • Character select: Choose starting character/loadout
  • Challenge board: Daily challenges, special modifiers
  • Lore archive: Collected lore and story fragments

Balancing a Roguelike

The Balance Challenge

Roguelike balance is uniquely difficult because you're balancing combinations, not individual items. Item A might be balanced alone, and Item B might be balanced alone, but together they break the game.

Approaches

Playtest extensively: The only reliable way to find broken combinations is to play thousands of runs. Use automated playtesting where possible.

Cap stacking: Limit how many times the same effect can stack (max 5 burn stacks, max 200% attack speed).

Diminishing returns: Each instance of the same stat gives less benefit (first +10% speed, second +8%, third +5%).

Anti-synergy items: Some items should deliberately not combine well, forcing players to make choices rather than collecting everything.

Run difficulty scaling: As the player gets stronger, increase enemy scaling. This prevents any build from trivializing the game.

The Death Loop: Why Players Keep Playing

Making Death Meaningful, Not Frustrating

Death in a roguelike should feel like:

  • "I almost had that run" (motivates another attempt)
  • "Next time I'll prioritize defensive items" (learning)
  • "I unlocked a new weapon, let me try that" (anticipation)

Death should NOT feel like:

  • "That was unfair" (balance problem)
  • "I just lost an hour of progress" (run length problem)
  • "Every run feels the same" (variety problem)

Run Length Sweet Spot

Run DurationPlayer ExperienceBest For
5-15 minQuick, arcade feel. Easy to start "one more run"Mobile, casual roguelites
20-40 minStandard. Long enough to build power, short enough to not sting on deathMost roguelikes
45-90 minEpic runs. Each death feels significant.Hardcore roguelikes, story-focused
2+ hoursVery long. Needs save-and-quit between sessions.Traditional roguelikes

The 20-40 minute sweet spot is where most commercially successful roguelikes land. It's the "one more run" zone.

Information on Death

When the player dies, show them:

  • How far they got (floor/room number)
  • What killed them (enemy, trap, boss)
  • Run statistics (time, kills, items collected)
  • Meta-currency earned
  • New unlocks from this run
  • "Try again" button prominently displayed

Make death a moment of reflection and anticipation, not frustration.

Roguelikes are one of the best-suited genres for indie development — deep gameplay from systems, not content volume. Get the core loop right (satisfying combat + meaningful choices + fair but challenging difficulty + meta-progression that adds variety), and you have a game players will sink hundreds of hours into.

Tags

Unreal EngineRoguelikeProcedural GenerationGame DesignTutorial

Continue Reading

tutorial

World Partition Deep Dive: Streaming, Data Layers, and HLOD for Massive Open Worlds

Read more
tutorial

Motion Matching and Control Rig in UE5: The Future of Character Animation

Read more
tutorial

CI/CD Build Pipelines for UE5: Unreal Horde, GitHub Actions, and Jenkins

Read more
All posts