官方案例解析6
开始之前的准备工作:
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/6. SpawnFromEntity ,并打开6. SpawnFromEntity场景
6. SpawnFromEntity
这个案例演示了我们如何使用预设游戏对象来生成实体和组件,场景将由此生成一大堆旋转的方块对,貌似和上一个案例一样,实则不是,下面一起来一探究竟吧:
- Main Camera ……主摄像机
- Directional Light……光源
- Spawner……旋转方块生成器
我们注意到这个Spawner生成器相对于案例5多了一个ConvertToEntity脚本,这个脚本是我们最早接触到的,还记得它的功能吗?就是把游戏对象转化成实体,由此我们就明白这个案例的用意了。相对于案例5的从游戏对象上生成实体,我们这一次先将Spawner转化成实体,再由实体来生成实体:
/// <summary>
/// 先将自己转换成实体,再由预设生成新的实体
/// </summary>
[RequiresEntityConversion]
public class SpawnerAuthoring_FromEntity : MonoBehaviour, IDeclareReferencedPrefabs, IConvertGameObjectToEntity
{
public GameObject Prefab;
public int CountX;
public int CountY;
// Referenced prefabs have to be declared so that the conversion system knows about them ahead of time
/// <summary>
/// IDeclareReferencedPrefabs接口的实现,声明引用的预设,好让转化系统提前知道它们的存在
/// </summary>
/// <param name="referencedPrefabs">引用的预设</param>
public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
{
referencedPrefabs.Add(Prefab);
}
// Lets you convert the editor data representation to the entity optimal runtime representation
/// <summary>
/// 我们将编辑器的数据表述转化成实体最佳的运行时表述
/// </summary>
/// <param name="entity">实体</param>
/// <param name="dstManager">目标实体管理器</param>
/// <param name="conversionSystem">转化系统</param>
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
var spawnerData = new Spawner_FromEntity
{
// The referenced prefab will be converted due to DeclareReferencedPrefabs.
// So here we simply map the game object to an entity reference to that prefab.
//被引用的预设因为做了声明将被转化成实体
//所以我们这里只是将游戏对象标记到一个引用该预设的实体上
Prefab = conversionSystem.GetPrimaryEntity(Prefab),
CountX = CountX,
CountY = CountY
};
dstManager.AddComponentData(entity, spawnerData);
}
}
因为是将Spawner先转化成实体,再由Spawner实体来生成旋转方块儿实体,所以这里需要组件Spawner实体的ECS结构。
/// <summary>
/// 我是Spawner的组件,我只储存数据
/// </summary>
public struct Spawner_FromEntity : IComponentData
{
public int CountX;
public int CountY;
public Entity Prefab;
}
Component和实体的代码都非常简单,所以后期开发的时候,我会写一个工具来自动生成代码,从而大大提高工作效率。既然我们是面向数据的编程模式,那么我认为可以直接通过策划的表格或数据库来直接生成实体和组件,我们要做的只是编写各种System系统。先这么构思好了,后面很快就能制作出对应的工具来的,我会把工具放到开发者联盟的群文件共享,如果有需要的话可以自行下载。稍后我会把制作代码生成工具排进计划中,不如明天好了。
跑题了,我们继续来看看Spawner的System脚本SpawnerSystem_FromEntity:
// JobComponentSystems can run on worker threads.
// However, creating and removing Entities can only be done on the main thread to prevent race conditions.
// The system uses an EntityCommandBuffer to defer tasks that can't be done inside the Job.
/// <summary>
/// 任务组件系统(JobComponentSystems)可以在工作线程上运行,但是创建和移除实体只能在主线程上做,从而防止线程之间的竞争
/// Jobs系统使用一个实体命令缓存(EntityCommandBuffer)来延迟那些不能在任务系统内完成的任务。
/// </summary>
[UpdateInGroup(typeof(SimulationSystemGroup))]//标记更新组为模拟系统组
public class SpawnerSystem_FromEntity : JobComponentSystem
{
// BeginInitializationEntityCommandBufferSystem is used to create a command buffer which will then be played back
// when that barrier system executes.
// Though the instantiation command is recorded in the SpawnJob, it's not actually processed (or "played back")
// until the corresponding EntityCommandBufferSystem is updated. To ensure that the transform system has a chance
// to run on the newly-spawned entities before they're rendered for the first time, the SpawnerSystem_FromEntity
// will use the BeginSimulationEntityCommandBufferSystem to play back its commands. This introduces a one-frame lag
// between recording the commands and instantiating the entities, but in practice this is usually not noticeable.
/// <summary>
/// 开始初始化实体命令缓存系统(BeginInitializationEntityCommandBufferSystem)被用来创建一个命令缓存,
/// 这个命令缓存将在阻塞系统执行时被回放。虽然初始化命令在生成任务(SpawnJob)中被记录下来,
/// 它并非真正地被执行(或“回放”)直到相应的实体命令缓存系统(EntityCommandBufferSystem)被更新。
/// 为了确保transform系统有机会在新生的实体初次被渲染之前运行,SpawnerSystem_FromEntity将使用
/// 开始模拟实体命令缓存系统(BeginSimulationEntityCommandBufferSystem)来回放其命令。
/// 这就导致了在记录命令和初始化实体之间一帧的延迟,但是该延迟实际通常被忽略掉。
/// </summary>
BeginInitializationEntityCommandBufferSystem m_EntityCommandBufferSystem;
/// <summary>
/// 在这个字段中缓存BeginInitializationEntityCommandBufferSystem,这样我们就不需要每一帧去创建
/// </summary>
protected override void OnCreate()
{
// Cache the BeginInitializationEntityCommandBufferSystem in a field, so we don't have to create it every frame
m_EntityCommandBufferSystem = World.GetOrCreateSystem<BeginInitializationEntityCommandBufferSystem>();
}
/// <summary>
/// 生成实体任务,实现了IJobForEachWithEntity接口
/// </summary>
struct SpawnJob : IJobForEachWithEntity<Spawner_FromEntity, LocalToWorld>
{
/// <summary>
/// 当前实体命令缓存
/// </summary>
public EntityCommandBuffer.Concurrent CommandBuffer;
/// <summary>
/// 这里循环实例化实体
/// </summary>
/// <param name="entity">实体</param>
/// <param name="index">索引</param>
/// <param name="spawnerFromEntity">生成器实体</param>
/// <param name="location">相对位置</param>
public void Execute(Entity entity, int index, [ReadOnly] ref Spawner_FromEntity spawnerFromEntity,
[ReadOnly] ref LocalToWorld location)
{
for (var x = 0; x < spawnerFromEntity.CountX; x++)
{
for (var y = 0; y < spawnerFromEntity.CountY; y++)
{
var instance = CommandBuffer.Instantiate(index, spawnerFromEntity.Prefab);
// Place the instantiated in a grid with some noise
var position = math.transform(location.Value,
new float3(x * 1.3F, noise.cnoise(new float2(x, y) * 0.21F) * 2, y * 1.3F));
CommandBuffer.SetComponent(index, instance, new Translation {Value = position});
}
}
CommandBuffer.DestroyEntity(index, entity);
}
}
/// <summary>
/// 任务系统OnUpdate每帧调用
/// </summary>
/// <param name="inputDeps">输入依赖</param>
/// <returns></returns>
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
//Instead of performing structural changes directly, a Job can add a command to an EntityCommandBuffer to perform such changes on the main thread after the Job has finished.
//Command buffers allow you to perform any, potentially costly, calculations on a worker thread, while queuing up the actual insertions and deletions for later.
//取代直接执行结构的改变,一个任务可以添加一个命令到EntityCommandBuffer(实体命令缓存),从而在主线程上完成其任务后执行这些改变
//命令缓存允许在工作线程上执行任何潜在消耗大的计算,同时把实际的增删排到之后
// Schedule the job that will add Instantiate commands to the EntityCommandBuffer.
//把将要添加实例化命令到EntityCommandBuffer的任务加入计划
var job = new SpawnJob
{
CommandBuffer = m_EntityCommandBufferSystem.CreateCommandBuffer().ToConcurrent()
}.Schedule(this, inputDeps);
// SpawnJob runs in parallel with no sync point until the barrier system executes.
// When the barrier system executes we want to complete the SpawnJob and then play back the commands (Creating the entities and placing them).
// We need to tell the barrier system which job it needs to complete before it can play back the commands.
///生成任务并行且没有同步机会直到阻塞系统执行
///当阻塞系统执行时,我们想完成生成任务,然后再执行那些命令(创建实体并放置到指定位置)
/// 我们需要告诉阻塞系统哪个任务需要在它能回放命令之前完成
m_EntityCommandBufferSystem.AddJobHandleForProducer(job);
return job;
}
}
这个脚本涉及到的线程知识比较多,所以我额外解释一下:
- 任务系统Jobs是C#为了让我们安全地使用多线程而封装的;
- 不能滥用任务系统,否则会引起线程之间的竞争,例如你有四个线程,但是现在有三个被占用,却有五个任务要完成,这时就会五个任务去争夺一个线程,从而造成线程安全问题;
- 任务组件系统可以在工作线程上运行,但是创建和移除实体只能在主线程上做,从而防止线程之间的竞争;
- 为了确保任务可以完成,这里引入了命令缓存机制,就在先把任务缓存起来,等待主线程完成工作后,再进行增删实体的操作。
- 关于阻塞系统,是为了确保安全而生,当线程在执行任务的时候,将其阻塞起来,避免其他任务误入,等任务完成之后,再执行下一个任务,从而有序进行。
- 有的任务由于等待太久而错过时机怎么办,Play Back回放,大概是把没有执行的任务重新添加到队列(我猜的)
- 这些多线程的东西,知道一些常识即可,然后按照常识操作。
小结
这里因为Spawner生成器转化成实体之后生成的是案例二的旋转方块实体,所以列出来做个类比,生成的方块实体会按照案例二制定的规则自动运行。但凡组合成ECS都会这样,生成之后就会按照既定System系统执行。
案例二:
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 |
这里Spawner因为要转化成实体,再生成实体,所以相对会多实现一个接口IDeclareReferencedPrefabs,来声明其引用的预设。看起来会繁琐一些,实际上却是我们不得不做的,例如敌方AI弓箭手阵营不断射箭,那么首先弓箭手就是实体,射出来的箭也是实体,这就是实体生成实体了,弓箭手就得声明箭是自己的预设!
DOTS 逻辑图表
Spawn流程大体如下:
DOTS系统:
这一篇主要是多线程的地方要注意一下线程安全问题,其他的都是之前梳理过的。
更新计划
作者的话
如果喜欢我的文章可以点赞支持一下,谢谢鼓励!如果有什么疑问可以给我留言,有错漏的地方请批评指证!
如果有技术难题需要讨论,可以加入开发者联盟:566189328(付费群)为您提供有限的技术支持,以及,心灵鸡汤!
当然,不需要技术支持也欢迎加入进来,随时可以请我喝咖啡、茶和果汁!( ̄┰ ̄*)