官方案例解析7
开始之前的准备工作:
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目录下找到HelloCube/7. SpawnAndRemove,并打开SpawnAndRemove场景
7. SpawnAndRemove
上一个案例演示了如何使用实体来生成别的实体,那么有生成,自然就有移除,下面一起来一探究竟吧:
- Main Camera ……主摄像机
- Directional Light……光源
- Spawner……旋转方块生成器
打开Spawner生成器的Inspector窗口,我们发现这个案例是建议在第六个之上的,因为它也是先将自身转化成实体,再生成别的实体。所以我们忽略相同之处,看不同的地方,也就是移除操作了。
- 实体和案例六几乎一致,所以对应的Entity脚本SpawnerAuthoring_SpawnAndRemove就跳过了。
- Component组件脚本Spawner_SpawnAndRemove也跳过,该脚本存储的数据和案例六几乎一致。
- 实体生成实体的System和案例六一致,所以SpawnerSystem_SpawnAndRemove跳过。
因此移除操作就只能在LifeTimeSystem(生命周期系统)实现了,且看:
/// <summary>
/// 生命周期,这里属于Component
/// </summary>
public struct LifeTime : IComponentData
{
public float Value;
}
/// <summary>
/// 这个系统负责场景中所有实体的生命周期
/// 也可以将其改装来负责特定实体的生命周期,添加刷选条件Filter即可
/// </summary>
public class LifeTimeSystem : JobComponentSystem
{
/// <summary>
/// 实体命令缓存系统--阻塞
/// </summary>
EntityCommandBufferSystem m_Barrier;
/// <summary>
/// 将阻塞缓存起来
/// </summary>
protected override void OnCreate()
{
m_Barrier = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
}
[BurstCompile]//Burst加速编译器
struct LifeTimeJob : IJobForEachWithEntity<LifeTime>
{
public float DeltaTime;
[WriteOnly]//只写
public EntityCommandBuffer.Concurrent CommandBuffer;
/// <summary>
/// 每帧执行,如果寿命 < 0 则摧毁实体
/// </summary>
/// <param name="entity">实体</param>
/// <param name="jobIndex">任务索引</param>
/// <param name="lifeTime">寿命</param>
public void Execute(Entity entity, int jobIndex, ref LifeTime lifeTime)
{
lifeTime.Value -= DeltaTime;
if (lifeTime.Value < 0.0f)
{
CommandBuffer.DestroyEntity(jobIndex, entity);
}
}
}
// OnUpdate runs on the main thread.
/// <summary>
/// 在主线程上每帧运行OnUpdate
/// </summary>
/// <param name="inputDependencies">输入依赖</param>
/// <returns>任务</returns>
protected override JobHandle OnUpdate(JobHandle inputDependencies)
{
var commandBuffer = m_Barrier.CreateCommandBuffer().ToConcurrent();
var job = new LifeTimeJob
{
DeltaTime = Time.deltaTime,
CommandBuffer = commandBuffer,
}.Schedule(this, inputDependencies);
m_Barrier.AddJobHandleForProducer(job);
return job;
}
}
其实这里的写法和我们OOP的写法差不多的,只是我们写在Update里面而已,也没有利用到Jobs和Burst。
没有什么好讲的地方,寿命到了就摧毁实体,再简单不过了。
这里我们可以看到另外一个ECS的特性,那就是多组件,多系统同时协作。这和我们原来的开发模式差不多,把需要的脚本挂在对象上面执行,多个脚本之间不会互相干涉(如果解耦的话)
这里是彻底解耦了的,你在生命周期系统上的更改,对旋转系统并没有影响。如果实体摧毁了,那么对应的组件也不存在了,对应的系统自然也会停止工作。这些蝴蝶效应应该是自发的,因为没有多余的代码来做这个工作。
小结
这个案例结合了案例二和案例六,因此都列出来了。
案例二:
ECS | Scripts | Interface |
---|---|---|
Entity | RotationSpeedAuthoring_IJobForEach | IConvertGameObjectToEntity |
Component | RotationSpeed_IJobForEach | IComponentData |
System | RotationSpeedSystem_IJobForEach | JobComponentSystem |
案例六:
ECS | Scripts | Interface1 | Interface2 |
---|---|---|---|
Entity | SpawnerAuthoring_FromEntity | IConvertGameObjectToEntity | IDeclareReferencedPrefabs |
Component | Spawner_FromEntity | IComponentData | |
System | SpawnerSystem_FromEntity | JobComponentSystem |
案例七:
ECS 1 | Scripts | Interface1 | Interface2 |
---|---|---|---|
Entity 1 | SpawnerAuthoring_SpawnAndRemove | IConvertGameObjectToEntity | IDeclareReferencedPrefabs |
Component1 | Spawner_SpawnAndRemove | IComponentData | |
System 1 | SpawnerSystem_SpawnAndRemove | JobComponentSystem |
ECS 2 | Scripts | Interface1 | Interface2 |
---|---|---|---|
Entity 2 | RotationSpeedAuthoring_SpawnAndRemove | IConvertGameObjectToEntity | |
Component2 | RotationSpeed_SpawnAndRemove | IComponentData | |
System 2 | RotationSpeedSystem_SpawnAndRemove | JobComponentSystem |
ECS 3 | Scripts | Interface1 | Interface2 |
---|---|---|---|
Entity 3 | RotationSpeedAuthoring_SpawnAndRemove | IConvertGameObjectToEntity | |
Component3 | LifeTime | IComponentData | |
System 3 | LifeTimeSystem | JobComponentSystem |
案例七并没有复用之前的ECS,而是独立创建了,其实脚本是大同小异的。通过对比,我们发现不同的组件和系统,可以共用同一个实体,就像表格ECS2和ECS3中所列出来的一样,它们共用了一个实体。
因此我查询了官方文档,发现ECS中实体+组件+系统三者之间并非缺一不可的关系,我发现实体是可有可无的,组件和系统因为实体而存在,却并不依赖它。这就是解耦,需要却并不依赖。
而实体却依赖组件,否则实体的数据没地方传递给系统,所以对于实体来说组件是必须的。
至于系统,实体可以不需要系统,组件也可以不需要系统,系统也可以当它们不存在,系统是完全解耦的。
DOTS 逻辑图表
Remove流程大体如下:
这个图画得有点乱,稍微废话解释一下,Entities把寿命通过LifeTime传给LifeTimeSystem来控制的,在生命周期系统中OnUpdate会在主线程上每帧执行发布新的LifeTimeJob任务,该任务会排到主线程的Schedule表上等待执行,每次的任务都会执行这样的判定,如果实体的寿命到了,那么就会把摧毁实体的命令缓存到CommandBuffer中。
那么CommandBuffer什么时候摧毁实体呢?CommandBuffer会在OnUpdate中传给LifeTimeJob任务,该任务会交给主线程。
执行的时机实际上由EntityCommandBufferSystem来掌控,通过AddJobHandleForProducer,最终的任务由阻塞(EntityCommandBufferSystem)和主线程协调。
大概是这样!
Spawn流程大体如下:
DOTS系统:
这一篇主要是多线程的地方要注意一下线程安全问题,其他的都是之前梳理过的。
更新计划
作者的话
如果喜欢我的文章可以点赞支持一下,谢谢鼓励!如果有什么疑问可以给我留言,有错漏的地方请批评指证!
如果有技术难题需要讨论,可以加入开发者联盟:566189328(付费群)为您提供有限的技术支持,以及,心灵鸡汤!
当然,不需要技术支持也欢迎加入进来,随时可以请我喝咖啡、茶和果汁!( ̄┰ ̄*)