You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Unity C#中依赖其他类的函数多线程实现求助(地形生成扩展)

解决Unity地形GameObject生成的多线程卡顿问题

嘿,一看你这问题就知道踩了Unity多线程的经典坑——Unity绝大多数核心API(包括Object.Instantiate实例化物体)只能在主线程调用,你之前想把直接包含实例化逻辑的PlaceObjectsOnChunks丢去线程执行,肯定会报一堆错误。下面我给你一步步拆解修复方案:

核心思路

线程只能用来做纯计算逻辑:比如判断哪些Chunk需要生成物体、收集要生成的位置和预制体信息;而实际创建GameObject的操作,必须回到主线程执行。我们用「线程计算数据 + 主线程消费数据」的模式来实现无卡顿的生成。


步骤1:重构计算逻辑,剥离实例化

先把PlaceObjectsOnChunks里的实例化代码拆出来,改造成只负责收集需要生成的物体数据的方法:

// 这个方法只做计算:收集需要生成的物体的位置和预制体,不碰Unity实例化API
private List<AssetData> CalculateObjectsToSpawn()
{
    List<AssetData> spawnList = new List<AssetData>();
    
    // 多线程访问共享HashSet必须加锁,防止竞争条件
    lock (alreadyGeneratedObjectAtThisChunkTransform)
    {
        foreach (Transform chunkTransform in this.transform)
        {
            MeshCollider collider = chunkTransform.GetComponent<MeshCollider>();
            // 检查Chunk有碰撞体且未被处理过
            if (collider?.sharedMesh != null && !alreadyGeneratedObjectAtThisChunkTransform.Contains(chunkTransform))
            {
                // 收集草生成器的数据
                spawnList.Add(new AssetData(chunkTransform.position, grasGeneratorPrefab));
                // 收集物理模拟器的数据
                spawnList.Add(new AssetData(chunkTransform.position, physicsSimulatorPrefab));
                
                // 标记这个Chunk已处理,避免重复生成
                alreadyGeneratedObjectAtThisChunkTransform.Add(chunkTransform);
            }
        }
    }
    
    return spawnList;
}

步骤2:修正线程调用与队列逻辑

你的线程现在应该调用上面的计算方法,把结果放到线程安全的队列里,等主线程处理:

// 启动线程计算需要生成的物体数据
public void RequestObjectSpawnData(Action<List<AssetData>> onCalculated)
{
    // 用ThreadPool比直接开新线程更高效,避免线程创建销毁的开销
    ThreadPool.QueueUserWorkItem(_ => 
    {
        List<AssetData> spawnData = CalculateObjectsToSpawn();
        lock (assetDataInfoQueue)
        {
            assetDataInfoQueue.Enqueue(new AssetDataInfoThread<List<AssetData>>(onCalculated, spawnData));
        }
    });
}

步骤3:主线程处理实例化

Update里消费队列里的数据,执行安全的实例化操作:

void Update()
{
    if (allowGrasUpdate && AssetPlacement.updateGras)
    {
        // 启动线程计算,传入主线程回调
        RequestObjectSpawnData(OnSpawnDataReady);
        AssetPlacement.StopGrasUpdate();
    }

    // 处理队列里的生成任务(必须在主线程)
    lock (assetDataInfoQueue)
    {
        while (assetDataInfoQueue.Count > 0)
        {
            var threadInfo = assetDataInfoQueue.Dequeue();
            threadInfo.callback(threadInfo.parameter);
        }
    }
}

// 主线程专属的实例化回调
private void OnSpawnDataReady(List<AssetData> spawnDataList)
{
    foreach (var data in spawnDataList)
    {
        // 这里在主线程调用Instantiate,完全安全
        Object.Instantiate(data.prefab, new Vector3(data.centre.x, 0, data.centre.z), Quaternion.identity);
    }
}

步骤4:微调数据结构(可选)

你的AssetDataAssetDataInfoThread结构没问题,保持原样即可,不过可以给AssetData加个可读性注释:

// 存储要生成的物体的位置和预制体信息
public struct AssetData
{
    public Vector3 centre;
    public readonly GameObject prefab;
    public AssetData(Vector3 centre, GameObject prefab)
    {
        this.centre = centre;
        this.prefab = prefab;
    }
}

// 线程与主线程之间的回调数据容器
struct AssetDataInfoThread<T>
{
    public readonly Action<T> callback;
    public readonly T parameter;
    public AssetDataInfoThread(Action<T> callback, T parameter)
    {
        this.callback = callback;
        this.parameter = parameter;
    }
}

关键注意事项

  1. 线程安全第一:所有跨线程访问的共享变量(比如alreadyGeneratedObjectAtThisChunkTransformassetDataInfoQueue)必须用lock包裹,避免多线程竞争导致的数据错乱。
  2. 别在线程里碰Unity组件:比如this.transform虽然遍历不会直接报错,但如果主线程动态销毁了Chunk,线程里访问可能会出问题。如果你的Chunk是动态加载的,建议先把Chunk的位置数据复制到线程安全的集合里再处理。
  3. 分帧实例化优化:如果生成的物体数量极大,一次性实例化还是会卡顿,可以把生成队列拆成分帧处理,比如每帧只生成10个:
private Queue<AssetData> delayedSpawnQueue = new Queue<AssetData>();
private int spawnPerFrame = 10;

private void OnSpawnDataReady(List<AssetData> spawnDataList)
{
    lock (delayedSpawnQueue)
    {
        foreach (var data in spawnDataList)
        {
            delayedSpawnQueue.Enqueue(data);
        }
    }
}

void LateUpdate()
{
    lock (delayedSpawnQueue)
    {
        int spawnCount = Math.Min(spawnPerFrame, delayedSpawnQueue.Count);
        for (int i = 0; i < spawnCount; i++)
        {
            var data = delayedSpawnQueue.Dequeue();
            Object.Instantiate(data.prefab, new Vector3(data.centre.x, 0, data.centre.z), Quaternion.identity);
        }
    }
}

这样就能彻底解决生成时的卡顿问题啦!

内容的提问来源于stack exchange,提问作者M_NEN

火山引擎 最新活动