本章节为大家解析ECS案例,资源来自,大家自行获取:
https://github.com/Unity-Technologies/EntityComponentSystemSamples
5、SubScene——新的场景工作流
这个案例演示了SubScene的工作流,SubScene提供了一个更加有效的在Unity内编辑和加载场景的方式
这个案例使用了HelloCube中IJobForEach的Components和Systems,这个场景包括了一对正在旋转的Cube,而它们是在开始之后自动从一个SubScene中加载的
- 当你保存一个场景,Unity会将SubScene转化成一个原生的二进制格式,这个格式对内存而言是随时可以调度的,同时在RAM中也可以最小的数据更改量被加载和传输,该格式非常适合流式传输大量实体。
- 你可以在开始之后自动加载一个Sub Scene;你也可以延迟加载知道你从你的代码中流式传输SubScene(使用RequestSceneLoaded组件)
- 在默认情况下,即使在编辑器模式下,SubScene也是通过Entity二进制文件被加载
- 当你选择一个SubScene然后在Unity的Inspector点击Edit按钮对该场景进行编辑,在编辑的时候,你可以在Scene界面中看到Entities的层级结构,同时也能像之前一样对其进行编辑,一个"实时链接"转换管线可以应用任何你对Scene的改变
- 仅编辑场景的一部分并且仍然加载其他子场景的实体的功能,创建了一个可用于编辑大量场景的可扩展的工作流程
创建方式
在Unity的Hierarchy面板创建一个GameObject,添加SubScene组件,在SceneAsset中选择需要加载的场景,然后点击Rebuild Entity Cache(如果有警告就点击Clear),通过Edit按钮可以进入便编辑模式,按Close退出,使用Save保存场景,使用Load和Unload选择是否加载资源
6、SpawnFromMonoBehaviour
public GameObject Prefab;
public int CountX = 100;
public int CountY = 100;
void Start()
{
Entity prefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(Prefab, World.Active);
EntityManager entityManager = World.Active.EntityManager;
for (var x = 0; x < CountX; x++)
{
for (var y = 0; y < CountY; y++)
{
var instance = entityManager.Instantiate(prefab);
float3 position = transform.TransformPoint(new float3(x * 1.3F, noise.cnoise(new float2(x, y) * 0.21F) * 2, y * 1.3F));
entityManager.SetComponentData(instance, new Translation {Value = position});
}
}
}
这个案例和案例2一样,通过GameObjectConversionUtility.ConvertGameObjectHierarchy函数将Prefab转化成Entity,然后使用EntityManager克隆
7、SpawnFromEntity——之后还会研究
这个案例是通过生成一个Entity,然后使用这个Entity的组件数据来生成其他Entity,可以说第一个生成的实体是一个实体生成器
7.1、组件
using Unity.Entities;
public struct Spawner_FromEntity : IComponentData
{
public int CountX;
public int CountY;
public Entity Prefab;
}
组件中的数据分别代表生成的位置和预制体,这很简单
7.2、预制体生成器
using System;
using System.Collections.Generic;
using Unity.Entities;
using UnityEngine;
[RequiresEntityConversion]
public class SpawnerAuthoring_FromEntity : MonoBehaviour, IDeclareReferencedPrefabs, IConvertGameObjectToEntity
{
public GameObject Prefab;
public int CountX;
public int CountY;
public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
{
referencedPrefabs.Add(Prefab);
}
实现IDeclareReferencedPrefabs接口,预先声明了Prefab的参考,让转换系统能够预先知道它
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
var spawnerData = new Spawner_FromEntity
{
Prefab = conversionSystem.GetPrimaryEntity(Prefab),
CountX = CountX,
CountY = CountY
};
dstManager.AddComponentData(entity, spawnerData);
}
}
因为需要引用的Prefab是基于DeclareReferencedPrefabs的,所以这里我们简单地将游戏对象映射到该预制件的引用上
并且这边会因为Convert To Entity组件调用Convert函数,然后添加上7.1中的组件
7.3、生成系统
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
这里首先要说明一下,JobComponentSystems可以在工作线程上运行,但是添加和删除实体的操作只能在主线程上执行——为了避免线程上的冲突,因此在这里需要使用EntityCommandBuffer(实体指令缓冲)来延迟进行这些操作
[UpdateInGroup(typeof(SimulationSystemGroup))]
我们把这个Job放入SimulationSystemGroup组中
public class SpawnerSystem_FromEntity : JobComponentSystem
{
BeginInitializationEntityCommandBufferSystem m_EntityCommandBufferSystem;
protected override void OnCreate()
{
m_EntityCommandBufferSystem = World.GetOrCreateSystem<BeginInitializationEntityCommandBufferSystem>();
}
BeginInitializationEntityCommandBufferSystem可以创建一个命令缓存系统在阻塞系统被执行的时候运行,虽然这个初始化命令在SpawnJob中被记录下来,但是它并不会真正地被执行直到相应的实体命令缓存系统更新
(可能不太恰当)我现在缓存了一个“给我苹果”的操作,直到我执行了“我想吃苹果”这个操作,我发现我没有苹果,我就会执行缓存的操作,然后我就有苹果吃了
那么我们就得先声明变量并初始化
struct SpawnJob : IJobForEachWithEntity<Spawner_FromEntity, LocalToWorld>
{
public EntityCommandBuffer.Concurrent CommandBuffer;
声明实体指令缓冲
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);
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);
}
}
缓冲“生成预制体,然后销毁自己”这个指令
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
var job = new SpawnJob
{
CommandBuffer = m_EntityCommandBufferSystem.CreateCommandBuffer().ToConcurrent()
}.Schedule(this, inputDeps);
m_EntityCommandBufferSystem.AddJobHandleForProducer(job);
return job;
}
}
将缓冲指令作为参数传入,然后在缓冲区添加Job句柄
4.4、小结
- 为了避免直接的结构更改(增删操作),可以将这个命令加入到EntityCommandBuffer
- 使用命令缓存可以在执行增删操作之后再执行任何消耗大的计算
- 目前该任务在被阻塞之前时没有同步点的
- 当阻塞系统执行时,我们要告诉阻塞系统——在阻塞系统能继续执行前需要完成什么
8、SpawnAndRemove
这个案例是之前的综合案例,非常类似案例7,重点在于LifeTime,也就是在LifeTime小于0之后销毁实例,其实也是通过命令缓冲执行的,这里就不再赘述了