Notes on Procedural Map Generation Techniques
- Introduction
- Examples of Procedural Generation in Games
- Map Generation Algorithms
- Combining Techniques
- Dijkstra Maps
- Conclusion
- Additional Resources
- Video: Herbert Wolverson - Procedural Map Generation Techniques
- Source Code for Talk: GitHub Repository
- Online Book: Roguelike Tutorial - In Rust
Introduction
- Procedural Map Generation: Using algorithms to create maps for games, and then using additional algorithms to refine and ensure usability. (Source: Speaker’s definition)
- Benefits of Procedural Generation:
- Infinite Replayability: Creates unique maps each playthrough, preventing memorization and encouraging players to learn game systems. (citing Rogue as an example)
- Dynamic Gameplay: Prevents static level design, enhancing replayability.
Examples of Procedural Generation in Games
- Rogue (1980):
- One of the first examples of procedural generation.
- Used simple room placement and random corridors to keep game size small.
- Resulted in a unique map for each new game.
- Dwarf Fortress:
- Extensive use of procedural generation for various aspects of the game.
- Examples:
- Overworld generation: Mountains, forests, volcanoes, etc.
- Civilizations: History, relationships, deities, etc.
- Individual characters: Beliefs, personalities, histories, etc.
- Key Takeaway:
- Randomness in these games is not purely random.
- Algorithms guide the randomness to create desired outcomes while ensuring variation.
Map Generation Algorithms
Simple Room Placement
- Algorithm:
- Start with a solid map (all walls).
- Pick a random rectangle for a room.
- If the rectangle doesn’t overlap existing rooms, carve it out.
- Repeat steps 2-3 until a desired number of rooms are placed.
- Connect rooms with corridors. (referencing Python roguelike dev tutorial)
- Dogleg Corridors: A simple corridor generation method that randomly switches between vertical and horizontal segments.
- Limitations: Can result in clustered rooms and inefficient layouts.
Binary Space Partition (BSP) Rooms
- Binary Space Partition (BSP): Recursively dividing a space into two parts.
- Algorithm:
- Divide the map in half (either vertically or horizontally).
- Recursively divide each half until the desired room size is reached.
- Optionally add a gutter (1 tile border) around each room to prevent merging.
- Benefits:
- Better room spacing compared to simple room placement.
- Used in games like NetHack.
- Limitations: Can lead to predictable, rectangular layouts.
Cellular Automata
- Based on the principle of Conway’s Game of Life, where simple rules govern the evolution of a grid-based system.
- Algorithm:
- Initialize the map randomly with walls and floors (approximately 50/50).
- Iterate through each tile (excluding edges):
- Count the number of neighboring walls (including diagonals).
- Apply rules based on neighbor count:
- 0 neighbors: Become a wall.
- 1-4 neighbors: Become a floor.
- 5+ neighbors: Become a wall. (suggesting rule customization)
- Repeat step 2 for a set number of iterations.
- Benefits:
- Creates organic, natural-looking maps from random initial states.
- Simple and fast algorithm.
- Deterministic: Same seed produces the same map.
- Limitations: Can be difficult to control the specific shapes and features generated.
Drunkard’s Walk
- Algorithm:
- Start with a solid map.
- Place a “drunkard” (e.g., a digging entity) at a random point.
- The drunkard moves randomly, carving out a path as it goes.
- Set a maximum distance for the drunkard to travel before it “passes out”.
- Repeat steps 2-4, spawning new drunkards within the open areas, until a desired percentage of the map is open. (using 1/3 as an example)
- Benefits:
- Guarantees a contiguous map (no unreachable areas).
- Creates maps that resemble natural formations like caverns.
- Limitations: Can lead to winding, inefficient paths.
Diffusion-Limited Aggregation (DLA)
Algorithm:
- Start with a small “seed” of open tiles.
- Pick a random point on the map and shoot a “particle” in a random direction.
- If the particle hits an open tile, carve out the last solid tile it passed through.
- If the particle doesn’t hit an open tile, keep shooting until it does.
Benefits:
- Creates winding, branching open areas.
- Guarantees a contiguous map.
Variations:
Central Attractor: Particles are shot towards the center of the map, creating a central open area surrounded by a more complex perimeter. (suggesting dragon hoard placement in the center)
Symmetry: Applying symmetry to the algorithm can create more structured and deliberate patterns.
Voronoi Diagrams
Algorithm:
- Place “seed” points randomly or deliberately across the map.
- For each tile on the map, determine the closest seed point using a distance algorithm (e.g., Euclidean, Manhattan, Chebyshev).
- Assign the tile to the region belonging to its closest seed point.
Benefits:
Creates regions that represent areas of influence around each seed point.
Can be used for various purposes (e.g., city generation, monster placement). (referencing Apocalypse Taxi)
Distance Algorithms:
- Euclidean (Pythagoras): Standard straight-line distance, resulting in smooth edges.
- Manhattan: Distance measured as the sum of horizontal and vertical steps, creating sharp, grid-like edges.
- Chebyshev: Distance measured as the maximum of the horizontal and vertical distances, producing a mix between Euclidean and Manhattan.
Uses:
- Alien cell structures (walls along edges).
- Monster placement based on relationships (e.g., keeping allies together, separating enemies).
- City generation (roads along edges, different regions for different city zones).
Perlin and Simplex Noise
- Perlin Noise and Simplex Noise: Algorithms that generate continuous, smoothly varying values across a space.
- Properties:
- Output values typically range from -1 to 1.
- Adjacent values are smoothly related.
- Continuous: Zooming in on a section of the noise produces a similar pattern at a finer scale.
- Variables:
- Octaves: Number of different noise functions blended together, affecting detail.
- Gain: How much each octave contributes to the final result, affecting amplitude.
- Lacunarity: Adjusts the frequency of each octave, introducing randomness and detail.
- Frequency: How quickly the noise values change across space, affecting the scale of features.
- Uses:
- Overworld generation: Creating terrain heightmaps.
- Cloud generation.
- Particle effects.
- Wood grain textures.
- Recommendations: Experiment with different Perlin/Simplex noise tools and variable values to achieve desired results.
Combining Techniques
- Key Concept: Rarely use just one algorithm for map generation.
- Examples:
- Combining BSP (for a fortress) with Cellular Automata (for an underworld) to create a multi-themed map.
- Adding prefabs (e.g., fortifications) to enhance the combined map and tell a story.
- Using DLA to carve out sections of a BSP or Cellular Automata map, creating a more organic and weathered look.
Prefabs
- Prefabs: Pre-designed map sections that can be inserted into a procedurally generated map.
- Benefits:
- Introduce deliberate design elements into a random map.
- Add specific features, challenges, or story elements. (using a trap room as an example)
- Placement:
- In room-based maps: Find a room large enough to accommodate the prefab.
- In non-room-based maps: Randomly sample locations and check for fit.
- Considerations: Use prefabs sparingly to maintain variety and avoid predictability.
Dijkstra Maps
- Explanation
- Dijkstra Maps: Represent the distance from one or more starting points to all other reachable points on a map. (referencing Rogue Basin article “The Incredible Power of Dijkstra Maps”)
- Algorithm:
- Initialize the map with a sentinel value (representing unreachable areas).
- Set the starting point(s) to a value of 0.
- Iterate through the map, assigning increasing distance values to reachable tiles based on their distance from the starting point(s).
- Uses:
- Identifying and removing unreachable areas in maps generated with algorithms like Cellular Automata.
- Placing starting and ending points:
- Starting Point: Find an open tile near a desired location that is reachable from the main map area.
- Ending Point:
- For simple progression, place near the opposite edge of the map from the starting point.
- To hide secrets, place in the least accessible area.
- Hot Path Analysis:
- Generate a path between the starting and ending points (e.g., using A*).
- Create a Dijkstra map using the points on the path as starting points.
- Tiles with low distance values on the Dijkstra map represent areas close to the optimal path. (using a threshold of 10 as an example)
- Applications of Hot Path Analysis:
- Removing irrelevant map sections in linear progression games.
- Hiding bonus content or challenges in areas off the hot path.
- Ordering story elements or puzzle placement based on likely player progression. (using examples of grandfather’s advice and a locked door puzzle)
Conclusion
- Procedural map generation involves guiding randomness with algorithms to create diverse and engaging maps.
- Combining multiple techniques and using tools like Dijkstra maps can lead to more complex and interesting results.
- The choice of algorithms and their parameters should be driven by the desired gameplay experience and the story you want to tell through your map.
Additional Resources
- Source code for the talk: https://github.com/thebracket/roguelike-celebration-2020
- Rust Roguelike Tutorial: https://bfnightly.bracketproductions.com/rustbook/
- “The Incredible Power of Dijkstra Maps” (Rogue Basin): http://www.roguebasin.com/index.php?title=The_Incredible_Power_of_Dijkstra_Maps
- “Hands on Rust, Effective Learning Through 2D Game Development and Play” (PragProg): https://pragprog.com/
I’m Christian Mills, a deep learning consultant specializing in practical AI implementations. I help clients leverage cutting-edge AI technologies to solve real-world problems.
Interested in working together? Fill out my Quick AI Project Assessment form or learn more about me.