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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 06:39:28