You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

程序化地牢生成器: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 04:48:08