0下载Unity编辑器(2019.1.0f1 or 更新的版本),if(已经下载了)continue;
1下载官方案例,打开Git Shell输入:
git clone https://github.com/Unity-Technologies/EntityComponentSystemSamples.git --recurse
or 点击Unity官方ECS示例下载代码
if(已经下载了)continue;
2用Unity Hub打开官方的项目:ECSSamples
3在Assets目录下找到Advanced/Boids,并打开其下的BoidExample场景
Boids其实是类鸟群的意思,在咱们IT领域叫做“集群模拟算法”,但我是个漫威粉,所以称之大群,LOL。
如上图所示,这个案例向我们展示了大量(50000)实体的风采,虽然鲨鱼的模型不怎么精致,但是大群巍巍壮观,这才是ECS的真正用法吧!下面一起来看看这样震撼的场面是如何实现的吧:
///
/// E:把大群的数据传递给C
///
[RequiresEntityConversion]
public class Boid : MonoBehaviour, IConvertGameObjectToEntity
{
public float CellRadius;
public float SeparationWeight;
public float AlignmentWeight;
public float TargetWeight;
public float ObstacleAversionDistance;
public float MoveSpeed;
// Lets you convert the editor data representation to the entity optimal runtime representation
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
dstManager.AddSharedComponentData(entity, new Samples.Boids.Boid
{
CellRadius = CellRadius,
SeparationWeight = SeparationWeight,
AlignmentWeight = AlignmentWeight,
TargetWeight = TargetWeight,
ObstacleAversionDistance = ObstacleAversionDistance,
MoveSpeed = MoveSpeed
});
}
}
以上是大群的实体Entity(以后注释统一用E缩略,表明其身份)部分,E将数据传递给C保存,下面我们一起看C:
///
/// C:大群的共享数据
///
[Serializable]
[WriteGroup(typeof(LocalToWorld))]
public struct Boid : ISharedComponentData
{
///
/// 单元半径
///
public float CellRadius;
///
/// 间隔宽度
///
public float SeparationWeight;
///
/// 对齐宽度
///
public float AlignmentWeight;
///
/// 目标宽度
///
public float TargetWeight;
///
/// 排斥距离
///
public float ObstacleAversionDistance;
///
/// 移动速度
///
public float MoveSpeed;
}
C(Component组件,以后简写C,表明身份即可)和以前不一样了,C实现了空接口 ISharedComponentData,唯一目的只是为了表明其储存的数据是共享的,一直以来C都是最简单的脚本。
下面一起来看看最复杂的S(System系统,今后简写S),大群系统BoidSystem。
由于大群系统比较复杂,因此我画了一副图来描摹几个概念,如图:
解释下脚本中用到的几个定语:
[RequireComponentTag(typeof(组件))]:这里的组件是指C
[BurstCompile]:Burst编译,编译速度更快
[ReadOnly]:只读的方式访问,速度更快
[DeallocateOnJobCompletion]:在任务完成的时候释放内存(解除分配内存)
///
/// 麦克在GDC上面的讲话‘面向数据的方式来使用组件系统’对于开发大群的案例代码来说有非常大的参考价值
/// https://youtu.be/p65Yt20pw0g?t=1446 (这是油管链接,需要观看,有兴趣的朋友可以在参考)
/// 它解释了这个案例之前版本的实现,但是几乎所有的信息仍然具有参考价值。
/// 目标(2条红鱼)和对手(1条鲨鱼)基于Unity UI里的ActorAnimation(角色动画)栏移动
/// 这样它们才能基于关键帧动画来移动
///
namespace Samples.Boids
{
///
/// S:大群系统,在模拟系统组中更新,在Transform系统组之前更新
///
[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateBefore(typeof(TransformSystemGroup))]
public class BoidSystem : JobComponentSystem
{
private EntityQuery m_BoidQuery;//大群实体查询缓存
private EntityQuery m_TargetQuery;//目标实体查询缓存
private EntityQuery m_ObstacleQuery;//对手实体查询缓存
///
/// 在这个案例中总共有3个独特的大群变体,各有各的共享组件值
/// (提示:这包含了在索引0上默认未初始化的值,该值并未被实际用在这个案例中,)
///
private List<Boid> m_UniqueTypes = new List<Boid>(3);
private List<NativeMultiHashMap<int, int>> m_PrevFrameHashmaps = new List<NativeMultiHashMap<int, int>>();
///
/// `CopyPositions`和`CopyHeadings`都是为了提取相对位置、导向组件到NativeArrays(原生数组)
/// 这样它们才能被`MergeCells`(合并单元)和`Steer`(导航)任务随机访问到 (下面是这两个结构体)
///
[BurstCompile]
struct CopyPositions : IJobForEachWithEntity<LocalToWorld>
{
///
/// 位置,原生数组
///
public NativeArray<float3> positions;
///
/// 把本地位置放到原生数组里
///
/// 实体
/// 索引
/// 只读本地位置
public void Execute(Entity entity, int index, [ReadOnly]ref LocalToWorld localToWorld)
{
positions[index] = localToWorld.Position;
}
}
//同上
[BurstCompile]
struct CopyHeadings : IJobForEachWithEntity<LocalToWorld>
{
public NativeArray<float3> headings;
public void Execute(Entity entity, int index, [ReadOnly]ref LocalToWorld localToWorld)
{
headings[index] = localToWorld.Forward;
}
}
///
/// 生成一个哈希表,每一栏里包含了所有大群的索引,其位置量化成同一个值,即提供的单元半径
/// 这样它们才能被`MergeCells`(合并单元)和`Steer`(导航)任务随机访问到
///
[BurstCompile]
[RequireComponentTag(typeof(Boid))]
struct HashPositions : IJobForEachWithEntity<LocalToWorld>
{
public NativeMultiHashMap<int, int>.ParallelWriter hashMap;
public float cellRadius;
public void Execute(Entity entity, int index, [ReadOnly]ref LocalToWorld localToWorld)
{
var hash = (int)math.hash(new int3(math.floor(localToWorld.Position / cellRadius)));
hashMap.Add(hash, index);
}
}
///
/// 合并所有单元
/// 这里收集了所有单元大群的位置和导向,为了做以下三件事情:
/// 1.计算每个单元的数量
/// 2.找到最近的对手并将互相标记为目标
/// 3.找到包含已收集每个大群单元值的数组索引
///
[BurstCompile]
struct MergeCells : IJobNativeMultiHashMapMergedSharedKeyIndices
{
public NativeArray<int> cellIndices;//单元索引原生数组
public NativeArray<float3> cellAlignment;//单元对齐原生数组
public NativeArray<float3> cellSeparation;//单元间隔
public NativeArray<int> cellObstaclePositionIndex;//单元对手位置索引
public NativeArray<float> cellObstacleDistance;//单元对手距离
public NativeArray<int> cellTargetPositionIndex;//单元目标位置索引
public NativeArray<int> cellCount;//单元数量
[ReadOnly] public NativeArray<float3> targetPositions;//目标位置
[ReadOnly] public NativeArray<float3> obstaclePositions;//对手位置
///
/// 循环数组找到最近的位置
///
/// 目标
/// 位置
/// 最近位置索引
/// 最近距离
void NearestPosition(NativeArray<float3> targets, float3 position, out int nearestPositionIndex, out float nearestDistance )
{
nearestPositionIndex = 0;
nearestDistance = math.lengthsq(position-targets[0]);
for (int i = 1; i < targets.Length; i++)
{
var targetPosition = targets[i];
var distance = math.lengthsq(position-targetPosition);
var nearest = distance < nearestDistance;
nearestDistance = math.select(nearestDistance, distance, nearest);
nearestPositionIndex = math.select(nearestPositionIndex, i, nearest);
}
nearestDistance = math.sqrt(nearestDistance);
}
///
/// 首先执行:找到最近的对手和目标,并储存单元索引
///
///
public void ExecuteFirst(int index)
{
var position = cellSeparation[index] / cellCount[index];
int obstaclePositionIndex;
float obstacleDistance;
NearestPosition(obstaclePositions, position, out obstaclePositionIndex, out obstacleDistance);
cellObstaclePositionIndex[index] = obstaclePositionIndex;
cellObstacleDistance[index] = obstacleDistance;
int targetPositionIndex;
float targetDistance;
NearestPosition(targetPositions, position, out targetPositionIndex, out targetDistance);
cellTargetPositionIndex[index] = targetPositionIndex;
cellIndices[index] = index;
}
///
/// 其次执行:合计被考虑到的实际索引的对齐和间隔,并保存我们正在保存单元的第一个值的索引
///
/// 单元索引
/// 索引
public void ExecuteNext(int cellIndex, int index)
{
cellCount[cellIndex] += 1;
cellAlignment[cellIndex] = cellAlignment[cellIndex] + cellAlignment[index];
cellSeparation[cellIndex] = cellSeparation[cellIndex] + cellSeparation[index];
cellIndices[index] = cellIndex;
}
}
///
/// 导航:这里读取之前计算的大群信息,再基于使用标准集群蜂拥算法新计算出来的导向,
/// 用来更新每一个大群的本地位置
///
[BurstCompile]
[RequireComponentTag(typeof(Boid))]
struct Steer : IJobForEachWithEntity<LocalToWorld>
{
public float dt;
[ReadOnly] public Boid settings;
[DeallocateOnJobCompletion] [ReadOnly] public NativeArray<int> cellIndices;
[DeallocateOnJobCompletion] [ReadOnly] public NativeArray<float3> targetPositions;
[DeallocateOnJobCompletion] [ReadOnly] public NativeArray<float3> obstaclePositions;
[DeallocateOnJobCompletion] [ReadOnly] public NativeArray<float3> cellAlignment;
[DeallocateOnJobCompletion] [ReadOnly] public NativeArray<float3> cellSeparation;
[DeallocateOnJobCompletion] [ReadOnly] public NativeArray<int> cellObstaclePositionIndex;
[DeallocateOnJobCompletion] [ReadOnly] public NativeArray<float> cellObstacleDistance;
[DeallocateOnJobCompletion] [ReadOnly] public NativeArray<int> cellTargetPositionIndex;
[DeallocateOnJobCompletion] [ReadOnly] public NativeArray<int> cellCount;
public void Execute(Entity entity, int index, ref LocalToWorld localToWorld)
{
// temporarily storing the values for code readability
//为了代码的可读性缓存这些值
var forward = localToWorld.Forward;
var currentPosition = localToWorld.Position;
var cellIndex = cellIndices[index];
var neighborCount = cellCount[cellIndex];
var alignment = cellAlignment[cellIndex];
var separation = cellSeparation[cellIndex];
var nearestObstacleDistance = cellObstacleDistance[cellIndex];
var nearestObstaclePositionIndex = cellObstaclePositionIndex[cellIndex];
var nearestTargetPositionIndex = cellTargetPositionIndex[cellIndex];
var nearestObstaclePosition = obstaclePositions[nearestObstaclePositionIndex];
var nearestTargetPosition = targetPositions[nearestTargetPositionIndex];
//基于集群模拟算法的导航计算
var obstacleSteering = currentPosition - nearestObstaclePosition;
var avoidObstacleHeading = (nearestObstaclePosition + math.normalizesafe(obstacleSteering)
* settings.ObstacleAversionDistance)- currentPosition;
var targetHeading = settings.TargetWeight
* math.normalizesafe(nearestTargetPosition - currentPosition);
var nearestObstacleDistanceFromRadius = nearestObstacleDistance - settings.ObstacleAversionDistance;
var alignmentResult = settings.AlignmentWeight
* math.normalizesafe((alignment/neighborCount)-forward);
var separationResult = settings.SeparationWeight
* math.normalizesafe((currentPosition * neighborCount) - separation);
var normalHeading = math.normalizesafe(alignmentResult + separationResult + targetHeading);
var targetForward = math.select(normalHeading, avoidObstacleHeading, nearestObstacleDistanceFromRadius < 0);
var nextHeading = math.normalizesafe(forward + dt*(targetForward-forward));
//基于新的导向更新本地位置
localToWorld = new LocalToWorld
{
Value = float4x4.TRS(
new float3(localToWorld.Position + (nextHeading * settings.MoveSpeed * dt)),
quaternion.LookRotationSafe(nextHeading, math.up()),
new float3(1.0f, 1.0f, 1.0f))
};
}
}
///
/// 停止运行的时候释放上一帧的哈希表
///
protected override void OnStopRunning()
{
for (var i = 0; i < m_PrevFrameHashmaps.Count; ++i)
{
m_PrevFrameHashmaps[i].Dispose();
}
m_PrevFrameHashmaps.Clear();
}
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
EntityManager.GetAllUniqueSharedComponentData(m_UniqueTypes);
var obstacleCount = m_ObstacleQuery.CalculateEntityCount();
var targetCount = m_TargetQuery.CalculateEntityCount();
///暂时还不能在哈希表上调用释放内存定语([DeallocateOnJobCompletion]),
///所以在这里手动释放上一次迭代循环产生的哈希
for (int i = 0; i < m_PrevFrameHashmaps.Count; ++i)
{
m_PrevFrameHashmaps[i].Dispose();
}
m_PrevFrameHashmaps.Clear();
///每一个大群的变体代表一个不同的SharedComponentData值,并且是独立的
///意味着同一种变体的群只与别的变体群互动。因此这个循环单独处理每一个变体类型
for (int boidVariantIndex = 0; boidVariantIndex < m_UniqueTypes.Count; boidVariantIndex++)
{
var settings = m_UniqueTypes[boidVariantIndex];
m_BoidQuery.SetFilter(settings);
var boidCount = m_BoidQuery.CalculateEntityCount();
if (boidCount == 0)
{
///如果变体不包含任何大群,则提前跳过当前循环。
/// 举个栗子,变体0总是会跳过,因为它代表默认的,未初始化的
/// 大群结构体并没有在这个案例里出现
continue;
}
///下面计算相邻大群单元之间的空间
/// 注意:使用的是一个稀疏的网格而不是密集带边界的网格,
/// 所以这里没有预定义的空间边界
var hashMap = new NativeMultiHashMap<int,int>(boidCount,Allocator.TempJob);
var cellIndices = new NativeArray<int>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
var cellObstaclePositionIndex = new NativeArray<int>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
var cellTargetPositionIndex = new NativeArray<int>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
var cellCount = new NativeArray<int>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
var cellObstacleDistance = new NativeArray<float>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
var cellAlignment = new NativeArray<float3>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
var cellSeparation = new NativeArray<float3>(boidCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
var copyTargetPositions = new NativeArray<float3>(targetCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
var copyObstaclePositions = new NativeArray<float3>(obstacleCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
///接下来的任务全部并行,因为在安排任务的时候同一个JobHandle被传递给它们的输入依赖,
///因此它们可以任意顺序运行(或并发运行),并发是它们任务安排的属性,而不是任务结构体本身
var initialCellAlignmentJob = new CopyHeadings
{
headings = cellAlignment
};
var initialCellAlignmentJobHandle = initialCellAlignmentJob.Schedule(m_BoidQuery, inputDeps);
var initialCellSeparationJob = new CopyPositions
{
positions = cellSeparation
};
var initialCellSeparationJobHandle = initialCellSeparationJob.Schedule(m_BoidQuery, inputDeps);
var copyTargetPositionsJob = new CopyPositions
{
positions = copyTargetPositions
};
var copyTargetPositionsJobHandle = copyTargetPositionsJob.Schedule(m_TargetQuery, inputDeps);
var copyObstaclePositionsJob = new CopyPositions
{
positions = copyObstaclePositions
};
var copyObstaclePositionsJobHandle = copyObstaclePositionsJob.Schedule(m_ObstacleQuery, inputDeps);
///暂时还不能在哈希表上调用释放内存定语([DeallocateOnJobCompletion]),
/// 所以把已解决的哈希添加到列表,这样它们就能在接下来的单元任务中使用了,并且也能直接清除
m_PrevFrameHashmaps.Add(hashMap);
// setting up the jobs for position and cell count
///为位置和单元数量设置任务
var hashPositionsJob = new HashPositions
{
hashMap = hashMap.AsParallelWriter(),
cellRadius = settings.CellRadius
};
var hashPositionsJobHandle = hashPositionsJob.Schedule(m_BoidQuery, inputDeps);
var initialCellCountJob = new MemsetNativeArray<int>
{
Source = cellCount,
Value = 1
};
var initialCellCountJobHandle = initialCellCountJob.Schedule(boidCount, 64, inputDeps);
var initialCellBarrierJobHandle = JobHandle.CombineDependencies(initialCellAlignmentJobHandle, initialCellSeparationJobHandle, initialCellCountJobHandle);
var copyTargetObstacleBarrierJobHandle = JobHandle.CombineDependencies(copyTargetPositionsJobHandle, copyObstaclePositionsJobHandle);
var mergeCellsBarrierJobHandle = JobHandle.CombineDependencies(hashPositionsJobHandle, initialCellBarrierJobHandle, copyTargetObstacleBarrierJobHandle);
//合并所有单元
var mergeCellsJob = new MergeCells
{
cellIndices = cellIndices,
cellAlignment = cellAlignment,
cellSeparation = cellSeparation,
cellObstacleDistance = cellObstacleDistance,
cellObstaclePositionIndex = cellObstaclePositionIndex,
cellTargetPositionIndex = cellTargetPositionIndex,
cellCount = cellCount,
targetPositions = copyTargetPositions,
obstaclePositions = copyObstaclePositions
};
var mergeCellsJobHandle = mergeCellsJob.Schedule(hashMap,64,mergeCellsBarrierJobHandle);
//导航
var steerJob = new Steer
{
cellIndices = cellIndices,
settings = settings,
cellAlignment = cellAlignment,
cellSeparation = cellSeparation,
cellObstacleDistance = cellObstacleDistance,
cellObstaclePositionIndex = cellObstaclePositionIndex,
cellTargetPositionIndex = cellTargetPositionIndex,
cellCount = cellCount,
targetPositions = copyTargetPositions,
obstaclePositions = copyObstaclePositions,
dt = Time.deltaTime,
};
var steerJobHandle = steerJob.Schedule(m_BoidQuery, mergeCellsJobHandle);
inputDeps = steerJobHandle;
m_BoidQuery.AddDependency(inputDeps);
}
m_UniqueTypes.Clear();
return inputDeps;
}
protected override void OnCreate()
{
///查询大群
m_BoidQuery = GetEntityQuery(new EntityQueryDesc
{
All = new [] { ComponentType.ReadOnly<Boid>(), ComponentType.ReadWrite<LocalToWorld>() },
});
//目标大群
m_TargetQuery = GetEntityQuery(new EntityQueryDesc
{
All = new [] { ComponentType.ReadOnly<BoidTarget>(), ComponentType.ReadOnly<LocalToWorld>() },
});
//对手大群
m_ObstacleQuery = GetEntityQuery(new EntityQueryDesc
{
All = new [] { ComponentType.ReadOnly<BoidObstacle>(), ComponentType.ReadOnly<LocalToWorld>() },
});
}
}
}
这个大群系统是最复杂的,至于生成Spawn就相对比较简单了,都是之前几篇解析过的实体生成实体,所以这里就不用再多讲。
大群的复杂在于其算法,这种比较大量的操作,先合并所有单元,再进行导航的方式是比较高效的。即使达到100000的量也感觉不到卡顿,完全展示出了ECS的强大速度优势,在拥有大量实体的项目中性能得到极大优化。
Boid:
ECS | Scripts | Interface1 |
---|---|---|
Entity | Boid | IConvertGameObjectToEntity |
Component | Boid | IComponentData |
System | BoidSystem | JobComponentSystem |
BoidSystem流程大体如下:
太混乱了,拆分一下:
1.OnCreate:
2.OnUpdate
还是乱,再做减法:
DOTS系统:
如果喜欢我的文章可以点赞支持一下,谢谢鼓励!如果有什么疑问可以给我留言,有错漏的地方请批评指证!
如果有技术难题需要讨论,可以加入开发者联盟:566189328(付费群)为您提供有限的技术支持,以及,心灵鸡汤!
当然,不需要技术支持也欢迎加入进来,随时可以请我喝咖啡、茶和果汁!( ̄┰ ̄*)