Table of Contents
We’ve reached the final chapter of our mini-series on how to create a 2D roguelike in Unity, inspired by the open-source project 2DRogueTest.
In Part 1, we set up the project, imported the graphics, and implemented the basic player movement. In Part 2, we added enemies, artificial intelligence, and a simple but effective combat system.
Now, to complete the roguelike experience, we’ll tackle procedural room generation: a system that allows us to create a different map in every playthrough, increasing replayability and the thrill of the unknown.
1. The Heart of the System: the DungeonGenerator Class
In the project, we find the /Scripts/DungeonGenerator.cs
script, responsible for dynamically creating rooms and arranging them in space.
This script uses a list of room prefabs (found in /Prefabs/Rooms/
) and instantiates them at runtime following a simple expansion logic.
Here’s an overview of the main code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public class DungeonGenerator : MonoBehaviour { public GameObject[] roomPrefabs; public int numberOfRooms = 5; private List<Vector2Int> usedPositions = new(); void Start() { GenerateDungeon(); } void GenerateDungeon() { Vector2Int currentPos = Vector2Int.zero; usedPositions.Add(currentPos); Instantiate(roomPrefabs[0], Vector3.zero, Quaternion.identity); for (int i = 1; i < numberOfRooms; i++) { Vector2Int nextPos; do { nextPos = currentPos + GetRandomDirection(); } while (usedPositions.Contains(nextPos)); usedPositions.Add(nextPos); Vector3 worldPos = new Vector3(nextPos.x * 16, nextPos.y * 10, 0); Instantiate(roomPrefabs[Random.Range(0, roomPrefabs.Length)], worldPos, Quaternion.identity); currentPos = nextPos; } } Vector2Int GetRandomDirection() { Vector2Int[] directions = { Vector2Int.up, Vector2Int.right, Vector2Int.down, Vector2Int.left }; return directions[Random.Range(0, directions.Length)]; } } |
This code creates a grid-based layout where each room has logical coordinates (like (0,0)
, (1,0)
, etc.) and is placed at a fixed distance from others to prevent overlaps.
Rooms are randomly selected from the prefabs defined in the roomPrefabs
field.
2. Creating Room Prefabs
In the project, you’ll find various room prefabs in the /Prefabs/Rooms/
folder, such as:
Room_Empty
Room_Enemies
Room_Loot
Each prefab has a consistent structure:
- a floor (Tilemap or Sprite)
- walls and colliders
- optional spawn points for enemies and objects
You can create new prefabs starting from a base model, customizing their contents as needed.
3. Dynamic Spawning of Enemies and Objects
Each room can contain special objects or enemies. To do this, we use Empty GameObjects as markers/spawn points.
In a prefab, you can create an empty GameObject named EnemySpawn
, then use a script like RoomContent.cs
to detect it at runtime:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class RoomContent : MonoBehaviour { public GameObject enemyPrefab; void Start() { foreach (Transform child in transform) { if (child.CompareTag("EnemySpawn")) { Instantiate(enemyPrefab, child.position, Quaternion.identity); } } } } |
This allows each room to easily define what it contains.
4. Player Positioning
In the project, the Player
is instantiated in the first generated room, which is always located at (0,0)
(Vector3.zero
). Make sure the Player
prefab is added to the scene on startup, either manually or via script:
1 |
Instantiate(playerPrefab, Vector3.zero, Quaternion.identity); |
In the 2DRogueTest
project, this happens via prefab placement in the main scene.
5. Room Transition
Rooms are static but can be linked using door triggers or colliders with scripts that move the player into the next room.
A simple example:
1 2 3 4 5 6 7 |
void OnTriggerEnter2D(Collider2D other) { if (other.CompareTag("Player")) { other.transform.position = new Vector3(16, 0, 0); // position of the adjacent room } } |
A more advanced system would use a map of connections between rooms, but for now the prototype manages linear progression.
6. Polish and Future Improvements
Now that the dungeon is procedurally generated, we can consider several improvements:
- room transition effects (fade, animations)
- generation of larger and more complex levels
- boss rooms and checkpoints
- map saving and replayability
- graphical interface (HUD, HP, minimap)
The 2DRogueTest
project remains intentionally simple, but the structure is designed to be expandable.
Conclusion
With this third part, we complete our journey in building a 2D roguelike in Unity. Starting from a simple character in a static map, we’ve built:
- an organized and reusable Unity 2D project
- a smooth and simple movement system
- enemies with basic AI and melee combat
- a health and death system for all entities
- procedural generation of connected dungeon rooms
The result is a working, solid prototype that’s ready to be expanded. The full source code is available on GitHub.
We hope this guide has inspired you to build your own personal dungeon. Whether you want to improve the project, release your own version, or simply learn, you’re in the right place.
See you in your next game!