MazeGenerator.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. using UnityEngine;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. /// <summary>
  5. /// Generates procedural mazes with rooms, hallways, and various terrain types
  6. /// Designed with AI pathfinding in mind - guarantees multiple paths and accessibility
  7. /// </summary>
  8. public class MazeGenerator
  9. {
  10. public delegate void ProgressUpdate(int completedTiles, int totalTiles, string message);
  11. private MazeConfig config;
  12. private MazeData mazeData;
  13. private System.Random random;
  14. private ProgressUpdate progressCallback;
  15. private int totalTiles;
  16. private int completedTiles;
  17. private string currentPhase;
  18. public MazeGenerator(MazeConfig config, ProgressUpdate progressCallback = null)
  19. {
  20. this.config = config;
  21. this.progressCallback = progressCallback;
  22. config.Validate();
  23. if (config.UseRandomSeed)
  24. {
  25. random = new System.Random();
  26. }
  27. else
  28. {
  29. random = new System.Random(config.RandomSeed);
  30. }
  31. }
  32. private void ReportProgress(string message)
  33. {
  34. var fullMessage = string.IsNullOrEmpty(currentPhase)
  35. ? message
  36. : $"{currentPhase}: {message}";
  37. progressCallback?.Invoke(completedTiles, totalTiles, fullMessage);
  38. }
  39. private void SetPhase(string phase)
  40. {
  41. currentPhase = phase;
  42. ReportProgress("Starting");
  43. }
  44. /// <summary>
  45. /// Generates a complete maze based on the config
  46. /// </summary>
  47. public MazeData Generate()
  48. {
  49. mazeData = new MazeData(config.Width, config.Height);
  50. totalTiles = config.Width * config.Height;
  51. completedTiles = 0;
  52. SetPhase("Initialize");
  53. // Step 1: Generate room layouts
  54. GenerateRooms();
  55. // Step 2: Connect rooms with hallways
  56. ConnectRooms();
  57. // Step 3: Apply terrain types
  58. ApplyTerrains();
  59. // Step 4: Place start and exit points
  60. PlaceStartAndExitPoints();
  61. // Step 5: Ensure connectivity (verify all rooms are reachable)
  62. EnsureConnectivity();
  63. SetPhase("Complete");
  64. Debug.Log(mazeData.GetStatistics());
  65. return mazeData;
  66. }
  67. /// <summary>
  68. /// Step 1: Generates room layouts
  69. /// </summary>
  70. private void GenerateRooms()
  71. {
  72. SetPhase("Generate Rooms");
  73. var rooms = new List<MazeRoom>();
  74. int attempts = 0;
  75. int maxAttempts = config.TargetRoomCount * 10;
  76. while (rooms.Count < config.TargetRoomCount && attempts < maxAttempts)
  77. {
  78. attempts++;
  79. int width = random.Next(config.MinRoomWidth, config.MaxRoomWidth + 1);
  80. int height = random.Next(config.MinRoomHeight, config.MaxRoomHeight + 1);
  81. int x = random.Next(1, config.Width - width - 1);
  82. int y = random.Next(1, config.Height - height - 1);
  83. var newRoom = new MazeRoom(rooms.Count, x, y, x + width - 1, y + height - 1);
  84. // Check if room overlaps with existing rooms or walls (with spacing)
  85. if (CanPlaceRoom(newRoom, rooms))
  86. {
  87. rooms.Add(newRoom);
  88. }
  89. }
  90. // Assign room types based on configuration
  91. AssignRoomTypes(rooms);
  92. // Fill rooms in maze data
  93. for (int i = 0; i < rooms.Count; i++)
  94. {
  95. var room = rooms[i];
  96. mazeData.AddRoom(room.MinX, room.MinY, room.MaxX, room.MaxY, room.Type);
  97. int filled = mazeData.FillRoom(room.MinX + 1, room.MinY + 1, room.MaxX - 1, room.MaxY - 1, i);
  98. completedTiles += filled;
  99. ReportProgress($"Generated room {i + 1}/{rooms.Count} ({completedTiles}/{totalTiles} tiles)");
  100. }
  101. Debug.Log($"Generated {rooms.Count} rooms");
  102. }
  103. /// <summary>
  104. /// Checks if a room can be placed without overlapping or being too close to others
  105. /// </summary>
  106. private bool CanPlaceRoom(MazeRoom newRoom, List<MazeRoom> existingRooms)
  107. {
  108. // Check bounds
  109. if (newRoom.MinX - config.MinRoomSpacing < 0 || newRoom.MaxX + config.MinRoomSpacing >= config.Width ||
  110. newRoom.MinY - config.MinRoomSpacing < 0 || newRoom.MaxY + config.MinRoomSpacing >= config.Height)
  111. {
  112. return false;
  113. }
  114. // Check overlap/spacing with existing rooms
  115. foreach (var room in existingRooms)
  116. {
  117. // Add spacing buffer
  118. int minX = room.MinX - config.MinRoomSpacing;
  119. int minY = room.MinY - config.MinRoomSpacing;
  120. int maxX = room.MaxX + config.MinRoomSpacing;
  121. int maxY = room.MaxY + config.MinRoomSpacing;
  122. if (!(newRoom.MaxX < minX || newRoom.MinX > maxX ||
  123. newRoom.MaxY < minY || newRoom.MinY > maxY))
  124. {
  125. return false;
  126. }
  127. }
  128. return true;
  129. }
  130. /// <summary>
  131. /// Assigns room types (safe, rest, boss, etc.) to rooms
  132. /// </summary>
  133. private void AssignRoomTypes(List<MazeRoom> rooms)
  134. {
  135. var roomList = new List<MazeRoom>(rooms);
  136. // Assign special room types
  137. DistributeRoomType(roomList, MazeRoom.RoomType.Safe, config.SafeRoomCount);
  138. DistributeRoomType(roomList, MazeRoom.RoomType.Rest, config.RestRoomCount);
  139. DistributeRoomType(roomList, MazeRoom.RoomType.Boss, config.BossRoomCount);
  140. // Reset unassigned rooms to Normal
  141. foreach (var room in rooms)
  142. {
  143. if (room.Type != MazeRoom.RoomType.Safe &&
  144. room.Type != MazeRoom.RoomType.Rest &&
  145. room.Type != MazeRoom.RoomType.Boss)
  146. {
  147. room.Type = MazeRoom.RoomType.Normal;
  148. }
  149. }
  150. }
  151. /// <summary>
  152. /// Distributes a specific room type to random rooms
  153. /// </summary>
  154. private void DistributeRoomType(List<MazeRoom> rooms, MazeRoom.RoomType type, int count)
  155. {
  156. int assigned = 0;
  157. var available = rooms.Where(r => r.Type == MazeRoom.RoomType.Normal).ToList();
  158. while (assigned < count && available.Count > 0)
  159. {
  160. int idx = random.Next(available.Count);
  161. var room = available[idx];
  162. room.Type = type;
  163. available.RemoveAt(idx);
  164. assigned++;
  165. }
  166. }
  167. /// <summary>
  168. /// Step 2: Connects rooms with hallways
  169. /// Uses a modified Minimum Spanning Tree approach
  170. /// </summary>
  171. private void ConnectRooms()
  172. {
  173. SetPhase("Connect Rooms");
  174. var rooms = mazeData.Rooms;
  175. if (rooms.Count == 0) return;
  176. var connected = new HashSet<int> { rooms[0].Id };
  177. var unconnected = new HashSet<int>(rooms.Select(r => r.Id));
  178. unconnected.Remove(rooms[0].Id);
  179. while (unconnected.Count > 0)
  180. {
  181. // Find closest connected-unconnected room pair
  182. float minDist = float.MaxValue;
  183. MazeRoom from = null;
  184. MazeRoom to = null;
  185. foreach (var connectedId in connected)
  186. {
  187. var connectedRoom = rooms.First(r => r.Id == connectedId);
  188. foreach (var unconnectedId in unconnected)
  189. {
  190. var unconnectedRoom = rooms.First(r => r.Id == unconnectedId);
  191. float dist = Vector2Int.Distance(connectedRoom.GetCenter(), unconnectedRoom.GetCenter());
  192. if (dist < minDist)
  193. {
  194. minDist = dist;
  195. from = connectedRoom;
  196. to = unconnectedRoom;
  197. }
  198. }
  199. }
  200. if (from != null && to != null)
  201. {
  202. // Create hallway between rooms
  203. CreateHallway(from, to);
  204. connected.Add(to.Id);
  205. unconnected.Remove(to.Id);
  206. }
  207. }
  208. // Add extra connections for multiple paths
  209. AddExtraConnections(rooms);
  210. Debug.Log("Connected rooms with hallways");
  211. }
  212. /// <summary>
  213. /// Creates a hallway between two rooms
  214. /// </summary>
  215. private int CreateHallway(MazeRoom from, MazeRoom to)
  216. {
  217. var pathA = GeneratePath(from.GetCenter(), to.GetCenter());
  218. int changed = mazeData.DrawPath(pathA, config.MinHallwayWidth);
  219. completedTiles += changed;
  220. ReportProgress($"Connected room {from.Id} to {to.Id} ({completedTiles}/{totalTiles} tiles)");
  221. return changed;
  222. }
  223. /// <summary>
  224. /// Generates a path between two points using A* or simple line algorithm
  225. /// </summary>
  226. private List<Vector2Int> GeneratePath(Vector2Int start, Vector2Int end)
  227. {
  228. var path = new List<Vector2Int>();
  229. // Simple L-shaped path (can be enhanced with A* for better aesthetics)
  230. int x = start.x;
  231. int y = start.y;
  232. // Move horizontally first
  233. while (x != end.x)
  234. {
  235. x += x < end.x ? 1 : -1;
  236. path.Add(new Vector2Int(x, y));
  237. }
  238. // Then vertically
  239. while (y != end.y)
  240. {
  241. y += y < end.y ? 1 : -1;
  242. path.Add(new Vector2Int(x, y));
  243. }
  244. return path;
  245. }
  246. /// <summary>
  247. /// Adds extra connections between rooms to create multiple paths
  248. /// </summary>
  249. private void AddExtraConnections(List<MazeRoom> rooms)
  250. {
  251. // For each room, randomly connect to nearby rooms
  252. int extraConnections = Mathf.Max(1, rooms.Count / 10);
  253. for (int i = 0; i < extraConnections; i++)
  254. {
  255. var room1 = rooms[random.Next(rooms.Count)];
  256. var room2 = rooms[random.Next(rooms.Count)];
  257. if (room1 != room2)
  258. {
  259. CreateHallway(room1, room2);
  260. }
  261. }
  262. }
  263. /// <summary>
  264. /// Step 3: Applies different terrain types to areas
  265. /// </summary>
  266. private void ApplyTerrains()
  267. {
  268. SetPhase("Apply Terrains");
  269. var normalRooms = mazeData.GetRoomsByType(MazeRoom.RoomType.Normal);
  270. // Apply swampy terrain to some normal rooms
  271. for (int i = 0; i < normalRooms.Count * 0.2f; i++)
  272. {
  273. var room = normalRooms[random.Next(normalRooms.Count)];
  274. ApplyTerrainToRoom(room, MazeTile.TerrainType.Swamp);
  275. }
  276. // Apply stone terrain to some normal rooms
  277. for (int i = 0; i < normalRooms.Count * 0.1f; i++)
  278. {
  279. var room = normalRooms[random.Next(normalRooms.Count)];
  280. ApplyTerrainToRoom(room, MazeTile.TerrainType.Stone);
  281. }
  282. }
  283. /// <summary>
  284. /// Applies a specific terrain type to a room
  285. /// </summary>
  286. private void ApplyTerrainToRoom(MazeRoom room, MazeTile.TerrainType terrain)
  287. {
  288. for (int x = room.MinX + 1; x < room.MaxX; x++)
  289. {
  290. for (int y = room.MinY + 1; y < room.MaxY; y++)
  291. {
  292. var tile = mazeData.GetTile(x, y);
  293. if (tile.Type == MazeTile.TileType.Floor)
  294. {
  295. tile.Terrain = terrain;
  296. }
  297. }
  298. }
  299. }
  300. /// <summary>
  301. /// Step 4: Places start and exit points
  302. /// </summary>
  303. private void PlaceStartAndExitPoints()
  304. {
  305. SetPhase("Place Start/Exit Points");
  306. var rooms = mazeData.Rooms;
  307. if (rooms.Count == 0) return;
  308. // Place start points
  309. int startCount = random.Next(config.MinStartPoints, config.MaxStartPoints + 1);
  310. for (int i = 0; i < startCount && i < rooms.Count; i++)
  311. {
  312. var room = rooms[i];
  313. room.IsStart = true;
  314. var point = room.GetRandomPoint();
  315. mazeData.AddStartPoint(point);
  316. }
  317. // Place exit points
  318. int exitCount = random.Next(config.MinExits, config.MaxExits + 1);
  319. var endRooms = mazeData.GetRoomsByType(MazeRoom.RoomType.End);
  320. if (endRooms.Count == 0)
  321. {
  322. // Create end rooms if they don't exist
  323. int exitsPlaced = 0;
  324. int roomIndex = 0;
  325. while (exitsPlaced < exitCount && roomIndex < rooms.Count)
  326. {
  327. if (!rooms[roomIndex].IsStart)
  328. {
  329. rooms[roomIndex].Type = MazeRoom.RoomType.End;
  330. rooms[roomIndex].IsEnd = true;
  331. var point = rooms[roomIndex].GetRandomPoint();
  332. mazeData.AddExitPoint(point);
  333. exitsPlaced++;
  334. }
  335. roomIndex++;
  336. }
  337. }
  338. else
  339. {
  340. foreach (var room in endRooms.Take(exitCount))
  341. {
  342. room.IsEnd = true;
  343. var point = room.GetRandomPoint();
  344. mazeData.AddExitPoint(point);
  345. }
  346. }
  347. Debug.Log($"Placed {startCount} start points and {exitCount} exit points");
  348. }
  349. /// <summary>
  350. /// Step 5: Ensures all rooms are connected and reachable
  351. /// </summary>
  352. private void EnsureConnectivity()
  353. {
  354. SetPhase("Ensure Connectivity");
  355. // Simple BFS from first start point to check connectivity
  356. if (mazeData.StartPoints.Count == 0) return;
  357. var start = mazeData.StartPoints[0];
  358. var visited = new HashSet<Vector2Int>();
  359. var queue = new Queue<Vector2Int>();
  360. queue.Enqueue(start);
  361. visited.Add(start);
  362. while (queue.Count > 0)
  363. {
  364. var current = queue.Dequeue();
  365. var adjacent = mazeData.GetAdjacentWalkable(current.x, current.y);
  366. foreach (var next in adjacent)
  367. {
  368. if (!visited.Contains(next))
  369. {
  370. visited.Add(next);
  371. queue.Enqueue(next);
  372. }
  373. }
  374. }
  375. Debug.Log($"Connectivity check: {visited.Count} tiles reachable from start point");
  376. }
  377. }