运行效果
该示例生成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),