文章目录
- 前言
- DOTS 核心组成
- DOTS 解决传统问题的痛点
- 1、优化内存布局:
- 2、减少垃圾回收和内存管理开销:
- 3、提高并行计算能力:
- 4、高效的系统和组件设计:
- 5、易于扩展和优化:
- 安装
- 文档
- 在编辑器下构建 ECS World
- 查看Entity的属性
- 最简单的ecs程序
- 传统方式
- ecs方式
- 1. 创建一个组件(Component)
- 2. 创建一个系统(System)
- 3. 触发打印
- 4. 新建空物体挂载脚本
- 旋转组件
- 优化
- 多线程并行执行
- 使用 Burst 编译器优化性能
- 屏蔽系统间的干扰
- 调试窗口
- 后续
- 完结
前言
Unity DOTS(Data-Oriented Technology Stack)是 Unity 推出的数据驱动型技术栈,旨在帮助开发者构建高性能的游戏和应用,特别是在需要处理大量实体(Entities)时。DOTS 通过提供不同的工具和框架(如 ECS、Jobs 和 Burst),能够极大提升性能,尤其是在大规模的并行计算和高帧率下的优化。
在传统的面向对象(OOP)编程方式中,游戏开发者通常依赖继承和类之间的关系来管理数据和行为,这会导致性能瓶颈,尤其是在面对大规模实体时。随着现代游戏中世界规模和交互数量的增加,传统方法的缺点变得尤为突出。
DOTS 的设计核心是“数据优先”,它通过优化内存布局和计算过程,能够显著提高性能,解决传统开发中的一些常见瓶颈,特别是在性能密集型的场景下(如大量物体的物理计算、AI 行为等)。
DOTS 核心组成
- ECS(Entity Component System):以数据为中心,使用实体(Entities)、组件(Components)和系统(Systems)来组织和管理数据,旨在提升内存访问效率。
- Jobs System:通过多线程并行化任务,能将大量的计算工作分配给多个处理器核心,从而提升性能。
- Burst Compiler:通过静态分析代码并优化生成的机器码,能大幅度提升 CPU 计算性能。
DOTS 解决传统问题的痛点
1、优化内存布局:
DOTS 中的 ECS 通过将数据按组件组织,并优化内存布局,能够让数据按照连续块存储在内存中,从而大幅提高 CPU 缓存命中率,减少缓存未命中的问题。
这种数据布局优化使得大量实体的处理能更高效,尤其在涉及到大规模游戏世界或复杂场景时。
2、减少垃圾回收和内存管理开销:
DOTS 避免了传统对象实例化和销毁的开销,通过使用对象池(或“内存分配池”)来复用实体,减少 GC(垃圾回收)的频繁调用,从而降低性能损耗。
3、提高并行计算能力:
DOTS 通过 Jobs System 将任务分割并并行化,能够充分利用多核 CPU 的计算能力。开发者可以轻松编写多线程代码,而无需担心线程安全问题,系统会自动管理并行化。
Burst Compiler 进一步优化了代码的执行效率,确保多线程任务能够最大化地提高性能。
4、高效的系统和组件设计:
DOTS 的 ECS 模式简化了逻辑和数据的分离,让每个系统(System)仅处理数据(Components)中的某一部分。通过这种设计,开发者可以更方便地对数据进行分区、筛选和批处理,提高程序的执行效率。
这种“数据驱动”模型使得开发者可以更直观地分析和优化性能瓶颈。
5、易于扩展和优化:
DOTS 提供了一个高效的调试和分析工具链(如 Profiler),可以帮助开发者实时监控和调整性能瓶颈。与此同时,ECS 和 Jobs 使得开发者能够在代码级别进行细粒度的优化。
安装
我这里使用的是最新的unity6,unity6 Entities已经从预览版放出来了(貌似2022版本就不是预览版了),直接搜索安装即可.
如果你用的是其他,可能方法各有不同,需要自行去查找如何进行安装
(注意:推荐在URP或者hdrp中使用DOTS
)
安装Entities Graphics,实体的dots显示包
文档
https://docs.unity3d.com/Packages/com.unity.entities@1.3/manual/index.html
在编辑器下构建 ECS World
之前的流程是在 GameObject 挂上一个 Convert To Entity
的组件,就能转换成 Entity。不过新的流程修改了,这个组件被移除了,新的流程如下:
在 Hierarchy 窗口下右键,选择 New Subscene > Empty Scene,创建一个新的 SubScene。
现在只要在这个SubScene里面的东西,都会自动转换成实体(Entity)
查看Entity的属性
之前这一块是在 EntityDebugger 里面的,相信大家已经发现了,在Entities 1.0 中,已经没有这个 EntityDebugger 了。
查看 Entity 的方法如下:
直接 Play,然后在 Hierarchy 中选择对应的 Cube(已经转成实体)
可以通过右上角查看实体情况
里面你就能看到Entity的属性了
可以看到这里加了很多Data,除了LTW相关的就是渲染相关的,东西还是非常多的。不过这里我们都完全不需要管它,只要知道这里能看各个Data的数据就OK了。
最简单的ecs程序
这里我我带大家实现一个简单的打印效果
传统方式
传统方式应该就不用我过多介绍了,相信大家用的已经非常多了
public class test : MonoBehaviour
{void Update(){Debug.Log("传统方式打印");}
}
ecs方式
要使用 Unity DOTS 实现类似于传统方式的 Update() 打印功能,首先需要了解如何在 DOTS 中使用 ECS(Entity Component System)。DOTS的核心思想是将数据(组件)与行为(系统)分离,使用不同的方式来处理每帧更新的逻辑。
1. 创建一个组件(Component)
定义一个结构体,表示一个组件,用于存储打印信息
using Unity.Collections;
using Unity.Entities;public struct PrintMessageComponent : IComponentData {// 固定长度字符串,用于存储打印的消息public FixedString128Bytes printData;
}
2. 创建一个系统(System)
定义一个系统,处理所有包含 PrintMessageComponent 的实体
using Unity.Entities;
using Unity.Transforms;
using UnityEngine;partial class PrintMessageSystem : SystemBase {// 系统每帧更新时会调用此方法protected override void OnUpdate(){// 查询所有包含 LocalTransform 和 PrintMessageComponent 的实体foreach ((RefRW<LocalTransform> localTransform, RefRO<PrintMessageComponent> printMessageComponent)in SystemAPI.Query<RefRW<LocalTransform>, RefRO<PrintMessageComponent>>()){// 打印每个实体中的 printMessageComponent 内容Debug.Log(printMessageComponent.ValueRO.printData);}}
}
3. 触发打印
定义一个 MonoBehaviour 脚本,用于在场景中生成实体
using Unity.Entities;
using UnityEngine;public class EntitySpawner : MonoBehaviour
{//用于在 Inspector 面板中输入的字符串值public string value;// 内部类 Baker,用于将 MonoBehaviour 转换为实体private class Baker : Baker<EntitySpawner>{// Bake 方法会在编辑器模式下调用,将 MonoBehaviour 组件数据转化为实体组件数据public override void Bake(EntitySpawner authoring){// 创建一个动态使用的实体Entity entity = GetEntity(TransformUsageFlags.Dynamic);// 给实体添加 PrintMessageComponent 组件,并设置其 printData 为 authoring 中的值AddComponent(entity, new PrintMessageComponent{printData = authoring.value,});}}
}
4. 新建空物体挂载脚本
效果
旋转组件
定义一个旋转速度组件,用于存储每个实体的旋转速度
using Unity.Entities;// 定义一个实现 IComponentData 接口的结构体,用于表示旋转速度
public struct RotateSpeed : IComponentData {// 旋转速度的数值public float value;
}
定义一个旋转速度的 Authoring 类,用于在 Unity 编辑器中设置旋转速度的值
public class RotateSpeedAuthoring : MonoBehaviour {// 用于设置旋转速度的公共字段public float value;// Baker 类用于将 MonoBehaviour 组件转换为 ECS 的组件数据private class Baker : Baker<RotateSpeedAuthoring>{// 该方法在实体生成时被调用,负责将 MonoBehaviour 上的属性数据转化为 ECS 组件public override void Bake(RotateSpeedAuthoring authoring){// 获取当前的实体,并设置为动态使用(TransformUsageFlags.Dynamic)Entity entity = GetEntity(TransformUsageFlags.Dynamic);// 为实体添加 RotateSpeed 组件,并将作者的旋转速度值传递给它AddComponent(entity, new RotateSpeed{value = authoring.value, // 设置旋转速度});}}
}
定义一个旋转立方体系统,负责处理与旋转速度相关的逻辑
public partial struct RotatingCubeSystem : ISystem
{// 系统创建时调用的方法public void OnCreate(ref SystemState state){// 在系统创建时要求必须有 RotateSpeed 组件才能进行更新state.RequireForUpdate<PrintMessageComponent>();}// 系统更新时调用的方法void OnUpdate(ref SystemState state){// 查询所有包含 LocalTransform 和 RotateSpeed 组件的实体foreach ((RefRW<LocalTransform> localTransform, RefRO<RotateSpeed> rotateSpeed)in SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotateSpeed>>()){// 通过当前的旋转速度(rotateSpeed)来更新实体的旋转// 使用 RotateY 方法按照 Y 轴旋转,旋转角度由旋转速度与 DeltaTime 计算得出localTransform.ValueRW = localTransform.ValueRO.RotateY(rotateSpeed.ValueRO.value * SystemAPI.Time.DeltaTime);}}
}
挂载脚本
效果
优化
新增OnCreate,当系统第一次被创建时,OnCreate
方法会被调用。在此方法中,调用 state.RequireForUpdate<RotateSpeed>()
来确保系统只会在实体中包含 RotateSpeed
组件时进行更新。这保证了只有需要旋转的实体会触发系统的更新。
using Unity.Transforms;
using Unity.Entities;// 定义一个旋转立方体系统,负责处理与旋转速度相关的逻辑
public partial struct RotatingCubeSystem : ISystem
{// 系统创建时调用的方法public void OnCreate(ref SystemState state){// 在系统创建时要求必须有 RotateSpeed 组件才能进行更新state.RequireForUpdate<RotateSpeed>();}// 系统更新时调用的方法void OnUpdate(ref SystemState state){// 查询所有包含 LocalTransform 和 RotateSpeed 组件的实体foreach ((RefRW<LocalTransform> localTransform, RefRO<RotateSpeed> rotateSpeed)in SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotateSpeed>>()){// 通过当前的旋转速度(rotateSpeed)来更新实体的旋转// 使用 RotateY 方法按照 Y 轴旋转,旋转角度由旋转速度与 DeltaTime 计算得出localTransform.ValueRW = localTransform.ValueRO.RotateY(rotateSpeed.ValueRO.value * SystemAPI.Time.DeltaTime);}}
}
多线程并行执行
系统仍然在主线程运行
修改代码,使用rotatingCubeJob.ScheduleParallel();
系统在多线程并行执行
public partial struct RotatingCubeSystem : ISystem
{// 系统创建时调用的方法public void OnCreate(ref SystemState state){// 在系统创建时要求必须有 RotateSpeed 组件才能进行更新state.RequireForUpdate<RotateSpeed>();}// 系统更新时调用的方法void OnUpdate(ref SystemState state){// 创建旋转立方体的工作作业,并传递时间增量信息RotatingCubeJob rotatingCubeJob = new RotatingCubeJob{deltaTime = SystemAPI.Time.DeltaTime // 获取每帧的时间增量};// 调度工作作业// rotatingCubeJob.Schedule();// state.Dependency = rotatingCubeJob.Schedule(state.Dependency);//在多个线程上并行rotatingCubeJob.ScheduleParallel();}// 定义旋转立方体的作业,IJobEntity 用于操作实体public partial struct RotatingCubeJob : IJobEntity{public float deltaTime; // 存储时间增量,用于旋转计算// 执行作业时的具体操作:旋转实体public void Execute(ref LocalTransform localTransform, in RotateSpeed rotateSpeed){// 旋转的倍率系数float power = 1f;// 根据旋转速度和时间增量来旋转实体localTransform = localTransform.RotateY(rotateSpeed.value * deltaTime * power);}}
}
使用 Burst 编译器优化性能
加入关键字[BurstCompile]
即可,使用 Burst 编译器优化性能
记得查看Burst编译器是否勾选使用
屏蔽系统间的干扰
前面我们实现了一个打印和旋转系统,虽然我们的打印功能已经被隐藏了
但是如果通过在打印系统OnUpdate里打印日志
你会发现系统其实仍然在执行
我们可以加上[DisableAutoCreation]
特性,屏蔽该系统的干扰,这样这个系统就不会再运行了
调试窗口
-
Hierarchy : 显示当前场景中的实体信息
-
Components :显示所有 ComponentData 的结构体信息。
-
Systems:显示当前运行的所有 System 信息,能看到其使用了哪些实体。
-
Archetypes:显示原型信息。
-
Journaling:日志记录,可以显示用了哪些方法,有哪些实体、ComponentData之类,应该是可以用来分析性能,但我还没有仔细研究。
后续
DOTS基础篇就先写到这里了,如果你感兴趣也和传统方式进行性能比较,这里我就不写了。
后续我看有时间再考虑写写一些进阶知识或者dots项目实战内容。
完结
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,你的每一次支持
都是我不断创作的最大动力。当然如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!
好了,我是向宇
,https://xiangyu.blog.csdn.net
一位在小公司默默奋斗的开发者,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!如果你遇到任何问题,也欢迎你评论私信或者加群找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~