Unity的Dots技术入门

前言

看过Dots技术宣传片,当时确实被震惊到了,脑袋里闪过一句话:时代变了!大人。DOTS是Data-Oriented Technology Stack(数据导向的技术栈):借助Unity全新高性能多线程式数据导向型技术堆栈(DOTS),充分利用当今的多核处理器。游戏的运行速度会变得更快。

1.Unity的Dots由来

这种技术在游戏领域最先成功的案例来自守望先锋,各位可以去看看《守望先锋》架构设计与网络同步,ECS全称是Entity Component System,即实体、组件和系统。

  • 实体一般用途的对象,可以用唯一ID来标识。
  • 组件是一组数据,可以是实体一部分,也可以是实体和世界交互的中间数据(注意:没有功能)。实体由若干组件组成的,否则它是空盒子。
  • 系统基于实体中的特定组件来实现特定功能,但是系统是集中批量处理所有的实体的。

ECS这种架构有什么好处呢?相比OOP方式实现Component,ECS中相同组件的数据是连续的,System集中处理这些组件,可以方便利用CPU Cache提升效率。由于System处理的可能是几个组件,ECS将组件尽量的最小化(否则会有很多数据冗余),这样就实现数据的扁平化。同时System间禁止相互调用,最大程度的实现代码解耦。而OOP最被诟病的调用层次深,结构复杂等问题都被解决了。
Dots就是由实体组件系统(ECS)、任务系统(Job System)、Burst Compiler编译器三部分组成。DOTS保证相同类型组件在内存中都是顺序排列,极大程度增加缓存的命中率,此外配合任务系统(Job System)让开发者无需头疼多线程同时访问数据需要手动加解锁的麻烦,最终加持Burst Compiler让性能飞起来。

2.与OOP编程对比

在对比之前,需要了解CPU相关概念:

  • 1.CPU处理数据速度非常快,内存条和硬盘跟不上它的速度。
  • 2.CPU自身有三级缓存,用来提高CPU获取数据速度。n级缓存最快(n是1->3),但容量最小。
  • 3.内存是指CPU拿取数据的最开始的地方,CPU访问内存速率远小于三级缓存速率。
  • 4.CPU操作数据会从三级缓存中取得数据,速度非常快,但有些情况下数据不在三级缓存中(缺页中断),就需要寻址到内存中,并且把目标数据重新放到三级缓存中,提高下一次访问速度。

很多传统游戏引擎是基于面向对象来设计的,游戏世界中的东西都是对象(GameObject),每个对象都有叫做Update方法,游戏引擎遍历所有对象,依次调用其Update方法。有些引擎甚至定义多种Update方法,在同一帧的不同时机去调用(Unity通过反射机制调用这些Update函数)。 OOP编程模式其实是有极大的缺陷的。因为游戏对象由很多部分聚合而成,引擎的功能模块很多,不同模块关注的部分往往互不相关。比如音效模块数据并不需要关心物理碰撞数据、渲染模块数据不关心游戏逻辑数据,对于某种模块没有必要的数据保存到CPU中,我们可以称为垃圾数据。从自然意义上说,把游戏对象属性聚合是很正常的事情,对象生命期管理也是最合理的方式。但这种对象大量存在时就会严重性能问题,接下来展示Unity的MonoBehaviour所继承的Component子类。

exp1

其实MonoBehaviour继承关系是这样的,Behaviour->Component->Object,这些子类全部加起来的数据、属性可不是以上这么点,有兴趣可以进去看看这些子类都定义了什么数据、属性、方法。MonoBehaviour对象过多会导致大量无用数据占据CPU的缓存(比如控制transform旋转,里面却有音效、物理、相机等等属性),结论是传统引擎的设计思路对CPU缓存不太友好导致利用率不高,出现CPU缺页中断情况会比较频繁,下面给出图解:

exp2

使用Ecs技术对CPU缓存利用率有质的提高,因为保存到CPU缓存的数据都比较纯净(ECS中的实体、组件),这样可以有效降低CPU缺页中断,所以Dots相对于OOP编程有以下优点:

  • 1.ECS对CPU缓存友好,减少CPU缺页中断,防止CPU获取数据慢,从而CPU出现等待情况。
  • 2.JobSystem编写多线程代码可以提供高性能,显著提高帧速率和延长移动设备的电池寿命。
  • 3.Burst Compiler,用于生成高度优化的本地代码。

3.Dots官方案例

拉取Unity官方案例AngryBots_ECS,博主使用Unity2020.2.0a打开案例工程,发现一大堆的问题和报错(剩下渲染管线方面问题无法解决,但不影响运行),接下来分析如何一步步解决这些问题。error首先是Hybrid Renderer版本问题,看报错好像依赖了High Definition RP包(但是下载RP后还是不行),所以这里尝试把Hybrid Renderer更新到最新的版本,于是第一个报错就消失了。出现了以下报错就和接龙一样…error1Job System不存在OnCreateManager接口,可以进入到基类ComponentSystemBase里,发现了OnCreate生命周期函数,可以重写它来代替报错函数。error2World.Active去代替World.DefaultGameObjectInjectionWorld,可能Active是老版本的命名方式,GameObjectConversionUtility.ConvertGameObjectHierarchy接口所需参数也不一致,可以改成一下代码段:

	var settings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, null);
	manager = World.DefaultGameObjectInjectionWorld.EntityManager;
	bulletEntityPrefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(bulletPrefab, settings);

error3最后TimeData.deltaTime改成大写开头即可,全部报错改好后会发现运行时还有问题,可能是Burst Compiler的问题,重启Unity后所有问题都得到解决了。
如果你终于走到这步,可能会很高兴。但启动游戏后会发现看不到子弹和敌人…不使用Ecs是可以看到子弹的,如下图所示:

exp3

ps:莫名其妙死亡的话,只是看不到的敌人将主角杀死,上面说过可能是渲染管线的问题,经过在下长时间努力,大概知道问题所在了。尝试Edit/Project Settings/Graphics下将Scriptable Render Pipeline Settings设置成none,可以看到一下画面(可以看到子弹、敌人)。

exp4

具体解决方案不太清楚,如果有人知道麻烦下面留言,万分感谢~
感觉GameObjectConversionSettings或Hybrid Rendering问题,但不知道如何修改。
案例演示视频

4.实战DOTS技术

使用Unity2020.2.0a创建新工程后,需要导入hybrid、entities(entities依赖jobs、burst),如下图所示:exp5低版本Unity可能不存在此文件,并且manifest.json也不存在jobs、burst这些依赖包,这时需要手动拉取。接下来讲述下3种拉取方式:

  • 使用Package Manager拉取需要包(可能搜索不出来)。
  • 打开Package Manager后,窗口左上角有个加号,点开有三个选项Add package from xxx(有时低版本不出现Package Manager)。
  • 在manifest.json里添加两个依赖包(最稳定的方法):
    “com.unity.entities”: “0.14.0-preview.18”,
    “com.unity.rendering.hybrid”: “0.8.0-preview.18”,

依赖包全部导入后,接下来先尝试用DOTS技术写多个Cube旋转吧。

  • 1.创建旋转方块组件:
using Unity.Entities;

public struct RotationCubeComponent : IComponentData
{
    public float speed;
}

注意必须是结构体,以后文章会分析为什么是结构体,如何才可以使用类,结构体比类的优点等等。

  • 2.创建旋转方块事件
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.Jobs;

public class RotationCubeSystem : SystemBase
{
    protected override void OnUpdate()
    {
        float deltaTime = Time.DeltaTime;
        Entities
           .WithName("RotationCubeSystem")
           .ForEach((ref RotationCubeComponent rotationSpeed, ref Rotation rotation) =>
           {
               rotation.Value = math.mul(
                   math.normalize(rotation.Value),
                   quaternion.AxisAngle(math.up(), rotationSpeed.speed * deltaTime));
           })
           .ScheduleParallel();
    }
}

ScheduleParallel已经使用Job System多线程技术了,不使用多线程处理可以自行百度。

  • 3.创建实体管理器
using Unity.Entities;
using UnityEngine;

public class EntitiesManger : MonoBehaviour, IConvertGameObjectToEntity
{
    public float FloCubeSpeed = 10f;

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        var data = new RotationCubeComponent { speed = FloCubeSpeed };
        dstManager.AddComponentData(entity,data);
    }
}

GameObject转Entity,继承IConvertGameObjectToEntity重写Convert函数。

  • 4.批量生成实体
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

public class EntitiesBuilder : MonoBehaviour
{
    public GameObject cube;

    void Start()
    {
        var settings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, null);
        var prefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(cube, settings);
        var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;

        for (var x = 0; x < 100; x++)
        {
            for (var y = 0; y < 100; y++)
            {
                var instance = entityManager.Instantiate(prefab);
                var position = transform.TransformPoint(new float3(x * 1.3F, noise.cnoise(new float2(x, y) * 0.21F) * 2, y * 1.3F));
                entityManager.SetComponentData(instance, new Translation { Value = position });
            }
        }
    }
}

将EntitiesManger和Convert To Entity挂载到Cube之后,制作成Prefab即可。然后将EntitiesBuilder随便挂载到场景的物体。
具体原理分析和DOTS深度的东西后面文章会说明,至于官方案例子弹为什么不显示,博主知道具体原因也会更新的。

5.总结

既然DOTS这么牛逼,为啥还没普及?很简单,DOTS现阶段在完成度上就是接近废品的残次品,虽然经过Unity长达近三年的宣传,DOTS到今天完成度依然极低,基本上处于不可用的状态,一款游戏引擎必不可少的部分可简单列为下面八个,要把Dots技术推动起来,游戏引擎要进行大量重构,或者保留传统的MonoBehaviour将Dots技术插接进去,接下来看看Unity对这八大模块完成情况。

  • Physics-3分 物理自然是游戏引擎不可或缺的部分,DOTS配套的Unity Physics处于非常早期的状态,Authoring工具几乎为零,甚至Joint或者CharacterConroller都要从Sample里找代码,由于它提供了一套新的API,开发过程只能用极难使用来形容,来看看Trigger事件代码就知道了,而使用过程中也是Bug层出不穷,经常会遇到类似Mesh Collider Bake错误,而与之同期宣传的Havok Physics for Unity更是奇妙,在长达半年的时间里,只是单纯引入Havok便会导致HDRP的渲染出问题.而更进阶的布料、粒子、地形等也处于几乎为零的状态。
  • Graphics-3分 DOTS与之配套的是图形系统是Hybrid Renderer,不过目前也只支持Mesh Renderer,什么Particle、Trail、VFX都还没影儿,更可惜的是官方现在优先关注于HDRP兼容性(虽然也不怎么样),URP或者Built-In Pipeline就基本别想了,遇到了问题也只能双手一摊。
  • Audio-0分 Unity目前只开发了底层的 DSP 系统,上层的DOTS Audio说了一两年了连影子都没有,完成度接近于零。
  • Animation-0分 Unity Animation依然处于极早期的开发中,DOTS Sample展示了些最基本的使用,类似 Animator 这样的高层方案现在还没影儿。
  • Network-2分 也是Unity吹了两年的东西,DOTS Sample中用的NetCode只不过把FPSSample中的部分代码挪过去了,目前版本号 0.0.4,意思就是太早期了别用。
  • UI-0分 IMGUI,UI Element和UGUI三个Unity的产品均不支持DOTS。
  • AI-0分 Navmesh,Ml-Agent,Ai Planner三个Unity的产品均不支持DOTS。
  • Input-2分 老的Input System相对简单与DOTS没啥关系,可新的Input System居然也不支持DOTS,幸运的是InputSystem相对独立,嫁接进DOTS并不困难。

对于游戏引擎模块全部改成DOTS是有难度的,不然Unity公司对于DOTS支持程度就不是如此了(毕竟对游戏行业算是革命),在下大胆预言未来有某个天才或Unity公司完全实现了DOTS并且达到可以商用的地方,将会是巨大的利润和商机。大家可能比较关心的是目前DOTS能不能接入游戏,只能说部分模块可以使用(某些经典批处理场景),个人建议不要把还没成型的技术接入到项目中,现在DOTS还有很多限制,知道有这个东西即可。参考资料和案例:DOTS开源案例DOTS参考资料DOTS实用案例

### Unity DOTS 入门教程 #### 了解 Data-Oriented Technology Stack (DOTS) Data-Oriented Technology Stack 是一组用于构建高效、可扩展的游戏和技术应用程序的技术栈。它旨在利用现代硬件特性,特别是多核处理器的能力来提高性能。 #### 使用 Unity 版本和 Package Manager 安装必要组件 对于 Unity 2020.X 或更高版本,在 Package Manager 中可以通过添加特定包 URL 来安装必要的 DOTS 组件[^2]: - `com.unity.dots.editor` - `com.unity.physics` - `com.unity.entities` - `com.unity.rendering.hybrid` 这些工具提供了创建基于实体系统的逻辑所需的功能支持。 #### 创建并配置 Entities Prefab 为了使对象能够参与数据导向型编程模型,需将 GameObject 转换为 Entity 并设置相应的组件。例如,可以将 EntityManager 和 ConvertToEntity 组件附加到 Cube 上,并将其转换为预制件(Prefab)[^3]。这一步骤允许开发者定义如何管理游戏世界内的大量实例化对象的数据结构。 #### 编写简单的 Jobsystem 示例代码 下面是一个简单的工作系统(Job System)实现示例,展示了如何分配任务给多个线程以加速处理速度: ```csharp using Unity.Burst; using Unity.Collections; using Unity.Jobs; [BurstCompile] struct SimpleJob : IJobParallelFor { public NativeArray<float> data; public void Execute(int index){ data[index] *= 2f; // 对数组中的每一个元素乘以2 } } void ScheduleSimpleJob(){ var job = new SimpleJob{ data = someNativeArray }; JobHandle handle = job.Schedule(someLength, 64); // 启动作业并将工作划分为大小为64的批次 } ``` 此代码片段使用了 Burst 编译器优化后的平行循环操作,从而提高了大规模数值运算的速度。 #### 实践案例:Unity DOTS-MAN 小游戏项目 通过实际动手制作一个小游戏如 "DOTS-MAN", 可加深对上述概念的理解。该项目不仅涉及到了基本的 ECS 构建模式,还可能包括碰撞检测、动画控制等功能模块的设计与集成。
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值