最近在学习unity的ecs框架,转载几篇写的比较好的文章帮助理解
原文日期 2019-12-5 避免误导未来使用正式版的开发者。
经过前面的介绍,我们已经知道了JobComponentSystem和IJobForEach,也了解了C# Job System可以并行执行Job来提升性能。
我们知道,IJobForEach这种Job,只能一个个实体进行处理,这次要介绍的IJobChunk,是可以对某个块(Chunk)里的所有实体进行批量处理。
虽然结果是一样的(仍然是一个Cube在旋转),但代码却大不相同,一起来看看吧。
1.组件(Component)
接下来开始看代码吧,先创建一个组件类:
1 2 3 4 5 6 7 8 | using System; using Unity.Entities;
[Serializable] public struct RotationSpeed_IJobChunk : IComponentData { public float RadiansPerSecond; } |
这个组件仍然是带有一个字段,但是,大家注意一下,和之前的组件对比,这次的组件并没有使用GenerateAuthoringComponent特性,而是使用了Serializable特性。
因为这个Demo里,官方又给我们展示了另外一种相对而言更灵活的实体创建方式,这里先不纠结,后面会说到。
2.系统(System)
接下来,我们要看新的System是怎么做的了,注意,现在要看新System的代码了,我们来创建一个System类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Jobs; using Unity.Mathematics; using Unity.Transforms;
// This system updates all entities in the scene with both a RotationSpeed_IJobChunk and Rotation component. public class RotationSpeedSystem_IJobChunk : JobComponentSystem { EntityQuery m_Group;
protected override void OnCreate () { // Cached access to a set of ComponentData based on a specific query m_Group = GetEntityQuery(typeof(Rotation), ComponentType.ReadOnly<RotationSpeed_IJobChunk>()); }
// Use the [BurstCompile] attribute to compile a job with Burst. You may see significant speed ups, so try it! [BurstCompile] struct RotationSpeedJob : IJobChunk { // 太复杂,这个结构体的内容被我删掉了 }
// OnUpdate runs on the main thread. protected override JobHandle OnUpdate (JobHandle inputDependencies) { // Explicitly declare: // - Read-Write access to Rotation // - Read-Only access to RotationSpeed_IJobChunk var rotationType = GetArchetypeChunkComponentType<Rotation>(); var rotationSpeedType = GetArchetypeChunkComponentType<RotationSpeed_IJobChunk>(true);
var job = new RotationSpeedJob() { RotationType = rotationType, RotationSpeedType = rotationSpeedType, DeltaTime = Time.DeltaTime };
return job.Schedule(m_Group, inputDependencies); } } |
好,感觉有点乱是不是,不要着急,慢慢来。
首先,我们仍然继承了JobComponentSystem,这个很熟悉。
其次,仍然重写了OnUpdate函数,这个也很熟悉,我们最终也需要返回一个JobHandle对象。
3.EntityQuery
然后,重写了OnCreate函数,并且多了一个EntityQuery字段,这个是重点。
EntityQuery是用来筛选实体的,通过调用GetEntityQuery函数,可以获取EntityQuery对象。
GetEntityQuery可以传递多个组件的类型作为参数,后续将会筛选出包含指定类型的实体。
注:GetEntityQuery还有更多更强大的筛选方式(and、or、not),这里不展开。
关于EntityQuery,后续我会专门写一篇教程。
我们获得的EntityQuery对象要用在什么地方呢?
我们看看OnUpdate函数的最后一行:return job.Schedule(m_Group, inputDependencies); 这里把EntityQuery传递给Schedule函数,于是就能筛选实体数据,传递给Job。
4.RotationSpeedJob(IJobChunk)
和IJobForEach的用法类似,我们把逻辑放到Job中,由于RotationSpeedJob的代码看起来比较复杂,我没把它贴出来,先不管。
我们先看看OnUpdate函数里是怎么使用RotationSpeedJob的:
RotationSpeedJob需要三个字段,其中DeltaTime是我们大家熟悉的,不多说。
另外两个字段代表了某个原型的块的组件类型(ArchetypeChunkComponentType),很绕,但其实说白了,就是组件类型。
通过GetArchetypeChunkComponentType函数,可以获得某个原型的块的组件类型…
唔,总之,我们获得了Rotation组件和RotationSpeed_IJobChunk组件的类型(ArchetypeChunkComponentType)。
另外,GetArchetypeChunkComponentType函数可以传递一个参数,代表是否只读。
这是什么意思呢?代表筛选出来的数据是否是只读的,这可以提升游戏的性能,当然这里又不说了(旁白:嗯嗯,你不懂的就不说是吧)。
5.IJobChunk
好了,现在来到最重要的部分。
我们的RotationSpeedJob是实现了IJobChunk接口的,之前我没有把RotationSpeedJob的内容贴出来,现在来看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | [BurstCompile] struct RotationSpeedJob : IJobChunk { public float DeltaTime; public ArchetypeChunkComponentType<Rotation> RotationType; [ReadOnly] public ArchetypeChunkComponentType<RotationSpeed_IJobChunk> RotationSpeedType;
public void Execute (ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) { var chunkRotations = chunk.GetNativeArray(RotationType); var chunkRotationSpeeds = chunk.GetNativeArray(RotationSpeedType); for (var i = 0; i < chunk.Count; i++) { var rotation = chunkRotations[i]; var rotationSpeed = chunkRotationSpeeds[i];
// Rotate something about its up vector at the speed given by RotationSpeed_IJobChunk. chunkRotations[i] = new Rotation { Value = math.mul(math.normalize(rotation.Value), quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * DeltaTime)) }; } } } |
顶部的BurstComplie特性就是所谓的“爆破”编译,总之加上去就能提升Job的性能,具体不展开,以后再聊。
RotationSpeedJob有三个字段,之前已经提到过了,现在来看看Execute函数。
稍微有点乱,我们一步步来:
a. 首先,我们在外部调用了RotationSpeedJob的Schedule函数:
并且给Schedule函数传递了m_group(EntityQuery)对象,inputDependencies参数不管它,我不懂,所以它不重要(害羞)。
这代表,我们的RotationSpeedJob将会获得通过EntityQuery筛选后的块数据。
b. 回到RotationSpeedJob的Execute函数,第一个参数【ArchetypeChunk chunk】是什么呢?它就是经过EntityQuery筛选后获得的块数据。
c. 注意,这个chunk代表的是一个块数据。也就是说,我们筛选出来的实体可能存放在不同的块里,Execute函数每次只能获得一个块里的数据。并且,同一时间可能会有很多个RotationSpeedJob被执行(并行),大幅提升了性能,这个和IJobForEach类似。
d. 通过调用chunk的GetNativeArray函数,可以获得这个块里的所有实体的某个类型的组件。
如:var chunkRotations = chunk.GetNativeArray(RotationType);
可以获得当前块的所有实体的RotationType类型的组件(即,Rotation组件)。
再次看看下面这张图,EntityA和EntityB是属于同一个块的,假设当前chunk就是这个块,那么,chunkRotations变量保存的就是EntityA和EntityB的Rotation组件(两条数据)。
e. 最后就简单了,对获取出来的组件进行取值或者赋值。
按照官方的说法,使用IJobChunk来获取数据,更高效、灵活,这里暂时不深入。
6.创建实体
最后一步,创建实体,以前是直接给组件加上一个GenerateAuthoringComponent特性就能自动创建实体。
这次不一样,这个Demo又给我们展示了另外一种更加灵活的方式去创建实体,先新建一个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | using Unity.Entities; using Unity.Mathematics; using UnityEngine;
[RequiresEntityConversion] public class RotationSpeedAuthoring_IJobChunk : MonoBehaviour, IConvertGameObjectToEntity { public float DegreesPerSecond = 360.0F;
// The MonoBehaviour data is converted to ComponentData on the entity. // We are specifically transforming from a good editor representation of the data (Represented in degrees) // To a good runtime representation (Represented in radians) public void Convert (Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem) { var data = new RotationSpeed_IJobChunk { RadiansPerSecond = math.radians(DegreesPerSecond) }; dstManager.AddComponentData(entity, data); } } |
也是一步步来解释:
a. 这个类继承了MonoBehaviour,实现了IConvertGameObjectToEntity接口,使用了RequiresEntityConversion特性。
b. 将这个类挂到Cube上,就会自动将Cube转换为ECS的实体对象。
c. IConvertGameObjectToEntity接口需要实现Convert函数。
d. 这个Convert函数里,我们new一个RotationSpeed_IJobChunk组件对象,然后通过EntityManager的AddComponentData将组件附加到实体上。
e. 于是,我们就将Cube转换为了一个带有RotationSpeed_IJobChunk的ECS实体。
为什么说这种方式更加灵活?因为我们可以给实体附加任意多个组件(在Convert函数里可以给实体Add任意多个组件对象)。
而如果使用GenerateAuthoringComponent特性将组件转换为实体,那就只能给实体附加单个组件了。
7.运行
同样的,我们新建一个场景,创建一个Cube,把RotationSpeedAuthoring_IJobChunk、ConvertToEntity挂到Cube上,然后运行,就能看到新Demo的效果了。
唔…结果仍然是一个Cube在转,我就不贴效果图了。
8.IJobChunk的优势
官方的说法是,IJobChunk可以对整个块的实体进行操作,更加灵活。
目前来说,我是体会不到特别明显的优势,可能要深入学习ECS后才能体会。
也许IJobForEach就是简化后的IJobChunk?(瞎猜)
后续还有其他一些Job,我们未来再说。
注意,本系列教程基于DOTS相关预览版的Package包,是预览版,不代表正式版的时候也适用。
下图出自同样不错的官方案例讲解:https://blog.csdn.net/qq_30137245/article/details/99068336