MazeSceneBuilder.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  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. // Assign each agent to a start room, then spawn per-room with independent stagger
  75. var assignments = AssignAgentsToStartRooms(agentCount, startRooms.Count);
  76. for (int r = 0; r < startRooms.Count; r++)
  77. StartCoroutine(SpawnAgentsForRoomRoutine(startRooms[r], assignments[r]));
  78. }
  79. /// <summary>
  80. /// Picks a uniformly random point within a disk of radius `startRoomSpread` (instead
  81. /// of evenly around its edge), retrying a few times to avoid landing on top of a
  82. /// start room already placed this call.
  83. /// </summary>
  84. private static Vector2 PickRandomStartPosition(MazeGenerationSettings settings, List<Room> placed)
  85. {
  86. float spread = Mathf.Max(settings.startRoomSpread, 0.01f);
  87. float minDistance = settings.startRoomRadius * 2f;
  88. for (int attempt = 0; attempt < 30; attempt++)
  89. {
  90. Vector2 candidate = Random.insideUnitCircle * spread;
  91. bool overlapsPlaced = false;
  92. foreach (var room in placed)
  93. {
  94. if (Vector2.Distance(candidate, room.Position) < minDistance)
  95. {
  96. overlapsPlaced = true;
  97. break;
  98. }
  99. }
  100. if (!overlapsPlaced) return candidate;
  101. }
  102. return Random.insideUnitCircle * spread; // give up avoiding overlap, use whatever we last rolled
  103. }
  104. /// <summary>
  105. /// Randomly assigns each agent index (0..agentCount-1) to one of the start rooms, decided
  106. /// upfront before any staggering/spawning begins, so the same statistical distribution as
  107. /// today's "pick a random room per spawn" applies, but grouped so each room's agents can be
  108. /// staggered independently and concurrently with other rooms.
  109. /// </summary>
  110. private static List<int>[] AssignAgentsToStartRooms(int agentCount, int roomCount)
  111. {
  112. var result = new List<int>[roomCount];
  113. for (int r = 0; r < roomCount; r++)
  114. result[r] = new List<int>();
  115. for (int i = 0; i < agentCount; i++)
  116. result[Random.Range(0, roomCount)].Add(i);
  117. return result;
  118. }
  119. /// <summary>
  120. /// One independent instance of this runs per start room, all started concurrently from
  121. /// Start(). Staggers only the agents assigned to THIS room by spawnStaggerRange, so agents
  122. /// in different start rooms can spawn at the same wall-clock time while agents sharing a
  123. /// room are still staggered relative to each other, same as before.
  124. /// </summary>
  125. private IEnumerator SpawnAgentsForRoomRoutine(Room startRoom, List<int> agentIndices)
  126. {
  127. foreach (int index in agentIndices)
  128. {
  129. float delay = Random.Range(spawnStaggerRange.x, spawnStaggerRange.y);
  130. if (delay > 0f) yield return new WaitForSeconds(delay);
  131. SpawnAgent(startRoom, index);
  132. }
  133. }
  134. private void SpawnAgent(Room startRoom, int index)
  135. {
  136. var agentGO = new GameObject($"Agent_{index}");
  137. agentGO.transform.SetParent(transform, false);
  138. var controller = agentGO.AddComponent<MazeAgentController>();
  139. controller.Initialize(_generator);
  140. // Golden-ratio hue spread gives visually distinct colors even for many agents.
  141. Color color = Color.HSVToRGB((index * 0.618034f) % 1f, 0.65f, 1f);
  142. controller.Spawn(startRoom, color);
  143. var pathGO = new GameObject("Path");
  144. pathGO.transform.SetParent(agentGO.transform, false);
  145. pathGO.AddComponent<LineRenderer>();
  146. var path = pathGO.AddComponent<PathVisualizer>();
  147. path.AppendPoint(startRoom.Position);
  148. pathGO.SetActive(false); // only the followed agent's trail is shown
  149. var brain = agentGO.AddComponent<AutonomousAgentBrain>();
  150. float initialDelay = Random.Range(initialDecisionDelayRange.x, initialDecisionDelayRange.y);
  151. brain.Initialize(controller, _generator, initialDelay, decisionInterval, exploreChance, 1.0f, () => _levelEnded);
  152. controller.Agent.RoomEntered += room => path.AppendPoint(room.Position);
  153. controller.Agent.RoomEntered += _ => RefreshMarkerVisuals();
  154. controller.Agent.RoomEntered += _ => CheckFinishCondition();
  155. controller.Agent.ReachedExitRoom += room => Debug.Log($"[MazeGen] {agentGO.name} reached the exit room (id {room.Id})!");
  156. _agents.Add(new AgentEntry { Controller = controller, Path = path, Color = color });
  157. }
  158. private void OnRoomGenerated(Room room)
  159. {
  160. var roomGO = new GameObject($"Room_{room.Id}_{room.Kind}");
  161. roomGO.transform.SetParent(transform, false);
  162. roomGO.AddComponent<RoomView>().Initialize(room);
  163. foreach (var exit in room.Exits)
  164. SpawnExitMarker(room, exit);
  165. }
  166. private void OnExitAdded(Room room, Exit exit)
  167. {
  168. SpawnExitMarker(room, exit);
  169. RefreshMarkerVisuals();
  170. }
  171. private void SpawnExitMarker(Room room, Exit exit)
  172. {
  173. var markerGO = new GameObject($"Exit_{exit.Id}");
  174. markerGO.transform.SetParent(transform, false);
  175. var marker = markerGO.AddComponent<ExitMarker>();
  176. marker.Initialize(room, exit);
  177. _exitMarkers[exit.Id] = marker;
  178. }
  179. private void RefreshMarkerVisuals()
  180. {
  181. Room currentRoom = _followedAgent?.Controller.Agent?.CurrentRoom;
  182. MazeAgent agent = _followedAgent?.Controller.Agent;
  183. foreach (var marker in _exitMarkers.Values)
  184. marker.UpdateVisual(currentRoom, agent);
  185. }
  186. private void Update()
  187. {
  188. if (_levelEnded) return;
  189. _elapsedTime += Time.deltaTime;
  190. if (timeLimitSeconds > 0f && _elapsedTime >= timeLimitSeconds)
  191. EndLevel($"Time limit of {timeLimitSeconds:0.0}s reached.");
  192. }
  193. private void CheckFinishCondition()
  194. {
  195. if (_levelEnded) return;
  196. int finished = _agents.Count(a => a.Controller.Agent.IsFinished);
  197. if (targetFinishedAgentCount > 0 && finished >= targetFinishedAgentCount)
  198. EndLevel($"{finished} agent(s) reached the exit.");
  199. }
  200. private void EndLevel(string reason)
  201. {
  202. _levelEnded = true;
  203. Debug.Log($"[MazeGen] LEVEL COMPLETE - {reason}");
  204. }
  205. private void FollowAgent(AgentEntry entry)
  206. {
  207. if (_followedAgent == entry) return;
  208. StopFollowing();
  209. _followedAgent = entry;
  210. entry.Controller.SetFollowed(true);
  211. entry.Path.gameObject.SetActive(true);
  212. entry.Controller.Agent.RoomEntered += OnFollowedAgentMoved;
  213. _cameraController?.FocusOn(entry.Controller.Agent.CurrentRoom.Position);
  214. RefreshMarkerVisuals();
  215. }
  216. private void StopFollowing()
  217. {
  218. if (_followedAgent == null) return;
  219. _followedAgent.Controller.SetFollowed(false);
  220. _followedAgent.Path.gameObject.SetActive(false);
  221. _followedAgent.Controller.Agent.RoomEntered -= OnFollowedAgentMoved;
  222. _followedAgent = null;
  223. RefreshMarkerVisuals();
  224. }
  225. private void OnFollowedAgentMoved(Room room) => _cameraController?.FocusOn(room.Position);
  226. private Bounds ComputeMazeBounds()
  227. {
  228. var rooms = _generator.Graph.Rooms.Values;
  229. if (rooms.Count == 0) return new Bounds(Vector3.zero, Vector3.one * 8f);
  230. var bounds = new Bounds(rooms.First().Position, Vector3.zero);
  231. foreach (var room in rooms)
  232. bounds.Encapsulate(new Bounds(room.Position, Vector3.one * (room.Radius * 2f)));
  233. return bounds;
  234. }
  235. private void OnGUI()
  236. {
  237. GUILayout.BeginArea(new Rect(10, 10, 260, Screen.height - 20), GUI.skin.box);
  238. string timeLabel = timeLimitSeconds > 0f ? $"{_elapsedTime:0.0}s / {timeLimitSeconds:0.0}s" : $"{_elapsedTime:0.0}s";
  239. GUILayout.Label($"Elapsed: {timeLabel}");
  240. int finishedCount = _agents.Count(a => a.Controller.Agent.IsFinished);
  241. string finishLabel = targetFinishedAgentCount > 0 ? $"{finishedCount} / {targetFinishedAgentCount}" : $"{finishedCount}";
  242. GUILayout.Label($"Finished: {finishLabel}");
  243. if (_levelEnded)
  244. GUILayout.Label("LEVEL COMPLETE", GUI.skin.box);
  245. if (GUILayout.Button("Zoom to fit maze"))
  246. _cameraController?.FrameBounds(ComputeMazeBounds());
  247. if (_followedAgent != null && GUILayout.Button("Stop following"))
  248. StopFollowing();
  249. GUILayout.Space(6);
  250. GUILayout.Label("Agents (click to follow):");
  251. _hudScroll = GUILayout.BeginScrollView(_hudScroll);
  252. foreach (var entry in _agents)
  253. {
  254. var agent = entry.Controller.Agent;
  255. string status = agent.IsFinished ? "reached exit" : "exploring";
  256. bool isFollowed = entry == _followedAgent;
  257. GUI.color = entry.Color;
  258. if (GUILayout.Button($"{(isFollowed ? "> " : "")}{entry.Controller.gameObject.name} - {status}"))
  259. FollowAgent(entry);
  260. }
  261. GUI.color = Color.white;
  262. GUILayout.EndScrollView();
  263. GUILayout.EndArea();
  264. }
  265. }
  266. }