|
|
@@ -57,8 +57,10 @@ namespace MazeGen
|
|
|
/// checks whether the spawn spot would overlap an existing room that has an unused door
|
|
|
/// facing back toward fromRoom - if so, connects into that door (straight, or via a
|
|
|
/// single 90-degree elbow hallway) instead of creating a new, overlapping room. Failing
|
|
|
- /// that, if the spawn spot overlaps anything, the candidate room is nudged to a nearby
|
|
|
- /// clear spot. Either way, the two-way connection is wired up before returning.
|
|
|
+ /// that, if the spawn spot overlaps anything, tries routing around it via cardinal
|
|
|
+ /// directions. If all cardinal routing fails, retries TryConnectViaExistingDoor with a
|
|
|
+ /// much wider search radius. If that also fails, marks the exit as blocked and returns
|
|
|
+ /// null (agent will pick a different exit next decision tick).
|
|
|
/// </summary>
|
|
|
public Room GenerateConnectedRoom(Room fromRoom, Exit fromExit, MazeAgent agent)
|
|
|
{
|
|
|
@@ -86,7 +88,16 @@ namespace MazeGen
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
- position = FindClearPosition(position, radius, fromRoom.Id);
|
|
|
+ // Routing failed - try one more time with a much wider search in TryConnectViaExistingDoor
|
|
|
+ float extraSearchRadius = settings.maxRouteSearchSteps * settings.repositionRingStep;
|
|
|
+ Room wideConnected = TryConnectViaExistingDoorWidened(fromRoom, fromExit, position, radius, extraSearchRadius);
|
|
|
+ if (wideConnected != null)
|
|
|
+ return wideConnected;
|
|
|
+
|
|
|
+ // All options exhausted - mark the exit as blocked and refuse to generate
|
|
|
+ Debug.LogError($"[MazeGen] Could not find any collision-free, path-clear spot for a room beyond room {fromRoom.Id}'s exit {fromExit.Id} even after widening the search. Blocking this exit permanently.");
|
|
|
+ fromExit.IsBlocked = true;
|
|
|
+ return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -154,11 +165,11 @@ namespace MazeGen
|
|
|
Vector2 edgeFrom = fromRoom.Position + dir * fromRoom.Radius;
|
|
|
float straightEntryAngle = NormalizeAngle(fromExit.DirectionDegrees + 180f);
|
|
|
|
|
|
- for (int i = 0; i < settings.maxRepositionRings; i++)
|
|
|
+ for (int i = 0; i < settings.maxRouteSearchSteps; i++)
|
|
|
{
|
|
|
float distance = settings.roomSpacing + i * settings.repositionRingStep;
|
|
|
Vector2 candidate = fromRoom.Position + dir * distance;
|
|
|
- if (!HasOverlap(candidate, radius, fromRoom.Id) && IsPathClear(edgeFrom, candidate, fromRoom.Id, -1))
|
|
|
+ if (!HasOverlap(candidate, radius, fromRoom.Id) && IsPathClear(edgeFrom, candidate, settings.hallwayRadius, fromRoom.Id, -1))
|
|
|
{
|
|
|
finalPosition = candidate;
|
|
|
bendCorner = null;
|
|
|
@@ -168,21 +179,21 @@ namespace MazeGen
|
|
|
}
|
|
|
|
|
|
Vector2[] perpendiculars = { new Vector2(-dir.y, dir.x), new Vector2(dir.y, -dir.x) };
|
|
|
- for (int i = 0; i < settings.maxRepositionRings; i++)
|
|
|
+ for (int i = 0; i < settings.maxRouteSearchSteps; i++)
|
|
|
{
|
|
|
float cornerDistance = settings.roomSpacing + i * settings.repositionRingStep;
|
|
|
Vector2 corner = fromRoom.Position + dir * cornerDistance;
|
|
|
- if (!IsPathClear(edgeFrom, corner, fromRoom.Id, -1)) continue;
|
|
|
+ if (!IsPathClear(edgeFrom, corner, settings.hallwayRadius, fromRoom.Id, -1)) continue;
|
|
|
if (HasOverlap(corner, settings.hallwayRadius, fromRoom.Id)) continue;
|
|
|
|
|
|
foreach (var perp in perpendiculars)
|
|
|
{
|
|
|
- for (int j = 0; j < settings.maxRepositionRings; j++)
|
|
|
+ for (int j = 0; j < settings.maxRouteSearchSteps; j++)
|
|
|
{
|
|
|
float sideDistance = settings.roomSpacing + j * settings.repositionRingStep;
|
|
|
Vector2 candidate = corner + perp * sideDistance;
|
|
|
if (HasOverlap(candidate, radius, fromRoom.Id)) continue;
|
|
|
- if (!IsPathClear(corner, candidate, fromRoom.Id, -1)) continue;
|
|
|
+ if (!IsPathClear(corner, candidate, settings.hallwayRadius, fromRoom.Id, -1)) continue;
|
|
|
|
|
|
finalPosition = candidate;
|
|
|
bendCorner = corner;
|
|
|
@@ -248,6 +259,71 @@ namespace MazeGen
|
|
|
/// a brand new room being created. Returns the room fromExit now leads to, or null if
|
|
|
/// no overlapping room offers a usable, reachable door.
|
|
|
/// </summary>
|
|
|
+ /// <summary>
|
|
|
+ /// Last-resort version of TryConnectViaExistingDoor: searches with a much larger radius
|
|
|
+ /// to find *any* room that can be reached and merged, not just ones overlapping the
|
|
|
+ /// immediate candidate spot.
|
|
|
+ /// </summary>
|
|
|
+ private Room TryConnectViaExistingDoorWidened(Room fromRoom, Exit fromExit, Vector2 candidatePosition, float candidateRadius, float extraSearchRadius)
|
|
|
+ {
|
|
|
+ float searchRadius = candidateRadius + MaxRoomRadius() + extraSearchRadius;
|
|
|
+ var candidates = new List<Room>();
|
|
|
+ foreach (var other in _graph.QueryRoomsNear(fromRoom.Position, searchRadius))
|
|
|
+ {
|
|
|
+ if (other.Id == fromRoom.Id) continue;
|
|
|
+ candidates.Add(other);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Sort by distance to fromRoom position (try nearest first)
|
|
|
+ candidates.Sort((a, b) => Vector2.Distance(fromRoom.Position, a.Position).CompareTo(Vector2.Distance(fromRoom.Position, b.Position)));
|
|
|
+
|
|
|
+ foreach (var other in candidates)
|
|
|
+ {
|
|
|
+ Vector2 towardFromRoom = fromRoom.Position - other.Position;
|
|
|
+ if (towardFromRoom.sqrMagnitude < 0.0001f) continue;
|
|
|
+ float desiredAngle = NearestCardinal(Mathf.Atan2(towardFromRoom.y, towardFromRoom.x) * Mathf.Rad2Deg);
|
|
|
+
|
|
|
+ Exit targetExit = null;
|
|
|
+ foreach (var exit in other.UnusedExits)
|
|
|
+ {
|
|
|
+ if (Mathf.Approximately(NormalizeAngle(exit.DirectionDegrees), desiredAngle))
|
|
|
+ {
|
|
|
+ targetExit = exit;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ bool needsNewExit = targetExit == null;
|
|
|
+ if (needsNewExit)
|
|
|
+ {
|
|
|
+ bool sideOccupied = other.Exits.Exists(e => Mathf.Approximately(NormalizeAngle(e.DirectionDegrees), desiredAngle));
|
|
|
+ if (sideOccupied) continue;
|
|
|
+ if (other.Exits.Count >= settings.maxExitsPerRoom) continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ int exitsConsumed = needsNewExit ? 1 : 2;
|
|
|
+ if (WouldStarveComponent(fromRoom, other, exitsConsumed)) continue;
|
|
|
+
|
|
|
+ if (needsNewExit)
|
|
|
+ {
|
|
|
+ targetExit = new Exit { Id = _graph.AllocateExitId(), OwnerRoomId = other.Id, DirectionDegrees = desiredAngle };
|
|
|
+ _graph.AddExit(other, targetExit);
|
|
|
+ }
|
|
|
+
|
|
|
+ Room connected = TryBuildDoorConnection(fromRoom, fromExit, other, targetExit);
|
|
|
+ if (connected != null)
|
|
|
+ {
|
|
|
+ if (needsNewExit) ExitAdded?.Invoke(other, targetExit);
|
|
|
+ return connected;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (needsNewExit)
|
|
|
+ _graph.RemoveUnusedExit(other, targetExit);
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
private Room TryConnectViaExistingDoor(Room fromRoom, Exit fromExit, Vector2 candidatePosition, float candidateRadius)
|
|
|
{
|
|
|
float searchRadius = candidateRadius + MaxRoomRadius();
|
|
|
@@ -255,8 +331,7 @@ namespace MazeGen
|
|
|
foreach (var other in _graph.QueryRoomsNear(candidatePosition, searchRadius))
|
|
|
{
|
|
|
if (other.Id == fromRoom.Id) continue;
|
|
|
- float dist = Vector2.Distance(candidatePosition, other.Position);
|
|
|
- if (dist > candidateRadius + other.Radius) continue; // footprints don't actually overlap
|
|
|
+ if (!SquaresOverlap(candidatePosition, candidateRadius, other.Position, other.Radius)) continue;
|
|
|
overlapping.Add(other);
|
|
|
}
|
|
|
|
|
|
@@ -346,9 +421,16 @@ namespace MazeGen
|
|
|
|
|
|
private Room ConnectStraight(Room fromRoom, Exit fromExit, Room other, Exit targetExit)
|
|
|
{
|
|
|
+ // Defensive check: the two rooms shouldn't already overlap
|
|
|
+ if (SquaresOverlap(fromRoom.Position, fromRoom.Radius, other.Position, other.Radius))
|
|
|
+ {
|
|
|
+ Debug.LogWarning($"[MazeGen] ConnectStraight refused: room {fromRoom.Id} and room {other.Id} already overlap.");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
Vector2 pFrom = fromRoom.Position + fromExit.Direction * fromRoom.Radius;
|
|
|
Vector2 pTo = other.Position + targetExit.Direction * other.Radius;
|
|
|
- if (!IsPathClear(pFrom, pTo, fromRoom.Id, other.Id)) return null;
|
|
|
+ if (!IsPathClear(pFrom, pTo, settings.hallwayRadius, fromRoom.Id, other.Id)) return null;
|
|
|
|
|
|
ConnectExits(fromExit, targetExit);
|
|
|
RoomsConnected?.Invoke(fromRoom, fromExit, other);
|
|
|
@@ -368,8 +450,8 @@ namespace MazeGen
|
|
|
if (Vector2.Dot(corner - pFrom, fromExit.Direction) <= 0f) return null;
|
|
|
if (Vector2.Dot(pTo - corner, -targetExit.Direction) <= 0f) return null;
|
|
|
|
|
|
- if (!IsPathClear(pFrom, corner, fromRoom.Id, other.Id)) return null;
|
|
|
- if (!IsPathClear(corner, pTo, fromRoom.Id, other.Id)) return null;
|
|
|
+ if (!IsPathClear(pFrom, corner, settings.hallwayRadius, fromRoom.Id, other.Id)) return null;
|
|
|
+ if (!IsPathClear(corner, pTo, settings.hallwayRadius, fromRoom.Id, other.Id)) return null;
|
|
|
if (HasOverlap(corner, settings.hallwayRadius, fromRoom.Id, other.Id)) return null;
|
|
|
|
|
|
var hallway = new Room(_graph.AllocateRoomId(), RoomKind.Hallway, corner, settings.hallwayRadius, fromRoom.DepthFromStart + 1);
|
|
|
@@ -399,68 +481,76 @@ namespace MazeGen
|
|
|
return hallway;
|
|
|
}
|
|
|
|
|
|
- /// <summary>True if any registered room (other than those excluded) overlaps a circle at position/radius.</summary>
|
|
|
+ /// <summary>True if any registered room or corridor segment (other than those excluded) overlaps a square at position/radius.</summary>
|
|
|
private bool HasOverlap(Vector2 position, float radius, params int[] excludeRoomIds)
|
|
|
{
|
|
|
float searchRadius = radius + MaxRoomRadius();
|
|
|
foreach (var room in _graph.QueryRoomsNear(position, searchRadius))
|
|
|
{
|
|
|
if (Array.IndexOf(excludeRoomIds, room.Id) >= 0) continue;
|
|
|
- if (Vector2.Distance(position, room.Position) <= radius + room.Radius) return true;
|
|
|
+ if (SquaresOverlap(position, radius, room.Position, room.Radius)) return true;
|
|
|
+ }
|
|
|
+ foreach (var seg in _graph.QueryCorridorSegmentsNear(position, radius))
|
|
|
+ {
|
|
|
+ if (Array.IndexOf(excludeRoomIds, seg.OwnerRoomId) >= 0) continue;
|
|
|
+ if (SquareOverlapsRect(position, radius, seg.Min, seg.Max)) return true;
|
|
|
}
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
- /// <summary>True if the straight segment a-b stays clear of every room's footprint (other than those excluded), with a small buffer.</summary>
|
|
|
- private bool IsPathClear(Vector2 a, Vector2 b, int excludeRoomId1, int excludeRoomId2)
|
|
|
+ /// <summary>True if the axis-aligned corridor rectangle from a to b (thickness 2*halfWidth) stays clear of every room's square footprint and every other registered corridor segment (other than those excluded).</summary>
|
|
|
+ private bool IsPathClear(Vector2 a, Vector2 b, float halfWidth, int excludeRoomId1, int excludeRoomId2)
|
|
|
{
|
|
|
+ Vector2 min = new Vector2(Mathf.Min(a.x, b.x), Mathf.Min(a.y, b.y));
|
|
|
+ Vector2 max = new Vector2(Mathf.Max(a.x, b.x), Mathf.Max(a.y, b.y));
|
|
|
+
|
|
|
+ // Pad only in the thin (perpendicular) axis
|
|
|
+ if (Mathf.Approximately(min.y, max.y))
|
|
|
+ {
|
|
|
+ min.y -= halfWidth;
|
|
|
+ max.y += halfWidth;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ min.x -= halfWidth;
|
|
|
+ max.x += halfWidth;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add clearance buffer
|
|
|
+ min -= Vector2.one * settings.hallwayClearance;
|
|
|
+ max += Vector2.one * settings.hallwayClearance;
|
|
|
+
|
|
|
Vector2 mid = (a + b) * 0.5f;
|
|
|
- float searchRadius = Vector2.Distance(a, b) * 0.5f + MaxRoomRadius() + settings.hallwayRadius;
|
|
|
+ float searchRadius = Vector2.Distance(a, b) * 0.5f + MaxRoomRadius() + halfWidth + settings.hallwayClearance;
|
|
|
|
|
|
foreach (var room in _graph.QueryRoomsNear(mid, searchRadius))
|
|
|
{
|
|
|
if (room.Id == excludeRoomId1 || room.Id == excludeRoomId2) continue;
|
|
|
- float distToSegment = DistancePointToSegment(room.Position, a, b);
|
|
|
- float requiredClearance = room.Radius + settings.hallwayRadius + settings.hallwayClearance;
|
|
|
- if (distToSegment < requiredClearance) return false;
|
|
|
+ if (SquareOverlapsRect(room.Position, room.Radius, min, max)) return false;
|
|
|
+ }
|
|
|
+ foreach (var seg in _graph.QueryCorridorSegmentsNear(mid, searchRadius))
|
|
|
+ {
|
|
|
+ if (seg.OwnerRoomId == excludeRoomId1 || seg.OwnerRoomId == excludeRoomId2) continue;
|
|
|
+ if (RectsOverlap(min, max, seg.Min, seg.Max)) return false;
|
|
|
}
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
- private static float DistancePointToSegment(Vector2 p, Vector2 a, Vector2 b)
|
|
|
- {
|
|
|
- Vector2 ab = b - a;
|
|
|
- float lengthSq = ab.sqrMagnitude;
|
|
|
- if (lengthSq < 0.0001f) return Vector2.Distance(p, a);
|
|
|
+ // AABB overlap primitives (axis-aligned, since all directions are cardinal)
|
|
|
|
|
|
- float t = Mathf.Clamp01(Vector2.Dot(p - a, ab) / lengthSq);
|
|
|
- Vector2 closest = a + ab * t;
|
|
|
- return Vector2.Distance(p, closest);
|
|
|
- }
|
|
|
+ /// <summary>True if two axis-aligned squares (centers posA/posB, half-sides rA/rB) overlap.</summary>
|
|
|
+ private static bool SquaresOverlap(Vector2 posA, float rA, Vector2 posB, float rB) =>
|
|
|
+ Mathf.Abs(posA.x - posB.x) < rA + rB && Mathf.Abs(posA.y - posB.y) < rA + rB;
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Searches outward in a ring pattern around `origin` for the nearest spot where a
|
|
|
- /// room of `radius` wouldn't overlap anything already placed, so an overlapping spawn
|
|
|
- /// can be nudged aside instead of just sitting on top of another room.
|
|
|
- /// </summary>
|
|
|
- private Vector2 FindClearPosition(Vector2 origin, float radius, int excludeRoomId)
|
|
|
- {
|
|
|
- for (int ring = 1; ring <= settings.maxRepositionRings; ring++)
|
|
|
- {
|
|
|
- float ringRadius = ring * settings.repositionRingStep;
|
|
|
- for (int i = 0; i < settings.repositionSamplesPerRing; i++)
|
|
|
- {
|
|
|
- float angle = (360f / settings.repositionSamplesPerRing) * i;
|
|
|
- Vector2 offset = new Vector2(Mathf.Cos(angle * Mathf.Deg2Rad), Mathf.Sin(angle * Mathf.Deg2Rad)) * ringRadius;
|
|
|
- Vector2 candidate = origin + offset;
|
|
|
- if (!HasOverlap(candidate, radius, excludeRoomId))
|
|
|
- return candidate;
|
|
|
- }
|
|
|
- }
|
|
|
+ /// <summary>True if an axis-aligned square (pos/radius) overlaps an axis-aligned rect [min,max].</summary>
|
|
|
+ private static bool SquareOverlapsRect(Vector2 pos, float radius, Vector2 rectMin, Vector2 rectMax) =>
|
|
|
+ pos.x + radius > rectMin.x && pos.x - radius < rectMax.x &&
|
|
|
+ pos.y + radius > rectMin.y && pos.y - radius < rectMax.y;
|
|
|
+
|
|
|
+ /// <summary>True if two axis-aligned rects overlap.</summary>
|
|
|
+ private static bool RectsOverlap(Vector2 aMin, Vector2 aMax, Vector2 bMin, Vector2 bMax) =>
|
|
|
+ aMin.x < bMax.x && aMax.x > bMin.x && aMin.y < bMax.y && aMax.y > bMin.y;
|
|
|
|
|
|
- Debug.LogWarning("[MazeGen] Could not find a collision-free spot for an overlapping room; leaving it at its original (overlapping) position.");
|
|
|
- return origin;
|
|
|
- }
|
|
|
|
|
|
/// <summary>
|
|
|
/// True if connecting fromRoom and other (consuming `exitsConsumed` unused exits in
|