程序化地牢生成器:Coroutines与Async await方案选型咨询
问题描述
我正在开发一款基于预制房间随机连接的3D非网格程序化地牢生成器,核心逻辑是:放置房间后随机选择出口尝试放置新房间,通过OverlapBox检测碰撞,适配则保留房间,否则销毁重试,循环生成至目标规模。
当前遇到的问题:
- 为让碰撞检测生效,必须在实例化房间后等待一帧,用协程实现但难以跟踪各协程的完成状态
- 尝试用Async/await处理单帧等待场景,但可靠性不足
请求针对当前脚本的优化方案,以下是当前实现代码:
当前实现代码
using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; using Random = UnityEngine.Random; public class HouseGenerator : MonoBehaviour { public int roomsToGoal = 4; public float spaceBetweenRooms = 0.2f; public int maxBranchLength; public List<GameObject> roomPrefabs = new List<GameObject>(); public Stack<Room> SceneRoomsStack = new Stack<Room>(); private List<Connection> mainConnectionList = new List<Connection>(); private List<Room> lastRoomsList = new List<Room>(); public GameObject startingRoom; public GameObject connectionPrefab; private int lastID; void Start() { StartRoomSpawn(); StartCoroutine(SpawnRooms()); } IEnumerator SpawnRooms() { int maxLoop = 100; for (int i = 0; i < roomsToGoal; i++) { bool roomPlaced = PlaceRoomBool(mainConnectionList[mainConnectionList.Count-1], mainConnectionList, true); if (!roomPlaced && maxLoop > 0) i--; maxLoop--; yield return null; } mainConnectionList[mainConnectionList.Count - 1].ShowDoor(); Room lastRoom = mainConnectionList[mainConnectionList.Count - 1].GetComponentInChildren<Room>(); lastRoomsList.Add(lastRoom); Debug.Log(mainConnectionList[mainConnectionList.Count - 1].transform.parent.name + "'s door was closed because it was the last door in the main branch."); yield return null; Debug.Log(SceneRoomsStack.Count); yield return StartCoroutine(LookForFreeDoors()); Debug.Log(SceneRoomsStack.Count); SelectSafeRoom(); Actions.OnFinishLoading?.Invoke(); } IEnumerator LookForFreeDoors() { Queue<Connection> newBranches = new Queue<Connection>(); int count = SceneRoomsStack.Count; for (int i = 0; i < count; i++) { Room room = SceneRoomsStack.Pop(); foreach(Connection connection in room.connectionPoints) { if(connection.isFree == true) { newBranches.Enqueue(connection); connection.isFree = false; } } } int branchNumber = newBranches.Count; for(int i = 0; i < branchNumber; i++) { List<Connection> branchConnections = new List<Connection>(); branchConnections.Add(newBranches.Dequeue()); for (int j = 0; j < maxBranchLength; j++) { yield return null; Room room = PlaceRoom(branchConnections[branchConnections.Count - 1], branchConnections, false); branchConnections[branchConnections.Count - 1].isFree = false; if (j == maxBranchLength - 1) lastRoomsList.Add(room); } if (branchConnections[branchConnections.Count - 1] != null) branchConnections[branchConnections.Count - 1].ShowDoor(); Debug.Log(branchConnections[branchConnections.Count - 1].transform.parent.name + "'s door was closed because it was the last door in the branch."); } GameObject[] finalConnectionList = GameObject.FindGameObjectsWithTag("Connection"); foreach (GameObject connection in finalConnectionList) { var component = connection.GetComponent<Connection>(); if (component.isFree) { component.ShowDoor(); Debug.Log(component.transform.parent.name + "'s door was closed because it was open to the void."); } } } void StartRoomSpawn() { var room = Instantiate(startingRoom).GetComponent<Room>(); foreach(Connection connection in room.connectionPoints) { connection.openConnection = false; connection.isFree = false; } var selectedInConnection = SelectRandomConnection(room); selectedInConnection.openConnection = true; selectedInConnection.isFree = false; selectedInConnection.ShowFrame(); mainConnectionList.Add(selectedInConnection); SceneRoomsStack.Push(room); foreach(Connection connection in room.connectionPoints) { if (!connection.openConnection) { connection.ShowDoor(); Debug.Log(connection.transform.parent.name + "'s door was closed because it wasn't the chosen opening in the first room"); } } } void SelectSafeRoom() { Room[] roomList = FindObjectsOfType<Room>(); int randomIndex = Random.Range(0, roomList.Length); if (roomList[randomIndex].roomType != RoomType.Corridor || roomList[randomIndex].roomType != RoomType.Stairs) { int maxLoops = 50; while ((roomList[randomIndex].roomType == RoomType.Corridor || roomList[randomIndex].roomType == RoomType.Stairs) && maxLoops > 0) { randomIndex = Random.Range(0, roomList.Length); maxLoops--; } } roomList[randomIndex].isSafeRoom = true; roomList[randomIndex].TurnOnLights(); } bool PlaceRoomBool(Connection outConnection, List<Connection> connectionList, bool isMainBranch) { Room room = PlaceRoom(outConnection, connectionList, isMainBranch); return room != null; } Room PlaceRoom(Connection outConnection, List<Connection> connectionList, bool isMainBranch) { Connection anchor = CreateRoom(outConnection); var room = anchor.GetComponentInChildren<Room>(); if (room == null) throw new Exception(anchor.name + " did not create a room"); room.name = room.name + Time.deltaTime; anchor.name = anchor.name + Time.deltaTime; anchor.isFree = false; anchor.ShowFrame(); Collider[] colliders = CheckCollider(room); Debug.Log(colliders.Length); if(colliders.Length > 0) { foreach(Collider collider in colliders) { if(collider.transform.parent != null && collider.transform.parent.GetComponent<Room>().ID != room.ID) { Destroy(anchor.gameObject); return null; } } } var selectedExit = SelectRandomConnection(room); selectedExit.name = selectedExit.name + Time.deltaTime; int counter = 0; if (selectedExit.isEntrance == true) { while(selectedExit.isEntrance == true && counter < 100) { selectedExit = SelectRandomConnection(room); } } selectedExit.openConnection = false; selectedExit.isFree = false; connectionList.Add(selectedExit); if (isMainBranch) { SceneRoomsStack.Push(room); room.isMainBranch = true; } return room; } Collider[] CheckCollider(Room room) { Vector3 worldExtent = room.childRenderer.bounds.extents; Collider[] colliders = Physics.OverlapBox(room.transform.position + new Vector3(0, worldExtent.y, 0), worldExtent); GameObject debugCube = GameObject.CreatePrimitive(PrimitiveType.Cube); debugCube.transform.position = room.transform.position + new Vector3(0, worldExtent.y, 0); debugCube.transform.localScale = worldExtent * 2; debugCube.GetComponent<Renderer>().material.color = Color.red; debugCube.transform.SetParent(room.transform); debugCube.SetActive(false); debugCube.name = "Debugging Cube"; return colliders; } Connection CreateRoom(Connection outConnection) { var room = Instantiate(roomPrefabs[Random.Range(0, roomPrefabs.Count)]); room.GetComponent<Room>().ID = lastID; lastID++; var selectedInConnection = SelectRandomConnection(room.GetComponent<Room>()); selectedInConnection.isEntrance = true; selectedInConnection.openConnection = false; selectedInConnection.isFree = false; var anchor = SetupRoomAnchor(selectedInConnection, room); anchor.transform.position = outConnection.transform.position; anchor.transform.rotation = outConnection.transform.rotation; anchor.transform.Rotate(0, 180, 0); anchor.transform.position += anchor.transform.forward * spaceBetweenRooms * -1; anchor.name = room.name; return anchor.GetComponent<Connection>(); } Connection SelectRandomConnection(Room room) { int randomIndex = Random.Range(0, room.connectionPoints.Count()); var selectedConnection = room.connectionPoints[randomIndex].GetComponent<Connection>(); return selectedConnection; } GameObject SetupRoomAnchor(Connection connection, GameObject room) { var anchor = Instantiate(connectionPrefab); anchor.transform.position = connection.transform.position; anchor.transform.rotation = connection.transform.rotation; room.transform.SetParent(anchor.transform); return anchor; } }
优化方案
1. 消除帧等待:预计算碰撞体积,提前检测
当前必须等待一帧是因为实例化后碰撞体数据需要同步,解决核心是预存每个房间预制体的碰撞边界,在放置前完成碰撞检测,完全不需要等待帧:
实现步骤:
- 在
Room脚本中添加[SerializeField] Bounds precomputedBounds字段,编辑器中手动设置或用编辑模式工具批量计算(基于预制体的Renderer.bounds或Collider.bounds) - 修改
CreateRoom方法,先计算房间最终位置和旋转,用预存边界转换为世界空间边界,直接调用Physics.OverlapBox检测 - 仅在检测通过后实例化房间,避免销毁重试的性能浪费
核心代码修改:
// Room类新增字段和方法 [SerializeField] private Bounds precomputedBounds; public Bounds GetWorldBounds(Vector3 position, Quaternion rotation) { Bounds worldBounds = precomputedBounds; worldBounds.center = position + rotation * precomputedBounds.center; worldBounds.extents = Vector3.Scale(precomputedBounds.extents, rotation * Vector3.one); return worldBounds; } // 修改CreateRoom为预检测逻辑 Connection CreateRoom(Connection outConnection, out bool hasCollision) { hasCollision = false; var roomPrefab = roomPrefabs[Random.Range(0, roomPrefabs.Count)]; var roomData = roomPrefab.GetComponent<Room>(); // 计算锚点最终位置和旋转 Quaternion anchorRot = outConnection.transform.rotation * Quaternion.Euler(0, 180, 0); Vector3 anchorPos = outConnection.transform.position + anchorRot * Vector3.forward * spaceBetweenRooms * -1; // 计算房间世界空间位置 Vector3 roomWorldPos = anchorPos + anchorRot * (roomData.transform.localPosition - roomData.connectionPoints[0].transform.localPosition); // 预检测碰撞 Bounds roomWorldBounds = roomData.GetWorldBounds(roomWorldPos, anchorRot); Collider[] colliders = Physics.OverlapBox(roomWorldBounds.center, roomWorldBounds.extents, anchorRot); foreach(var collider in colliders) { var otherRoom = collider.transform.parent?.GetComponent<Room>(); if(otherRoom != null && otherRoom.ID != lastID) { hasCollision = true; return null; } } // 检测通过才实例化 var room = Instantiate(roomPrefab); room.GetComponent<Room>().ID = lastID; lastID++; var selectedInConnection = SelectRandomConnection(room.GetComponent<Room>()); selectedInConnection.isEntrance = true; selectedInConnection.openConnection = false; selectedInConnection.isFree = false; var anchor = SetupRoomAnchor(selectedInConnection, room); anchor.transform.position = anchorPos; anchor.transform.rotation = anchorRot; anchor.name = room.name; return anchor.GetComponent<Connection>(); }
2. 协程状态跟踪:主协程驱动+状态标记
嵌套协程难以跟踪状态,建议将所有生成逻辑整合到一个主协程,或用简单状态标记管理:
实现方式:
- 移除
LookForFreeDoors独立协程,将其逻辑整合到SpawnRooms协程,用顺序执行替代嵌套 - 添加
private bool isGenerating标记,供外部判断生成是否完成 - 如需并行生成分支,可维护协程列表,用自定义等待类等待所有分支完成:
// 自定义等待所有协程完成的类 public class WaitForAllCoroutines : CustomYieldInstruction { private List<Coroutine> _runningCoroutines; private MonoBehaviour _owner; public WaitForAllCoroutines(MonoBehaviour owner, List<IEnumerator> coroutines) { _owner = owner; _runningCoroutines = new List<Coroutine>(); foreach(var coroutine in coroutines) { _runningCoroutines.Add(_owner.StartCoroutine(RunAndTrack(coroutine))); } } private IEnumerator RunAndTrack(IEnumerator coroutine) { yield return coroutine; _runningCoroutines.RemoveAll(c => c == null); } public override bool keepWaiting => _runningCoroutines.Count > 0; }
3. 重构为状态机:明确生成阶段
将生成过程拆分为独立状态,每个状态完成后自动进入下一阶段,便于跟踪调试:
private enum GenerationState { GeneratingMainBranch, GeneratingBranches, Finalizing, Done } private GenerationState _currentState; void Start() { StartRoomSpawn(); _currentState = GenerationState.GeneratingMainBranch; StartCoroutine(GenerationStateMachine()); } IEnumerator GenerationStateMachine() { while(_currentState != GenerationState.Done) { switch(_currentState) { case GenerationState.GeneratingMainBranch: yield return GenerateMainBranch(); _currentState = GenerationState.GeneratingBranches; break; case GenerationState.GeneratingBranches: yield return GenerateBranches(); _currentState = GenerationState.Finalizing; break; case GenerationState.Finalizing: FinalizeGeneration(); _currentState = GenerationState.Done; break; } } } IEnumerator GenerateMainBranch() { int maxLoop = 100; for (int i = 0; i < roomsToGoal; i++) { bool hasCollision; Connection anchor = CreateRoom(mainConnectionList[mainConnectionList.Count-1], out hasCollision); if(!hasCollision) { // 完成房间放置逻辑... } else if(maxLoop > 0) { i--; maxLoop--; } // 无需等待帧,可根据需要保留帧率控制 yield return null; } // 主分支收尾逻辑... yield break; }
4. 碰撞检测优化:过滤无关碰撞体
当前CheckCollider会检测所有碰撞体,建议:
- 给所有房间碰撞体设置专属
Layer,比如Room层 - 调用
Physics.OverlapBox时传入该层的LayerMask,只检测房间碰撞体,减少误判和检测范围
// HouseGenerator新增字段 public LayerMask roomLayerMask; // 修改碰撞检测调用 Collider[] colliders = Physics.OverlapBox(roomWorldBounds.center, roomWorldBounds.extents, anchorRot, roomLayerMask);
内容的提问来源于stack exchange,提问作者Daniel Fernández
相关产品推荐
相关产品推荐

