美文网首页
转换你的游戏到DOTS(一)

转换你的游戏到DOTS(一)

作者: Uonfan | 来源:发表于2020-03-04 13:16 被阅读0次

    本文转自Unity Connect博主 雨松MOMO

    Unite2019哥本哈根学习笔记之Converting your game to DOTS

    将传统开发的游戏模式转成DOTS的方式,DOTS全称(Data Oriented Tech Stack) 面向数据的技术堆栈

    image

    DOTS能干什么?大家看看下面发射这么密集的子弹,如果是传统GameObject的方式肯定会卡死,然而Dots却非常流畅。 可以在Github上下载这个学习工程 https://github.com/UnityTechnologies/AngryBots_ECS

    image

    DOTS是什么?

    1. Job System :任务系统

    2. Entity Component System(ECS) :实体组件系统

    3. Burst Compiler :Burst编译器 ,也是LLVM编译器

    4. Project Tiny :Unity基于以上3个构成的H5小游戏解决方案

    image

    需要注意的是以上任务系统、实体组件系统、Burst编译器各自都是独立的系统可以单独使用,它们的集合称之为DOTS.

    image

    为什么要在项目中使用DOTS?

    image

    传统面向对象模式下内存就像下图一样,杂乱无序的排放在内存中。当CPU需要读取某个数据时,就要先从这一堆杂乱无序的内中找到需要的内存,然后将它读取并且移动到L1缓存中。缓存的内存大小是固定的远比内存要小很多,拿进来一个内存到缓存中必然就要移除一个。游戏进行中需要读取的内存多了,如果大量的都无法从缓存中拿到,就要重走内存读取在放入L1缓存的流程,势必效率就很差。称之为Cache miss。

    image

    Entity Component System(ECS) :实体组件系统 ,做的事情就是将传统游戏对象杂乱无序的组件归类,将相同组件整齐排列在内存中。当我们开始遍历相同组件时并不需要一个个从内存中读取,可以指定一个长度一次性全读进来放入缓存中。

    image

    ECS中我们以块的形式进行保存,每个块有16K字节,每个块也整齐的排列在一起。

    image

    接着就轮到Job System出场了,任务系统中会找到自己关心的组件依次遍历它们,执行自己的任务。

    image

    任务之间是可以并行的,这样我们就可以充分利用CPU的多核心,并行任务了。其实就是多线程,多线程中数据很可能出现竞争的情况,所以传统开发需要程序员手动对某个数据“加锁“ 然而Unity实现了一套更好的方案,开发者可以不用写加解锁的代码,通过提供这样的标签[OnlyRead] [OnlyWrite] 系统会自动处理,并且保证数据不会被竞争改写。原理大概是[OnlyRead] 只读数据线程访问是完全安全的,[OnlyWrite] 可写数据则比较危险,需要做好数据的同步。

    image

    最后轮到Burst编译器出场了,它会编译Job中的代码提升它的性能。

    image

    当项目中使用DOTS后应该怎么做?

    image

    当项目中使用DOTS后应该怎么做? 1.解决一个特定问题

    2.绕过技术障碍

    3.一点点来

    4. 对DOTS目前的限制和DOTS未来的功能有清晰的了解。 说人话的意思就是目前DOTS也还在开发中,大家要时刻关注DOTS最新更新的信息,才能正确的使用DOTS(2019.3目前DOTS已经趋于稳定,但是讲这个PPT的时候DOTS还并不太稳定)

    image

    DOTS和传统开发确实有些区别,你需要仔细思考你的逻辑和数据。 不过作者说的这个图片梗,作为Chinese确实Get不到,哈哈哈~~

    image

    重点来了,让我们开始集成DOTS进你的游戏中。

    image

    假如,我们有3个脚本,PlayerMovementAndLook.cs 负责处理角色的移动、旋转、生命值、EnemyBehaviour.cs负责敌人的移动、生命值、ProjectileBehaviour.cs负责导弹的移动、生命值。

    image

    这种做法是不太好的,聪明的你仔细想想。

    image

    3个脚本中、速度、生命值、刚体、被重复了3份,这将直接导致每个脚本都需要将这三个对象保存起来,内存长度会更长,显然缓存命中率不友好。而且毫无复用性可言,如果现在新加一个魔法值比变量,主角、怪物的类中都需要添加这个新变量,代码的维护性更加麻烦。

    image

    再来看看代码中的方法,由于敌人需要旋转、移动两个功能,但是导弹只需要移动功能,被迫只能将重复的逻辑分别写在两个类中。

    image

    在来看看敌人和主角的碰撞,依然有部分代码是重复的,被迫还是得写两份。

    image

    ECS的核心就是将红色部分和黄色部分代码分成两个系统,一个系统负责减血、另一个系统负责处理死亡,这样代码的利用率就非常高。还是上面的代码,如果我们写了减血系统和死亡系统后,如果有一天需要添加一个加血功能,我们并不需要修改减血系统和死亡系统的代码,直接添加一个加血系统就可以,这将大幅度提升代码的利用率。 下面我们打开AngryBots_ECS工程。 我们需要将传统的Prefab制作的子弹变成实体对象,打开子弹Prefab挂载的ProjectileBehaviour.cs 添加IConvertGameObjectToEntity接口。

    image

    然后将原本写在一起的speed 、lifeTime变量保存在不同的组件MoveForward、MoveSpeed、TimeToLive中,将原本序列化在Prefab面板上的值拷贝进实体组件中。分开保存一方面可以起到组件的复用性,另一方面通过增加缓存的命中率。

    image

    加载的时候,通过GameObjectConversionUtility将Prefab转换成Entity对象。

    image

    有了Entity对象后就可以通过EntityManager对它实例化了,并且绑定需要的实体组件。

    image

    每发射一发子弹时就可以在Entity Debugger窗口中看见实体对象以及实体组件信息了,当选择某个实体组件右侧面板中会显示它的序列化信息。

    image

    通过设置totalAmout可以同时实例化多个实体,并且在循环中对每个实体组件设置坐标信息。

    image

    修改了发射频率,6W个子弹毫无压力!

    image

    接着我们需要添加怪物了,怪物需要始终朝向主角的逻辑。我们来将怪物朝向和旋转放到Job多线程中完成。IJobForEach就是只去遍历保存Translation位置和Rotation旋转的组件,然后在Execute方法中执行怪物的朝向。float3 playerPosition表示主角的坐标,需要外部传进来。

    image

    在OnUpdate中首先判断主角有没有死,如果没有死亡就开始执行Job任务,Job.Schedule表示异步执行。

    image

    TurnTowardsPlayerSystem的TurnJob中遍历了同时包含Translation位置和Rotation旋转的组件敌人,但是子弹和主角也包含了这个两个组件,所以有时候我们需要在Job中进行区分。 我们可以创建一个空的组件EnemyTag,绑定给怪物。

    image

    RequireComponentTag就表示遍历的实体组件,必须前置包含EnemtyTag组件,[BurstCompile]表示开始Burst编译器,加载Job代码的执行效率

    image

    完美!

    image image

    2000多个敌人·实体

    image

    2.7W颗子弹实体

    image

    发射子弹的瞬超过60FPS毫无压力!

    image

    原文链接:https://connect.unity.com/p/zhuan-huan-ni-de-you-xi-dao-dots-yi?app=true (内含PPT)

    欢迎戳上方原文链接,下载Unity官方技术社区app,在线技术答疑,发现更多资源干货!

    相关文章

      网友评论

          本文标题:转换你的游戏到DOTS(一)

          本文链接:https://www.haomeiwen.com/subject/xyrmlhtx.html