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

Unity ECS中实体间属性设置与访问的性能优化咨询

针对ECS AI目标缓存与性能优化的解决方案

核心思路:用ECS原生组件+结构化更新替代BlobArray缓存

BlobArray是只读不可变结构,完全不适合存储需实时更新的Transform数据。我们需要通过NativeArray结合ECS系统的结构化更新实现高效的目标位置缓存,同时兼容Burst编译器与Job系统。

问题1:BlobArray无法动态更新LocalTransform

BlobAsset的设计初衷是存储初始化后不再修改的只读数据,因此直接放弃用它存储动态位置,改用单例组件托管的NativeArray,通过专用系统定期同步实体位置到数组中。

问题2:Job中无法传入BlobArray更新

BlobArray的指针特性导致无法安全传入Job,且本身不支持修改。解决方式如下:

  1. 创建单例缓存组件,存储人类实体的位置与对应实体引用;
  2. 编写后台Job同步位置数据,完全兼容Burst编译。

示例代码结构:

// 单例缓存组件:存储人类实体的位置与实体关联
public struct HumanPositionCache : IComponentData
{
    public NativeArray<LocalTransform> Positions;
    public NativeArray<Entity> Entities;
}

// 人类实体标记组件
public struct HumanTag : IComponentData {}

// 定期更新位置缓存的系统
[BurstCompile]
public partial struct HumanPositionUpdateSystem : ISystem
{
    private EntityQuery _humanQuery;

    public void OnCreate(ref SystemState state)
    {
        _humanQuery = state.GetEntityQuery(ComponentType.ReadOnly<HumanTag>(), ComponentType.ReadOnly<LocalTransform>());
        // 创建单例缓存实体
        var cacheEntity = state.EntityManager.CreateEntity();
        state.EntityManager.AddComponentData(cacheEntity, new HumanPositionCache());
        state.RequireForUpdate<HumanPositionCache>();
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        var cacheEntity = SystemAPI.GetSingletonEntity<HumanPositionCache>();
        var cache = state.EntityManager.GetComponentData<HumanPositionCache>(cacheEntity);
        var humanCount = _humanQuery.CalculateEntityCount();

        // 动态调整NativeArray大小以匹配当前人类实体数量
        if (!cache.Positions.IsCreated || cache.Positions.Length != humanCount)
        {
            cache.Positions.Dispose();
            cache.Entities.Dispose();
            cache.Positions = new NativeArray<LocalTransform>(humanCount, Allocator.Persistent);
            cache.Entities = new NativeArray<Entity>(humanCount, Allocator.Persistent);
            state.EntityManager.SetComponentData(cacheEntity, cache);
        }

        // 执行后台Job同步位置数据
        var job = new UpdateHumanPositionJob
        {
            Positions = cache.Positions,
            Entities = cache.Entities,
            TransformLookup = state.GetComponentLookup<LocalTransform>(true)
        };
        state.Dependency = job.ScheduleParallel(_humanQuery, state.Dependency);
    }

    public void OnDestroy(ref SystemState state)
    {
        var cache = SystemAPI.GetSingleton<HumanPositionCache>();
        cache.Positions.Dispose();
        cache.Entities.Dispose();
    }

    [BurstCompile]
    private struct UpdateHumanPositionJob : IJobEntity
    {
        public NativeArray<LocalTransform> Positions;
        public NativeArray<Entity> Entities;
        [ReadOnly] public ComponentLookup<LocalTransform> TransformLookup;

        public void Execute(Entity entity, [IndexInQuery] int index)
        {
            Positions[index] = TransformLookup[entity];
            Entities[index] = entity;
        }
    }
}

问题3:SystemAPI.Query无法传入Job

SystemAPI.Query是主线程查询API,不能直接传入Job,但可以通过EntityQuery结合IJobEntity/IJobChunk实现Job内的实体遍历。AI目标选择逻辑可直接基于缓存的NativeArray实现,避免双重循环:

示例:AI寻找最近目标的Job

[BurstCompile]
public partial struct AiFindTargetSystem : ISystem
{
    private EntityQuery _aiQuery;

    public void OnCreate(ref SystemState state)
    {
        _aiQuery = state.GetEntityQuery(ComponentType.ReadWrite<AiState>(), ComponentType.ReadOnly<LocalTransform>());
        state.RequireForUpdate<HumanPositionCache>();
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        var cache = SystemAPI.GetSingleton<HumanPositionCache>();
        if (!cache.Positions.IsCreated || cache.Positions.Length == 0) return;

        var job = new AiFindTargetJob
        {
            HumanPositions = cache.Positions,
            HumanEntities = cache.Entities,
            AiTransformLookup = state.GetComponentLookup<LocalTransform>(true)
        };
        state.Dependency = job.ScheduleParallel(_aiQuery, state.Dependency);
    }

    [BurstCompile]
    private struct AiFindTargetJob : IJobEntity
    {
        [ReadOnly] public NativeArray<LocalTransform> HumanPositions;
        [ReadOnly] public NativeArray<Entity> HumanEntities;
        [ReadOnly] public ComponentLookup<LocalTransform> AiTransformLookup;

        public void Execute(ref AiState aiState, Entity aiEntity)
        {
            if (aiState.CurrentState != AiStateType.Idle) return;

            var aiPos = AiTransformLookup[aiEntity].Position;
            float closestDistance = float.MaxValue;
            int closestIndex = -1;

            // 遍历缓存数组快速查找最近目标
            for (int i = 0; i < HumanPositions.Length; i++)
            {
                var distance = math.distance(aiPos, HumanPositions[i].Position);
                if (distance < closestDistance)
                {
                    closestDistance = distance;
                    closestIndex = i;
                }
            }

            if (closestIndex != -1)
            {
                aiState.TargetEntity = HumanEntities[closestIndex];
                aiState.TargetIndex = closestIndex; // 存储索引用于后续快速取位置
                aiState.CurrentState = AiStateType.Chase;
            }
        }
    }
}

// AI状态组件示例
public struct AiState : IComponentData
{
    public AiStateType CurrentState;
    public Entity TargetEntity;
    public int TargetIndex;
}

public enum AiStateType { Idle, Chase, Attack }

问题4:单例C#类的NativeArray与Burst不兼容

独立C#单例的内存管理无法被Burst安全识别,改用ECS单例组件(如HumanPositionCache)即可解决:这类组件由Unity实体管理器托管内存,可通过SystemAPI.GetSingleton在Job中安全访问,完全兼容Burst编译。

额外优化建议

  • 为人类实体添加ChangeFilter<LocalTransform>,让位置更新系统只处理位置发生变化的实体,减少无效计算;
  • 若目标实体可能被销毁,需在AI目标选择Job中添加EntityManager.Exists检查,或在位置缓存系统中清理无效实体索引;
  • 针对大规模实体场景,可引入**空间划分(如网格分区)**进一步优化目标查找,避免遍历整个数组。

内容的提问来源于stack exchange,提问作者Uğur Tuna Koca

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 11:12:10