ESC框架知识点

📅 2026/7/23 22:57:56 👁️ 阅读次数 📝 编程学习
ESC框架知识点

安装ECS

在PackageManager中搜索并安装Entities Graphics

烘焙Entity

public class FallingCubeSpawnerAuthoring : MonoBehaviour { public class Baker : Baker<FallingCubeSpawnerAuthoring> { public override void Bake(FallingCubeSpawnerAuthoring authoring) { Entity spawnerEntity = GetEntity(TransformUsageFlags.None); } } }

烘焙显示在场景中的物体用TransformUsageFlags.Dynamic


构建Component

创建

所有Component必须继承IComponentData

标签

public struct CreatCubeTag : IComponentData { }

数据

public struct FallingCubeSpawner : IComponentData { public Entity CubePrefab; public int MaxCount; }

赋值

public class FallingCubeSpawnerAuthoring : MonoBehaviour { [SerializeField] private GameObject cubePrefab; [SerializeField] private int maxCount = 1000; public class Baker : Baker<FallingCubeSpawnerAuthoring> { public override void Bake(FallingCubeSpawnerAuthoring authoring) { Entity spawnerEntity = GetEntity(TransformUsageFlags.None); Entity cubePrefabEntity = GetEntity( authoring.cubePrefab, TransformUsageFlags.Dynamic ); AddComponent<CreatCubeTag>(spawnerEntity); AddComponent(spawnerEntity, new FallingCubeSpawner { CubePrefab = cubePrefabEntity, MaxCount = authoring.maxCount, }); } } }

创建System

框架

public partial struct FallingCubeSystem : ISystem { public void OnCreate(ref SystemState state) { state.RequireForUpdate<FallingCubeSpawner>(); } public void OnUpdate(ref SystemState state) { float deltaTime = SystemAPI.Time.DeltaTime; EntityCommandBuffer ecb = new EntityCommandBuffer(state.WorldUpdateAllocator); //todo 这里写工作流 ecb.Playback(state.EntityManager); ecb.Dispose(); } }
state.RequireForUpdate<FallingCubeSpawner>();

泛型传入参数代表烘焙场景中存在指定组件才会执行Update

可以执行多次,添加多次

float deltaTime = SystemAPI.Time.DeltaTime;

获取每一帧的时间间隔

EntityCommandBuffer ecb = new EntityCommandBuffer(state.WorldUpdateAllocator);

创建ecb对象,工作流中需要使用,Entity的删除与创建,激活与隐藏,添加和删除组件,都需要走ecb,这里自己实例化了一个,在涉及到物理的System当中不能这么用

ecb.Playback(state.EntityManager); ecb.Dispose();

调用ecb并释放ecb,通常一起使用,处理工作流中注册的对于Entity的操作,然后释放,自己实例化的ecb对象需要手动调用,不调用会出错

工作流

创建工作流

[BurstCompile] [WithAll(typeof(CreatCubeTag))] public partial struct CreatCubeJob : IJobEntity { public int countNow; public EntityCommandBuffer.ParallelWriter Ecb; void Execute([EntityIndexInQuery]int sortKey, ref FallingCubeSpawner cubeValue) { if (countNow < cubeValue.MaxCount) { Entity cube = Ecb.Instantiate(sortKey,cubeValue.CubePrefab); float randomX = new Random().NextFloat(-10f, 10f); Ecb.SetComponent(sortKey, cube, LocalTransform.FromPosition(new float3(randomX, 20f, 0f)) ); } } }

工作流必须继承IJobEntity基类,然后实现Execute方法,方法参数任意

如果涉及到操作Entity物体,激活或隐藏,创建或释放,则需要在类中创建参数

public EntityCommandBuffer.ParallelWriter Ecb;

ecb是操作Entity物体的关键,然后还需要在Execute方法,指定第一个参数为

void Execute([EntityIndexInQuery]int sortKey)

其中[EntityIndexInQuery]标签为必添加的,这个标签添加以后,这个变量能够自动赋值,每次调用ecb的时候,都需要传入这个参数,用来指定在ecb中逻辑执行的顺序,如果不传或者传0会出现代码顺序错乱的问题

然后仔细讲一下Execute方法

这个方法在工作流中会自动执行,根据参数筛选需要被处理的Entity,同时除了带有,比如[EntityIndexInQuery]标签的顺序参数和Entity参数以外,所有的参数必须是Component,也就是必须继承IComponentData基类

void Execute([EntityIndexInQuery]int sortKey, ref FallingCubeSpawner cubeValue)

这种写法,就代表无视第一个顺序参数,筛选出所有挂载FallingCubeSpawner这个Component的Entity,然后在方法中处理,如果有一千个满足条件的Entity,就会执行一千次,如果只有一个,那就只会执行一次,参数可以无限往后叠加,每增加一个参数就增加一个筛选条件

void Execute([EntityIndexInQuery]int sortKey,ref LocalTransform localTransform,in FallCubeUsingValue cubeValue)

同时,这种写法还可以拥有一个参数,类型为Entity,工作流会自动将满足这些Component的Entity传入进来,如果只需要操作数据参数则不需要获取实体

void Execute([EntityIndexInQuery]int sortKey,Entity entity,ref LocalTransform localTransform,in FallCubeUsingValue cubeValue)

同时,参数中的Component前面需要有关键字:

ref 代表读写,会读取并修改里面的值

in 代表只读,只会读取,不会修改

然后解释一下工作流上的标签

[BurstCompile]

意思是用 Burst 编译器编译这段代码(机器码)

运行效率更快的,编译更高效的编译器,但是存在限制

通过Burst编译的代码只能处理值类型数据,以及将操作注册到ecb中

不能调用引用类型的值,比如string,class,不能调用Unity原生方法(Debug.Log),不能调用Unity原生对象(GameObject)

Vector3在值类型中使用float3代替

string在值类型中使用FixedString64Bytes代替

使用方式完全相同

还有FixedString32Bytes,FixedString128Bytes,FixedString512Bytes,容纳更多字符,按需使用

[WithAll(typeof(CreatCubeTag))]

额外增加一条筛选规则,满足Execute参数筛选的同时,额外拥有CreatCubeTag这个参数的Component的Entity才行,这种通常只作为筛选条件,不会用到里面的数据,通常传入标签

调用工作流(System中)

顺序调用
public partial struct FallingCubeSystem : ISystem { public void OnCreate(ref SystemState state) { state.RequireForUpdate<FallingCubeSpawner>(); } public void OnUpdate(ref SystemState state) { float deltaTime = SystemAPI.Time.DeltaTime; EntityCommandBuffer ecb = new EntityCommandBuffer(state.WorldUpdateAllocator); //这里开始是调用工作流 MoveCubeJob moveJob = new MoveCubeJob(){deltaTime = deltaTime,Ecb = ecb.AsParallelWriter()}; ChangeAnimJob changeAnimJob = new ChangeAnimJob() { deltaTime = deltaTime }; state.Dependency = moveJob.ScheduleParallel(state.Dependency); state.Dependency = changeAnimJob.ScheduleParallel(state.Dependency); state.Dependency.Complete(); //在这里结束 ecb.Playback(state.EntityManager); ecb.Dispose(); } }

先是创建工作流,直接通过new进行实例化,实例化的时候需要传入参数,计算帧数的需要传入当前帧时间,操作Entity的需要传入ecb对象

state.Dependency = IJobEntity.ScheduleParallel(state.Dependency);

将实例化好的工作流添加到执行列表当中,可以重复添加多个,会按照添加的先后顺序执行

state.Dependency.Complete();

开始按照添加顺序执行工作流,线程会开始等待直到工作流完成,再执行后面的逻辑,这样做性能非常好,等工作流执行完成了再处理ecb中注册的Entity操作

并行工作流
public partial struct FallingCubeSystem : ISystem { public void OnCreate(ref SystemState state) { state.RequireForUpdate<FallingCubeSpawner>(); } public void OnUpdate(ref SystemState state) { float deltaTime = SystemAPI.Time.DeltaTime; EntityCommandBuffer ecb = new EntityCommandBuffer(state.WorldUpdateAllocator); //这里开始是调用工作流 MoveCubeJob moveJob = new MoveCubeJob(){deltaTime = deltaTime,Ecb = ecb.AsParallelWriter()}; ChangeAnimJob changeAnimJob = new ChangeAnimJob() { deltaTime = deltaTime }; JobHandle moveHandle = moveJob.ScheduleParallel(state.Dependency); JobHandle animHandle = changeAnimJob.ScheduleParallel(state.Dependency); state.Dependency = JobHandle.CombineDependencies(moveHandle, animHandle); //在这里结束 ecb.Playback(state.EntityManager); ecb.Dispose(); } }

并行工作流在实例化工作流对象之后,需要将工作流转化成JobHandle对象,然后再将所有的JobHandle一起赋值给state.Dependency

这种情况下多个工作流会一起执行,没有先后顺序,执行效率更高,但是更容易出现ecb执行混乱,出现先调用再注册的情况,开发和调试难度要高于按顺序调用

在工作流中改变能直接影响显示的属性

1.LocalTransfrom
[BurstCompile] [WithAll(typeof(FallingCubeTag))] public partial struct MoveCubeJob : IJobEntity { public float deltaTime; void Execute([EntityIndexInQuery]int sortKey,ref LocalTransform localTransform) { localTransform.Position.y -= cubeValue.Value * deltaTime; } }

LocalTransform必须带有ref,修改LocalTransform中的值能够直接改变场景中Entity的位置

2.改变Shader中字段的值

首先,需要在Component中进行一下特殊处理,需要添加标签

[MaterialProperty("_FrameIndex")] public struct ChangeShaderValue : IComponentData { public float Value; }

其中这个标签尤其重要

[MaterialProperty("_FrameIndex")]

代表着这个Component被添加到Entity之后,会自动寻找Entity上挂载的Renderer,然后从Renderer上获取材质球,然后找到材质球的Shader,然后从Shader上寻找字符串中的字段,也就是_FrameIndex这个字段,意思是shader中必须存在_FrameIndex这个字段

_FrameIndex("Frame Index", Float) = 0

同时,在Component中,必须有一个变量,变量名必须为Value,变量类型必须与Shader中的对应字段的类型一致

public partial struct ChangeAnimJob : IJobEntity { public float deltaTime; void Execute(ref ChangeAnimValue animValue,ref ChangeShaderValue shaderValue) { animValue.index++; if (animValue.index >= animValue.indexMax) { animValue.index = 0; } shaderValue.Value = animValue.index; } }

然后直接在Job工具流中修改Value的值,就能直接应用到材质球当中

想要实时应用材质球,必须使用MeshRenderer,即使是2D,也不能使用SpriteRenderer,在2D中使用MeshRenderer来渲染2D图的时候,MeshFilter的网格选择Quad

待补充......

Ecb实体处理器

1.创建Entity
Entity entity = Ecb.Instantiate(sortKey,mainEntity);

传入工作流顺序和需要创建的实体的原型,返回创建出来的实体

2.删除Entity
Ecb.DestroyEntity(sortKey,entity);

传入工作流顺序和需要删除的实体,删除传入的实体

3.激活或隐藏Entity
Ecb.SetEnabled(sortKey, entity, true); Ecb.SetEnabled(sortKey, entity, false);

传入工作流顺序和需要操作的实体,然后最后传入是否激活,true为激活,false为隐藏

4.为实体增加组件
Ecb.AddComponent<FallingCubeTag>(sortKey,entity); Ecb.AddComponent(sortKey,entity,new FallCubeUsingValue{ Value = 1.0f });

用Ecb添加组件,添加标签使用泛型,添加数据需要实例化,传入工作流顺序,需要添加的实体,以及泛型类型或实例化对象

5.为实体的组件设置属性
Ecb.SetComponent(sortKey, entity,LocalTransform.FromPosition(new float3(0, 20f, 0f)));

用Ecb给已经添加的Component中的字段设置值,这里是给刚创建的Entity设置初始位置

也可以用另一种方式

var t = LocalTransform.FromPosition(new float3(randomX, 20f, 0f)); t.Scale = 0.25f; Ecb.SetComponent(sortKey, cube, t);

先根据位置创建出LocalTransfrom,再给它的Scale赋值,然后再传入SetComponent当中


Ecs物理

安装插件

先在PackageManager安装Unity Physics

然后再预制体中直接挂载Collider和RigBody就能直接在烘焙出来的Entity中使用物理了

要注意,必须使用3D物理,即使是2D游戏也要使用3D物理,使用2D物理没有效果,

处理物理碰撞的工作流

创建物理工作流

public partial struct TriggerJob : ITriggerEventsJob { public int sortKey; public ComponentLookup<BulletTag> bulletLook; public ComponentLookup<FallingCubeTag> cubeLook; public EntityCommandBuffer.ParallelWriter ecb; public void Execute(TriggerEvent triggerEvent) { Entity bulletEntity = Entity.Null; Entity cubeEntity = Entity.Null; bulletEntity = bulletLook.HasComponent(triggerEvent.EntityA) ? triggerEvent.EntityA : bulletEntity; bulletEntity = bulletLook.HasComponent(triggerEvent.EntityB) ? triggerEvent.EntityB : bulletEntity; cubeEntity = cubeLook.HasComponent(triggerEvent.EntityA) ? triggerEvent.EntityA : cubeEntity; cubeEntity = cubeLook.HasComponent(triggerEvent.EntityB) ? triggerEvent.EntityB : cubeEntity; if (bulletEntity != Entity.Null && cubeEntity != Entity.Null) { ecb.DestroyEntity(sortKey, cubeEntity); } } }

处理物理碰撞分为Trigger和Collision,这里处理的是Trigger,如果想要处理Collision的话

工作流需要继承ICollisionEventsJob,同时Execute方法的参数为CollisionEvent,使用方式相同

ComponentLookup<> 这个是物理工作流中必须要有的,用来判断碰撞的两个物体是否拥有泛型中传入的这个标签

继承了ITriggerEventsJob和ICollisionEventsJob的物理工作流,里面的Execute方法,会在任意两个拥有碰撞体且附带Triger和Rigbody的物体后执行,并且出现持续碰撞的时候,每帧都会执行,无法在调用方法前筛选出要处理哪些碰撞

所以必须要在方法内判断,碰撞的两个物体是否是自己想要检测的物体,判断就必须要通过ComponentLookup<>来判断

triggerEvent.EntityA和triggerEvent.EntityB可以在方法中访问被碰撞的两个实体:Entity

然后调用ComponentLookup<>.HasComponent(Entity)方法,在参数中传入想要判断的Entity,可以判断这个实体中是否拥有创建时的泛型包含的组件

用这种方式,就可以判断出两个Entity是否为想要检测碰撞的两个物体,然后处理逻辑

调用工作流

[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))] [UpdateAfter(typeof(PhysicsSimulationGroup))] public partial struct EcsPhysicsJobSystem : ISystem { public void OnCreate(ref SystemState state) { state.RequireForUpdate<SimulationSingleton>(); state.RequireForUpdate<EndFixedStepSimulationEntityCommandBufferSystem.Singleton>(); } public void OnUpdate(ref SystemState state) { var ecbSing = SystemAPI.GetSingleton<EndFixedStepSimulationEntityCommandBufferSystem.Singleton>(); var ecb = ecbSing.CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter(); TriggerJob triggerJob = new TriggerJob { sortKey = 0, bulletLook = SystemAPI.GetComponentLookup<BulletTag>(), cubeLook = SystemAPI.GetComponentLookup<FallingCubeTag>(), ecb = ecb }; state.Dependency = triggerJob.Schedule(SystemAPI.GetSingleton<SimulationSingleton>(), state.Dependency); } }

先讲解两个标签

[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]

添加这个标签之后,这个System会在FixedUpdate运算完成之后,再进行运算,如果没有添加这个标签,就会在Update每帧渲染之后进行运算
在Physics当中务必添加这个标签,非Physics的System中不要添加

[UpdateAfter(typeof(PhysicsSimulationGroup))]

添加这个标签之后,这个System会在物理碰撞模拟完成之后,再运算System,不添加这个标签的话,可能会在物理运算完成之前执行System,物理效果可能会出现错误或者延迟

在Physics当中务必添加这个标签,非Physics的System中不要添加

var ecbSing = SystemAPI.GetSingleton<EndFixedStepSimulationEntityCommandBufferSystem.Singleton>(); var ecb = ecbSing.CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter();

在调用物理工作流的System中,Ecb不能直接用New实例化,需要采用特定的方式进行创建

SystemAPI.GetSingleton<EndFixedStepSimulationEntityCommandBufferSystem.Singleton>();

这个方法会返回一个固定的Ecs原型,通过这个方法返回的Ecs无法控制什么时候执行或者释放,而是固定在每次FxedUpdate结束时的最后执行,物理需要使用这种类型的Ecb,不然会出现问题

ecbSing.CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter()

ecbSing.CreateCommandBuffer(state.WorldUnmanaged)这个方法会返回一个可用的Ecb,然后通过.AsParallelWriter()返回一个可以在Job工作流中使用的Ecb实体

SystemAPI.GetComponentLookup<IComponentData>()

通过这个方法,可以返回一个查找器,这个查找器用来查找Entity中是否挂载着泛型中传入的Component

state.Dependency = triggerJob.Schedule(SystemAPI.GetSingleton<SimulationSingleton>(), state.Dependency)

这句代码是真正的将Job添加到工作流,

.Schedule(SystemAPI.GetSingleton<SimulationSingleton>(), state.Dependency)

这一段是固定的,不需要修改任何参数,只是前面的是自己实例化出来的Job就可以