在以前的案例中,通常实体都是在主线程进行动态创建的,而想在Job中进行动态创建,Unity提供了EntityCommandBuffer(实体命令缓冲)概念,它解决了两个问题:
1.在Job中无法访问EntityManager。
2.当访问EntityManager时(例如,创建实体),将使所有注入的数组和EntityQuery对象无效。
脚本:Spawner_FromEntity
using Unity.Entities;
public struct Spawner_FromEntity : IComponentData
{
public float time;
public Entity Prefab;
}
脚本:SpawnerAuthoring_FromEntity
using System.Collections.Generic;
using UnityEngine;
using Unity.Entities;
// 将自己转换成实体,再由预设生成新的实体
[RequiresEntityConversion] // IDeclareReferencedPrefabs接口的实现,声明引用的预设,好让转化系统提前知道它们的存在
public class SpawnerAuthoring_FromEntity : MonoBehaviour, IDeclareReferencedPrefabs, IConvertGameObjectToEntity
{
public GameObject prefab;
public void DeclareReferencedPrefabs(List referencedPrefabs)
{
referencedPrefabs.Add(prefab);
}
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
var spawnerData = new Spawner_FromEntity
{
//被引用的预设因为做了声明将被转化成实体
//所以我们这里只是将游戏对象标记到一个引用该预设的实体上
Prefab = conversionSystem.GetPrimaryEntity(prefab),
time = 5
};
dstManager.AddComponentData(entity, spawnerData);
}
脚本:SpawnerSystem_FromEntity
using UnityEngine;
using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
using Unity.Mathematics;
using Unity.Collections;
[UpdateInGroup(typeof(SimulationSystemGroup))] // 标记更新组为模拟系统组
public class SpawnerSystem_FromEntity : JobComponentSystem
{
/// 开始初始化实体命令缓存系统(BeginInitializationEntityCommandBufferSystem)被用来创建一个命令缓存,
/// 这个命令缓存将在阻塞系统执行时被回放。虽然初始化命令在生成任务(SpawnJob)中被记录下来,
/// 它并非真正地被执行(或“回放”)直到相应的实体命令缓存系统(EntityCommandBufferSystem)被更新。
/// 为了确保transform系统有机会在新生的实体初次被渲染之前运行,SpawnerSystem_FromEntity将使用
/// 开始模拟实体命令缓存系统(BeginSimulationEntityCommandBufferSystem)来回放其命令。
/// 这就导致了在记录命令和初始化实体之间一帧的延迟,但是该延迟实际通常被忽略掉。
///
BeginInitializationEntityCommandBufferSystem m_BeginInitializationEntityCommandBufferSystem;
protected override void OnCreate()
{
////在一个字段中缓存BeginInitializationEntityCommandBufferSystem,这样我们就不必在每一帧都创建它
m_BeginInitializationEntityCommandBufferSystem = World.GetOrCreateSystem();
}
struct SpawnJob : IJobForEachWithEntity
{
public EntityCommandBuffer.Concurrent commandBuffer;
public float delatime;
public float3 position;
public void Execute(Entity entity, int index, ref Spawner_FromEntity spawnerData, ref LocalToWorld location)
{
if (spawnerData.time <= 0)
{
spawnerData.time = 5;
Debug.Log("创建一个cube");
var instance = commandBuffer.Instantiate(index, spawnerData.Prefab);
commandBuffer.SetComponent(index, instance, new Translation { Value = position });
commandBuffer.SetComponent(index, instance, new Spawner_FromEntity { time = 5, Prefab = spawnerData.Prefab });
}
else
{
spawnerData.time -= delatime;
}
//commandBuffer.DestroyEntity(index, entity);
}
}
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
//取代直接执行结构的改变,一个任务可以添加一个命令到EntityCommandBuffer(实体命令缓存),从而在主线程上完成其任务后执行这些改变
//命令缓存允许在工作线程上执行任何潜在消耗大的计算,同时把实际的增删排到之后把将要添加实例化命令到EntityCommandBuffer的任务加入计划
var job = new SpawnJob
{
commandBuffer = m_BeginInitializationEntityCommandBufferSystem.CreateCommandBuffer().ToConcurrent(),
delatime = Time.deltaTime,
position = UnityEngine.Random.insideUnitSphere * 50
};
JobHandle jobHandle = job.Schedule(this, inputDeps);
//Debug.Log("场景实体个数===" + EntityManager.EntityCapacity); 错误
///生成任务并行且没有同步机会直到阻塞系统执行
///当阻塞系统执行时,我们想完成生成任务,然后再执行那些命令(创建实体并放置到指定位置)
/// 我们需要告诉阻塞系统哪个任务需要在它能回放命令之前完成
m_BeginInitializationEntityCommandBufferSystem.AddJobHandleForProducer(jobHandle);
return jobHandle;
}
}
如上图,将自身设定为预制体,并拖拽到Inspector面板里作为动态创建的对象。
涉及到的线程知识比较多:
1.任务系统Jobs是C#为了让我们安全地使用多线程而封装的;
2.不能滥用任务系统,否则会引起线程之间的竞争,例如你有四个线程,但是现在有三个被占用,却有五个任务要完成,这时就会五个任务去争夺一个线程,从而造成线程安全问题;
3.任务组件系统可以在工作线程上运行,但是创建和移除实体只能在主线程上做,从而防止线程之间的竞争;
4.为了确保任务可以完成,这里引入了命令缓存机制,就在先把任务缓存起来,等待主线程完成工作后,再进行增删实体的操作。
5.关于阻塞系统,是为了确保安全而生,当线程在执行任务的时候,将其阻塞起来,避免其他任务误入,等任务完成之后,再执行下一个任务,从而有序进行。
6.有的任务由于等待太久而错过时机怎么办,Play Back回放,大概是把没有执行的任务重新添加到队列(我猜的)
这些多线程的东西,知道一些常识即可,然后按照常识操作。
ECS规定,以下行为都不能在Job中处理:(上面的线程知识也就是在复述下面的四个问题)
1.创建实体(Create Entities)
2.销毁实体(Destroy Entities)
3.给实体添加组件(Add Components)
4.删除实体的组件(Remove Components)
很多情况下,只有在Job中才能决定要不要创建实体、添加组件等,这种时候应该怎么办?于是,就有了EntityCommandBufferSystem(实体命令缓存系统)。简单地说,EntityCommandBufferSystem(实体命令缓存系统)可以让我们在Job里添加一些任务队列,然后在主线程中执行这些任务。
读到这里一定要看这篇文章(注意一定要看):http://www.benmutou.com/archives/2829 ,并结合下面图中的红框指示内容,了解ECS的运行流程。
本文参考:https://blog.csdn.net/qq_30137245/article/details/99083411
远程项目仓库(github):https://github.com/h1031464180/UnityECS