- Home /
Procedural Map Using Predefined Tiles
I'm looking to create a random map generator from a set of predefined tiles. The application in question is intended to create custom levels/scenarios for a board game where the user can input what sets/expansions they have and the application randomly spits out a unique scenario from what they have. (The game in question is Resident Evil 2: The Board Game).
The game has several shaped times, as shown below. The tiles can be oriented in any 90 degree turn and can be connected at any square's edge. Tiles are linked by "doorways" and typically only 1 doorway leading from any tile to any other tile. If a door has been placed, any other doorway leading into the other tile should be invalid; additionally, any "wall" touching another tile should also be invalid. The goal is to create a visual map that the user can replicate on the table, so there's no need for a PlayerController or anything like that, only the logic that there's a coherent path.
The problem I am facing is the placement of tiles as I am getting overlapping tiles. When it come to the rectangular and square tiles, I can get it to work fine, but once I introduce the L-Shape & P-Shape tiles I start having issues.
I tried having my pieces as 3D models with quads as the doors and checking if the base's bounding box is intersecting with another bounding box. If true, then loop try again until you find a spot that's valid. It worked with the square pieces until I got to the L-Shape where the bounding box is not shaped properly. I tried changing to mesh colliders but I couldn't get that to work, but I could have been being dumb.
Does anyone have any insight to how I should go about this? Should I try to continue with meshes? Should I try moving to 2D Tilemaps that I've never worked with? Maybe there's a tool that already solves this?
LevelBuilder.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;
public class LevelBuilder : MonoBehaviour
{
public Room startRoomPrefab, endRoomPrefab;
public List<Room> roomPrefabs = new List<Room>();
public Vector2 iterationRange = new Vector2(3, 10);
List<Doorway> availableDoorways = new List<Doorway>();
StartRoom startRoom;
EndRoom endRoom;
List<Room> placedRooms = new List<Room>();
LayerMask roomLayerMask;
Coroutine mainCoroutine = null;
public static bool TotalCollisions = false;
//Player player;
void Start()
{
roomLayerMask = LayerMask.GetMask("Room");
mainCoroutine = StartCoroutine(GenerateLevel());
}
IEnumerator GenerateLevel()
{
WaitForSeconds startup = new WaitForSeconds(1);
WaitForFixedUpdate interval = new WaitForFixedUpdate();
//yield return startup;
// Place start room
PlaceStartRoom();
yield return interval;
// Random iterations
int iterations = Random.Range((int)iterationRange.x, (int)iterationRange.y);
for (int i = 0; i < iterations; i++)
{
// Place random room from list
PlaceRoom();
yield return interval;
}
// Place end room
PlaceEndRoom();
yield return interval;
// Level generation finished
//Debug.Log("Level generation finished");
yield return new WaitForSeconds (5);
ResetLevelGenerator ();
}
void PlaceStartRoom()
{
// Instantiate room
startRoom = Instantiate(startRoomPrefab) as StartRoom;
startRoom.transform.parent = this.transform;
// Get doorways from current room and add them randomly to the list of available doorways
List<Doorway> allAvailableDoorways = new List<Doorway>(availableDoorways);
List<Doorway> currentRoomDoorways = new List<Doorway>();
AddDoorwaysToList(startRoom, ref currentRoomDoorways);
// Get doorways from current room and add them randomly to the list of available doorways
AddDoorwaysToList(startRoom, ref availableDoorways);
// Position room
startRoom.transform.position = Vector3.zero;
startRoom.transform.rotation = Quaternion.identity;
// Room couldn't be placed. Restart generator and try again
}
void AddDoorwaysToList(Room room, ref List<Doorway> list)
{
foreach (Doorway doorway in room.doorways)
{
int r = Random.Range(0, list.Count);
list.Insert(r, doorway);
}
for (int i = 0; i < list.Count; i++)
{
Doorway temp = list[i];
int randomIndex = Random.Range(i, list.Count);
list[i] = list[randomIndex];
list[randomIndex] = temp;
}
}
void PlaceRoom()
{
// Instantiate room
Room currentRoom = Instantiate(roomPrefabs[Random.Range(0, roomPrefabs.Count)]) as Room;
currentRoom.transform.parent = this.transform;
// Create doorway lists to loop over
List<Doorway> allAvailableDoorways = new List<Doorway>(availableDoorways);
List<Doorway> currentRoomDoorways = new List<Doorway>();
AddDoorwaysToList(currentRoom, ref currentRoomDoorways);
// Get doorways from current room and add them randomly to the list of available doorways
AddDoorwaysToList(currentRoom, ref availableDoorways);
bool roomPlaced = false;
// Try all available doorways
foreach (Doorway availableDoorway in allAvailableDoorways)
{
// Try all available doorways in current room
foreach (Doorway currentDoorway in currentRoomDoorways)
{
// Position room
PositionRoomAtDoorway(ref currentRoom, currentDoorway, availableDoorway);
// Check room overlaps
if (CheckRoomOverlap(currentRoom))
{
continue;
}
roomPlaced = true;
// Add room to list
placedRooms.Add(currentRoom);
// Remove occupied doorways
currentDoorway.gameObject.SetActive(false);
availableDoorways.Remove(currentDoorway);
availableDoorway.gameObject.SetActive(false);
availableDoorways.Remove(availableDoorway);
// Exit loop if room has been placed
break;
}
// Exit loop if room has been placed
if (roomPlaced)
{
break;
}
}
// Room couldn't be placed. Restart generator and try again
if (!roomPlaced)
{
Destroy(currentRoom.gameObject);
ResetLevelGenerator();
}
}
void PositionRoomAtDoorway(ref Room room, Doorway roomDoorway, Doorway targetDoorway)
{
// Reset room position and rotation
room.transform.position = Vector3.zero;
room.transform.rotation = Quaternion.identity;
// Rotate room to match previous doorway orientation
Vector3 targetDoorwayEuler = targetDoorway.transform.eulerAngles;
Vector3 roomDoorwayEuler = roomDoorway.transform.eulerAngles;
float deltaAngle = Mathf.DeltaAngle(roomDoorwayEuler.y, targetDoorwayEuler.y);
Quaternion currentRoomTargetRotation = Quaternion.AngleAxis(deltaAngle, Vector3.up);
room.transform.rotation = currentRoomTargetRotation * Quaternion.Euler(0, 180f, 0);
// Position room
Vector3 roomPositionOffset = roomDoorway.transform.position - room.transform.position;
room.transform.position = targetDoorway.transform.position - roomPositionOffset;
}
bool CheckRoomOverlap(Room room)
{
Bounds bounds = room.RoomBounds;
//bounds.center = room.transform.position;
bounds.Expand(-0.1f);
Collider[] colliders = Physics.OverlapBox(room.transform.position, bounds.size, room.transform.rotation, roomLayerMask);
//Debug.Log(colliders.Length / 2);
if (colliders.Length > 1)
{
// Ignore collisions with current room
foreach (Collider c in colliders)
{
if (c.transform.parent.gameObject.Equals(room.gameObject))
{
continue;
}
else
{
Debug.LogError("Overlap detected");
return true;
}
}
}
return false;
}
void PlaceEndRoom()
{
// Instantiate room
Room currentRoom = Instantiate(endRoomPrefab) as EndRoom;
currentRoom.transform.parent = this.transform;
// Create doorway lists to loop over
List<Doorway> allAvailableDoorways = new List<Doorway>(availableDoorways);
List<Doorway> currentRoomDoorways = new List<Doorway>();
AddDoorwaysToList(currentRoom, ref currentRoomDoorways);
// Get doorways from current room and add them randomly to the list of available doorways
AddDoorwaysToList(currentRoom, ref availableDoorways);
bool roomPlaced = false;
// Try all available doorways
foreach (Doorway availableDoorway in allAvailableDoorways)
{
// Try all available doorways in current room
foreach (Doorway currentDoorway in currentRoomDoorways)
{
// Position room
PositionRoomAtDoorway(ref currentRoom, currentDoorway, availableDoorway);
// Check room overlaps
if (CheckRoomOverlap(currentRoom))
{
roomPlaced = false;
continue;
}
roomPlaced = true;
// Add room to list
placedRooms.Add(currentRoom);
// Remove occupied doorways
currentDoorway.gameObject.SetActive(false);
availableDoorways.Remove(currentDoorway);
availableDoorway.gameObject.SetActive(false);
availableDoorways.Remove(availableDoorway);
// Exit loop if room has been placed
break;
}
// Exit loop if room has been placed
if (roomPlaced)
{
break;
}
}
// Room couldn't be placed. Restart generator and try again
if (!roomPlaced)
{
Destroy(currentRoom.gameObject);
ResetLevelGenerator();
}
}
void ResetLevelGenerator()
{
//Debug.LogError("Reset level generator");
StopCoroutine(mainCoroutine);
// Delete all rooms
if (startRoom)
{
Destroy(startRoom.gameObject);
}
if (endRoom)
{
Destroy(endRoom.gameObject);
}
foreach (Room room in placedRooms)
{
Destroy(room.gameObject);
}
// Clear lists
placedRooms.Clear();
availableDoorways.Clear();
// Reset coroutine
mainCoroutine = StartCoroutine(GenerateLevel());
}
}
Room.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Room : MonoBehaviour
{
public Doorway[] doorways;
public MeshCollider meshCollider;
public Bounds RoomBounds
{
get { return meshCollider.bounds; }
}
void OnDrawGizmos()
{
// Draw a yellow cube at the transform position
Gizmos.color = Color.blue;
Gizmos.DrawWireCube(transform.position, meshCollider.bounds.size);
}
}
The other scripts either draw Gizmos or are extensions for the future.
It does look similar to a Wave Function Collapse, although I haven't really tried it