PuzzleGame/PuzzleGameProject/Assets/Scripts/DungeonGenerator/DungeonGenerator.cs

607 lines
No EOL
27 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using Unity.VisualScripting;
using Unity.VisualScripting.Dependencies.NCalc;
using Random = System.Random;
namespace DungeonGenerator
{
public class DungeonGenerator
{
private const int SIDE_LENGTH_OF_MONSTER = 4;
private const int SIDE_LENGTH_OF_NORMAL = 2;
private const int SIDE_LENGTH_OF_ENTRANCE = 2;
private const int WIDTH_OF_BOSS = 10;
private const int HEIGHT_OF_BOSS = 6;
private int _xLength = 40;
private int _yLength = 28;
public DungeonMap GenerateDungeon(int length, float monsterRoomRatio)
{
_xLength = 40;
_yLength = 28;
Random random = new Random();
DungeonMap dungeonMap = new DungeonMap(_xLength, _yLength);
dungeonMap.AddRooms(GenerateEntranceRooms(_xLength, _yLength, random.Next(1,4)));
dungeonMap.AddRoom(GenerateOnlyBossRoom(_xLength, _yLength, WIDTH_OF_BOSS, HEIGHT_OF_BOSS));
EvenDisperser disperser = new EvenDisperser(_xLength, _yLength, dungeonMap.GetUnoccupiedPoints()); //TODO calculate L and W from length
int numberOfMonsterRooms = 7; // TODO: Calculate from ratio
for (var i = 0; i < numberOfMonsterRooms; i ++)
{
dungeonMap.AddRoom(disperser.GenerateAndPlaceRoom(
SIDE_LENGTH_OF_MONSTER,
SIDE_LENGTH_OF_MONSTER,
RoomType.Monster));
}
AddNormalRoomsAroundMonsterRooms(dungeonMap);
AddNormalRoomsAroundBossRoom(dungeonMap);
AddConnectionRooms(dungeonMap);
return dungeonMap;
}
private void AddConnectionRooms(DungeonMap dungeon)
{
// For each room adjacent to a monster and boos room connect it with the closest thing (entrance room, or monster adjacent room)
// unless there is something already going there.
List<Room> rootRooms = new List<Room>(dungeon.GetMonsterRooms());
rootRooms.Add(dungeon.GetBossRoom());
// Create list of connection rooms
Dictionary<RoomSide, List<Room>> nodeRoomAndSideItIsOn = dungeon.GetNodesWithSides();
List<Room> nodeRoomsWithEntrances = new List<Room>(dungeon.GetNodeRooms());
nodeRoomsWithEntrances.AddRange(dungeon.GetEntranceRooms());
// For each connection room that is not an entrance room,
// find the next closest uninterrupted connection room that is not on the same root room.
foreach (var roomSideWithRoom in nodeRoomAndSideItIsOn)
{
foreach (Room room in roomSideWithRoom.Value)
{
Room closestSeenRoom = FindClosestSeenConnection(room,
roomSideWithRoom.Key,
nodeRoomsWithEntrances,
rootRooms);
if (closestSeenRoom != null)
{
//Place rooms along the line until they are connected
dungeon.AddRooms(PlaceRoomsOrganicallyTowardRoom(room, closestSeenRoom, RoomType.Normal,
dungeon.GetOccupiedPoints(), dungeon.GetUnoccupiedPoints()));
}
}
}
}
private void AddNormalRoomsAroundBossRoom(DungeonMap dungeon)
{
AddNormalRoomsAroundRoom(dungeon.GetBossRoom(), dungeon);
}
private void AddNormalRoomsAroundMonsterRooms(DungeonMap dungeon)
{
foreach (Room monsterRoom in dungeon.GetMonsterRooms())
{
AddNormalRoomsAroundRoom(monsterRoom, dungeon);
}
}
private void AddNormalRoomsAroundRoom(Room room, DungeonMap dungeon)
{
Random random = new Random();
List<Point> topUnoccupiedPoints = new List<Point>(GetUnoccupiedPointsOnSide(room, RoomSide.Top, dungeon.GetUnoccupiedPoints()));
List<Point> bottomUnoccupiedPoints = new List<Point>(GetUnoccupiedPointsOnSide(room, RoomSide.Bottom, dungeon.GetUnoccupiedPoints()));
List<Point> leftUnoccupiedPoints = new List<Point>(GetUnoccupiedPointsOnSide(room, RoomSide.Left, dungeon.GetUnoccupiedPoints()));
List<Point> rightUnoccupiedPoints = new List<Point>(GetUnoccupiedPointsOnSide(room, RoomSide.Right, dungeon.GetUnoccupiedPoints()));
int minNecessaryAvailablePoints = (SIDE_LENGTH_OF_MONSTER + SIDE_LENGTH_OF_NORMAL) / 2;
List<RoomSide> availableSides = new List<RoomSide>();
if (topUnoccupiedPoints.Count >= minNecessaryAvailablePoints){availableSides.Add(RoomSide.Top);}
if (bottomUnoccupiedPoints.Count >= minNecessaryAvailablePoints){availableSides.Add(RoomSide.Bottom);}
if (leftUnoccupiedPoints.Count >= minNecessaryAvailablePoints){availableSides.Add(RoomSide.Left);}
if (rightUnoccupiedPoints.Count >= minNecessaryAvailablePoints){availableSides.Add(RoomSide.Right);}
// Ensure between 3 and 4 rooms are added
int numRoomsToAdd = Math.Min(availableSides.Count, random.Next(3, 5));
// Randomly shuffle and take the required number of sides
foreach (RoomSide side in availableSides.OrderBy(_ => random.Next()).Take(numRoomsToAdd))
{
List<Point> unoccupiedPointsOnSide = side switch
{
RoomSide.Top => topUnoccupiedPoints,
RoomSide.Bottom => bottomUnoccupiedPoints,
RoomSide.Left => leftUnoccupiedPoints,
RoomSide.Right => rightUnoccupiedPoints,
_ => throw new ArgumentOutOfRangeException(nameof(side), $"Unexpected RoomSide value: {side}")
};
// Create room and add it if valid
Room newRoom = CreateAdjacentRoom(RoomType.Normal, unoccupiedPointsOnSide, side, dungeon.GetOccupiedPoints());
if (newRoom != null)
{
room.AddAdjacentRoom(newRoom, side);
dungeon.AddRoom(newRoom);
}
}
}
private List<Point> GetUnoccupiedPointsOnSide(Room room, RoomSide side, HashSet<Point> unoccupiedPoints)
{
List<Point> unoccupiedPointsOnSide = new List<Point>();
var roomPoints = room.GetPointsInRoom();
int minX = roomPoints.Min(p => p.X);
int maxX = roomPoints.Max(p => p.X);
int minY = roomPoints.Min(p => p.Y);
int maxY = roomPoints.Max(p => p.Y);
switch (side)
{
case RoomSide.Top:
for (int x = minX - SIDE_LENGTH_OF_NORMAL; x <= maxX + SIDE_LENGTH_OF_NORMAL; x++)
{
Point point = new Point(x, minY - SIDE_LENGTH_OF_NORMAL);
if (unoccupiedPoints.Contains(point))
{
unoccupiedPointsOnSide.Add(point);
}
}
return unoccupiedPointsOnSide;
case RoomSide.Bottom:
for (int x = minX - SIDE_LENGTH_OF_NORMAL; x <= maxX + SIDE_LENGTH_OF_NORMAL; x++)
{
Point point = new Point(x, maxY + SIDE_LENGTH_OF_NORMAL);
if (unoccupiedPoints.Contains(point))
{
unoccupiedPointsOnSide.Add(point);
}
}
return unoccupiedPointsOnSide;
case RoomSide.Left:
for (int y = minY - SIDE_LENGTH_OF_NORMAL; y <= maxY + SIDE_LENGTH_OF_NORMAL; y++)
{
Point point = new Point(minX - SIDE_LENGTH_OF_NORMAL, y);
if (unoccupiedPoints.Contains(point))
{
unoccupiedPointsOnSide.Add(point);
}
}
return unoccupiedPointsOnSide;
case RoomSide.Right:
for (int y = minY - SIDE_LENGTH_OF_NORMAL; y <= maxY + SIDE_LENGTH_OF_NORMAL; y++)
{
Point point = new Point(maxX + SIDE_LENGTH_OF_NORMAL, y);
if (unoccupiedPoints.Contains(point))
{
unoccupiedPointsOnSide.Add(point);
}
}
return unoccupiedPointsOnSide;
default:
throw new ArgumentException("Side not recognized");
}
}
private Room CreateAdjacentRoom(RoomType type, List<Point> unoccupiedPointsOnSide, RoomSide side, HashSet<Point> occupiedPoints)
{
int sizeOfNewRoom = GetRoomSizeByType(type);
Random random = new Random();
// Sort points by their coordinate based on the side of placement
var orderedPoints = side == RoomSide.Left || side == RoomSide.Right
? unoccupiedPointsOnSide.OrderBy(p => p.Y).ToList() // Sort by Y for vertical placement
: unoccupiedPointsOnSide.OrderBy(p => p.X).ToList();
// List to store possible valid room placements (top-left points of the room)
List<Point> validPlacements = new List<Point>();
// Iterate over the ordered points to find potential placements
for (int i = 0; i < orderedPoints.Count - sizeOfNewRoom + 1; i++)
{
// Get a sequence of SIZE_OF_ROOM points starting from index i
var potentialPoints = orderedPoints.Skip(i).Take(sizeOfNewRoom).ToList();
// Check if all points are consecutive and fit within the range
if (IsRoomWideEnough(potentialPoints, side))
{
Point firstPoint = potentialPoints.First();
// Adjust the first point for different sides
Point adjustedPoint = side switch
{
RoomSide.Top => new Point(firstPoint.X, firstPoint.Y), // Move up for top side
RoomSide.Bottom => new Point(firstPoint.X, firstPoint.Y - 1), // Move down for bottom side
RoomSide.Left => new Point(firstPoint.X, firstPoint.Y), // Move left for left side
RoomSide.Right => new Point(firstPoint.X - 1, firstPoint.Y), // Move right for right side
_ => firstPoint // Default case (shouldn't happen)
};
if (!potentialPoints.Any(occupiedPoints.Contains))
{
validPlacements.Add(adjustedPoint); // First point is the top-left point of the room
}
}
}
// If there are valid placements, select a random one and create the room
if (validPlacements.Any())
{
Point randomPlacement = validPlacements[random.Next(validPlacements.Count)];
Room newRoom = new Room(type, sizeOfNewRoom, sizeOfNewRoom, randomPlacement);
return newRoom;
}
// If no valid placements are found, return null or handle it as necessary
return null;
}
// Helper method to check if a sequence of points is wide enough (consecutive points)
private bool IsRoomWideEnough(List<Point> points, RoomSide side)
{
for (int i = 1; i < points.Count; i++)
{
if (side == RoomSide.Left || side == RoomSide.Right)
{
// Check if points are consecutive along the Y-axis for left/right side
if (points[i].Y != points[i - 1].Y + 1)
{
return false;
}
}
else
{
// Check if points are consecutive along the X-axis for top/bottom side
if (points[i].X != points[i - 1].X + 1)
{
return false;
}
}
}
return true;
}
private int GetRoomSizeByType(RoomType type)
{
switch (type)
{
case RoomType.Entrance:
return SIDE_LENGTH_OF_ENTRANCE;
case RoomType.Monster:
return
SIDE_LENGTH_OF_MONSTER;
case RoomType.Normal:
return SIDE_LENGTH_OF_NORMAL;
default:
return SIDE_LENGTH_OF_NORMAL;
}
}
private Room GenerateOnlyBossRoom(int xLengthOfDungeon, int yLengthOfDungeon, int width, int heigth)
{
// Place monster room in the middle third
int middleAreaX = xLengthOfDungeon / 3;
int middleAreaY = yLengthOfDungeon / 3;
Random random = new Random();
int bossX = middleAreaX + random.Next(0, middleAreaX - width);
int bossY = middleAreaY + random.Next(0, middleAreaY - heigth);
return new Room(RoomType.Boss, width, heigth, new Point(bossX, bossY));
}
private enum Side
{
Top,
Right,
Bottom,
Left
}
private List<Room> GenerateEntranceRooms(int xLengthOfDungeon, int yLengthOfDungeon, int numberOfEntranceLines)
{
if (numberOfEntranceLines > 4)
{
throw new Exception("Number of entrance lines cannot be greater than 4");
}
const int numEntranceRooms = 12;
List<Room> entranceRooms = new List<Room>();
Random random = new Random();
Dictionary<Side, int> sidesToEnterOn = new()
{
{ Side.Top, 0},
{ Side.Right, 0},
{ Side.Bottom, 0},
{ Side.Left, 0}
};
// Randomly assign starting lines to sides of dungeon
for (var i = 0; i < numberOfEntranceLines; i++)
{
while (true)
{
Side side = (Side)random.Next(0, 4);
if (sidesToEnterOn[side] < 2)
{
sidesToEnterOn[side] += 1;
break;
}
}
}
int roomsPerLine = numEntranceRooms / numberOfEntranceLines;
int bottom = yLengthOfDungeon - SIDE_LENGTH_OF_NORMAL;
int oneRoomFromBottom = bottom - SIDE_LENGTH_OF_NORMAL;
int right = xLengthOfDungeon - SIDE_LENGTH_OF_NORMAL;
// Generate entrance lines on each side
switch (sidesToEnterOn[Side.Top])
{
case 2:
// Add half of points for this entrance line to the top left side
for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
{
entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(i,0)));
}
// Add the rest of the points for this entrance line to the top right side.
for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
{
entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(right - i,0)));
}
break;
case 1:
int startOfLine = GetStartOfCenteredLine(xLengthOfDungeon, roomsPerLine, SIDE_LENGTH_OF_NORMAL);
for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
{
entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL,new Point(startOfLine + i, 0)));
}
break;
}
switch (sidesToEnterOn[Side.Right])
{
case 2:
// Add points for this entrance line to the top right side
for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
{
entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(right, i + SIDE_LENGTH_OF_NORMAL)));
}
// Add the rest of the points for this entrance line to the bottom right side.
for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
{
entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(right, oneRoomFromBottom - i)));
}
break;
case 1:
int startOfLine = GetStartOfCenteredLine(yLengthOfDungeon, roomsPerLine, SIDE_LENGTH_OF_NORMAL);
for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
{
entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(right, startOfLine + i)));
}
break;
}
switch (sidesToEnterOn[Side.Bottom])
{
case 2:
// Add half of points for this entrance line to the bottom left side
for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
{
entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(i, bottom)));
}
// Add the rest of the points for this entrance line to the bottom right side.
for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
{
entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point((xLengthOfDungeon - 1) - i, bottom)));
}
break;
case 1:
int startOfLine = GetStartOfCenteredLine(xLengthOfDungeon, roomsPerLine, SIDE_LENGTH_OF_NORMAL);
for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
{
entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(startOfLine + i, bottom)));
}
break;
}
switch (sidesToEnterOn[Side.Left])
{
case 2:
// Add half of points for this entrance line to the top left side
for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
{
entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(0, i + SIDE_LENGTH_OF_NORMAL)));
}
// Add the rest of the points for this entrance line to the bottom left side.
for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
{
entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(0, oneRoomFromBottom - i)));
}
break;
case 1:
int startOfLine = GetStartOfCenteredLine(yLengthOfDungeon, roomsPerLine, SIDE_LENGTH_OF_NORMAL);
for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL)
{
entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(0, startOfLine + i)));
}
break;
}
return entranceRooms;
}
private int GetStartOfCenteredLine(int length, int numberOfRooms, int sizeOfRoom)
{
int midpoint = length / 2;
return midpoint - (numberOfRooms * sizeOfRoom / 2);
}
private Room FindClosestSeenConnection(Room nodeRoom, RoomSide attachedSide, List<Room> possibleConnections,
List<Room> obstructions)
{
const float minClosestLength = 0;
Vector2 lookDirection = attachedSide switch
{
RoomSide.Top => new Vector2(0, -1), // Expanding upward
RoomSide.Bottom => new Vector2(0, 1), // Expanding downward
RoomSide.Left => new Vector2(-1, 0), // Expanding left
RoomSide.Right => new Vector2(1, 0), // Expanding right
_ => Vector2.Zero // Default (should not happen)
};
Point nodeCenter = nodeRoom.GetCenterOfRoom();
float visionConeAngle = MathF.PI / 4; // 45 degree cone in each side
Room closestSeenRoom = null;
float closestDistance = float.MaxValue;
foreach (Room otherRoom in possibleConnections)
{
if (otherRoom == nodeRoom) continue;
Point otherCenter = otherRoom.GetCenterOfRoom();
Vector2 toOther = new Vector2(otherCenter.X - nodeCenter.X, otherCenter.Y - nodeCenter.Y);
float distance = toOther.Length();
if (distance <= minClosestLength || distance >= closestDistance) continue;
// Normalize direction vectors
toOther = Vector2.Normalize(toOther);
Vector2 lookNormalized = Vector2.Normalize(lookDirection);
float angle = MathF.Acos(Vector2.Dot(lookNormalized, toOther));
if (angle <= visionConeAngle)
{
if (!IsObstructed(nodeCenter, otherCenter, obstructions))
{
closestSeenRoom = otherRoom;
closestDistance = distance;
}
}
}
return closestSeenRoom; // Could be null if no valid room is found
}
private bool IsObstructed(Point start, Point end, List<Room> obstructions)
{
List<Point> linePoints = GetPointsOnLine(start, end);
foreach (Point point in linePoints)
{
if (obstructions.Any(room => room.ContainsPoint(point)))
{
return true;
}
}
return false;
}
// Bresenhams Line Algorithm to get points between two points
private List<Point> GetPointsOnLine(Point p0, Point p1)
{
List<Point> points = new List<Point>();
int dx = Math.Abs(p1.X - p0.X), sx = p0.X < p1.X ? 1 : -1;
int dy = -Math.Abs(p1.Y - p0.Y), sy = p0.Y < p1.Y ? 1 : -1;
int err = dx + dy, e2;
int x = p0.X, y = p0.Y;
while (true)
{
points.Add(new Point(x, y));
if (x == p1.X && y == p1.Y) break;
e2 = 2 * err;
if (e2 >= dy) { err += dy; x += sx; }
if (e2 <= dx) { err += dx; y += sy; }
}
return points;
}
private List<Room> PlaceRoomsOrganicallyTowardRoom(Room startingRoom, Room targetRoom, RoomType type,
HashSet<Point> occupiedPoints, HashSet<Point> unoccupiedPoints)
{
List<Room> placedRooms = new List<Room>();
Room currentRoom = startingRoom;
// TODO I don't think I actually need this
List<Point> path = GetPointsOnLine(startingRoom.GetCenterOfRoom(), targetRoom.GetCenterOfRoom());
bool roomPlaced = true;
while (currentRoom.GetCenterOfRoom().ManhattanDistance(targetRoom.GetCenterOfRoom()) >=
GetRoomSizeByType(type) && roomPlaced)
{
Room newRoom;
// Get sides of room to try and place new room
(RoomSide primary, RoomSide secondary) =
GetSidesTowardsEndPoint(currentRoom.GetCenterOfRoom(), targetRoom.GetCenterOfRoom());
List<Point> availablePointsOnSide = GetUnoccupiedPointsOnSide(currentRoom, primary, unoccupiedPoints);
newRoom = CreateAdjacentRoom(type, availablePointsOnSide, primary, occupiedPoints);
// If the primary side doesn't have room for a new room the try secondary
if (newRoom == null)
{
availablePointsOnSide = GetUnoccupiedPointsOnSide(currentRoom, secondary, unoccupiedPoints);
newRoom = CreateAdjacentRoom(type, availablePointsOnSide, secondary, occupiedPoints);
}
if (newRoom != null)
{
placedRooms.Add(newRoom);
occupiedPoints.AddRange(newRoom.GetPointsInRoom());
currentRoom = newRoom;
}
else
{
roomPlaced = false;
}
}
return placedRooms;
}
public (RoomSide primarySide, RoomSide secondarySide) GetSidesTowardsEndPoint(Point start, Point end)
{
// Calculate the differences in X and Y
int deltaX = end.X - start.X;
int deltaY = end.Y - start.Y;
// Determine the primary side
RoomSide primarySide;
RoomSide secondarySide;
if (Math.Abs(deltaX) > Math.Abs(deltaY))
{
// If horizontal movement is greater, primary side is horizontal
primarySide = deltaX > 0 ? RoomSide.Right : RoomSide.Left;
secondarySide = deltaY > 0 ? RoomSide.Bottom : RoomSide.Top;
}
else
{
// If vertical movement is greater, primary side is vertical
primarySide = deltaY > 0 ? RoomSide.Bottom : RoomSide.Top;
secondarySide = deltaX > 0 ? RoomSide.Right : RoomSide.Left;
}
return (primarySide, secondarySide);
}
} // Class
} // Namespace