本章节为大家解析ECS案例,资源来自,大家自行获取:
https://github.com/Unity-Technologies/EntityComponentSystemSamples
3、IJobForEach——Job System入门案例
这个案例的构造和案例1十分相似(如果没看过案例1的,可以先去看一下案例1的内容),ECS中的Entity和Component都是差不多的,区别就在于System
之前的案例使用的是ComponentSystem,我们也说了,可以用,但是不推荐,因为它是在主线程中运行的,并没有利用到Job System,所以在这个案例中就使用到了Job System
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
public class RotationSpeedSystem_IJobForEach : JobComponentSystem
{
[BurstCompile]
那么首先,现在代码继承的是JobComponentSystem而不是ComponentSystem,并且在这里BurstCompile属性可以让代码使用Burst来对Job进行编写,会有很大的速度提升!
// OnUpdate runs on the main thread.
protected override JobHandle OnUpdate(JobHandle inputDependencies)
{
var job = new RotationSpeedJob
{
DeltaTime = Time.deltaTime
};
return job.Schedule(this, inputDependencies);
}
因为继承了JobComponentSystem,所以要实现其中的虚函数OnUpdate函数(在主线程上运行的),而在OnUpdate函数中要实现的就是具体的Job(分配在工作线程上执行),那么我们就需要创建这个Job(这里设置了DeltaTime这个参数),最后执行这个Job:
struct RotationSpeedJob : IJobForEach<Rotation, RotationSpeed_IJobForEach>
{
public float DeltaTime;
// The [ReadOnly] attribute tells the job scheduler that this job will not write to rotSpeedIJobForEach
public void Execute(ref Rotation rotation, [ReadOnly] ref RotationSpeed_IJobForEach rotSpeedIJobForEach)
{
// Rotate something about its up vector at the speed given by RotationSpeed_IJobForEach.
rotation.Value = math.mul(math.normalize(rotation.Value), quaternion.AxisAngle(math.up(), rotSpeedIJobForEach.RadiansPerSecond * DeltaTime));
}
}
}
在之前的ECS(五)中,我们已经简单介绍过了,这里再复习一下:
- 定义一个结构体,然后继承一个Job接口,在这里继承的是IJobForEach,参数里填上需要查询的组件;在这个例子中Job需要读取RotationSpeed_IJobForEach组件中的数据并写入Rotation组件中
- 定义变量,例如DeltaTime,作为执行时的参数传入
- 编写Execute()方法(在这个案例中,执行了旋转这个行为),JobComponentSystem为每个符合条件的实体调用Execute()方法,传入到IJobForEach所标识的实体中去,因此,Execute()的参数必须与为结构体所定义的泛型参数匹配
- 识别组件的读写性(这里指[ReadOnly])可以帮助Job scheduler更有效地分配Jobs
小结
在这个案例中,我们了解了如何使用Job System
- 创建一个类(System)继承自JobComponentSystem,并实现OnUpdate接口
- 创建一个Job继承自IJobForEach并在参数里填上需要查询的组件
2.1 实现Execute()方法 - 在OnUpdate中执行Job
4、IJobChunk——根据区块更新实体
不说了,这个案例的构造和案例3很像,但是首先让我们回顾一个概念——原型(Archetype),当你创建一个实体,并且对其添加了组件,EntityManager就会对这个实体上的独一无二的组件组合保持追踪,而这个独一无二的组件组合,我们就称之为Archetype(原型),而相同原型的实体是被存在同一区块(Chunk) 内的,当实体的原型受到了影响发生了改变,EntityManager必须将改变后的数据移动到新的区块中
由此可见,我们可以根据原型来追踪一类实体:
以下内容在ECS(六)中都有介绍
4.1、通过EntityQuery查询数据
public class RotationSpeedSystem : JobComponentSystem
{
private EntityQuery m_Group;
protected override void OnCreate()
{
m_Group = GetEntityQuery(typeof(RotationQuaternion), ComponentType.ReadOnly<RotationSpeed>());
}
//…
}
我们首先声明一个EntityQuery类型的查询,使用GetEntityQuery函数,参数是原型所拥有的组件,注意:这里是包含但不限于 RotationQuaternion和RotationSpeed组件的实体,如果想要查询只包含这两个组件的实体,需要采用All,例如:
var query = new EntityQueryDesc
{
None = new ComponentType[]{ typeof(Frozen) },
All = new ComponentType[]{ typeof(RotationQuaternion), ComponentType.ReadOnly<RotationSpeed>()
}
4.2、创建Job
那么接下来我们已经得到了该区块,接下来就是对区块内的实体进行更新,同样建立一个系统继承JobComponentSystem,实现OnUpdate函数,在这之前,我们需要创建Job,这个Job是继承IJobChunk的
[BurstCompile]
struct RotationSpeedJob : IJobChunk
{
public float DeltaTime;
public ArchetypeChunkComponentType<Rotation> RotationType;
[ReadOnly] public ArchetypeChunkComponentType<RotationSpeed_IJobChunk> RotationSpeedType;
因为需要访问区块内的组件数组,所以必须对需要读取或写入的每种类型组件的对象创建
ArchetypeChunkComponentType类型的变量,这些变量允许你获取NativeArrays的实例,从而提供对实体组件的访问
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
{
var chunkRotations = chunk.GetNativeArray(RotationType);
var chunkRotationSpeeds = chunk.GetNativeArray(RotationSpeedType);
使用chunk参数获取组件的NativeArray实例,由于这些数组都是对齐的,因此每个实体在不同数组中有着相同的索引
举个例子 查询的区块中有4个实体,那么:
数组A保存了4个a类型的组件,数组B保存了4个b类型的组件,数组C保存了4个c类型的组件
在这里A1 B1 C1 都是实体1的组件
在这里A2 B2 C2 都是实体2的组件
…
这就是数组对齐
也就可以正常使用for循环遍历组件数组,使用chunk.Count得到存储在当前块的实体的数量:
for (var i = 0; i < chunk.Count; i++)
{
var rotation = chunkRotations[i];
var rotationSpeed = chunkRotationSpeeds[i];
chunkRotations[i] = new Rotation
{
Value = math.mul(math.normalize(rotation.Value),
quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * DeltaTime))
};
}
}
}
最后修改chunkRotations[i]的数据来旋转chunkRotations[i]
4.3、执行Job
protected override JobHandle OnUpdate(JobHandle inputDependencies)
{
var rotationType = GetArchetypeChunkComponentType<Rotation>();
var rotationSpeedType = GetArchetypeChunkComponentType<RotationSpeed_IJobChunk>(true);
因为RotationSpeedJob中的参数我们需要加入,所以我们需要通过GetArchetypeChunkComponentType函数来声明ArchetypeChunkComponentType类型的变量,然后传入Job中使用
var job = new RotationSpeedJob()
{
RotationType = rotationType,
RotationSpeedType = rotationSpeedType,
DeltaTime = Time.deltaTime
};
return job.Schedule(m_Group, inputDependencies);
}
对m_Group执行声明的RotationSpeedJob,在这里要确保ArchetypeChunkComponentType在EntityQuery中的类型、OnUpdate中的类型和在Job中的类型是一致的! 在这个案例中,因为需要保持一致,rotationSpeedType都是只读的
4.4、小结
利用IJobChunk我们就可以对具有一类组件的实体进行更新
- 利用EntityQuery来查询所需的区块
- 创建一个类(System)继承自JobComponentSystem,并实现OnUpdate接口
- 创建一个Job继承自IJobChunk并建立ArchetypeChunkComponentType类型的变量
2.1 所需的组件组通过GetNativeArray在Chunk获取
2.2 利用For循环对Chunk内的实现所有实体的Execute()方法
3.3 这里的重点在于通过Chunk获取组件的NativeArray是对齐的 - 在OnUpdate中执行Job,Job所需的ArchetypeChunkComponentType类型的参数通过GetArchetypeChunkComponentType来获取并传入
这两个案例的效果都和案例1一样,就不放图了