MazeGraph.cs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. using System.Collections.Generic;
  2. using UnityEngine;
  3. namespace MazeGen
  4. {
  5. /// <summary>
  6. /// The full set of generated rooms so far, plus O(1)-ish id lookup, spatial queries and
  7. /// exit-usage bookkeeping. Everything else (agent tracking, visualization, collision/merge
  8. /// logic) reads from here instead of walking the scene hierarchy.
  9. ///
  10. /// Also tracks connected components (union-find over rooms, merged whenever two exits get
  11. /// connected) so "does an exit already exist" and "are there still unused exits" can be
  12. /// asked per-component instead of globally. That distinction matters once there are
  13. /// multiple start rooms: a global answer would let one isolated branch's safety valves get
  14. /// silently satisfied by an exit room that lives in a totally different, disconnected part
  15. /// of the maze - which is exactly how a branch can end up with no way out at all.
  16. /// </summary>
  17. public class MazeGraph
  18. {
  19. public readonly Dictionary<int, Room> Rooms = new Dictionary<int, Room>();
  20. public readonly List<Room> StartRooms = new List<Room>();
  21. public readonly List<Room> ExitRooms = new List<Room>();
  22. private readonly SpatialRoomIndex _spatialIndex;
  23. // Union-find over room ids, keyed by root id.
  24. private readonly Dictionary<int, int> _componentParent = new Dictionary<int, int>();
  25. private readonly Dictionary<int, bool> _componentHasExit = new Dictionary<int, bool>();
  26. private readonly Dictionary<int, int> _componentUnusedExitCount = new Dictionary<int, int>();
  27. private int _nextRoomId;
  28. private int _nextExitId;
  29. public int RoomCount => Rooms.Count;
  30. /// <summary>Total exits across the whole graph that are not yet used (informational only - generation decisions should use ComponentUnusedExitCount instead).</summary>
  31. public int UnusedExitCount { get; private set; }
  32. public MazeGraph(float spatialCellSize)
  33. {
  34. _spatialIndex = new SpatialRoomIndex(spatialCellSize);
  35. }
  36. public int AllocateRoomId() => _nextRoomId++;
  37. public int AllocateExitId() => _nextExitId++;
  38. public void Register(Room room)
  39. {
  40. Rooms[room.Id] = room;
  41. if (room.Kind == RoomKind.Start) StartRooms.Add(room);
  42. if (room.Kind == RoomKind.Exit)
  43. {
  44. ExitRooms.Add(room);
  45. _componentHasExit[Find(room.Id)] = true;
  46. }
  47. _spatialIndex.Add(room);
  48. }
  49. public Room Get(int id) => Rooms.TryGetValue(id, out var room) ? room : null;
  50. public IEnumerable<Room> QueryRoomsNear(Vector2 position, float radius) =>
  51. _spatialIndex.QueryNearby(position, radius);
  52. /// <summary>Adds an exit to a room and keeps the unused-exit counters (global and per-component) in sync.</summary>
  53. public void AddExit(Room room, Exit exit)
  54. {
  55. room.Exits.Add(exit);
  56. if (!exit.IsUsed)
  57. {
  58. UnusedExitCount++;
  59. _componentUnusedExitCount[Find(room.Id)]++;
  60. }
  61. }
  62. /// <summary>Marks an exit as used (idempotent) and keeps the unused-exit counters in sync.</summary>
  63. public void MarkExitUsed(Exit exit)
  64. {
  65. if (exit.IsUsed) return;
  66. exit.IsUsed = true;
  67. UnusedExitCount--;
  68. _componentUnusedExitCount[Find(exit.OwnerRoomId)]--;
  69. }
  70. /// <summary>
  71. /// Undoes a speculative AddExit for an exit that turned out to be unusable (e.g. a
  72. /// door added on-the-fly to close a loop, where the path to it was later found to be
  73. /// blocked). Only valid for still-unused exits.
  74. /// </summary>
  75. public void RemoveUnusedExit(Room room, Exit exit)
  76. {
  77. if (exit.IsUsed) return;
  78. if (room.Exits.Remove(exit))
  79. {
  80. UnusedExitCount--;
  81. _componentUnusedExitCount[Find(room.Id)]--;
  82. }
  83. }
  84. /// <summary>Merges the connected components containing the two rooms - call whenever a connection is made between them.</summary>
  85. public void Union(int roomIdA, int roomIdB)
  86. {
  87. int a = Find(roomIdA);
  88. int b = Find(roomIdB);
  89. if (a == b) return;
  90. _componentParent[b] = a;
  91. _componentHasExit[a] = _componentHasExit[a] || _componentHasExit[b];
  92. _componentUnusedExitCount[a] += _componentUnusedExitCount[b];
  93. _componentHasExit.Remove(b);
  94. _componentUnusedExitCount.Remove(b);
  95. }
  96. public bool AreSameComponent(int roomIdA, int roomIdB) => Find(roomIdA) == Find(roomIdB);
  97. /// <summary>True if an Exit room already exists anywhere in the same connected component as this room.</summary>
  98. public bool ComponentHasExit(int roomId) => _componentHasExit[Find(roomId)];
  99. /// <summary>Unused exits remaining within this room's connected component only.</summary>
  100. public int ComponentUnusedExitCount(int roomId) => _componentUnusedExitCount[Find(roomId)];
  101. private int Find(int roomId)
  102. {
  103. if (!_componentParent.ContainsKey(roomId))
  104. {
  105. _componentParent[roomId] = roomId;
  106. _componentHasExit[roomId] = false;
  107. _componentUnusedExitCount[roomId] = 0;
  108. return roomId;
  109. }
  110. int parent = _componentParent[roomId];
  111. if (parent != roomId)
  112. {
  113. parent = Find(parent); // path compression
  114. _componentParent[roomId] = parent;
  115. }
  116. return parent;
  117. }
  118. }
  119. }