Infinite Random Dungeon
Project Goal
To create an infinite roguelike dungeon crawler, full of runes to collect, and rooms to explore.
The goal of this part of the project is an infinite dungeon generator, created in real time as the player explores, so it can level up with their skill.
Why
this is needed, and new
This dungeon creator is different than most of the other options out there, because it is truly infinite.
Most pre-existing dungeon randomizers start by assigning a start and end point, then create a dungeon structure around that, full of random pathways. This technique is different because it does not have an arbitrarily assigned end point, and there are no edges to the dungeon.
I wanted to do this because of a game prototype I have in mind, which is a roguelike dungeon crawler, that does not feature the usual "floors" of rooms that most have. Instead the dungeon would increase in difficulty as the player explored more rooms. This requires the dungeon to have no end, so that players could conceivably explore forever
Requirements
This infinite dungeon generator needs to be created as the player explores the dungeon. To do this the dungeon will spawn a room every time the player enters it.
This will also allow the dungeon to scale as the player explores more rooms, a key feature of Roons, the prototype this generator was first built for
This generator also needs to have many different room furnishing types, that will work for any door layout in a room.
It would also be ideal if new variants could be made quickly, and placed somewhere separate in editor so users do not need to hunt down the proper location for their furnishing
Not only does this generator have to work for the Roons prototype it will be in, but also needs to be easily useable in many other circumstances.
Because of this, all functionality should be accessible through function calls, and as foolproof as possible
How
Most of the rooms spawning is done in two scripts. "RoomPlacement" which is more of a manager script, and handles the placement, and "RoomControl" which contains a lot of the functions that happen per room, like populating it.
RoomPlacement
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RoomPlacement : MonoBehaviour
{
public int roomCount;
public GameObject room;
public GameObject door;
public List<GameObject> openRooms = new List<GameObject>();
public List<GameObject> rooms = new List<GameObject>();
Score score;
public float width, height;
// Start is called before the first frame update
void Start()
{
score = GetComponent<Score>();
GameObject start = GameObject.Find("Start");
rooms.Add(start);
openRooms.Add(start);
width = start.GetComponent<SpriteRenderer>().bounds.size.x;
height = start.GetComponent<SpriteRenderer>().bounds.size.y;
for(int i =0; i<4; i++)
{
start.GetComponent<RoomControl>().doors.Add(i);
SpawnDoors(i, start.GetComponent<RoomControl>());
openRooms.Remove(start);
}
start.GetComponent<RoomControl>().DestroyWalls();
while (rooms.Count < roomCount && openRooms.Count > 0)
{
RoomControl currentRoom = openRooms[0].GetComponent<RoomControl>();
//int fiftyFifty = Random.Range(0, 2);
int num = Random.Range(1, 4);
for(int i =0; i < num; i++)
{
int place = Random.Range(0, 4);
while(currentRoom.doors.Contains(place))
{
place = Random.Range(0, 4);
}
if(SpawnDoors(place, currentRoom))
{
if(!currentRoom.doors.Contains(place))
{
currentRoom.doors.Add(place);
}
}
}
openRooms.Remove(currentRoom.gameObject);
currentRoom.GetComponent<RoomControl>().DestroyWalls();
currentRoom.GetComponent<RoomControl>().DisplayOnMiniMap();
}
}
// Update is called once per frame
void Update()
{
}
bool SpawnDoors(int dir, RoomControl currentRoom)
{
Vector3 placement = Vector3.zero;
int newDir = dir;
switch (dir)
{
case 1:
placement = new Vector3(1, 0, 0);
newDir = 3;
break;
case 3:
placement = new Vector3(-1, 0, 0);
newDir = 1;
break;
case 0:
placement = new Vector3(0, 1, 0);
newDir = 2;
break;
case 2:
placement = new Vector3(0, -1, 0);
newDir = 0;
break;
default:
break;
}
RaycastHit2D raycast = Physics2D.CircleCast(currentRoom.transform.position + new Vector3(width * placement.x, height * placement.y, placement.z), 0.5f, Vector3.zero);
if(!raycast || !raycast.collider.gameObject.GetComponent<RoomControl>())
{
GameObject newDoor = GameObject.Instantiate(door, currentRoom.transform.position + new Vector3(width * placement.x * .5f, height * placement.y * .5f, -1), Quaternion.identity);
GameObject newRoom = GameObject.Instantiate(room, currentRoom.transform.position + new Vector3(width * placement.x, height * placement.y, placement.z), Quaternion.identity);
if(!newRoom.GetComponent<RoomControl>().doors.Contains(newDir))
{
newRoom.GetComponent<RoomControl>().doors.Add(newDir);
}
newRoom.GetComponent<RoomControl>().DestroyWalls();
//currentRoom.DestroyWalls();
openRooms.Add(newRoom); rooms.Add(newRoom);
return true;
}
else
{
if(raycast.collider.gameObject.GetComponent<RoomControl>())
{
if(openRooms.Contains(raycast.collider.gameObject))
{
GameObject newDoor = GameObject.Instantiate(door, currentRoom.transform.position + new Vector3(width * placement.x * .5f, height * placement.y * .5f, -1), Quaternion.identity);
if(!raycast.collider.gameObject.GetComponent<RoomControl>().doors.Contains(newDir))
{
raycast.collider.gameObject.GetComponent<RoomControl>().doors.Add(newDir);
}
raycast.collider.gameObject.GetComponent<RoomControl>().DestroyWalls();
Debug.Log("Added a door to existing un discovered room");
return true;
}
else
{
return false;
}
}
else
{
return false;
}
//Debug.LogError("Already a room there dumbo :)");
//raycast.collider.gameObject.GetComponent<RoomControl>().doors.Add(newDir);
}
}
public void AddRooms(GameObject nearRoom)
{
if(openRooms.Contains(nearRoom))
{
RoomControl currentRoom = nearRoom.GetComponent<RoomControl>();
int num = Random.Range(1, 4);
for (int i = 0; i < num; i++)
{
int place = Random.Range(0, 4);
int j = 0;
while (currentRoom.doors.Contains(place) && j < 2)
{
place = Random.Range(0, 4);
j++;
}
if(SpawnDoors(place, currentRoom))
{
if(!currentRoom.doors.Contains(place))
{
currentRoom.doors.Add(place);
}
}
}
openRooms.Remove(currentRoom.gameObject);
currentRoom.GetComponent<RoomControl>().PopulateRoom();
currentRoom.GetComponent<RoomControl>().DestroyWalls();
currentRoom.GetComponent<RoomControl>().DisplayOnMiniMap();
score.addRoom();
}
}
}
Spawn Doors
SpawnDoors() works based on a room a direction that the door should be spawned in.
The section precluding to raycasts is checking whether or not a room is there, and if the room is there if the player has been entered. This keeps multiple rooms from spawning on top of eachother, but allows new doors to be added to rooms that the player hasn't seen yet
Each time a door is spawned, and there is not a room on the otherside, a new room needs to be spawned. This keeps rooms lining up properly, and makes sure no doors lead to the outside
Add Rooms
AddRooms() is called when a player enters a room, and runs all the RoomControl functions that need to be run for the room to appear correctly. This function is called when the player enters a new room, makes sure to spawn are rooms next to this one, it determines a random number of doors to add to the room, and random locations for it. When creating this script I ran into a few errors with infinite loops, so that is why there are a few breakable loops to retry door placement.
After this is furnishes the room and displays it on the minmap correctly.
RoomControl
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RoomControl : MonoBehaviour
{
public List<GameObject> doorCovers = new List<GameObject>();
public List<int> doors = new List<int>();
public void DestroyWalls()
{
foreach (int num in doors)
{
if(doorCovers.Count >= num)
{
GameObject temp = doorCovers[num];
doorCovers[num] = null;
Destroy(temp);
}
}
}
public void DisplayOnMiniMap()
{
gameObject.layer = 6;
foreach (Transform child in transform)
{
child.gameObject.layer = 6;
}
}
public void PopulateRoom()
{
Object[] furnitureSets;
bool accross = false;
switch(doors.Count)
{
case 2:
if(Mathf.Abs(doors[0] - doors[1]) == 2)
{
furnitureSets = Resources.LoadAll("2DoorAccross");
accross = true;
}
else
{
furnitureSets = Resources.LoadAll("2Door");
accross = false;
}
break;
default:
furnitureSets = Resources.LoadAll(doors.Count.ToString() + "Door");
break;
}
GameObject roomStuff = Instantiate(furnitureSets[0],transform.position - new Vector3 (0,0,0.5f),Quaternion.identity) as GameObject;
switch(doors.Count)
{
case 1:
roomStuff.transform.Rotate(new Vector3(0,0,90 * (2-doors[0])));
break;
case 3:
int type = doors[0] + doors[1] + doors[2];
switch(type)
{
case 3:
roomStuff.transform.Rotate(new Vector3(0,0,180));
break;
case 4:
roomStuff.transform.Rotate(new Vector3(0,0,-90));
break;
case 6:
roomStuff.transform.Rotate(new Vector3(0,0,90));
break;
default: break;
}
break;
case 2:
if(accross)
{
if(!doors.Contains(2))
{
roomStuff.transform.Rotate(new Vector3(0,0,90));
}
}
else
{
int type2 = doors[0] + doors[1];
if(doors.Contains(0))
{
type2 ++;
}
switch(type2)
{
case 2:
roomStuff.transform.Rotate(new Vector3(0,0,180));
break;
case 3:
roomStuff.transform.Rotate(new Vector3(0,0,90));
break;
case 4:
roomStuff.transform.Rotate(new Vector3(0,0,-90));
break;
default:
break;
}
}
break;
default: break;
}
}
}
RoomControl is significantly shorter than RoomPlacement. It is on every "floor" object that RoomPlacement places, and is used to run the functions that happen for every room at a smaller level.
Destroy Walls
DestroyWalls() is used to delete the walls covering the doors.
It gets called twice, once when the room is spawned, so t can be entered, and again once the room has been entered and has all doors on it that it ever will
Display On Mini Map
The DisplayOnMiniMap() is used to make sure that only rooms a player has entered before are displayed on the mini map
It changes the floor, and its walls, to layer in unity, which is then the layer that the "minimap" camera can see
Populate Room
PopulateRoom() is a function dedicated to furnishing the rooms as the player enters it.
Doors are labeled by number around, meaning that a lot of choice of rooms, and how to rotate them can be done with math. For example, we can tell if two doors are directly across from each other by subtracting ones room door from another. For instance, door 0 and 2 are across from eachother, 2-0 = 2, meaning that the program knows this. If we choose instead door 3 and 2, which are not, we get 1, meaning the program knows that the doors are not across from each other. Similar math can be applied to the rotation of the room, as each combination of door locations creates a unique number, and a unique rotation.
The room layouts are stored in the "resources" folder in unity, allowing us to access and find them at runtime, meaning room furnishings don't have to be moved into the floor layout manually. This fulfills the need for this generator to be easy to use, and populate with furninshings,
Next Steps
This infinite dungeon generator is complete. There are some additions that can be added, like more furnishing types, but all relevant scripting is complete. This was created to be robust and usable in many products, so more can always be added if necessary.
However, the prototype that this generator was made for is not complete. Roons still requires enemies, combat, and upgrades. These will be added in extra scripts, like the ones that already exist, such as CameraControl and PlayerMovement. These two main scripts will most likely not change for the use of this generator in Roons.