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:微调数据结构(可选)
你的AssetData和AssetDataInfoThread结构没问题,保持原样即可,不过可以给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; } }
关键注意事项
- 线程安全第一:所有跨线程访问的共享变量(比如
alreadyGeneratedObjectAtThisChunkTransform、assetDataInfoQueue)必须用lock包裹,避免多线程竞争导致的数据错乱。 - 别在线程里碰Unity组件:比如
this.transform虽然遍历不会直接报错,但如果主线程动态销毁了Chunk,线程里访问可能会出问题。如果你的Chunk是动态加载的,建议先把Chunk的位置数据复制到线程安全的集合里再处理。 - 分帧实例化优化:如果生成的物体数量极大,一次性实例化还是会卡顿,可以把生成队列拆成分帧处理,比如每帧只生成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




