Unity Job系统输入输出传递与NativeArray多线程冲突解决
问题根源
你遇到的多线程写入报错和结果丢失问题,核心是NativeArray使用方式错误,同时代码里还有多处逻辑bug:
- 类全局声明的
isTouchingOtherObject、position、hit三个NativeArray仅分配了1个元素的空间,所有并行调度的Job都指向同一块内存,多线程同时写入同个索引位置直接触发Unity Job System的线程安全检查报错 - 所有Job都写入数组[0]位置,执行完成后你只能拿到最后一个Job的计算结果,前面所有Job的输出都会被覆盖
- Job中直接调用主线程的
UnityEngine.Random.Range会得到错误的重复随机值 - 最大生成高度计算逻辑笔误:
Mathf.Lerp(maxValue, maxValue, worldObjectMaxSpawnHeight)两个传入值都是maxValue,计算结果永远为maxValue,高度上限判断完全失效 - 重叠检测逻辑写反:检测到同层碰撞体时应该标记为「存在重叠、不可生成」,原代码统一写入false,重叠检测完全失效
- 内层循环重复生成同Chunk的HeightMap,产生大量无意义的性能开销
修复方案
不需要为每个Job单独创建NativeArray,只要按总生成对象数分配对应长度的数组,让每个Job只写入自己专属的索引位置,就能完全避免内存竞争:
- 先统计当前Chunk总生成对象数量,按总长度一次性分配三个输出用NativeArray,每个待生成对象对应唯一的数组索引
- 每个Job传入专属的写入索引,仅操作数组中对应下标的元素,和其他Job的写入位置完全隔离
- 替换为Job系统兼容的
Unity.Mathematics.Random,每个Job传入独立随机种子,避免随机值重复 - 所有Job执行完成后,主线程按索引遍历输出数组,读取结果实例化物体
- 注意:Burst编译器不支持直接调用Unity PhysX的射线检测API,若要开启Burst编译需要把地形高度数据提前传入Job,直接通过高度采样计算落点、用坐标距离判断重叠,完全脱离PhysX查询
修正后的完整代码如下:
using System.Collections; using System.Collections.Generic; using UnityEngine; using Unity.Jobs; using Unity.Collections; using Unity.Burst; using Unity.Mathematics; public class SpawnWorldObjects : MonoBehaviour { public WorldObject[] worldObjects; public void SpawnObjectsInChunk(Transform chunk, Vector2 sampleCenter, MeshSettings meshSettings, HeightMapSettings heightMapSettings) { // 统计当前Chunk总生成对象数 int totalSpawnCount = 0; for (int i = 0; i < worldObjects.Length; i++) { totalSpawnCount += worldObjects[i].numberOfObjectsPerChunk; } // 按总数量一次性分配输出数组 NativeArray<bool> isTouchingOtherObject = new NativeArray<bool>(totalSpawnCount, Allocator.TempJob); NativeArray<Vector3> spawnPositions = new NativeArray<Vector3>(totalSpawnCount, Allocator.TempJob); NativeArray<RaycastHit> hitResults = new NativeArray<RaycastHit>(totalSpawnCount, Allocator.TempJob); NativeList<JobHandle> jobHandleList = new NativeList<JobHandle>(totalSpawnCount, Allocator.Temp); // 同Chunk高度图仅生成一次 HeightMap heightMap = HeightMapGenerator.GenerateHeightMap(meshSettings.numVertsPerLine, meshSettings.numVertsPerLine, heightMapSettings, meshSettings, sampleCenter); int currentIndex = 0; uint baseSeed = (uint)System.DateTime.Now.Ticks; for (int i = 0; i < worldObjects.Length; i++) { for (int j = 0; j < worldObjects[i].numberOfObjectsPerChunk; j++) { baseSeed++; CalculatePosition job = new CalculatePosition { spawnIndex = currentIndex, random = new Unity.Mathematics.Random(baseSeed), sampleCenter = sampleCenter, meshWorldSize = meshSettings.meshWorldSize, minValue = heightMap.minValue, maxValue = heightMap.maxValue, worldObjectMinSpawnHeight = worldObjects[i].minSpawnHeight, worldObjectMaxSpawnHeight = worldObjects[i].maxSpawnHeight, worldObjectDistanceFromOtherObjects = worldObjects[i].distanceFromOtherObjects, isTouchingOtherObject = isTouchingOtherObject, position = spawnPositions, hit = hitResults, }; jobHandleList.Add(job.Schedule()); currentIndex++; } } JobHandle.CompleteAll(jobHandleList); // 主线程按索引读取结果生成物体 currentIndex = 0; for (int i = 0; i < worldObjects.Length; i++) { for (int j = 0; j < worldObjects[i].numberOfObjectsPerChunk; j++) { if (!isTouchingOtherObject[currentIndex]) { int typeOfObject = Random.Range(0, worldObjects[i].objectsToSpawn.Length); Vector3 spawnPos = new Vector3(spawnPositions[currentIndex].x, hitResults[currentIndex].point.y, spawnPositions[currentIndex].z); GameObject worldObject = Instantiate(worldObjects[i].objectsToSpawn[typeOfObject], spawnPos, worldObjects[i].objectsToSpawn[typeOfObject].transform.rotation); worldObject.transform.SetParent(chunk); } currentIndex++; } } // 释放所有非托管内存 jobHandleList.Dispose(); isTouchingOtherObject.Dispose(); spawnPositions.Dispose(); hitResults.Dispose(); } } [System.Serializable] public class WorldObject { public GameObject[] objectsToSpawn; public float distanceFromOtherObjects; public int numberOfObjectsPerChunk; [Range(0, 1)] public float minSpawnHeight; [Range(0, 1)] public float maxSpawnHeight; } // 如需开启Burst编译,请移除Job内的Physics调用,改用传入的高度数据计算 // [BurstCompile] public struct CalculatePosition : IJob { public int spawnIndex; public Unity.Mathematics.Random random; public Vector2 sampleCenter; public float meshWorldSize; public float minValue; public float maxValue; public float worldObjectMinSpawnHeight; public float worldObjectMaxSpawnHeight; public float worldObjectDistanceFromOtherObjects; public NativeArray<bool> isTouchingOtherObject; public NativeArray<Vector3> position; public NativeArray<RaycastHit> hit; public void Execute() { float minSpawnHeight = Mathf.Lerp(minValue, maxValue, worldObjectMinSpawnHeight); float maxSpawnHeight = Mathf.Lerp(minValue, maxValue, worldObjectMaxSpawnHeight); // Job兼容的随机坐标生成 float randomX = random.NextFloat(sampleCenter.x - meshWorldSize / 2f, sampleCenter.x + meshWorldSize / 2f); float randomZ = random.NextFloat(sampleCenter.y - meshWorldSize / 2f, sampleCenter.y + meshWorldSize / 2f); Vector3 rayOrigin = new Vector3(randomX, 1000, randomZ); position[spawnIndex] = rayOrigin; RaycastHit _hit; // 默认标记为不可生成 isTouchingOtherObject[spawnIndex] = true; if (Physics.Raycast(rayOrigin, Vector3.down, out _hit, 10000, 1 << 7)) { hit[spawnIndex] = _hit; if (_hit.point.y > minSpawnHeight && _hit.point.y < maxSpawnHeight) { RaycastHit[] hits = Physics.SphereCastAll(rayOrigin, worldObjectDistanceFromOtherObjects, Vector3.down, 10000); bool hasOverlap = false; foreach (RaycastHit __hit in hits) { if (__hit.collider.gameObject.layer == 9) { hasOverlap = true; break; } } isTouchingOtherObject[spawnIndex] = hasOverlap; } } } }
额外优化建议
- 不要把NativeArray声明为类全局成员,除非能严格管控生命周期,在方法内按需分配、执行完成后立即释放是最稳妥的写法,能避免绝大多数内存泄漏问题
- 后续如果要进一步提升性能,可以把已生成物体的坐标存入NativeList传入Job,直接用距离判断代替SphereCast,完全脱离PhysX后开启Burst编译,性能还能提升5~10倍
内容的提问来源于stack exchange,提问作者Elijah
相关产品推荐
相关产品推荐

