255 lines
9.7 KiB
C#

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Events;
public class DungeonGenerator : Singleton<DungeonGenerator>
{
public RoomObject DungeonStartingRoom { get; private set; }
[Header("Generation")]
public Transform DungeonRoot;
public Vector2Int RoomCountRange;
public RoomObject StartingRoomPrefab;
public int MaxRetries;
[Header("Callbacks")]
public UnityEvent OnDungeonRegenerated;
private RoomObject[] possibleRooms;
private void Start()
{
possibleRooms = Resources.LoadAll<RoomObject>("Rooms");
Debug.Log($"Loaded {possibleRooms.Length} room assets.");
MakeDungeon();
}
private void Update()
{
if (Application.isEditor && Input.GetKeyDown(KeyCode.Return))
{
MakeDungeon();
}
}
public void MakeDungeon() => MakeDungeon(MaxRetries);
private void MakeDungeon(int retries)
{
// Delete previous rooms.
foreach (Transform child in DungeonRoot)
{
Destroy(child.gameObject);
}
int rooms = Random.Range(RoomCountRange.x, RoomCountRange.y + 1);
Debug.Log($"Attempting to generate {rooms} rooms.");
List<RoomObject> addedRooms = new List<RoomObject>();
List<RoomDoor> unassignedDoors = new List<RoomDoor>();
// Pick random starting room.
RoomObject startingRoom = Instantiate(StartingRoomPrefab, DungeonRoot);
addedRooms.Add(startingRoom);
unassignedDoors.AddRange(startingRoom.Doors);
DungeonStartingRoom = startingRoom;
List<Collider2D> hitBuffer = new List<Collider2D>();
while (rooms > 0 && unassignedDoors.Count > 0)
{
// Pick a random available door (weighted).
float weightSum = (from door in unassignedDoors select door.AssignWeight).Sum();
RoomDoor chosenDoor = null;
float weightChosen = Random.Range(0, weightSum);
for (int doorIndex = 0; doorIndex < unassignedDoors.Count; doorIndex++)
{
RoomDoor possibleDoor = unassignedDoors[doorIndex];
if (weightChosen < possibleDoor.AssignWeight)
{
chosenDoor = possibleDoor;
break;
}
else weightChosen -= possibleDoor.AssignWeight;
}
if (chosenDoor.Disabled)
{
// Door is disabled, try again.
unassignedDoors.Remove(chosenDoor);
continue;
}
// Search for a door that matches the allowed tags of this one.
// First, search for a tag to pick from (also weighted).
List<TagWeight> matchDoorTags = new List<TagWeight>(chosenDoor.AllowedMatches);
List<RoomObject> tempRooms = new List<RoomObject>(possibleRooms);
_retryTag:
weightSum = (from tag in matchDoorTags select tag.Weight).Sum();
TagWeight chosenTag = null;
weightChosen = Random.Range(0, weightSum);
for (int tagIndex = 0; tagIndex < matchDoorTags.Count; tagIndex++)
{
TagWeight possibleTag = matchDoorTags[tagIndex];
if (weightChosen < possibleTag.Weight)
{
chosenTag = possibleTag;
break;
}
else weightChosen -= possibleTag.Weight;
}
if (chosenTag == null)
{
// None of the tags work here. Let's just skip this door.
chosenDoor.Disabled = true;
unassignedDoors.Remove(chosenDoor);
continue;
}
_retryRoom:
// Now that we have our tag, find all room types with matching doors
// and pick one at random (weighted yet again).
RoomObject[] matchRooms = tempRooms.Where(x => chosenDoor.AllowedMatches.Any(y => x.Doors.Any(z => z.Tags.Any(w => w == y.Tag)))).ToArray();
weightSum = (from room in matchRooms select room.Weight).Sum();
RoomObject chosenRoom = null;
weightChosen = Random.Range(0, weightSum);
for (int roomIndex = 0; roomIndex < matchRooms.Length; roomIndex++)
{
RoomObject possibleRoom = matchRooms[roomIndex];
if (weightChosen < possibleRoom.Weight)
{
chosenRoom = possibleRoom;
break;
}
else weightChosen -= possibleRoom.Weight;
}
if (chosenRoom == null)
{
// This tag has no matches. Skip it and retry the tag.
matchDoorTags.Remove(chosenTag);
goto _retryTag;
}
// Pick a random door in this room that fits the tag.
RoomObject chosenRoomClone = Instantiate(chosenRoom, DungeonRoot);
chosenRoomClone.name = $"Room {addedRooms.Count}"; // - {chosenRoom.name}"
List<RoomDoor> tempDoors = new List<RoomDoor>(chosenRoomClone.Doors);
_retryOtherDoor:
RoomDoor[] otherDoors = tempDoors.Where(x => chosenDoor.AllowedMatches.Any(y => x.Tags.Any(z => z == y.Tag))).ToArray();
weightSum = (from door in otherDoors select door.AssignWeight).Sum();
RoomDoor otherDoor = null;
weightChosen = Random.Range(0, weightSum);
for (int doorIndex = 0; doorIndex < otherDoors.Length; doorIndex++)
{
RoomDoor possibleDoor = otherDoors[doorIndex];
if (weightChosen < possibleDoor.AssignWeight)
{
otherDoor = possibleDoor;
break;
}
else weightChosen -= possibleDoor.AssignWeight;
}
if (otherDoor == null)
{
// The doors we might have thought were valid might not actually be.
// (For instance, if the room can't be rotated). Skip this room.
Destroy(chosenRoomClone.gameObject);
tempRooms.Remove(chosenRoom);
goto _retryRoom;
}
// Spawn the room and rotate it to the correct spot.
if (chosenRoomClone.CanBeRotated)
{
float expectedDoorRot = chosenDoor.transform.rotation.eulerAngles.z + 180;
float curDoorRot = otherDoor.transform.rotation.eulerAngles.z;
float diff = expectedDoorRot - curDoorRot;
Vector3 curRoomRot = chosenRoomClone.transform.rotation.eulerAngles;
curRoomRot.z += diff;
chosenRoomClone.transform.rotation = Quaternion.Euler(curRoomRot);
}
else
{
// Check if it's impossible.
float diff = Mathf.Abs(otherDoor.transform.rotation.eulerAngles.z -
chosenDoor.transform.rotation.eulerAngles.z);
if (Mathf.Abs(diff - 180) >= 1e-1)
{
// Must be rotated to fit. Skip door.
tempDoors.Remove(otherDoor);
goto _retryOtherDoor; // Retry the door, not the room or tag.
}
}
// Move the room to position.
Vector3 expectedDoorPos = chosenDoor.transform.position;
Vector3 curDoorPos = otherDoor.transform.position;
Vector3 diffPos = expectedDoorPos - curDoorPos;
chosenRoomClone.transform.position += diffPos;
Physics2D.SyncTransforms();
// See if this specific transform of the room causes it to overlap with
// any other room.
for (int i = 0; i < chosenRoomClone.Bounds.Length; i++)
{
Collider2D toCheck = chosenRoomClone.Bounds[i];
ContactFilter2D filter = new ContactFilter2D();
filter.NoFilter();
filter.SetLayerMask(1 << 6);
hitBuffer.Clear();
toCheck.OverlapCollider(filter, hitBuffer);
if (hitBuffer.Count(x => !chosenRoomClone.Bounds.Contains(x)) > 0)
{
// Overlapping another room, try a different orientation.
// TODO: This could be optimized by using an array, maybe.
tempDoors.Remove(otherDoor);
goto _retryOtherDoor;
}
}
// Extra stuff.
chosenDoor.Match = otherDoor;
otherDoor.Match = chosenDoor;
addedRooms.Add(chosenRoomClone);
unassignedDoors.AddRange(chosenRoomClone.Doors);
unassignedDoors.Remove(otherDoor);
unassignedDoors.Remove(chosenDoor);
rooms--;
}
if (rooms > 0)
{
Debug.Log($"Ran out of available doors! Skipped generating {rooms} rooms.");
}
// Make all remaining unavailable doors disabled.
foreach (RoomDoor door in unassignedDoors) door.Disabled = true;
if (addedRooms.Count < RoomCountRange.x && retries > 0)
{
// Too small, let's retry the whole thing.
Debug.Log($"Only generated {addedRooms.Count} rooms! Let's try that again...");
MakeDungeon(retries - 1);
return;
}
// Dungeon completed, invoke callback.
OnDungeonRegenerated.Invoke();
UpdateActiveRoom(startingRoom);
}
public void UpdateActiveRoom(RoomObject activeRoom)
{
GameObject obj = activeRoom.gameObject;
for (int i = 0; i < DungeonRoot.childCount; i++)
{
GameObject child = DungeonRoot.GetChild(i).gameObject;
child.SetActive(child == obj);
}
}
}