提示:选中右侧目录,可快速找到所需内容
本系列博客地址:传送门
本节我们要讲在主线程操作实体。如何在主线程:
- 创建实体(Create Entities)
- 销毁实体(Destroy Entities)
- 给实体添加组件(Add Components)
- 删除实体的组件(Remove Components)
一、在主线程操作的原因
至于为什么要在主线程做这件事,让我们思考下面问题:
我们已经知道,JobComponentSystem配合各种Job(IJobForEach、IJobChunk等),可以方便地实现并行(多线程、多核)执行逻辑。既然涉及到多线程,就会有一个麻烦的事情——某个线程做了破坏结构的操作,其他线程会受到影响。
注:System的OnUpdate函数都是在主线程调用,Job才是在多线程中并行调用的。
比如,某个Job给实体删除了一个组件,会发生什么事情?
我们的实体都是按块(Chunk)存储的,一旦某个实体的组件数量或类型改变了,它就不属于当前的块,它会被移到其他块里。所以,如果某Job给实体删除了一个组件,那么,这个实体就会被移到另一个块里。
那么,另外一个并行Job呢?这个并行的Job还不知道实体被移到另一个块了,也不知道这个实体被删除了某个组件,所以这个并行的Job会做出一些不太正确的操作。(操作了即将不存在的组件、或操作了错误的块里的实体)
为了解决这种冲突,ECS规定,以下行为都不能在Job中处理:
- 创建实体(Create Entities)
- 销毁实体(Destroy Entities)
- 给实体添加组件(Add Components)
- 删除实体的组件(Remove Components)
二、怎样在主线程搞事
上面的四种行为都不能在Job中处理,但是,很多情况下,只有在Job中才能决定要不要创建实体、添加组件等,这种时候应该怎么办?
于是,就有了EntityCommandBufferSystem。它可以让我们在Job(IJobForEach、IJobChunk等)里添加一些任务队列,然后在主线程中执行这些任务。
ECS默认的三个系统分组,分别都有一个 Begin 和 End 的 EntityCommandBufferSystem。为的是让我们可以在分组的开始或结束时作一些特定的操作。比如,创建/销毁实体,大部分情况下就是在第一个分组的BeginInitializationEntityCommandBufferSystem里进行。
BeginInitializationEntityCommandBufferSystem是在初始化阶段的第一个System,它是最先执行的。
我们只要把创建/销毁实体等操作放到BeginInitializationEntityCommandBufferSystem里面执行,就不怕后续逻辑出现冲突问题了。
System:
所以,如何把创建实体的操作放到BeginInitializationEntityCommandBufferSystem执行?
代码如下:
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
public class SpawnerSystem_FromEntity : JobComponentSystem
{
BeginInitializationEntityCommandBufferSystem beginIni;
protected override void OnCreate()
{
// 获取或创建 BeginInitializationEntityCommandBufferSystem
beginIni = World.GetOrCreateSystem<BeginInitializationEntityCommandBufferSystem>();
}
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
// 获取了队列
var commandBuffer = beginIni.CreateCommandBuffer().ToConcurrent();
var jobHandle = Entities // entity,通过筛选 Spawner_FromEntity 得到的实体
.ForEach((Entity entity, int entityInQueryIndex, in Spawner_FromEntity spawnerFromEntity) =>
{
// 在Buffer中创建实体
var instance = commandBuffer.Instantiate(entityInQueryIndex, spawnerFromEntity.prefab_SFE);
// 删除筛选出来的实体对象,这个很重要,后面会解释
commandBuffer.DestroyEntity(entityInQueryIndex, entity);
}).Schedule(inputDeps);
// 把Job添加到 EntityCommandBufferSystem
beginIni.AddJobHandleForProducer(jobHandle);
return jobHandle;
}
}
问题解释:
1、Job在Update里,beginIni.AddJobHandleForProducer(jobHandle)会无限添加Job吗?
答:在JobComponentSystem中,如果Job没有筛选出实体数据,那么,OnUpdate是不会被调用的。
ForEach里是筛选了Spawner_FromEntity组件的,而我们这个程序里只有一个实体拥有这个组件,而后面又通过DestroyEntity将筛选出来的实体删除了。
于是,在下一轮的循环中,已经筛选不出任何实体了,于是,OnUpdate函数也不会被调用。
Component
原先我们直接将转化实体的代码放在C里,现在我们分开了,C只放数据,转化实体代码在E下面。
之前的Component加了转化实体特性 [GenerateAuthoringComponent],使得C可以挂载到实体上。但下方代码没加,继承的不是Mono,所以不能挂在物体上。这个记录了数据的C只能通过下方的转化实体代码动态挂载到物体上。
Spawner_FromEntity 组件是Component,内容如下:
using Unity.Entities;
public struct Spawner_FromEntity : IComponentData
{
public Entity prefab_SFE;
}
Entity
之前我们学过了:
下面代码作用是将本物体转化成实体,且将本脚本上的预制体转化成实体赋值给C。
本节操作方法如下:
将如下代码挂在在空物体上,且挂上 ConvertToEntity 。
该代码挂载上预制体。这个预制体就是我们后面要生成的预制体。
那这个C上的预制体什么时候在场景中生成呢?看S代码。
using System.Collections.Generic;
using Unity.Entities;
using UnityEngine;
[RequiresEntityConversion]
public class SpawnerAuthoring_FromEntity : MonoBehaviour, IDeclareReferencedPrefabs, IConvertGameObjectToEntity
{
public GameObject prefab_SAFE;
// 被 IDeclareReferencedPrefabs 接口调用
// DeclareReferencedPrefabs:让GameObjectConversionSystem对象知道我们预制体的存在,以便通过预制体创建实体。
public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
{
referencedPrefabs.Add(prefab_SAFE);
}
// 现在拥有两个实体:
// 1、Convert 被 IConvertGameObjectToEntity 接口调用,将当前物体转化为实体
// 2、Convert 内,给当前实体添加了 Spawner_FromEntity 组件,且给这个组件赋值了转化后的实体。
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
var spawnerData = new Spawner_FromEntity
{
// GetPrimaryEntity:将我们的GameObject对象转化成Entity对象。
prefab_SFE = conversionSystem.GetPrimaryEntity(prefab_SAFE),
};
dstManager.AddComponentData(entity, spawnerData);
}
}
待我整理整理,看看这几种创建实体的方法到底有什么优缺点,头大
更新中