当前位置: 首页 > news >正文

Unity DOTS

面向数据设计(DOD)

Data-Oriented Technology Stack(面向数据的技术栈)

在这里插入图片描述

传统方式的问题

  • 数据冗余,一般脚本继承自 MonoBehaviour,很多用不到的变量,方法都会被带进来
  • 单线程处理,unity 的组件只能在主线程运行
  • 编译器不够高效

面向数据设计(DOD)本质是面向内存(缓存友好)设计

DOTS可以应用到哪些地方?

  • 具有大世界流式加载的游戏
  • 具有复杂的大规模模拟的游戏
  • 具有多种网络类型的多人联线游戏
  • 具有需要客户端模拟预测的网络游戏,如射击游戏

基础概念

Job System:提供安全,快速编写多线程代码的方式

Burst compiler:使用 LLVM(Low Level Virtual Machine),将 IL/.NET 的字节码编译成高度优化的本地机器码

Entity Component System(实体组件系统)

  • 遵循组合优于继承的原则
  • 面向数据设计
  • 弱耦合
  • 数据和行为分离

在这里插入图片描述

Entity:不是容器,只是是一个标识符,用来指示某个对象的存在。
Component:组件是一个数据容器,没有逻辑
System:负责对数据进行逻辑操作,对具有特定 Component 的特定 Entity 进行操作

在这里插入图片描述

Archetypes(原型):标识有相同 Component 组合的 Entity,例如这里 EntityA 和 EntityB 包含四个相同的 Component。Archetypes 可以理解为不同 Entity 在内存上的 Layout 布局

在这里插入图片描述

Archetypes Chunck:每个 Archetypes 会被分割成固定大小的内存块

EntityManager :负责创建,销毁,修改 Entity

World:一系列Entity组合,每个Entity在World中是唯一的,统一由 EntityManager 管理

在这里插入图片描述

Structural Change:操作导致内存布局改变,比如创建一个新的 Entity,或删除 Entity 上的一个 Component 导致所属的 Archetypes 改变,只能在主线程工作

在这里插入图片描述

Authoring mode:整个世界还没有执行之前的状态
Runtime mode:执行后的状态,在进入 Runtime 时候,会把所有需要转为 Entity 的物体,用 Bake 的方式烘培成 Entity

安装

在这里插入图片描述
通过Package Manager安装Entities及相关依赖,输入com.unity.entities,com.unity.entities.graphics

Job System 的基础知识

Job System 包含 C# Jobs System 与 C++ Jobs System
既可以与ECS结合使用也可以单独使用
不需要关心平台CPU核心资源情况

在这里插入图片描述

Blittable Types:在托管代码和非托管代码的内存中具有相同的表示形式,在托管与非托管代码之间可以直接使用,不需要进行任何转换

Job 只能使用 Blittable Types 类型的数据和非托管内存堆上的数据

Job Types

  • IJob:用于执行单一任务的Job,适合不需要并行的简单任务。
  • IJobFor:并行或顺序执行任务。
  • IJobParallelFor:与IJobFor类似,但更侧重于并行执行,适用于高度可并行化的数据处理任务。
  • IJobParallelForTransform:专门用于Transform组件的并行操作,适合在大量物体上执行相同的变换操作。
  • IJobBurstSchedulable:结合Burst编译器进行优化的Job,能够提高执行效率。

Job的调度方式

  • Run:主线程立即顺序执行
  • Schedule:单个工作线程或主线程,每个Job顺序执行
  • ScheduleParallel:在多个工作线程上同时执行,性能最好,但多个工作线程访问同一数据时可能会发生冲突

本身名字带有Parallel的Job类型,仅提供Schedule,不提供Run和ScheduleParallel的调度方式。

Job Dependencies

在这里插入图片描述

Race condition(竞态条件):指多个线程或进程在修改共享资源时,其最终结果依赖于它们的执行顺序,程序的行为变得不可预测。

在这里插入图片描述

JobHandle jobAHandle = jobA.Schedule();
//指定依赖关系,这样就会按照jobA,jobB的顺序执行
JobHandle jobBHandle = jobB.Schedule(jobAHandle);

在这里插入图片描述

JobHandle jobAHandle = jobA.Schedule();
JobHandle jobBHandle = jobB.Schedule();
JobHandle combined = JobHandle.CombineDependencies(jobAHandle, jobBHandle);
//jobA和jobB都完成后,才执行jobC
JobHandle jobCHandle = jobC.Schedule(combined);

在这里插入图片描述

用代码控制大量方块做波浪运动

面向对象写法

public class WaveCubes : MonoBehaviour
{public GameObject cubePrefab = null;[Range(10, 100)] public int xHalfCount = 40;[Range(10, 100)] public int zHalfCount = 40;private List<Transform> cubesList;//ProfilerMarker用于标记代码块,使其在 Profiler 中可见,泛型是为了显示额外参数,例如这里的 Objects Countstatic readonly ProfilerMarker<int> profilerMarker = new ProfilerMarker<int>("WaveCubes UpdateTransform", "Objects Count");void Start(){cubesList = new List<Transform>(4 * xHalfCount * zHalfCount);for (var z = -zHalfCount; z <= zHalfCount; z++){for (var x = -xHalfCount; x <= xHalfCount; x++){var cube = Instantiate(cubePrefab);cube.transform.position = new Vector3(x * 1.1f, 0, z * 1.1f);cubesList.Add(cube.transform);}}}void Update(){//Auto 方法会自动调用 Begin 和 End 方法,简化代码using (profilerMarker.Auto(cubesList.Count)){//要分析的代码块for (var i = 0; i < cubesList.Count; i++){var distance = Vector3.Distance(cubesList[i].position, Vector3.zero);cubesList[i].localPosition += Vector3.up * Mathf.Sin(Time.time * 3f + distance * 0.2f);}}}
}

在这里插入图片描述

Profiler 中显示耗时

面向数据写法

//IJobParallelForTransform 是一个专门用于并行处理 Transform 组件的接口
[BurstCompile]
struct WaveCubesJob : IJobParallelForTransform
{[ReadOnly] public float elapsedTime;public void Execute(int index, TransformAccess transform){var distance = Vector3.Distance(transform.position, Vector3.zero);transform.localPosition += Vector3.up * math.sin(elapsedTime * 3f + distance * 0.2f);}
}public class WaveCubesWithJobs : MonoBehaviour
{public GameObject cubeAchetype = null;[Range(10, 100)] public int xHalfCount = 40;[Range(10, 100)] public int zHalfCount = 40;static readonly ProfilerMarker<int> profilerMarker = new ProfilerMarker<int>("WaveCubesWithJobs UpdateTransform", "Objects Count");//用于处理 Transform 数据的一个结构体,与 Job System 结合使用//transform是托管堆上的数据,job 不能直接使用private TransformAccessArray transformAccessArray;void Start(){transformAccessArray = new TransformAccessArray(4 * xHalfCount * zHalfCount);for (var z = -zHalfCount; z < zHalfCount; z++){for (var x = -xHalfCount; x < xHalfCount; x++){var cube = Instantiate(cubeAchetype);cube.transform.position = new Vector3(x * 1.1f, 0, z * 1.1f);//在 job 调度时,自动将 Transform 的拷贝数据传递给 WaveCubesJob 中transformAccessArray.Add(cube.transform);}}}void Update(){using (profilerMarker.Auto(transformAccessArray.length)){var waveCubesJob = new WaveCubesJob{elapsedTime = Time.time,};var waveCubesJobhandle = waveCubesJob.Schedule(transformAccessArray);//等Job及其所有依赖项都完成执行,回到主线程waveCubesJobhandle.Complete();}}private void OnDestroy(){transformAccessArray.Dispose();}
}

Entity 的创建模式

在这里插入图片描述

Authoring 模式创建 Entity 过程

在这里插入图片描述

在这里插入图片描述

Runtime 模式下创建 Entity 过程

在这里插入图片描述

Sub Scene:Hierarchy 中右键添加 Sub Scene,任何放在 Sub scene 的 GameObject,在 Runtime 时会转成 Entity

在这里插入图片描述

在 Sub Scene 中添加一个 Cube,就可以看到 Cube Baking 后的 Entities 对象

在这里插入图片描述

点右上角的圆圈可以切换模式

Compnent类型

按功能类型划分

  • 一般的 Component
  • Shared Component(ISharedComponentData):用于多个实体之间共享相同的数据
  • Tag Component:用于标记实体,方便查找,不包容任何数据,不占用存储空间
  • Enableable Component(IEnableableComponent):启用或禁用的某个组件
  • Cleanup Component(ICleanupComponentData)用于处理实体销毁时的清理工作
  • Singleton Componet:全局唯一的组件,通常用于存储全局状态或配置
  • Buffer Component(IBufferElementData):用于存储可变长度的数据数组
  • System State Component(ISystemStateComponentData):用于跟踪系统状态,不会在实体被销毁时自动移除

实现物体旋转的功能

面向对象写法

public class RotateCube : MonoBehaviour
{[Range(0, 360)] public float rotateSpeed = 360.0f;void Update(){transform.Rotate(new Vector3(0, rotateSpeed * Time.deltaTime, 0));}
}

面向数据写法

/// <summary>
/// 组件
/// </summary>
struct RotateSpeed : IComponentData
{public float rotateSpeed;
}/// <summary>
/// GameObject 在Sub Scene
/// </summary>
public class RotateSpeedAuthoring : MonoBehaviour
{[Range(0, 360)]public float rotateSpeed = 360.0f;/// <summary>/// 将传统的 GameObject 和 MonoBehaviour 转换为实体和组件/// </summary>public class Baker : Baker<RotateSpeedAuthoring>{public override void Bake(RotateSpeedAuthoring authoring){//TransformUsageFlags.Dynamic 表示实体的转换可能会在运行时频繁变化。var entity = GetEntity(TransformUsageFlags.Dynamic);var data = new RotateSpeed{rotateSpeed = math.radians(authoring.rotateSpeed)};AddComponent(entity, data);}}
}

在这里插入图片描述

GameObject 添加脚本后,在 preview 中可以看到这个自定义组件

在这里插入图片描述

Window -》Entities -》 Components 中可以看到所有能识别到的组件,后面的 Data 表示组件类型

//这里定义一个 partial 结构体作为系统,其他部分 Dots 会自动生成
[BurstCompile]
public partial struct CubeRotateSystem : ISystem
{[BurstCompile]public void OnUpdate(ref SystemState state){float deltaTime = SystemAPI.Time.DeltaTime;//查询当前 World 中,具有 LocalTransform 和 RotateSpeed 组件的 entity//RefRW(Reference Read-Write)读写组件数据//RefRO (Reference Read-Only)只读取组件数据foreach (var (transform, speed) in SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotateSpeed>>()){transform.ValueRW = transform.ValueRO.RotateY(speed.ValueRO.rotateSpeed * deltaTime);}}
}

System 与 System Group

System类别

  • ISystem:提供对非托管内存访问
  • SystemBase:用于访问存储托管数据

在这里插入图片描述

在这里插入图片描述

Window -》Entities -》 Systems 中可以看到一个 World 下的所有的 System 和 System Group,World 可以看作是根目录,System Group 看作是文件夹,System 看作是文件
默认有 Initialization System Group,Simulation System Group,Presentation System Group 这个三个根 Group

在这里插入图片描述

运行后可以看到每个 System 处理的 Entity 数量和耗时

[UpdateBefore(typeof(BBBSystem))]
[BurstCompile]
public partial struct AAASystem : ISystem{}[BurstCompile]
public partial struct BBBSystem : ISystem{}

通过 UpdateBefore 或 UpdateAfter 调整多个 System 之间的更新顺序

// 定义一个自定义System Group
public class MyCustomSystemGroup : ComponentSystemGroup { }[UpdateInGroup(typeof(MyCustomSystemGroup ))]
[BurstCompile]
public partial struct XXXSystem : ISystem{}

自定义的 System 默认会放入 Simulation System Group,可以自定义一个 System Group,把 System 放入这个 Group 中

[DisableAutoCreation]
[BurstCompile]
public partial struct XXXSystem : ISystem{}

DisableAutoCreation 特性禁止 System 被默认创建并添加到默认的 SystemGroup 中
被修饰的 System 需手动调用 World.GetOnCreateSystem<T> 创建
被修饰的 System 需手动调用 XXXSystem.Update() 更新

遍历与查询

DOTS程序的三大主要工作

  1. 设计与组织数据
  2. 遍历与查找数据
  3. 修改与更新数据

遍历查询Entity数据的5种方式

  • SystemAPI.Query + foreach 最方便,适合查询很少的对象,但只能在主线程运行,有使用限制
  • IJobEntity / IJobChunk + EntityQuery 最高效,适合大量数据查询转换,但是有特例
  • Entities.ForEach 代码难看,限制多
  • 手动遍历

总结:查询多时用 Job,查询少时用 Query,随机访问莫滥用,其他方式可不用

SystemAPI.Query + foreach

SystemAPI.Query 的使用限制

  • DynamicBuffer 默认为读写访问,如果希望只读,必须自己实现
  • 不能存储 SystemAPI.Query<T>(), 然后在一个或多个 foreach 语句中使用它
  • 只能在主线程运行

在这里插入图片描述

当我们要对场景中不同类型的物体分别处理时,可以使用 Tag Component,它不占用空间,非常适合分类处理,比如对蓝色方块进行处理

struct BlueCubeTag : IComponentData
{
}public class BlueCubeTagAuthoring : MonoBehaviour
{public class Baker : Baker<BlueCubeTagAuthoring>{public override void Bake(BlueCubeTagAuthoring authoring){var entity = GetEntity(TransformUsageFlags.Dynamic);var blueCube = new BlueCubeTag();AddComponent(entity, blueCube);}}
}
// 没有查询到匹配的实体,跳过 OnUpdate
[RequireMatchingQueriesForUpdate]
[BurstCompile]
public partial struct BlueCubeRotateSystem : ISystem
{[BurstCompile]public void OnUpdate(ref SystemState state){float deltaTime = SystemAPI.Time.DeltaTime;foreach (var (transform, speed, tag) in SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotateSpeed>, RefRO<BlueCubeTag>>()){transform.ValueRW = transform.ValueRO.RotateY(speed.ValueRO.rotateSpeed * deltaTime);}}
}

红色,绿色方块处理逻辑一样,定义不同的 Tag,System 查询时,添加这个 Tag

在这里插入图片描述

在 System 中点击前面的 icon,可以开启或禁用这个 System

在这里插入图片描述
选择一个 System 然后切换到 Inspector,可以看到查询包含哪些 Component

在这里插入图片描述

点击 RelationShips,可以看到查询的结果

IJobEntity

  • 专门处理 entity,与 Entities.ForEach 功能类似,为每个 entity 调用一次 Execute
  • 可在多个 System 中重用
  • 底层是 IJobChunk 实现的
  • 查询方式 WithAll、WithAny、WithNone、WithChangeFilter、WithOptions
  • Execute 方法中可以使用 EntityIndexInQuery 属性,获取当前 entity 在遍历中的索引
[BurstCompile]
partial struct CopyPositionsJob : IjobEntity
{public NativeArray<float3> copyPositions;public void Execute([EntityIndexInQuery] int entityIndexInQuery, in LocalToWorld localToWorld){copyPositions[entityIndexInQuery] = localToWorld.Position;}
}

在这里插入图片描述

使用 IJobEntity 控制方块选择,旋转速度定义在 Component 和 Authoring 中,这是一个包含 Entity,Job,Burst 的完整实现

struct RotateSpeed : IComponentData
{public float rotateSpeed;
}public class RotateSpeedAuthoring : MonoBehaviour
{[Range(0, 360)]public float rotateSpeed = 360.0f;public class Baker : Baker<RotateSpeedAuthoring>{public override void Bake(RotateSpeedAuthoring authoring){var entity = GetEntity(TransformUsageFlags.Dynamic);var data = new RotateSpeed{rotateSpeed = math.radians(authoring.rotateSpeed)};AddComponent(entity, data);}}
}
[BurstCompile]
partial struct RotateCubeWithJobEntity : IJobEntity
{public float deltaTime;void Execute(ref LocalTransform transform, in RotateSpeed speed){transform = transform.RotateY(speed.rotateSpeed * deltaTime);}
}[BurstCompile]
[RequireMatchingQueriesForUpdate]
public partial struct CubeRotateWithIJobEntitySystem : ISystem
{[BurstCompile]public void OnUpdate(ref SystemState state){var job = new RotateCubeWithJobEntity { deltaTime = SystemAPI.Time.DeltaTime };job.ScheduleParallel();//job.Schedule();//job.Run();}
}

JobChunk

  • 遍历 ArcheType Chunk,为每个 Chunk 调用一次 Execute
  • 使用场景:① 不需要遍历每个 Chunk 中 Entity 的情况,② 对 Chunk 内的 Entity 执行多次遍历,③ 以不寻常顺序遍历
  • 遍历时不会跳过 Enableable Component 未激活的 Entity,使用 useEnabledMask 与 ChunkEnableMask 来辅助过滤
[BurstCompile]
struct RotateCubeWithJobChunk : IJobChunk
{public float deltaTime;//ComponentTypeHandle 是为了获取 Chunk上 的 component 数组public ComponentTypeHandle<LocalTransform> TransformTypeHandle;[ReadOnly]public ComponentTypeHandle<RotateSpeed> RotationSpeedTypeHandle;/// <param name="chunk">当前正在处理的实体块,包含一组具有相同组件组合的实体</param>/// <param name="unfilteredChunkIndex">当前块在未过滤查询中的索引</param>/// <param name="useEnabledMask">是否启用掩码对 chunk 内的 entities 进行过滤</param>/// <param name="chunkEnabledMask">128 位的向量</param>public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask){var chunkTransforms = chunk.GetNativeArray(ref TransformTypeHandle);var chunkRotationSpeeds = chunk.GetNativeArray(ref RotationSpeedTypeHandle);var enumerator = new ChunkEntityEnumerator(useEnabledMask, chunkEnabledMask, chunk.Count);while (enumerator.NextEntityIndex(out var i)){var speed = chunkRotationSpeeds[i];chunkTransforms[i] = chunkTransforms[i].RotateY(speed.rotateSpeed * deltaTime);}//for (int i = 0, chunkEntityCount = chunk.Count; i < chunkEntityCount; i++)//{//    //需要写代码过滤被禁用的 entity//    var speed = chunkRotationSpeeds[i];//    chunkTransforms[i] = chunkTransforms[i].RotateY(speed.rotateSpeed * deltaTime);//}}
}[BurstCompile]
[RequireMatchingQueriesForUpdate]
public partial struct CubeRotateWithIJobChunkSystem : ISystem
{EntityQuery rotateCubes;ComponentTypeHandle<LocalTransform> transformTypeHandle;ComponentTypeHandle<RotateSpeed> rotationSpeedTypeHandle;[BurstCompile]public void OnCreate(ref SystemState state){//在 System 更新时尽量采用 SystemState 的 GetEntityQuery, GetComponentTypeHanle<T> 等//来访问 chunk 的 component 数组,而不是通过 EntityManager 的方法去访问var queryBuilder = new EntityQueryBuilder(Allocator.Temp).WithAll<RotateSpeed, LocalTransform>();rotateCubes = state.GetEntityQuery(queryBuilder);transformTypeHandle = state.GetComponentTypeHandle<LocalTransform>();rotationSpeedTypeHandle = state.GetComponentTypeHandle<RotateSpeed>(true);}[BurstCompile]public void OnUpdate(ref SystemState state){transformTypeHandle.Update(ref state);rotationSpeedTypeHandle.Update(ref state);var job = new RotateCubeWithJobChunk{deltaTime = SystemAPI.Time.DeltaTime,TransformTypeHandle = transformTypeHandle,RotationSpeedTypeHandle = rotationSpeedTypeHandle};//确保 job 的正确执行顺序state.Dependency = job.ScheduleParallel(rotateCubes, state.Dependency);}
}

Entities.ForEach

  • 只用于继承 SystemBase 创建的 System
  • 定义是一个 Lambda 表达式
  • 有太多的使用限制

手动遍历

  • entityManager.GetAllEntities()
  • entityManager.GetAllChunks()

效率低

运行时通过预制体创建 Entity 实例

在这里插入图片描述
DOTS 中每个 World 可以有一个相同类型的单例

struct CubeGeneratorByPrefab : IComponentData
{public Entity cubeEntityProtoType;public int cubeCount;
}class CubeGeneratorByPrefabAuthoring : Singleton<CubeGeneratorByPrefabAuthoring>
{public GameObject cubePrefab = null;[Range(1, 10)] public int CubeCount = 6;class CubeBaker : Baker<CubeGeneratorByPrefabAuthoring>{public override void Bake(CubeGeneratorByPrefabAuthoring authoring){AddComponent(GetEntity(TransformUsageFlags.Dynamic), new CubeGeneratorByPrefab{cubeEntityProtoType = GetEntity(authoring.cubePrefab, TransformUsageFlags.None),cubeCount = authoring.CubeCount});}}
}
[BurstCompile]
[RequireMatchingQueriesForUpdate]
partial struct CubeGenerateByPrefabSystem : ISystem
{[BurstCompile]public void OnCreate(ref SystemState state){//CubeGeneratorByPrefab 这个组件存在时才运行state.RequireForUpdate<CubeGeneratorByPrefab>();}[BurstCompile]public void OnUpdate(ref SystemState state){var generator = SystemAPI.GetSingleton<CubeGeneratorByPrefab>();//临时容器var cubes = CollectionHelper.CreateNativeArray<Entity>(generator.cubeCount, Allocator.Temp);//根据 prefab 创建 entity 实例state.EntityManager.Instantiate(generator.cubeEntityProtoType, cubes);int count = 0;foreach (var cube in cubes){//设置每个 cube 的位置var position = new float3((count - generator.cubeCount * 0.5f) * 1.2f, 0, 0);var transform = SystemAPI.GetComponentRW<LocalTransform>(cube);transform.ValueRW.Position = position;count++;}cubes.Dispose();// 只需要创建一次,所以在第一次更新后关闭它。state.Enabled = false;}
}

Entity Command Buffer

EntityCommandBuffer 在工作线程上记录 entity 的各种修改操作,然后在主线程上将这些操作应用到 entity 上

Job 中不能直接创建和销毁 Entity,也不能添加和删除 Entity 组件。只能使用 EntityCommandBuffer 来记录这些操作,ECB 必须通过 Alocator.TempJob 或 Allocator.Persistent 来分配

EntityCommandBuffer ecb = new EntityCommandBuffer(Allocator.TempJob);

在 Parallel Job 中我们需要使用 ECB 的 ParallelWriter 将命令并行记录到 ECB 中

EntityCommandBuffer.ParallelWriter ecbParallel = ecb.AsParallelWriter();

为 ECB 回放创建专门的 System,ECB 的 Playback 方法必须在主线程上调用,记录 ECB 命令的 Job,必须在 Playback 方法调用前完成
ECB 需要在 PlayBack 后销毁

Dependency.Complete();
ecb.Playback(EntityManager);
ecb.Dispose();

在这里插入图片描述
实现 cube 向四周随机扩散的效果

Authoring 部分

struct RandomCubeGenerator : IComponentData
{public Entity cubeProtoType;//创建总数public int generationTotalNum;//每次 tick 创建的数量public int generationNumPerTicktime;//间隔时间public float tickTime;//是否使用ScheduleParallelpublic bool useScheduleParallel;
}public class RandomCubeGeneratorAuthoring : MonoBehaviour
{public GameObject redCubePrefab = null;public GameObject blueCubePrefab = null;[Range(10, 10000)] public int generationTotalNum = 500;[Range(1, 60)] public int generationNumPerTicktime = 5;[Range(0.1f, 1.0f)] public float tickTime = 0.2f;public bool useScheduleParallel = false;public class Baker : Baker<RandomCubeGeneratorAuthoring>{public override void Bake(RandomCubeGeneratorAuthoring authoring){var entity = GetEntity(TransformUsageFlags.Dynamic);var data = new RandomCubeGenerator{cubeProtoType = authoring.useScheduleParallel ? GetEntity(authoring.redCubePrefab, TransformUsageFlags.Dynamic) :GetEntity(authoring.blueCubePrefab, TransformUsageFlags.Dynamic),generationTotalNum = authoring.generationTotalNum,generationNumPerTicktime = authoring.generationNumPerTicktime,tickTime = authoring.tickTime,useScheduleParallel = authoring.useScheduleParallel};AddComponent(entity, data);}}
}
struct RandomTarget : IComponentData
{public float3 targetPos;
}public class RandomTargetAuthoring : MonoBehaviour
{public class Baker : Baker<RandomTargetAuthoring>{public override void Bake(RandomTargetAuthoring authoring){var entity = GetEntity(TransformUsageFlags.None);var data = new RandomTarget{targetPos = float3.zero};AddComponent(entity, data);}}
}
public struct RandomSingleton : IComponentData
{public Random random;
}public class RandomSingletonAuthoring : Singleton<RandomSingletonAuthoring>
{public uint seed = 1;public class Baker : Baker<RandomSingletonAuthoring>{public override void Bake(RandomSingletonAuthoring authoring){var entity = GetEntity(TransformUsageFlags.None);var data = new RandomSingleton{random = new Random(RandomSingletonAuthoring.Instance.seed)};AddComponent(entity,data);}}
}

Job 部分,IJobFor 适用于纯计算,处理与 entity 无关的作业,Execute 方法参数中,ref 表示读写,in 表示只读

[BurstCompile]
partial struct CubeRotateAndMoveEntityJob : IJobEntity
{public float deltaTime;public EntityCommandBuffer.ParallelWriter ecbParallel;void Execute([ChunkIndexInQuery] int chunkIndex, Entity entity, ref LocalTransform transform, in RandomTarget target, in RotateAndMoveSpeed speed){var distance = math.distance(transform.Position, target.targetPos);if (distance < 0.02f){ecbParallel.DestroyEntity(chunkIndex, entity);}else{float3 dir =  math.normalize(target.targetPos - transform.Position);transform.Position += dir * speed.moveSpeed * deltaTime;transform = transform.RotateY(speed.rotateSpeed * deltaTime);}}
}
[BurstCompile]
struct GenerateCubesJob : IJobFor
{[ReadOnly] public Entity cubeProtoType;public NativeArray<Entity> cubes;public EntityCommandBuffer ecb;//在使用 ScheduleParallel 进行调度时,一般要禁止使用 RW 引用,避免 race condition,//这里明确全局只有一个 RandomSingleton 对象,并且生成随机数是线程安全的,添加特性避免安全检查[NativeDisableUnsafePtrRestriction] public RefRW<RandomSingleton> random;public void Execute(int index){//注意这里是在记录操作,并不是真正的实例化和添加组件//通过 ecb 创建 entity id 是临时的,在此处上下文之外没有意义cubes[index] = ecb.Instantiate(cubeProtoType);ecb.AddComponent<RotateAndMoveSpeed>(cubes[index], new RotateAndMoveSpeed{rotateSpeed = math.radians(60.0f),moveSpeed = 5.0f});//不能使用 SetComponent//ecb.SetComponent<LocalTransform>(cubes[index], new LocalTransform//{//    Position = new float3(1, 2, 3)//});//随机设置移动的目标点float2 targetPos2D = random.ValueRW.random.NextFloat2(new float2(-15, -15), new float2(15, 15));ecb.AddComponent<RandomTarget>(cubes[index], new RandomTarget(){targetPos = new float3(targetPos2D.x, 0, targetPos2D.y)});}
}
[BurstCompile]
partial struct GenerateCubesWithParallelWriterJob : IJobFor
{[ReadOnly] public Entity cubeProtoType;public NativeArray<Entity> cubes;public EntityCommandBuffer.ParallelWriter ecbParallel;[NativeDisableUnsafePtrRestriction]public RefRW<RandomSingleton> random;public void Execute(int index){//注意这里是在记录操作,并不是真正的实例化和添加组件cubes[index] = ecbParallel.Instantiate(index, cubeProtoType);ecbParallel.AddComponent<RotateAndMoveSpeed>(index, cubes[index], new RotateAndMoveSpeed{rotateSpeed = math.radians(60.0f),moveSpeed = 5.0f});float2 targetPos2D = random.ValueRW.random.NextFloat2(new float2(-15, -15), new float2(15, 15));ecbParallel.AddComponent<RandomTarget>(index, cubes[index], new RandomTarget(){targetPos = new float3(targetPos2D.x, 0, targetPos2D.y)});}
}

System 部分

[BurstCompile]
[RequireMatchingQueriesForUpdate]
public partial struct RandomCubeGenerateSystem : ISystem
{private float timer;private int totalCubes;[BurstCompile]public void OnCreate(ref SystemState state){state.RequireForUpdate<RandomCubeGenerator>();timer = 0.0f;totalCubes = 0;}[BurstCompile]public void OnUpdate(ref SystemState state){var generator = SystemAPI.GetSingleton<RandomCubeGenerator>();if (totalCubes >= generator.generationTotalNum){state.Enabled = false;return;}if (timer >= generator.tickTime){CreateGenerateCubesEntityJob(ref state, generator);timer -= generator.tickTime;}timer += Time.deltaTime;}private void CreateGenerateCubesEntityJob(ref SystemState state, RandomCubeGenerator generator){//传递 RW 引用RefRW<RandomSingleton> random = SystemAPI.GetSingletonRW<RandomSingleton>();EntityCommandBuffer ecb = new EntityCommandBuffer(Allocator.TempJob);var cubes = CollectionHelper.CreateNativeArray<Entity>(generator.generationNumPerTicktime, Allocator.TempJob);if(generator.useScheduleParallel){//并行创建 entityEntityCommandBuffer.ParallelWriter ecbParallel = ecb.AsParallelWriter();var job = new GenerateCubesWithParallelWriterJob(){cubeProtoType = generator.cubeProtoType,cubes = cubes,ecbParallel = ecbParallel,random = random};state.Dependency = job.ScheduleParallel(cubes.Length, 1, state.Dependency);}else{ var job = new GenerateCubesJob{cubeProtoType = generator.cubeProtoType,cubes = cubes,ecb = ecb,random = random};state.Dependency = job.Schedule(cubes.Length, state.Dependency);}state.Dependency.Complete();ecb.Playback(state.EntityManager);//不能在 Playback 后修改刚刚创建的 Entity 的 LocalTransfom,也会报错totalCubes += generator.generationNumPerTicktime;cubes.Dispose();ecb.Dispose();}
}
[BurstCompile]
[RequireMatchingQueriesForUpdate]
[UpdateAfter(typeof(RandomCubeGenerateSystem))]
public partial struct RandomCubeMovementSystem : ISystem
{[BurstCompile]public void OnCreate(ref SystemState state){state.RequireForUpdate<RotateAndMoveSpeed>();}[BurstCompile]public void OnUpdate(ref SystemState state){EntityCommandBuffer ecb = new EntityCommandBuffer(Allocator.TempJob);EntityCommandBuffer.ParallelWriter ecbParallel = ecb.AsParallelWriter();var job = new CubeRotateAndMoveEntityJob{deltaTime = SystemAPI.Time.DeltaTime,ecbParallel = ecbParallel};state.Dependency = job.ScheduleParallel(state.Dependency);state.Dependency.Complete();ecb.Playback(state.EntityManager);ecb.Dispose();}
}

强烈不建议在多个 Job 中共享使用 EntityCommand Buffer,最好为每个Job创建和使用一个 ECB
如果多次调用 ECB 的 Playback 方法,可能会发生异常,如果你需要多次调用,请使用 PlaybackPolicy.MultiPlayBack 选项来创建EntityCommandBuffer 实例

EntityCommandBuffer ecb = new EntityCommandBuffer(Allocator.TempJob, PlaybackPolicy.MultiPlayback);
ecb.Playback(this.EntityManager);
// 这样额外调用Playback是没问题的
ecb.Playback(this.EntityManager);
ecb.Dispose();

创建自定义ECBSystem

public partial class MyECBSystem : EntityCommandBufferSystem { }

获取已经存在的 ECBSystem 并通过存在的 ECBSystem 创建 ECB

EntityCommandBufferSystem sys = state.World.GetExistingSystemManaged<MyECBSystem>();
EntityCommandBuffer ecb = sys.CreateCommandBuffer();
//...
sys.AddJobHandleForProducer(this.Dependency);

通常不需要创建自定义 ECBSystem,因为 DOTS 系统会默认创建一些 ECBSystem,我们只需要获取对应阶段这些 ECBSystem 并创建ECB 实例

var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);

系统默认创建的 ECBSystem 包括:

BeginInitializationEntityCommandBufferSystem
EndInitializationEntityCommandBufferSystem
BeginSimulationEntityCommandBufferSystem
EndSimulationEntityCommandBufferSystem
BeginPresentationEntityCommandBufferSystem

DynamicBufferComponent

  • 可以理解为非托管内存下一个可以调整大小的数组
  • 继承自 lBufferElementData 接口
  • Capacity:DynamicBuffer 的容量,默认为128字节以内的元素数
  • DefaultBufferCapacityNumerator,可以使用 [InternalBufferCapacity] 自定义容量,默认为8,
    DefaultBufferCapacityNumerator / sizeof(element) = 8,element默认为 16bytes
  • InternalPtr:指向缓冲区数据的具体位置

在这里插入图片描述

  • 通过 EntityManager.GetBuffer<T> 来获取 DynamicBufferComponent 的 DynamicBuffer<T>
  • 通过 AnchetypeChunk.GetBufferAccessor(BufferTypeHandle<T>) 来获取 DynamicBuffer 在 Chunk 中的访问器 BufferAccessor<T>
  • Jobs 访问中需要通过 [Read0nly]BufferLookup<T> 来传递通过 Systemstate.GetBufferLookup<T>(true) 获取的只读 DynamicBufferComponent,并且 BufferLookup 需要在 OnUpate 中更新
  • 可以使用 ECB 修改 DynamicBuffercomponent,ECB.AddBuffer<T>,ECB.SetBuffer<T>,ECB.AppendBuffer<T>
  • 如果T与U有相同大小时,可以使用 DynamicBuffer<T> 来重新解释另一个 DynamicBUffer<U>,但要注意类型的实际含义
  • StructuralChange 可能会破坏或移动 DynamicBuffer 的数组,这意味着结构更改后 DynamicBuffer 的任何句柄都将无效。因此需要重新通过 GetBuffer<T> 获取 DynamicBuffer,这个概念与 C++ 中 union 联合的概念比较类似

在这里插入图片描述
红,绿,蓝三个 cube 共享一个 DynamicBuffer 路径,循环移动,点击屏幕添加新的路点。

Authoring 部分,Authoring 场景中 MonoBehaviour 脚本的 Update 和 OnDrawGizmos 并不运行,只用来调试

//默认 8 个路点
[InternalBufferCapacity(8)]
struct WayPoint : IBufferElementData
{public float3 point;
}public class WayPointAuthoring : MonoBehaviour
{//开始的 8 个路点public List<Vector3> wayPoints;public class Baker : Baker<WayPointAuthoring>{public override void Bake(WayPointAuthoring authoring){var entity = GetEntity(TransformUsageFlags.None);DynamicBuffer<WayPoint> waypoints = AddBuffer<WayPoint>(entity);waypoints.Length = authoring.wayPoints.Count;for (int i = 0; i < authoring.wayPoints.Count; i++){waypoints[i] = new WayPoint { point = new float3(authoring.wayPoints[i]) };}}}private void Update(){if (Input.GetMouseButtonDown(0)){Vector3 worldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);float3 newWayPoint = new float3(worldPos.x, worldPos.y, 0);if (wayPoints.Count > 0){float mindist = float.MaxValue;int index = wayPoints.Count;for (int i = 0; i < wayPoints.Count; i++){float dist = math.distance(wayPoints[i], newWayPoint);if (dist < mindist){mindist = dist;index = i;}}wayPoints.Insert(index, new Vector3(newWayPoint.x, newWayPoint.y, newWayPoint.z));}}}void OnDrawGizmos(){if (wayPoints.Count >= 2){for (int i = 0; i < wayPoints.Count; i++){Gizmos.color = Color.yellow;Gizmos.DrawLine(wayPoints[i] - new Vector3(0,0,1), wayPoints[(i+1)%wayPoints.Count] - new Vector3(0,0,1));Gizmos.color = Color.cyan;Gizmos.DrawSphere(wayPoints[i]- new Vector3(0,0,1), 0.4f);}}}
}

记录下一个路点索引,每个 cube 添加了 RotateAndMoveSpeed 组件,代码和上面一致

struct NextPathIndex : IComponentData
{public uint nextIndex;
}public class NextPathIndexAuthoring : MonoBehaviour
{[HideInInspector] public uint nextIndex = 0;public class Baker : Baker<NextPathIndexAuthoring>{public override void Bake(NextPathIndexAuthoring authoring){var entity = GetEntity(TransformUsageFlags.None);var data = new NextPathIndex{nextIndex = authoring.nextIndex};AddComponent(entity, data);}}
}

System 部分

[BurstCompile]
[RequireMatchingQueriesForUpdate]
public partial struct MoveCubesWithWayPointsSystem : ISystem
{[BurstCompile]public void OnCreate(ref SystemState state){state.RequireForUpdate<WayPoint>();}[BurstCompile]public void OnUpdate(ref SystemState state){DynamicBuffer<WayPoint> path = SystemAPI.GetSingletonBuffer<WayPoint>();float deltaTime = SystemAPI.Time.DeltaTime;if (!path.IsEmpty){foreach (var (transform, nextIndex, speed) inSystemAPI.Query<RefRW<LocalTransform>, RefRW<NextPathIndex>, RefRO<RotateAndMoveSpeed>>()){float3 direction = path[(int)nextIndex.ValueRO.nextIndex].point - transform.ValueRO.Position;transform.ValueRW.Position =transform.ValueRO.Position + math.normalize(direction) * speed.ValueRO.moveSpeed*deltaTime;transform.ValueRW = transform.ValueRO.RotateY(speed.ValueRO.rotateSpeed * deltaTime);if (math.distance(path[(int)nextIndex.ValueRO.nextIndex].point, transform.ValueRO.Position) <=0.02f){nextIndex.ValueRW.nextIndex = (uint)((nextIndex.ValueRO.nextIndex + 1) % path.Length);}}}}
}
[BurstCompile]
[RequireMatchingQueriesForUpdate]
[UpdateAfter(typeof(MoveCubesWithWayPointsSystem))]
public partial struct InputSystem : ISystem
{[BurstCompile]public void OnCreate(ref SystemState state){state.RequireForUpdate<WayPoint>();}// Camera 是托管对象,不支持 BurstCompile//[BurstCompile]public void OnUpdate(ref SystemState state){//添加新路点时,如果元素个数大于容量,原始的 DynamicBuffer 会复制到外部数组中if (Input.GetMouseButtonDown(0)){DynamicBuffer<WayPoint> path = SystemAPI.GetSingletonBuffer<WayPoint>();Vector3 worldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);float3 newWayPoint = new float3(worldPos.x, worldPos.y, 0);if (path.Length > 0){float mindist = float.MaxValue;int index = path.Length;for (int i = 0; i < path.Length; i++){float dist = math.distance(path[i].point, newWayPoint);if (dist < mindist){mindist = dist;index = i;}}path.Insert(index, new WayPoint(){ point = newWayPoint });}}}
}

Enableable Component

  • 用来在运行时禁用或启用 Entity 对象上的某个组件,用于处理频繁修改组件状态的情况,避免 Structura Change

  • Enableable Component 还可以替换一组零大小的 TagComponent 来表示 Entity 状态的情况,这样可以减少 Entity Archetype 的数量,可以有更好的 Chunk 利用率以减少内存

  • Enableable Component 只能用在 IComponentData 与 IBufferElementDate 组件类型上,需要同时实现 IEnableableComponent 接口

  • 使用 EnableableComponent 不会改变 Entity 的 Archetype,也不会造成任何数据移动,组件的现有值也不会改变,这意味着你可以在 Job 中直接使用 Enableable Component 来开启和禁用组件,而不需要使用 ECB 操作或创建其他同步点

  • 一个 Job 具有对组件写访问权限时,应避免在另一个 Job 中开启禁用该组件,因为会导致 race condition 情况

  • 我们可以通过 EntityManager、ComponentLookup<T>,EntityCommandBuffer 和 ArchetypeChunk 的 IsComponentEnabled<T> 和
    SetComponentEnabled<T> 接口来启用或禁用组件

  • 默认情况下使用 CreateEntity 创建的新 Entity 上会启用所有可用组件,而从 Prefabs 实例化而来的 Entity 则继承 Prefab 之前的组件启用和禁用状态

  • 查询 Enableable 组件,EntityOuery 会过滤禁用的组件的 Entity 查询,但我们可以通过 2 个方式忽略这种过滤
    1.以 IgnoreFilter 结尾的方法条件创建查询
    2.使用 EntityQueryOption.IgnoreComponentEnabledState 来创建查询

在这里插入图片描述
cube 移动到中间时禁止旋转组件,停止旋转,开始缩放,到右边开启旋转组件

Authoring 部分

struct CubesGenerator : IComponentData
{public Entity cubeProtoType;public int generationTotalNum;//每次 tick 生成的数量public int generationNumPerTicktime;public float tickTime;//生成的区域public float3 generatorAreaPos;//生成的区域,在这个范围内随机public float3 generatorAreaSize;public float3 targetAreaPos;public float3 targetAreaSize;public float rotateSpeed;public float moveSpeed;
}public class CubesGeneratorAuthoring : MonoBehaviour
{public GameObject cubePrefab = null;[Range(10, 10000)] public int generationTotalNum = 500;[Range(1, 60)] public int generationNumPerTicktime = 5;[Range(0.1f, 1.0f)] public float tickTime = 0.2f;public float3 generatorAreaSize;public float3 targetAreaPos;public float3 targetAreaSize;public float rotateSpeed = 180.0f;public float moveSpeed = 5.0f;public class Baker : Baker<CubesGeneratorAuthoring>{public override void Bake(CubesGeneratorAuthoring authoring){var entity = GetEntity(TransformUsageFlags.None);var data = new CubesGenerator{cubeProtoType = GetEntity(authoring.cubePrefab, TransformUsageFlags.Dynamic),generationTotalNum = authoring.generationTotalNum,generationNumPerTicktime = authoring.generationNumPerTicktime,tickTime = authoring.tickTime,generatorAreaPos = authoring.transform.position,generatorAreaSize = authoring.generatorAreaSize,targetAreaPos = authoring.targetAreaPos,targetAreaSize = authoring.targetAreaSize,rotateSpeed = authoring.rotateSpeed,moveSpeed = authoring.moveSpeed};AddComponent(entity, data);}}
}

Component 部分

struct MovementSpeed : IComponentData, IEnableableComponent
{public float movementSpeed;
}
struct RotateSpeed : IComponentData, IEnableableComponent
{public float rotateSpeed;
}

Job 部分

[BurstCompile]
partial struct CubesMarchingEntityJob : IJobEntity
{public float deltaTime;public EntityCommandBuffer.ParallelWriter ecbParallel;void Execute([ChunkIndexInQuery] int chunkIndex, Entity entity, ref LocalTransform transform, in RandomTarget target, in MovementSpeed speed){var distance = math.distance(transform.Position, target.targetPos);if (distance < 0.02f){ecbParallel.DestroyEntity(chunkIndex, entity);}else{float3 dir = math.normalize(target.targetPos - transform.Position);transform.Position += dir * speed.movementSpeed * deltaTime;}}
}
[BurstCompile]
partial struct StopCubesRotateChunkJob : IJobChunk
{public float deltaTime;public float elapsedTime;//缩放区域的左右边界public float2 leftRightBound;public ComponentTypeHandle<LocalTransform> transformTypeHandle;public ComponentTypeHandle<RotateSpeed> rotateSpeedTypeHandle;public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask){var chunkTransforms = chunk.GetNativeArray(ref transformTypeHandle);var chunkRotateSpeeds = chunk.GetNativeArray(ref rotateSpeedTypeHandle);var enumerator = new ChunkEntityEnumerator(useEnabledMask, chunkEnabledMask, chunk.Count);while (enumerator.NextEntityIndex(out var i)){bool enabled = chunk.IsComponentEnabled(ref rotateSpeedTypeHandle, i);if(enabled){if (chunkTransforms[i].Position.x > leftRightBound.x &&chunkTransforms[i].Position.x < leftRightBound.y){//在区域内禁用旋转组件chunk.SetComponentEnabled(ref rotateSpeedTypeHandle, i, false);}else{//在区域外旋转var speed = chunkRotateSpeeds[i];chunkTransforms[i] = chunkTransforms[i].RotateY(speed.rotateSpeed * deltaTime);}}else{if (chunkTransforms[i].Position.x < leftRightBound.x ||chunkTransforms[i].Position.x > leftRightBound.y){//在区域外启用旋转组件chunk.SetComponentEnabled(ref rotateSpeedTypeHandle, i, true);var trans = chunkTransforms[i];trans.Scale = 1.0f;chunkTransforms[i] = trans;}else{//在区域内进行缩放var trans = chunkTransforms[i];trans.Scale = math.sin(elapsedTime*4)*0.3f + 1.0f;chunkTransforms[i] = trans;}}}}
}

System 部分

[BurstCompile]
[RequireMatchingQueriesForUpdate]
public partial struct CubesGeneratorSystem : ISystem
{private float timer;private int totalCubes;[BurstCompile]public void OnCreate(ref SystemState state){state.RequireForUpdate<CubesGenerator>();timer = 0.0f;totalCubes = 0;}[BurstCompile]public void OnUpdate(ref SystemState state){var generator = SystemAPI.GetSingleton<CubesGenerator>();if (totalCubes >= generator.generationTotalNum){state.Enabled = false;return;}if (timer >= generator.tickTime){var cubes = CollectionHelper.CreateNativeArray<Entity>(generator.generationNumPerTicktime, Allocator.Temp);//根据 prefab 创建 entity 实例state.EntityManager.Instantiate(generator.cubeProtoType, cubes);foreach (var cube in cubes){state.EntityManager.AddComponentData<RotateSpeed>(cube, new RotateSpeed{rotateSpeed = math.radians(generator.rotateSpeed)});state.EntityManager.AddComponentData<MovementSpeed>(cube, new MovementSpeed{movementSpeed = generator.moveSpeed});var randomSingleton = SystemAPI.GetSingletonRW<RandomSingleton>();//设置目标点随机值var randPos = randomSingleton.ValueRW.random.NextFloat3(-generator.targetAreaSize * 0.5f,generator.targetAreaSize * 0.5f);state.EntityManager.AddComponentData<RandomTarget>(cube, new RandomTarget(){targetPos = generator.targetAreaPos + new float3(randPos.x, 0, randPos.z)});//上面添加了组件导致 archytype 发生改变,这时需要再一次获取 randomrandomSingleton = SystemAPI.GetSingletonRW<RandomSingleton>();randPos = randomSingleton.ValueRW.random.NextFloat3(-generator.generatorAreaSize * 0.5f,generator.generatorAreaSize * 0.5f);var position = generator.generatorAreaPos + new float3(randPos.x, 0, randPos.z);var transform = SystemAPI.GetComponentRW<LocalTransform>(cube);transform.ValueRW.Position = position;}cubes.Dispose();totalCubes += generator.generationNumPerTicktime;timer -= generator.tickTime;}timer += Time.deltaTime;}
}
[BurstCompile]
[RequireMatchingQueriesForUpdate]
[UpdateAfter(typeof(CubesGeneratorSystem))]
public partial struct CubesMarchingSystem : ISystem
{static readonly ProfilerMarker profilerMarker = new ProfilerMarker("CubesMarchWithEntity");EntityQuery cubesQuery;ComponentTypeHandle<LocalTransform> transformTypeHandle;ComponentTypeHandle<RotateSpeed> rotateSpeedTypeHandle;[BurstCompile]public void OnCreate(ref SystemState state) {state.RequireForUpdate<MovementSpeed>();state.RequireForUpdate<RotateSpeed>();//查询忽略组件是否禁用var queryBuilder = new EntityQueryBuilder(Allocator.Temp).WithAll<RotateSpeed, LocalTransform>().WithOptions(EntityQueryOptions.IgnoreComponentEnabledState);cubesQuery = state.GetEntityQuery(queryBuilder);transformTypeHandle = state.GetComponentTypeHandle<LocalTransform>();rotateSpeedTypeHandle = state.GetComponentTypeHandle<RotateSpeed>();}[BurstCompile]public void OnUpdate(ref SystemState state){using (profilerMarker.Auto()){var generator = SystemAPI.GetSingleton<CubesGenerator>();transformTypeHandle.Update(ref state);rotateSpeedTypeHandle.Update(ref state);var job0 = new StopCubesRotateChunkJob(){deltaTime = SystemAPI.Time.DeltaTime,elapsedTime = (float)SystemAPI.Time.ElapsedTime,leftRightBound = new float2(generator.generatorAreaPos.x / 2, generator.targetAreaPos.x / 2),transformTypeHandle = transformTypeHandle,rotateSpeedTypeHandle = rotateSpeedTypeHandle};state.Dependency = job0.ScheduleParallel(cubesQuery, state.Dependency);EntityCommandBuffer ecb = new EntityCommandBuffer(Allocator.TempJob);EntityCommandBuffer.ParallelWriter ecbParallel = ecb.AsParallelWriter();var job1 = new CubesMarchingEntityJob(){deltaTime = SystemAPI.Time.DeltaTime,ecbParallel = ecbParallel};state.Dependency = job1.ScheduleParallel(state.Dependency);state.Dependency.Complete();ecb.Playback(state.EntityManager);ecb.Dispose();}}
}

Shared Component

共享组件,用于减少数据冗余。

在这里插入图片描述

DOTS 中的每个 World,共享组件的数据存储在与 ECS Chunk 分离的数组中。Chunk 中存储句柄,用来定位共享组件在数组中的值,同一个 Chunk 中的实体共享相同的 SharedComponent 的值,多个块可以存储相同的共享组件句柄。

在这里插入图片描述

把 Chunk1 中的 entity 的共享组件从 Value1 改为 Value2,这个 entity 会移动到 Chunk2 中。

在这里插入图片描述

把 Chunk1 中的 entity 的共享组件从 Value1 改为一个新的共享组件 ValueN,ValueN 将加入共享组件数组中,并将这个 entity 移动到新的 ChunkN 中,ChunkN 存储 ValueN 的索引。

优化使用 Shared Component

  • 避免频繁共享组件值的频繁更新,会导致 Structual Change
  • 避免大量有独特值的共享组件,单个 entity 会占用一个 Chunk 导致内存浪费
  • 避免实体具有多个共享组件类型组合,以免造成 Archetype 碎片化

托管与非托管类型 SharedComponent

  • 根据组件中存储的数据的 BlitableType 类型划分
  • 托管与非托管类型 SharedComponent 是分开存储的
  • 非托管类型的 SharedComponent 只需要继承 ISharedComponentData 接口(也可以选择继承 IEquatable<T> 实现自定义的 Equals 与GetHashCode 方法并可以使用 [BurstCompile] 属性修饰)
  • 托管类型的 SharedComponent 除了继承 ISharedComponentData 接口外,还需要继承 IEquatable<T> 接口,并实现 Equals 方法与复写GetHashCode 方法

在这里插入图片描述

红绿蓝三个 cube 共享相同的组件

Authoring 部分

struct MultiCubesGenerator : IComponentData
{public Entity redCubeProtoType;public Entity greenCubeProtoType;public Entity blueCubeProtoType;public int generationTotalNum;public int generationNumPerTicktime;public float tickTime;public float3 redCubeGeneratorPos;public float3 greenCubeGeneratorPos;public float3 blueCubeGeneratorPos;public float3 cubeTargetPos;
}public class MultiCubesGeneratorAuthoring : MonoBehaviour
{public GameObject redCubePrefab = null;public GameObject greenCubePrefab = null;public GameObject blueCubePrefab = null;[Range(10, 10000)] public int generationTotalNum = 500;[Range(1, 60)] public int generationNumPerTicktime = 5;[Range(0.1f, 1.0f)] public float tickTime = 0.2f;public float3 redCubeGeneratorPos;public float3 greenCubeGeneratorPos;public float3 blueCubeGeneratorPos;public float3 cubeTargetPos;public class Baker : Baker<MultiCubesGeneratorAuthoring>{public override void Bake(MultiCubesGeneratorAuthoring authoring){var entity = GetEntity(TransformUsageFlags.None);var data = new MultiCubesGenerator{redCubeProtoType = GetEntity(authoring.redCubePrefab, TransformUsageFlags.Dynamic),greenCubeProtoType = GetEntity(authoring.greenCubePrefab, TransformUsageFlags.Dynamic),blueCubeProtoType = GetEntity(authoring.blueCubePrefab, TransformUsageFlags.Dynamic),generationTotalNum = authoring.generationTotalNum,generationNumPerTicktime = authoring.generationNumPerTicktime,tickTime = authoring.tickTime,redCubeGeneratorPos = authoring.redCubeGeneratorPos,greenCubeGeneratorPos = authoring.greenCubeGeneratorPos,blueCubeGeneratorPos = authoring.blueCubeGeneratorPos,cubeTargetPos = authoring.cubeTargetPos};AddComponent(entity, data);}}
}
public struct CubeSharedComponentData : ISharedComponentData
{public float rotateSpeed;public float moveSpeed;
}//用于查询过滤
public struct SharingGroup : ISharedComponentData
{//0 red, 1 green, 2 bluepublic int group;
}

System 部分

[BurstCompile]
[RequireMatchingQueriesForUpdate]
public partial struct MultiCubesGenerateSystem : ISystem
{private float timer;private int totalCubes;[BurstCompile]public void OnCreate(ref SystemState state){state.RequireForUpdate<MultiCubesGenerator>();timer = 0.0f;totalCubes = 0;}[BurstCompile]public void OnUpdate(ref SystemState state){var generator = SystemAPI.GetSingleton<MultiCubesGenerator>();if (totalCubes >= generator.generationTotalNum){state.Enabled = false;return;}if (timer >= generator.tickTime){Entity redCube = state.EntityManager.Instantiate(generator.redCubeProtoType);Entity greenCube = state.EntityManager.Instantiate(generator.greenCubeProtoType);Entity blueCube = state.EntityManager.Instantiate(generator.blueCubeProtoType);state.EntityManager.AddSharedComponent<CubeSharedComponentData>(redCube, new CubeSharedComponentData{rotateSpeed = math.radians(180.0f),moveSpeed = 5.0f});state.EntityManager.AddSharedComponent<SharingGroup>(redCube, new SharingGroup{group = 0});state.EntityManager.AddSharedComponent<CubeSharedComponentData>(greenCube, new CubeSharedComponentData{rotateSpeed = math.radians(180.0f),moveSpeed = 5.0f});state.EntityManager.AddSharedComponent<SharingGroup>(greenCube, new SharingGroup{group = 1});state.EntityManager.AddSharedComponent<CubeSharedComponentData>(blueCube, new CubeSharedComponentData{rotateSpeed = math.radians(180.0f),moveSpeed = 5.0f});state.EntityManager.AddSharedComponent<SharingGroup>(blueCube, new SharingGroup{group = 2});var redCubeTransform = SystemAPI.GetComponentRW<LocalTransform>(redCube);redCubeTransform.ValueRW.Position = generator.redCubeGeneratorPos;var greenCubeTransform = SystemAPI.GetComponentRW<LocalTransform>(greenCube);greenCubeTransform.ValueRW.Position = generator.greenCubeGeneratorPos;var blueCubeTransform = SystemAPI.GetComponentRW<LocalTransform>(blueCube);blueCubeTransform.ValueRW.Position = generator.blueCubeGeneratorPos;totalCubes += 3;timer -= generator.tickTime;}timer += Time.deltaTime;}
}
[BurstCompile]
[RequireMatchingQueriesForUpdate]
[UpdateAfter(typeof(MultiCubesGenerateSystem))]
public partial struct MultiCubesMarchingSystem : ISystem
{EntityQuery cubesQuery;[BurstCompile]public void OnCreate(ref SystemState state){var queryBuilder = new EntityQueryBuilder(Allocator.Temp).WithAll<LocalTransform, CubeSharedComponentData, SharingGroup>();cubesQuery = state.GetEntityQuery(queryBuilder);}[BurstCompile]public void OnUpdate(ref SystemState state){float deltaTime = SystemAPI.Time.DeltaTime;double elapsedTime = SystemAPI.Time.ElapsedTime;var generator = SystemAPI.GetSingleton<MultiCubesGenerator>();//过滤某个类型//cubesQuery.SetSharedComponentFilter(new SharingGroup { group = 1 });var cubeEntities = cubesQuery.ToEntityArray(Allocator.Temp);var localTransforms = cubesQuery.ToComponentDataArray<LocalTransform>(Allocator.Temp);for (int i = 0; i < cubeEntities.Length; i++){var data = state.EntityManager.GetSharedComponent<CubeSharedComponentData>(cubeEntities[i]);LocalTransform temp = localTransforms[i];if (temp.Position.x > generator.cubeTargetPos.x){state.EntityManager.DestroyEntity(cubeEntities[i]);}else{temp.Position += data.moveSpeed * deltaTime * new float3(1, (float)math.sin(elapsedTime*20), 0);temp = temp.RotateY(data.rotateSpeed * deltaTime);localTransforms[i] = temp;state.EntityManager.SetComponentData(cubeEntities[i], localTransforms[i]);}}localTransforms.Dispose();cubeEntities.Dispose();}
}

在这里插入图片描述
过滤绿色的 cube

Blob Asset

Unity 引擎层的 Blob Asset

  • Blob(Binary Large Object) Asset 是 Unity 中存储数据的一种格式,是为了流式传输优化的二进制数据片,在内存中是存储连续字节块中的不可变的非托管二进制数据。
  • Blob 是一种二进制的只读数据结构,其数据创建时设置一次,之后不能再更改
  • Blob 是一次性分配的内存快,可以用 memcpy 重新定位,可以更有效的存储与复制,因此其中不能包含虚类对象数据
  • Unity 的 Blobification 框架可以更紧密的打包数据,借助 reduce copy 减少重复数据,可以使 Blob Asset 完全可以 relocatable 重新定位从磁盘直接读取。

DOTS 中的 Blob Asset

  • DOTS 中的 Blob Asset 同样可以有效的优化数据的存储与内存的使用
  • 在 DOTS 下我们不能直接使用 Blob Asset,而是通过 BlobAssetRef 的方式有效的加载存储在Entity上的组件中进行引用
  • 当不需要 Blob Asset 时,需通过调用 BlobAssetReference 上的 Dispose 接口来处理 Blob Asset
  • Bake Entity 场景中引用的 BlobAsset 将随场景一起序列化加载,不需要手动释放这些资产,他们会随场景一起自动处理。
  • 注意 Blob Asset 与 Shared Component 的区别

SharedComponent 与 Blob Asset 应用差异

Blob AssetShared Component
不能有任何拖管数据可以使用托管数据
不能运行时更改数据可以运行时更改数据但会导致 Structual Change
数据是只读的可以多线程访问,而不需要 DOTS 的安全检查数据是可读写的,会有 DOTS 的安全检查
数据需要定义在组件外,通过 BlobAssetRef 用访问数据以可直接定义在组件内

DOTS 中的 Blob 数据类型

  • 普通 Blitable Type 数据类型
  • BlobString
  • BlobArray
  • BlobPtr

Authoring 部分

//运行时会修改的数据
struct EntitySpawnerComponentData : IComponentData
{public float currentlife; //当前生命值                             4byte
}//运行时只读的数据
struct EntitySpawnerBlobData
{public Entity entityProtoType; //生成的entity原型对象,用于实例化克隆    8bytepublic BuidingType buildingType; //建筑类型                             4bytepublic int level; //当前等级                              				4bytepublic float tickTime; //每多少秒生成一次                       			4bytepublic int spawnCountPerTicktime; //每次生成几个entity                   4bytepublic float maxLife; //最大生命值                            			4bytepublic ArmorType armorType; //护甲类型                               	4bytepublic DamageType damageType; //伤害类型                                4bytepublic float maxDamage; //最大攻击力                             		4bytepublic float minDamage; //最大攻击力                             		4bytepublic float upgradeTime; //升级时间                              		4bytepublic float upgradeCost; //升级费用                              		4byte
}//对 EntitySpawnerBlobData 数据的引用
struct EntitySpawnerSettings : IComponentData
{public BlobAssetReference<EntitySpawnerBlobData> blobSettings;          // 8byte
}public class EntitySpawnerAuthoring : MonoBehaviour
{public GameObject protoTypePrefab = null;public BuidingType buildingType = BuidingType.BT_Spawner;public int level = 1;[Range(1.0f, 10.0f)] public float tickTime = 5.0f;[Range(1, 8)] public int spawnCountPerTicktime = 1;[Range(100, 3000)] public float maxLife = 1000;public ArmorType armorType = ArmorType.AT_Normal;public DamageType damageType = DamageType.DT_Magic;public float maxDamage = 0;public float minDamage = 0;public float upgradeTime = 100.0f;public float upgradeCost = 100;public class Baker : Baker<EntitySpawnerAuthoring>{public override void Bake(EntitySpawnerAuthoring authoring){var entity = GetEntity(TransformUsageFlags.None);AddComponent(entity, new EntitySpawnerComponentData{currentlife = authoring.maxLife});var settings = CreateSpawnerBlobSettings(authoring);//给 entity 添加 BlobAssetAddBlobAsset(ref settings, out var hash);AddComponent(entity, new EntitySpawnerSettings{blobSettings = settings});}BlobAssetReference<EntitySpawnerBlobData> CreateSpawnerBlobSettings(EntitySpawnerAuthoring authoring){//用于构建 Blob Assetsvar builder = new BlobBuilder(Allocator.Temp);//初始化 Blob 的根结构ref EntitySpawnerBlobData spawnerBlobData = ref builder.ConstructRoot<EntitySpawnerBlobData>();spawnerBlobData.entityProtoType = GetEntity(authoring.protoTypePrefab, TransformUsageFlags.Dynamic);spawnerBlobData.buildingType = authoring.buildingType;spawnerBlobData.level = authoring.level;spawnerBlobData.tickTime = authoring.tickTime;spawnerBlobData.spawnCountPerTicktime = authoring.spawnCountPerTicktime;spawnerBlobData.maxLife = authoring.maxLife;spawnerBlobData.armorType = authoring.armorType;spawnerBlobData.damageType = authoring.damageType;spawnerBlobData.maxDamage = authoring.maxDamage;spawnerBlobData.minDamage = authoring.minDamage;spawnerBlobData.upgradeTime = authoring.upgradeTime;spawnerBlobData.upgradeCost = authoring.upgradeCost;var result = builder.CreateBlobAssetReference<EntitySpawnerBlobData>(Allocator.Persistent);builder.Dispose();return result;}}
}

System 部分

[BurstCompile]
public partial struct BuildingSystem : ISystem
{[BurstCompile]public void OnCreate(ref SystemState state){state.RequireForUpdate<EntitySpawnerComponentData>();state.RequireForUpdate<EntitySpawnerSettings>();}[BurstCompile]public void OnUpdate(ref SystemState state){var soldierData = SystemAPI.GetSingleton<EntitySpawnerComponentData>();var soldierCommonData = SystemAPI.GetSingleton<EntitySpawnerSettings>();//do something}
}

Cleanup Component

  • 当销毁一个包含 Cleanup Component 的 Entity 实体时,Unity 会删除所有非 Cleanup Component 的其他组件,但该 Entity 实际上依然存在。
  • Cleanup Component 不会在随实体复制到另外一个 world 中
  • 主要用在创建 Entity 后帮助初始化 Entity 或在销毁 Entity 后帮助清理 Entity

Cleanup Component类型

  • Struct 继承 ICleanupComponentData //非托管类型Cleanup组件
  • Class 继承 ICleanupComponentData //托管类型Cleanup组件
  • Struct 继承 ICleanupBufferElementData //dynamic buffer类型
  • Struct 继承 ICleanupSharedComponentData //Shared Component类型

在这里插入图片描述
运行时异步加载 prefab 并转换成 entity

Authoring 部分

struct RespawnController : IComponentData
{//每隔多久更换 prefabpublic float timer;
}//存储可变长度的数据数组,这里保存几种 prefab 的引用
struct PrefabBufferElement : IBufferElementData
{public EntityPrefabReference prefab;
}struct RespawnCleanupComponent : ICleanupComponentData
{
}public class RespawnControllerAuthoring : MonoBehaviour
{public GameObject[] spawners = null;[Range(1, 5)]public float timer = 1.0f;public class Baker : Baker<RespawnControllerAuthoring>{public override void Bake(RespawnControllerAuthoring authoring){var entity = GetEntity(TransformUsageFlags.None);var data = new RespawnController{timer = authoring.timer};AddComponent(entity, data);var buffer = AddBuffer<PrefabBufferElement>(entity);for (int i = 0; i < authoring.spawners.Length; i++){var elem = new PrefabBufferElement{prefab = new EntityPrefabReference(authoring.spawners[i])};buffer.Add(elem);}}}
}

System 部分

[RequireMatchingQueriesForUpdate]
public partial struct EntityRespawnSystem : ISystem, ISystemStartStop
{private int index;private float timer;private Entity controllerEntity;private Entity instanceEntity;[BurstCompile]public void OnCreate(ref SystemState state){state.RequireForUpdate<RespawnController>();}[BurstCompile]public void OnUpdate(ref SystemState state){if (!controllerEntity.Equals(default)){if (state.EntityManager.HasComponent<PrefabLoadResult>(controllerEntity)){//销毁已存在的 prefab entity 对象if(!instanceEntity.Equals(default))state.EntityManager.DestroyEntity(instanceEntity);//实例化新的 prefab entity 对象var data = state.EntityManager.GetComponentData<PrefabLoadResult>(controllerEntity);instanceEntity = state.EntityManager.Instantiate(data.PrefabRoot);//清理除 RespawnCleanupComponent 之外的组件,并没有真正销毁 entitystate.EntityManager.DestroyEntity(controllerEntity);timer = 0;}var controller = SystemAPI.GetSingleton<RespawnController>();timer += SystemAPI.Time.DeltaTime;if (timer >= controller.timer){var prefabs = SystemAPI.GetSingletonBuffer<PrefabBufferElement>(true);//重新添加 RequestEntityPrefabLoaded,开始新的异步加载state.EntityManager.AddComponentData<RequestEntityPrefabLoaded>(controllerEntity, new RequestEntityPrefabLoaded{Prefab = prefabs[index % prefabs.Length].prefab});index++;timer = 0;}}}//OnUpdate之前以及系统在停止后恢复更新时调用,类似 mono 的 OnEable[BurstCompile]public void OnStartRunning(ref SystemState state){index = 0;timer = 0;controllerEntity = default;instanceEntity = default;//只要保障一个 world 只有一个 Component ,Getsingleton 就可以用var prefabs = SystemAPI.GetSingletonBuffer<PrefabBufferElement>(true);//运行时创建 EntitycontrollerEntity = state.EntityManager.CreateEntity();//AddComponentData 添加组件的同时,初始化该组件的数据//RequestEntityPrefabLoaded 实现 prefab 异步加载并转换成 entity,//转换完成后会自动添加 PrefabLoadResult 组件,PrefabLoadResult.PrefabRoot 获取这个 Entitystate.EntityManager.AddComponentData<RequestEntityPrefabLoaded>(controllerEntity, new RequestEntityPrefabLoaded{Prefab = prefabs[index % prefabs.Length].prefab});//AddComponent 添加一个组件类型,而不初始化组件的数据。state.EntityManager.AddComponent<RespawnCleanupComponent>(controllerEntity);index++;}// 类似 OnDisable[BurstCompile]public void OnStopRunning(ref SystemState state){if (!instanceEntity.Equals(default)){state.EntityManager.DestroyEntity(instanceEntity);instanceEntity = default;}if (!controllerEntity.Equals(default)){state.EntityManager.DestroyEntity(controllerEntity);index = 0;timer = 0;controllerEntity = default;}}
}

Chunk Component

Chunk Component 是一种按 Chunk 而不是按 Entity 存储的组件。

  • 与非托管组件创建一样,都继承接口 IComponentData 的。通过 AddComponentData 方式添加的是常规非托管组件,通过
    AddChunkComponentData 方式添加的就是 ChunkComponent。
  • 在 ChunkComponent 使用上,与其他组件类型相比,ChunkComponent 使用一组不同的 API 接口来添加、删除、获取和设置它们。如果只想从 ChunkComponent 中读取而不是写入,请在定义 Query 查询时使用 ComponentType.ChunkComponentReadOnly 将包含的组件标记为只读的。
  • 在Entity上添加或删除 ChunkCompnent 会带来 StructualChange 新创建的 ChunkComponent 中的值会被初始化为默认值。
  • 在执行 Job 时由于 ChunkComponent 时按 Chunk 存储,所以一般用 IJobChunk 接口而不用 IJobEntity 接口。

Chunk Component 是如何存储的
在这里插入图片描述
通过 External Components 指向外部的 ArcheType 中的 Chunk

struct ChunkComponentA : IComponentData
{public int numA;
}struct ChunkComponentB : IComponentData
{public int numB;
}struct GeneralComponent : IComponentData
{public int num;
}
[RequireMatchingQueriesForUpdate]
public partial struct GenerateEntitySystem : ISystem
{[BurstCompile]public void OnCreate(ref SystemState state){var entity1 = state.EntityManager.CreateEntity();state.EntityManager.AddComponentData<GeneralComponent>(entity1, new GeneralComponent{ num = 0});state.EntityManager.AddChunkComponentData<ChunkComponentA>(entity1);var entity2 = state.EntityManager.CreateEntity();//state.EntityManager.AddComponentData<GeneralComponent>(entity2, new GeneralComponent{ num = 0});state.EntityManager.AddChunkComponentData<ChunkComponentA>(entity2);//state.EntityManager.AddChunkComponentData<ChunkComponentB>(entity2);ArchetypeChunk chunk = state.EntityManager.GetChunk(entity1);state.EntityManager.SetChunkComponentData(chunk, new ChunkComponentA{ numA = 3});var entity3 = state.EntityManager.CreateEntity();state.EntityManager.AddChunkComponentData<ChunkComponentA>(entity3);state.EntityManager.AddChunkComponentData<ChunkComponentB>(entity3);state.Enabled = false;}
}

在这里插入图片描述
运行时可以看到 entity1 所在的 ArcheType 中, External Components 指向外部 Chunk

在这里插入图片描述
ChunkComponentA 被 entity1 和 entity2 同时引用

在这里插入图片描述

修改 entity1 对应的 ChunkComponentA 中的 numA 为3,运行时发现,entity1 对应的 numA 为3,entity2 对应的 numA 还是默认值0,说明虽然 entity1 和 entity2 都指向同一个 chunk,但 numA 是有不同的副本的

Chunk Component 与 Shared Component 比较

  • Chunk Component 组件值在概念属于 Chunk 本身,而不是 Chunk 上的各个 Entity
  • 设置 Chunk Component 的值不是一个 Structual Change,但 SharedComponent 是
  • 与 Shared Component 不同,Unity 不会对唯一的 Chunk Component 的值进行进行重复数据消除。具有相等 Chunk Component 的值的 Chunk 存储自己单独的副本。
  • Chunk Component 始终是非托管的,Shared Component 既有托管也可以是非托管的。
  • 当 Entity 的 Archetype 发生变化或 entity 的 Shared Component 组件值发生变化时,Unity 会将 entity 移动到新 Chunk 中,但这个移动不会影响源 Chunk Component 与目标 Chunk Component 中的值。

Entities Graphics 包

  • 充当 DOTS 与 Unity 现有渲染架构之间的桥梁
  • 使用 ECS Entity 而不是 GameObject 来显著改善大型场景中的运行时内存布局与性能
  • 保持 Unity 现有工作流的兼容性与易用性

Entities Graphics 包包含一个将 GameObject 转化为等效 DOTS Entity 的系统,支持编辑模式与运行模式转换。

  • MeshRenderer MeshFilter -> RenderMesh
  • LODGroup -> MeshLODGroupComponent
  • Transform -> LocalToWord

最好的运行时实例化 Entity 的方式

  • Prefabs
  • RenderMeshUtility.AddComponents API,不要手动添加渲染组件,效率低,也可能会与未来 Entities Graphics 包的版本不兼容

通过 mono 创建 Entity 实例,不需要 subscene

public class CreateEntityWithMonobehavior : MonoBehaviour
{public Mesh mesh;public Material material;void Start(){var world = World.DefaultGameObjectInjectionWorld;var entityManager = world.EntityManager;var renderMeshArray = new RenderMeshArray(new[] {material}, new []{mesh});var filterSettings = RenderFilterSettings.Default;filterSettings.ShadowCastingMode = ShadowCastingMode.Off;filterSettings.ReceiveShadows = false;//描述实体的渲染信息,定义实体如何渲染var renderMeshDescription = new RenderMeshDescription{FilterSettings = filterSettings,LightProbeUsage = LightProbeUsage.Off,};var cubeEntity = entityManager.CreateEntity();//这是一个主线程 API,频繁添加组件会导致 structual changeRenderMeshUtility.AddComponents(cubeEntity,entityManager,renderMeshDescription,renderMeshArray,MaterialMeshInfo.FromRenderMeshArrayIndices(0, 0));entityManager.SetComponentData(cubeEntity, new LocalToWorld{Value = float4x4.identity});}
}

正式写 dots 更推荐在 System 的 OnCreate 方法中创建 entity

Entity渲染组件的常规操作

Entities Graphics 可以实现对各种 HDRP/URP 材质资产属性值进行复写,包括自定义材质,有两种方法

  • 使用 Material Override Asset
  • 使用脚本添加组件修改

使用 Material Override Asset
右键 -> Create -> Shader -> Material Override Asset 创建资源

在这里插入图片描述
点 Add Property Override 勾选需要覆盖哪些属性

在这里插入图片描述

在 SubScene 中给对象添加 Material Override 脚本,并引用 Material Override Asset,在下面可以看到自动添加了组件用于复写,运行时修改脚本属性就可以了

在这里插入图片描述

运行时修改材质,网格

[MaterialProperty("_BaseColor")]
struct CustomColor : IComponentData
{public float4 customColor;
}/// <summary>
/// 需要替换的网格和材质
/// </summary>
class CustomMeshAndMaterial : IComponentData
{public Mesh sphere;public Mesh capsule;public Mesh cylinder;public Material red;public Material green;public Material blue;
}

创建实体

[BurstCompile]
public struct SpawnJob : IJobParallelFor
{public Entity prototype;public int halfCountX;public int halfCountZ;public bool useDisableRendering;public EntityCommandBuffer.ParallelWriter Ecb;public void Execute(int index){var e = Ecb.Instantiate(index, prototype);Ecb.SetComponent(index, e, new LocalToWorld { Value = ComputeTransform(index, e) });}public float4x4 ComputeTransform(int index, Entity e){int x = index % (halfCountX * 2) - halfCountX;int z = index / (halfCountX * 2) - halfCountZ;float4x4 M = float4x4.TRS(new float3(x*1.1f, 0, z*1.1f),quaternion.identity,new float3(1));//添加 DisableRendering 会剔除渲染if (useDisableRendering && math.sqrt(x*x + z*z) > 30)Ecb.AddComponent<DisableRendering>(index, e);return M;}
}public class CreateColoredWaveCubes : MonoBehaviour
{[Range(10, 100)] public int xHalfCount = 40;[Range(10, 100)] public int zHalfCount = 40;public bool useDisableRendering = false;//初始的网格材质public Mesh mesh;public Material material;//替换的网格材质public Mesh[] changeMeshes;public Material[] changeMaterials;void Start(){var world = World.DefaultGameObjectInjectionWorld;var entityManager = world.EntityManager;EntityCommandBuffer ecbJob = new EntityCommandBuffer(Allocator.TempJob);var filterSettings = RenderFilterSettings.Default;filterSettings.ShadowCastingMode = ShadowCastingMode.Off;filterSettings.ReceiveShadows = false;var renderMeshArray = new RenderMeshArray(new[] { material }, new[] { mesh });var renderMeshDescription = new RenderMeshDescription{FilterSettings = filterSettings,LightProbeUsage = LightProbeUsage.Off,};var prototype = entityManager.CreateEntity();RenderMeshUtility.AddComponents(prototype,entityManager,renderMeshDescription,renderMeshArray,MaterialMeshInfo.FromRenderMeshArrayIndices(0, 0));entityManager.AddComponentData(prototype, new CustomColor { customColor = new float4(0, 1, 1, 1) });entityManager.AddComponentData(prototype, new CustomMeshAndMaterial{sphere = changeMeshes[0],   capsule = changeMeshes[1],cylinder = changeMeshes[2],red = changeMaterials[0],green = changeMaterials[1],blue = changeMaterials[2]});var spawnJob = new SpawnJob{prototype = prototype,Ecb = ecbJob.AsParallelWriter(),halfCountX = xHalfCount,halfCountZ = zHalfCount,useDisableRendering = useDisableRendering};var spawnHandle = spawnJob.Schedule(4*xHalfCount*zHalfCount, 128);spawnHandle.Complete();ecbJob.Playback(entityManager);ecbJob.Dispose();entityManager.DestroyEntity(prototype);}
}

移动实体并根据位置信息修改颜色

[BurstCompile]
partial struct WaveCubeEntityJob : IJobEntity
{public EntityCommandBuffer.ParallelWriter ecb;[ReadOnly] public float elapsedTime;void Execute([ChunkIndexInQuery] int chunkIndex, Entity entity, ref LocalToWorld transform, ref CustomColor color){var distance = math.distance(transform.Position, float3.zero);float s = elapsedTime * 3f + distance * 0.2f;float3 newPos = transform.Position + new float3(0, 1, 0) * math.sin(s);transform.Value = float4x4.Translate(newPos);color.customColor = new float4((math.sin(s)+1)/2, (math.cos(s*1.1f)+1)/2, (math.sin(s*2.2f)+1)*(math.cos(s*2.2f)+1)/4, 1);}
}[RequireMatchingQueriesForUpdate]
[UpdateInGroup(typeof(GraphicsLesson2SystemGroup))]
public partial struct ChangeWaveCubesColorSystem : ISystem
{[BurstCompile]public void OnCreate(ref SystemState state){state.RequireForUpdate<CustomColor>();}[BurstCompile]public void OnUpdate(ref SystemState state){EntityCommandBuffer ecbJob = new EntityCommandBuffer(Allocator.TempJob);var job = new WaveCubeEntityJob(){ecb = ecbJob.AsParallelWriter(),elapsedTime = (float)SystemAPI.Time.ElapsedTime};state.Dependency = job.ScheduleParallel(state.Dependency);state.Dependency.Complete();ecbJob.Playback(state.EntityManager);ecbJob.Dispose();}
}

按高度替换网格,材质,Mesh 和 Material 都是托管对象,不能在 Brust 中使用,使用前需要先注册

[RequireMatchingQueriesForUpdate]
[UpdateAfter(typeof(ChangeWaveCubesColorSystem))]
public partial class ChangeWaveCubesMeshAndMaterialSystem : SystemBase
{private Dictionary<Mesh, BatchMeshID> m_MeshMapping;private Dictionary<Material, BatchMaterialID> m_MaterialMapping;protected override void OnStartRunning(){RequireForUpdate<CustomMeshAndMaterial>();var system = World.GetOrCreateSystemManaged<EntitiesGraphicsSystem>();m_MeshMapping = new Dictionary<Mesh, BatchMeshID>();m_MaterialMapping = new Dictionary<Material, BatchMaterialID>();//把网格,材质注册到系统中,以便在 ecs 环境中使用Entities.WithoutBurst().ForEach((in CustomMeshAndMaterial changer) =>{if (!m_MeshMapping.ContainsKey(changer.sphere))m_MeshMapping[changer.sphere] = system.RegisterMesh(changer.sphere);if (!m_MeshMapping.ContainsKey(changer.capsule))m_MeshMapping[changer.capsule] = system.RegisterMesh(changer.capsule);if (!m_MeshMapping.ContainsKey(changer.cylinder))m_MeshMapping[changer.cylinder] = system.RegisterMesh(changer.cylinder);if (!m_MaterialMapping.ContainsKey(changer.red))m_MaterialMapping[changer.red] = system.RegisterMaterial(changer.red);if (!m_MaterialMapping.ContainsKey(changer.green))m_MaterialMapping[changer.green] = system.RegisterMaterial(changer.green);if (!m_MaterialMapping.ContainsKey(changer.blue))m_MaterialMapping[changer.blue] = system.RegisterMaterial(changer.blue);}).Run();}protected override void OnUpdate(){//按高度替换网格和材质Entities.WithoutBurst().ForEach((CustomMeshAndMaterial changer, ref MaterialMeshInfo info, in LocalToWorld trans) =>{if (trans.Position.y < -5){info.MeshID = m_MeshMapping[changer.cylinder];info.MaterialID = m_MaterialMapping[changer.blue];}else if(trans.Position.y > 5){info.MeshID = m_MeshMapping[changer.sphere];info.MaterialID = m_MaterialMapping[changer.red];}else{info.MeshID = m_MeshMapping[changer.capsule];info.MaterialID = m_MaterialMapping[changer.green];}}).Run();}
}

参考

《DOTS之路》系列课程


http://www.mrgr.cn/news/62980.html

相关文章:

  • Java | Leetcode Java题解之第524题通过删除字母匹配到字典里最长单词
  • 太速科技-712-6U VPX飞腾处理器刀片计算机
  • B站狂神说+mybatis+如何创建一个最简单的mybatis程序
  • Android OpenGL ES详解——裁剪Scissor
  • [进阶]java基础之集合(三)数据结构
  • fmql之Linux以太网
  • 防爆电机技术要点、选型,一文搞定!
  • 必应Bing国内搜索广告代理商,必应广告如何开户投放?
  • STM32--STM32 微控制器详解
  • 基于Java的茶产品销售平台系统【附源码】
  • 假设检验简介
  • 组织如何防御日益增加的 API 攻击面
  • SpringBoot应用部署到Docker中MySQL8时间戳相差8小时问题及处理方式
  • 网络通信与并发编程(七)GIL、协程
  • 《揭秘 C++:确保模板函数重载决议正确的秘籍》
  • Redis ——发布订阅
  • Android 中View.post的用法
  • C++缺陷识别于调试
  • STM32的USB接口介绍
  • 使用GitLab CI/CD流水线自动化软件交付
  • leetcode 704 二分查找
  • .[support2022@cock.li].colony96勒索病毒数据怎么处理|数据解密恢复
  • 篡改猴 (Tampermonkey) 安装与使用
  • 【编程知识】C语言/c++的cast是什么
  • GitHub Spark:GitHub 推出零代码开发应用的 AI 编程产品
  • .net framework 3.5sp1开启错误进度条不动如何解决