【推荐100个unity插件之36】Unity6使用DOTS基础篇——Entities(非常适合做一些弹幕射击游戏)

前言

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/[email protected]/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

一位在小公司默默奋斗的开发者,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!如果你遇到任何问题,也欢迎你评论私信或者加群找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~
在这里插入图片描述