Unity-EntitiesSamples-HelloCube-15

发布于 2024-08-09  15 次阅读


运行效果

该示例生成5000个target(红色)和4000个unit(绿色),并计算每个unit最近的target是哪一个。在Game窗口中看不出,在Scene中可看到Debug用的线条来连接最近的unit和对应的target。

代码解读

TargetAuthoring

using Unity.Entities;
using UnityEngine;

namespace HelloCube.ClosestTarget
{
    public class TargetAuthoring : MonoBehaviour
    {
        class Baker : Baker<TargetAuthoring>
        {
            public override void Bake(TargetAuthoring authoring)
            {
                var entity = GetEntity(TransformUsageFlags.Dynamic);
                AddComponent<Target>(entity);
            }
        }
    }

    public struct Target : IComponentData
    {
        public Entity Value;
    }
}

将Target的data附在unit预制体上。

SettingAuthoring

using Unity.Entities;
using UnityEngine;

namespace HelloCube.ClosestTarget
{
    public class SettingsAuthoring : MonoBehaviour
    {
        public int unitCount;
        public GameObject unitPrefab;

        public int targetCount;
        public GameObject targetPrefab;

        public TargetingSystem.SpatialPartitioningType spatialPartitioning;

        class Baker : Baker<SettingsAuthoring>
        {
            public override void Bake(SettingsAuthoring authoring)
            {
                var entity = GetEntity(TransformUsageFlags.None);
                AddComponent(entity, new Settings
                {
                    UnitCount = authoring.unitCount,
                    UnitPrefab = GetEntity(authoring.unitPrefab, TransformUsageFlags.Dynamic),
                    TargetCount = authoring.targetCount,
                    TargetPrefab = GetEntity(authoring.targetPrefab, TransformUsageFlags.Dynamic),
                    SpatialPartitioning = authoring.spatialPartitioning
                });
            }
        }
    }

    public struct Settings : IComponentData
    {
        public int UnitCount;
        public Entity UnitPrefab;

        public int TargetCount;
        public Entity TargetPrefab;

        public TargetingSystem.SpatialPartitioningType SpatialPartitioning;
    }
}

存放unit和target的数据,还有空间分割的模式。

MovementAuthoring

using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;

namespace HelloCube.ClosestTarget
{
    public class MovementAuthoring : MonoBehaviour
    {
        class Baker : Baker<MovementAuthoring>
        {
            public override void Bake(MovementAuthoring authoring)
            {
                var entity = GetEntity(TransformUsageFlags.Dynamic);
                AddComponent<Movement>(entity);
            }
        }
    }

    public struct Movement : IComponentData
    {
        public float2 Value;
    }
}

存放一个随机数决定unit和target的移动方向。

InitializationSystem

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;

namespace HelloCube.ClosestTarget
{
    public partial struct InitializationSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<Settings>();
            state.RequireForUpdate<ExecuteClosestTarget>();
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            state.Enabled = false;

            var settings = SystemAPI.GetSingleton<Settings>();
            var random = Random.CreateFromIndex(1234);

            Spawn(ref state, settings.UnitPrefab, settings.UnitCount, ref random);
            Spawn(ref state, settings.TargetPrefab, settings.TargetCount, ref random);
        }

        void Spawn(ref SystemState state, Entity prefab, int count, ref Random random)
        {
            var units = state.EntityManager.Instantiate(prefab, count, Allocator.Temp);

            for (int i = 0; i < units.Length; i += 1)
            {
                var position = new float3();
                position.xz = random.NextFloat2() * 200 - 100;
                state.EntityManager.SetComponentData(units[i],
                    new LocalTransform { Position = position, Scale = 1 });
                state.EntityManager.SetComponentData(units[i],
                    new Movement { Value = random.NextFloat2Direction() });
            }
        }
    }
}

获取setting,根据setting将unit和target随机声称在方块区域内,并初始化运动方向。

MovementSystem

using Unity.Burst;
using Unity.Entities;
using Unity.Transforms;

namespace HelloCube.ClosestTarget
{
    public partial struct MovementSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<ExecuteClosestTarget>();
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            new MovementJob
            {
                Delta = SystemAPI.Time.DeltaTime * 10
            }.ScheduleParallel();
        }
    }

    [BurstCompile]
    public partial struct MovementJob : IJobEntity
    {
        public float Delta;

        void Execute(ref LocalTransform transform, in Movement movement)
        {
            const float size = 100f;

            transform.Position.xz += movement.Value * Delta;
            if (transform.Position.x < -size) transform.Position.x += size * 2;
            if (transform.Position.x > +size) transform.Position.x -= size * 2;
            if (transform.Position.z < -size) transform.Position.z += size * 2;
            if (transform.Position.z > +size) transform.Position.z -= size * 2;
        }
    }
}

根据Movement改变unit和target的移动方向,并保证不越界。

DebugLinesSystem

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Transforms;
using UnityEngine;

namespace HelloCube.ClosestTarget
{
    [UpdateAfter(typeof(TargetingSystem))]
    [BurstCompile]
    public partial struct DebugLinesSystem : ISystem
    {
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<ExecuteClosestTarget>();
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            foreach (var (transform, target) in
                     SystemAPI.Query<RefRW<LocalTransform>, RefRO<Target>>())
            {
                if (SystemAPI.Exists(target.ValueRO.Value))
                {
                    var targetTransform = SystemAPI.GetComponent<LocalTransform>(target.ValueRO.Value);
                    Debug.DrawLine(transform.ValueRO.Position, targetTransform.Position);
                }
            }
            // new JobDebugLine(){LT = SystemAPI.GetComponentLookup<LocalTransform>()}.Schedule();
        }
    }

    [BurstCompile]
    public partial struct JobDebugLine : IJobEntity
    {
        [ReadOnly]public ComponentLookup<LocalTransform> LT;
        public void Execute(in Target t,in Entity e)
        {
            Debug.DrawLine(LT[e].Position, LT[t.Value].Position);
        }
    }
}

该部分将unit和它对应target连线。target存在Target内,附在unit上。

注释部分是自己尝试用Job替代主线程内drawline,结果来看确实快了一点点。

TargetingSystem

using System.Collections.Generic;
using Unity.Burst;
using Unity.Burst.Intrinsics;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Profiling;
using Unity.Transforms;

namespace HelloCube.ClosestTarget
{
    public partial struct TargetingSystem : ISystem
    {
        public enum SpatialPartitioningType
        {
            None,
            Simple,
            KDTree,
        }

        static NativeArray<ProfilerMarker> s_ProfilerMarkers;

        public void OnCreate(ref SystemState state)
        {
            s_ProfilerMarkers = new NativeArray<ProfilerMarker>(3, Allocator.Persistent);
            s_ProfilerMarkers[0] = new(nameof(TargetingSystem) + "." + SpatialPartitioningType.None);
            s_ProfilerMarkers[1] = new(nameof(TargetingSystem) + "." + SpatialPartitioningType.Simple);
            s_ProfilerMarkers[2] = new(nameof(TargetingSystem) + "." + SpatialPartitioningType.KDTree);

            state.RequireForUpdate<Settings>();
            state.RequireForUpdate<ExecuteClosestTarget>();
        }

        public void OnDestroy(ref SystemState state)
        {
            s_ProfilerMarkers.Dispose();
        }

        public void OnUpdate(ref SystemState state)
        {
            var targetQuery = SystemAPI.QueryBuilder().WithAll<LocalTransform>().WithNone<Target, Settings>().Build();
            var kdQuery = SystemAPI.QueryBuilder().WithAll<LocalTransform, Target>().Build();

            var spatialPartitioningType = SystemAPI.GetSingleton<Settings>().SpatialPartitioning;

            using var profileMarker = s_ProfilerMarkers[(int)spatialPartitioningType].Auto();

            var targetEntities = targetQuery.ToEntityArray(state.WorldUpdateAllocator);
            var targetTransforms =
                targetQuery.ToComponentDataArray<LocalTransform>(state.WorldUpdateAllocator);

            switch (spatialPartitioningType)
            {
                case SpatialPartitioningType.None:
                {
                    var noPartitioning = new NoPartitioning
                        { TargetEntities = targetEntities, TargetTransforms = targetTransforms };
                    state.Dependency = noPartitioning.ScheduleParallel(state.Dependency);
                    break;
                }
                case SpatialPartitioningType.Simple:
                {
                    var positions = CollectionHelper.CreateNativeArray<PositionAndIndex>(targetTransforms.Length,
                        state.WorldUpdateAllocator);

                    for (int i = 0; i < positions.Length; i += 1)
                    {
                        positions[i] = new PositionAndIndex
                        {
                            Index = i,
                            Position = targetTransforms[i].Position.xz
                        };
                    }

                    state.Dependency = positions.SortJob(new AxisXComparer()).Schedule(state.Dependency);

                    var simple = new SimplePartitioning { TargetEntities = targetEntities, Positions = positions };
                    state.Dependency = simple.ScheduleParallel(state.Dependency);
                    break;
                }
                case SpatialPartitioningType.KDTree:
                {
                    var tree = new KDTree(targetEntities.Length, Allocator.TempJob, 64);

                    // init KD tree
                    for (int i = 0; i < targetEntities.Length; i += 1)
                    {
                        // NOTE - the first parameter is ignored, only the index matters
                        tree.AddEntry(i, targetTransforms[i].Position);
                    }

                    state.Dependency = tree.BuildTree(targetEntities.Length, state.Dependency);

                    var queryKdTree = new QueryKDTree
                    {
                        Tree = tree,
                        TargetEntities = targetEntities,
                        Scratch = default,
                        TargetHandle = SystemAPI.GetComponentTypeHandle<Target>(),
                        LocalTransformHandle = SystemAPI.GetComponentTypeHandle<LocalTransform>(true)
                    };
                    state.Dependency = queryKdTree.ScheduleParallel(kdQuery, state.Dependency);

                    state.Dependency.Complete();
                    tree.Dispose();
                    break;
                }
            }

            state.Dependency.Complete();
        }
    }

    [BurstCompile]
    public struct QueryKDTree : IJobChunk
    {
        [ReadOnly] public NativeArray<Entity> TargetEntities;
        public PerThreadWorkingMemory Scratch;
        public KDTree Tree;

        public ComponentTypeHandle<Target> TargetHandle;
        [ReadOnly] public ComponentTypeHandle<LocalTransform> LocalTransformHandle;

        public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask,
            in v128 chunkEnabledMask)
        {
            var targets = chunk.GetNativeArray(ref TargetHandle);
            var transforms = chunk.GetNativeArray(ref LocalTransformHandle);

            for (int i = 0; i < chunk.Count; i++)
            {
                if (!Scratch.Neighbours.IsCreated)
                {
                    Scratch.Neighbours = new NativePriorityHeap<KDTree.Neighbour>(1, Allocator.Temp);
                }

                Scratch.Neighbours.Clear();
                Tree.GetEntriesInRangeWithHeap(unfilteredChunkIndex, transforms[i].Position, float.MaxValue,
                    ref Scratch.Neighbours);
                var nearest = Scratch.Neighbours.Peek().index;
                targets[i] = new Target { Value = TargetEntities[nearest] };
            }
        }
    }

    [BurstCompile]
    public partial struct SimplePartitioning : IJobEntity
    {
        [ReadOnly] public NativeArray<Entity> TargetEntities;
        [ReadOnly] public NativeArray<PositionAndIndex> Positions;

        public void Execute(ref Target target, in LocalTransform translation)
        {
            var ownpos = new PositionAndIndex { Position = translation.Position.xz };
            var index = Positions.BinarySearch(ownpos, new AxisXComparer());
            if (index < 0) index = ~index;
            if (index >= Positions.Length) index = Positions.Length - 1;

            var closestDistSq = math.distancesq(ownpos.Position, Positions[index].Position);
            var closestEntity = index;

            Search(index + 1, Positions.Length, +1, ref closestDistSq, ref closestEntity, ownpos);
            Search(index - 1, -1, -1, ref closestDistSq, ref closestEntity, ownpos);

            target.Value = TargetEntities[Positions[closestEntity].Index];
        }

        void Search(int startIndex, int endIndex, int step, ref float closestDistSqRef, ref int closestEntityRef,
            PositionAndIndex ownpos)
        {
            for (int i = startIndex; i != endIndex; i += step)
            {
                var xdiff = ownpos.Position.x - Positions[i].Position.x;
                xdiff *= xdiff;

                if (xdiff > closestDistSqRef) break;

                var distSq = math.distancesq(Positions[i].Position, ownpos.Position);

                if (distSq < closestDistSqRef)
                {
                    closestDistSqRef = distSq;
                    closestEntityRef = i;
                }
            }
        }
    }


    [BurstCompile]
    public partial struct NoPartitioning : IJobEntity
    {
        [ReadOnly] public NativeArray<LocalTransform> TargetTransforms;

        [ReadOnly] public NativeArray<Entity> TargetEntities;

        public void Execute(ref Target target, in LocalTransform translation)
        {
            var closestDistSq = float.MaxValue;
            var closestEntity = Entity.Null;

            for (int i = 0; i < TargetTransforms.Length; i += 1)
            {
                var distSq = math.distancesq(TargetTransforms[i].Position, translation.Position);
                if (distSq < closestDistSq)
                {
                    closestDistSq = distSq;
                    closestEntity = TargetEntities[i];
                }
            }

            target.Value = closestEntity;
        }
    }

    public struct AxisXComparer : IComparer<PositionAndIndex>
    {
        public int Compare(PositionAndIndex a, PositionAndIndex b)
        {
            return a.Position.x.CompareTo(b.Position.x);
        }
    }

    public struct PositionAndIndex
    {
        public int Index;
        public float2 Position;
    }

    public struct PerThreadWorkingMemory
    {
        [NativeDisableContainerSafetyRestriction]
        public NativePriorityHeap<KDTree.Neighbour> Neighbours;
    }
}

当setting为None时,单纯用NoPartitioning遍历所有unit然后一个个计算距离,获得距离最小的Entity。

当setting为Simple时,将target的位置存贮在positions中,以及对应的序号index,然后根据x的值来排序。用BinarySearch获得x最近的序号位置对(PositionAndIndex),然后通过Search获得真正的最近的序号位置对。Search函数如果没比较x的部分,跟None模式一样。将期中的if注释后即可通过profiler看出。

当setting为KDTree时,使用KDTree来储存位置,并根据这个Tree搜索最近的点。这里调用unity提供的KDTree库。

单纯从这个脚本学了些不知道的东西。像直接从Query转变成EntityArray和ComponentArray(42-51),

null
最后更新于 2024-08-24