观前须知
ECS是一种用于处理大量运算,性能极高的架构,在某些特定的情况下可能发挥很大作用,由于所蕴含的知识很多,而我只粗学了10多个小时,某些地方可能会有纰漏,看不懂或者讲错了直接喷
注:需要导入的一些包这里就不再赘述了,加载SubScene什么的这里也不再提及
正式开始
ECS,即"Entity(实体)",“Component(组件)”,"System(系统)"简单来说就是组件挂载在实体上,执行系统中的规则,与传统unity的区别是原本unity执行的规则放在组件上,下面,我们分开来讲这三个部分
组件Component
这里的Component,实际上充当了Data的作用,主要是用来存放一些字段数据,下面给出一个简单的例子
public struct RotateSpeed : IComponentData { public float Speed; }注意这里使用结构体,是为了让Data为非托管类型,如果你非要用class,也不是不可以,只是会让性能下降很多,还有一点,请不要在结构体中放托管类型的字段,否则也会让它性能下降
实体Entity
实体,只负责承载Component,本身没有任何字段和方法,想要给实体上放置组件,我们不能直接把ComponentData拖到对象上面,我们可以用Baker的方法
public class RotateSpeedAuthoring : MonoBehaviour { [SerializeField] private float _speed; [SerializeField] private bool _startRotate; public float Speed => _speed; public bool StartRotate => _startRotate; private class Baker : Baker<RotateSpeedAuthoring> { public override void Bake(RotateSpeedAuthoring authoring) { Entity entity = GetEntity(TransformUsageFlags.Dynamic);//也可以用None,Dynamic是可动的,None一般是不需要它的位置的 AddComponent(entity, new RotateSpeed() { Speed = authoring.Speed, }); //先忽略 SetComponentEnabled<RotateSpeed>(entity,authoring.StartRotate); } } }这里的Baker是Mono脚本的一个子类,可以在游戏开始是给挂载该脚本的对象实体添加一个Component。注意这里的GetEntity方法,参数可以为动态或静态,它会让世界给你返回一个实体的引用,你可以为它添加自己想要的组件,并为对象设置想要的初始值。除了用Baker在开始时添加组件,还有另外三种常用的方法添加组件,它们分别是在主线程动态增删的EntityManager和EntityCommandBuffer,还有在多线程中增删的EntityCommandBuffer.ParallelWriter,至于它们,我们会在下一段见到
系统System
系统是最核心的一部分,包含了整个世界的规则,简单举一个例子就是"让所有长翅膀的动物会飞",这就是一个简单的系统了,系统要做的事其实就这么几步,查找所有动物,看谁长了翅膀,然后让它飞起来。最多需要在开始时先查找一遍有没有长翅膀的动物,如果没有就不查找了节省性能。
至于这里的查找,主要有三种常用方法,Query,Job和Chunk.我们来以一个简单的系统来依次看看
Query
[BurstCompile] partial struct RotateCubeSystem : ISystem { private float _timer; private const float INTERVAL = 1f; [BurstCompile] public void OnCreate(ref SystemState state) { state.Enabled = false;//这里如果用false系统就不运行了,记得删!!! state.RequireForUpdate<RotateSpeed>();//找寻世界里有没有对应组件,没有就不Update } [BurstCompile] public void OnUpdate(ref SystemState state) { foreach (var (transform, speed) in SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotateSpeed>>()) { transform.ValueRW = transform.ValueRO.RotateY( speed.ValueRO.Speed * DeltaTime); } } [BurstCompile] public void OnDestroy(ref SystemState state) { } }这里就是最简单的一个Query,下面我们依次解析可能会看不懂的地方,
- 首当其冲的是 [BurstCompile],这个标签是为了提升性能,但是如果方法中有托管对象就会报错,比如GameObject,
- 第二,查询Query,前面的类是var加一个元组,元组是用来简化代码的,关键看后面.Query<RefRW, RefRO>(),查询世界中所有带LocalTransform和RotateSpeed的实体,并返回LocalTransform的读写,RotateSpeed的可读引用,用LocalTransform主要是因为unity内置的Transform用不了,只能用它,改变它的旋转
- 后面就是简单的改变transform的值了,没有好讲的了
这里的Query性能不高,但是理解起来容易,我们来给他改成job,性能更高,理解起来稍稍有些困难
Job
job类 [BurstCompile] public partial struct RotateCubeJob : IJobEntity { public float DeltaTime; void Execute(ref LocalTransform transform, ref PostTransformMatrix postTransform, in RotateSpeed speed) { transform = transform.RotateY(speed.Speed * DeltaTime); } } //使用:把上面一段foreach改成 RotateCubeJob job = new RotateCubeJob() { DeltaTime = SystemAPI.Time.DeltaTime }; state.Dependency= job.ScheduleParallel(state.Dependency);这里为什么要传一个DeltaTime作为参数?因为job中不能使用SystemAPI.Time.DeltaTime,所以需要外界给他传进去
Execute方法?其实和上面的Query差不多,只不过把可写参数加上ref,可读参数加上in,其他几乎就是照搬
最后的使用,只要先new一个job,然后job.Schedule()就好了
最后的chunk性能最高,当然代码也有点繁琐,注意不是困难,是繁琐
Chunk
chunk类,注意Execute参数固定 public struct RotateCubeJobChunk : IJobChunk { public ComponentTypeHandle<LocalTransform> TransformTypeHandle; [ReadOnly] public ComponentTypeHandle<RotateSpeed> RotateSpeedTypeHandle; public float DeltaTime; [BurstCompile] public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask) { var transforms = chunk.GetNativeArray(ref TransformTypeHandle); var speeds = chunk.GetNativeArray(ref RotateSpeedTypeHandle); for(int i=0; i<chunk.Count;i++) { transforms[i] = transforms[i].RotateY(speeds[i].Speed * DeltaTime); } } } chunk的使用 var spinningCubesQuery = SystemAPI.QueryBuilder().WithAll<RotateSpeed, LocalTransform>().Build(); RotateCubeJobChunk chunk = new RotateCubeJobChunk() { DeltaTime = SystemAPI.Time.DeltaTime, TransformTypeHandle = SystemAPI.GetComponentTypeHandle<LocalTransform>(), RotateSpeedTypeHandle=SystemAPI.GetComponentTypeHandle<RotateSpeed>(true), }; //state.Dependency= chunk.Schedule(spinningCubesQuery,state.Dependency);chunk里面所有用到的Component都要用到句柄的方式。其中只读的要加上只读标签,然后在使用是从句柄中取到引用
spinningCubesQuery是取到所有有对应组件实体的队列,chunk的Schedule方法要用到这个队列,后面几乎就差不多了,把句柄通过SystemAPI的获得句柄的方式传进去
句柄最好在开始时缓存起来
附加内容
碍于篇幅限制,下面用几句简单的话解释知识点
- 在系统上加上[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]这个标签,它的Update函数会变成类似unity的FixedUpdate
- 系统除了可以继承自ISystem,还可以继承自SystemBase,在它的里面放托管类型字段方法,作为ECS世界和Mono世界的桥梁
- 想要大量生成预制体,用Component存Entity,再在烘焙时把Component里面的Entity=GetEntity(prefab,TransformUsageFlags.Dynamic),再state.EntityManager.Instantiate(prefab, 200, Allocator.Temp);就好,记得如果数量过多就要把第三个参数的Temp改成永久,并在完成后Dispose
- 可以通过SystemAPI.GetSingleton获取某个组件单例
大概基础只能写这么多,能看懂谢天谢地,希望能出第二期…………或者学HybridCLR+Addressable也好啊