MazeSceneBuilder.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using UnityEngine;
  5. namespace MazeGen
  6. {
  7. /// <summary>
  8. /// Scene entry point: drop this on one empty GameObject in the scene and press Play.
  9. /// Generates the configured start rooms, then spawns and drives a configurable number
  10. /// of autonomous agents that explore the maze on their own (see AutonomousAgentBrain).
  11. /// The level ends when enough agents reach an exit room or a time limit is hit. A simple
  12. /// IMGUI panel lists all agents (click to have the camera follow one) and offers a
  13. /// "zoom to fit maze" button.
  14. /// </summary>
  15. [RequireComponent(typeof(MazeGenerator))]
  16. public class MazeSceneBuilder : MonoBehaviour
  17. {
  18. [Header("Agents")]
  19. [SerializeField, Min(1)] private int agentCount = 1;
  20. [Tooltip("Each agent's spawn is delayed by a random amount in this range (seconds), so they enter the maze staggered over time instead of all at once.")]
  21. [SerializeField] private Vector2 spawnStaggerRange = new Vector2(0f, 3f);
  22. [Tooltip("Each agent's FIRST decision is delayed by a random amount in this range (seconds); every decision after that uses the steady decisionInterval.")]
  23. [SerializeField] private Vector2 initialDecisionDelayRange = new Vector2(0.5f, 2f);
  24. [Tooltip("Steady-state time (seconds) between an agent's decisions once it gets going.")]
  25. [SerializeField] private float decisionInterval = 1.5f;
  26. [Tooltip("Chance an agent explores a brand-new exit even when a 'follow' (known but personally-unvisited) option is also available.")]
  27. [SerializeField, Range(0f, 1f)] private float exploreChance = 0.85f;
  28. [Header("Win conditions (at least one must be > 0)")]
  29. [SerializeField] private int targetFinishedAgentCount = 1;
  30. [SerializeField] private float timeLimitSeconds = 0f;
  31. private class AgentEntry
  32. {
  33. public MazeAgentController Controller;
  34. public PathVisualizer Path;
  35. public Color Color;
  36. }
  37. private MazeGenerator _generator;
  38. private MazeCameraController _cameraController;
  39. private readonly Dictionary<int, ExitMarker> _exitMarkers = new Dictionary<int, ExitMarker>();
  40. private readonly List<AgentEntry> _agents = new List<AgentEntry>();
  41. private AgentEntry _followedAgent;
  42. private bool _levelEnded;
  43. private float _elapsedTime;
  44. private Vector2 _hudScroll;
  45. private void Awake()
  46. {
  47. _generator = GetComponent<MazeGenerator>();
  48. _generator.RoomGenerated += OnRoomGenerated;
  49. _generator.ExitAdded += OnExitAdded;
  50. var cam = Camera.main;
  51. if (cam != null)
  52. {
  53. _cameraController = cam.GetComponent<MazeCameraController>();
  54. if (_cameraController == null)
  55. _cameraController = cam.gameObject.AddComponent<MazeCameraController>();
  56. }
  57. if (targetFinishedAgentCount <= 0 && timeLimitSeconds <= 0f)
  58. {
  59. Debug.LogWarning("[MazeGen] No end condition configured (targetFinishedAgentCount and timeLimitSeconds are both 0) - defaulting targetFinishedAgentCount to agentCount so the level can still end.");
  60. targetFinishedAgentCount = agentCount;
  61. }
  62. }
  63. private void Start()
  64. {
  65. var settings = _generator.Settings;
  66. int startRoomCount = Mathf.Max(1, settings.startRoomCount);
  67. var startRooms = new List<Room>();
  68. for (int i = 0; i < startRoomCount; i++)
  69. {
  70. Vector2 pos = startRoomCount > 1 ? PickRandomStartPosition(settings, startRooms) : Vector2.zero;
  71. startRooms.Add(_generator.GenerateStartRoom(pos));
  72. }
  73. _cameraController?.FrameBounds(ComputeMazeBounds());
  74. StartCoroutine(SpawnAgentsRoutine(startRooms));
  75. }
  76. /// <summary>
  77. /// Picks a uniformly random point within a disk of radius `startRoomSpread` (instead
  78. /// of evenly around its edge), retrying a few times to avoid landing on top of a
  79. /// start room already placed this call.
  80. /// </summary>
  81. private static Vector2 PickRandomStartPosition(MazeGenerationSettings settings, List<Room> placed)
  82. {
  83. float spread = Mathf.Max(settings.startRoomSpread, 0.01f);
  84. float minDistance = settings.startRoomRadius * 2f;
  85. for (int attempt = 0; attempt < 30; attempt++)
  86. {
  87. Vector2 candidate = Random.insideUnitCircle * spread;
  88. bool overlapsPlaced = false;
  89. foreach (var room in placed)
  90. {
  91. if (Vector2.Distance(candidate, room.Position) < minDistance)
  92. {
  93. overlapsPlaced = true;
  94. break;
  95. }
  96. }
  97. if (!overlapsPlaced) return candidate;
  98. }
  99. return Random.insideUnitCircle * spread; // give up avoiding overlap, use whatever we last rolled
  100. }
  101. private IEnumerator SpawnAgentsRoutine(List<Room> startRooms)
  102. {
  103. for (int i = 0; i < agentCount; i++)
  104. {
  105. float delay = Random.Range(spawnStaggerRange.x, spawnStaggerRange.y);
  106. if (delay > 0f) yield return new WaitForSeconds(delay);
  107. Room startRoom = startRooms[Random.Range(0, startRooms.Count)];
  108. SpawnAgent(startRoom, i);
  109. }
  110. }
  111. private void SpawnAgent(Room startRoom, int index)
  112. {
  113. var agentGO = new GameObject($"Agent_{index}");
  114. agentGO.transform.SetParent(transform, false);
  115. var controller = agentGO.AddComponent<MazeAgentController>();
  116. controller.Initialize(_generator);
  117. // Golden-ratio hue spread gives visually distinct colors even for many agents.
  118. Color color = Color.HSVToRGB((index * 0.618034f) % 1f, 0.65f, 1f);
  119. controller.Spawn(startRoom, color);
  120. var pathGO = new GameObject("Path");
  121. pathGO.transform.SetParent(agentGO.transform, false);
  122. pathGO.AddComponent<LineRenderer>();
  123. var path = pathGO.AddComponent<PathVisualizer>();
  124. path.AppendPoint(startRoom.Position);
  125. pathGO.SetActive(false); // only the followed agent's trail is shown
  126. var brain = agentGO.AddComponent<AutonomousAgentBrain>();
  127. float initialDelay = Random.Range(initialDecisionDelayRange.x, initialDecisionDelayRange.y);
  128. brain.Initialize(controller, _generator, initialDelay, decisionInterval, exploreChance, 1.0f, () => _levelEnded);
  129. controller.Agent.RoomEntered += room => path.AppendPoint(room.Position);
  130. controller.Agent.RoomEntered += _ => RefreshMarkerVisuals();
  131. controller.Agent.RoomEntered += _ => CheckFinishCondition();
  132. controller.Agent.ReachedExitRoom += room => Debug.Log($"[MazeGen] {agentGO.name} reached the exit room (id {room.Id})!");
  133. _agents.Add(new AgentEntry { Controller = controller, Path = path, Color = color });
  134. }
  135. private void OnRoomGenerated(Room room)
  136. {
  137. var roomGO = new GameObject($"Room_{room.Id}_{room.Kind}");
  138. roomGO.transform.SetParent(transform, false);
  139. roomGO.AddComponent<RoomView>().Initialize(room);
  140. foreach (var exit in room.Exits)
  141. SpawnExitMarker(room, exit);
  142. }
  143. private void OnExitAdded(Room room, Exit exit)
  144. {
  145. SpawnExitMarker(room, exit);
  146. RefreshMarkerVisuals();
  147. }
  148. private void SpawnExitMarker(Room room, Exit exit)
  149. {
  150. var markerGO = new GameObject($"Exit_{exit.Id}");
  151. markerGO.transform.SetParent(transform, false);
  152. var marker = markerGO.AddComponent<ExitMarker>();
  153. marker.Initialize(room, exit);
  154. _exitMarkers[exit.Id] = marker;
  155. }
  156. private void RefreshMarkerVisuals()
  157. {
  158. Room currentRoom = _followedAgent?.Controller.Agent?.CurrentRoom;
  159. MazeAgent agent = _followedAgent?.Controller.Agent;
  160. foreach (var marker in _exitMarkers.Values)
  161. marker.UpdateVisual(currentRoom, agent);
  162. }
  163. private void Update()
  164. {
  165. if (_levelEnded) return;
  166. _elapsedTime += Time.deltaTime;
  167. if (timeLimitSeconds > 0f && _elapsedTime >= timeLimitSeconds)
  168. EndLevel($"Time limit of {timeLimitSeconds:0.0}s reached.");
  169. }
  170. private void CheckFinishCondition()
  171. {
  172. if (_levelEnded) return;
  173. int finished = _agents.Count(a => a.Controller.Agent.IsFinished);
  174. if (targetFinishedAgentCount > 0 && finished >= targetFinishedAgentCount)
  175. EndLevel($"{finished} agent(s) reached the exit.");
  176. }
  177. private void EndLevel(string reason)
  178. {
  179. _levelEnded = true;
  180. Debug.Log($"[MazeGen] LEVEL COMPLETE - {reason}");
  181. }
  182. private void FollowAgent(AgentEntry entry)
  183. {
  184. if (_followedAgent == entry) return;
  185. StopFollowing();
  186. _followedAgent = entry;
  187. entry.Controller.SetFollowed(true);
  188. entry.Path.gameObject.SetActive(true);
  189. entry.Controller.Agent.RoomEntered += OnFollowedAgentMoved;
  190. _cameraController?.FocusOn(entry.Controller.Agent.CurrentRoom.Position);
  191. RefreshMarkerVisuals();
  192. }
  193. private void StopFollowing()
  194. {
  195. if (_followedAgent == null) return;
  196. _followedAgent.Controller.SetFollowed(false);
  197. _followedAgent.Path.gameObject.SetActive(false);
  198. _followedAgent.Controller.Agent.RoomEntered -= OnFollowedAgentMoved;
  199. _followedAgent = null;
  200. RefreshMarkerVisuals();
  201. }
  202. private void OnFollowedAgentMoved(Room room) => _cameraController?.FocusOn(room.Position);
  203. private Bounds ComputeMazeBounds()
  204. {
  205. var rooms = _generator.Graph.Rooms.Values;
  206. if (rooms.Count == 0) return new Bounds(Vector3.zero, Vector3.one * 8f);
  207. var bounds = new Bounds(rooms.First().Position, Vector3.zero);
  208. foreach (var room in rooms)
  209. bounds.Encapsulate(new Bounds(room.Position, Vector3.one * (room.Radius * 2f)));
  210. return bounds;
  211. }
  212. private void OnGUI()
  213. {
  214. GUILayout.BeginArea(new Rect(10, 10, 260, Screen.height - 20), GUI.skin.box);
  215. string timeLabel = timeLimitSeconds > 0f ? $"{_elapsedTime:0.0}s / {timeLimitSeconds:0.0}s" : $"{_elapsedTime:0.0}s";
  216. GUILayout.Label($"Elapsed: {timeLabel}");
  217. int finishedCount = _agents.Count(a => a.Controller.Agent.IsFinished);
  218. string finishLabel = targetFinishedAgentCount > 0 ? $"{finishedCount} / {targetFinishedAgentCount}" : $"{finishedCount}";
  219. GUILayout.Label($"Finished: {finishLabel}");
  220. if (_levelEnded)
  221. GUILayout.Label("LEVEL COMPLETE", GUI.skin.box);
  222. if (GUILayout.Button("Zoom to fit maze"))
  223. _cameraController?.FrameBounds(ComputeMazeBounds());
  224. if (_followedAgent != null && GUILayout.Button("Stop following"))
  225. StopFollowing();
  226. GUILayout.Space(6);
  227. GUILayout.Label("Agents (click to follow):");
  228. _hudScroll = GUILayout.BeginScrollView(_hudScroll);
  229. foreach (var entry in _agents)
  230. {
  231. var agent = entry.Controller.Agent;
  232. string status = agent.IsFinished ? "reached exit" : "exploring";
  233. bool isFollowed = entry == _followedAgent;
  234. GUI.color = entry.Color;
  235. if (GUILayout.Button($"{(isFollowed ? "> " : "")}{entry.Controller.gameObject.name} - {status}"))
  236. FollowAgent(entry);
  237. }
  238. GUI.color = Color.white;
  239. GUILayout.EndScrollView();
  240. GUILayout.EndArea();
  241. }
  242. }
  243. }