本文转自Unity Connect博主 雨松MOMO
Unite2019哥本哈根学习笔记之Converting your game to DOTS
将传统开发的游戏模式转成DOTS的方式,DOTS全称(Data Oriented Tech Stack) 面向数据的技术堆栈
DOTS能干什么?大家看看下面发射这么密集的子弹,如果是传统GameObject的方式肯定会卡死,然而Dots却非常流畅。 可以在Github上下载这个学习工程 https://github.com/UnityTechnologies/AngryBots_ECS
DOTS是什么?
Job System :任务系统
Entity Component System(ECS) :实体组件系统
Burst Compiler :Burst编译器 ,也是LLVM编译器
Project Tiny :Unity基于以上3个构成的H5小游戏解决方案
需要注意的是以上任务系统、实体组件系统、Burst编译器各自都是独立的系统可以单独使用,它们的集合称之为DOTS.
为什么要在项目中使用DOTS?
传统面向对象模式下内存就像下图一样,杂乱无序的排放在内存中。当CPU需要读取某个数据时,就要先从这一堆杂乱无序的内中找到需要的内存,然后将它读取并且移动到L1缓存中。缓存的内存大小是固定的远比内存要小很多,拿进来一个内存到缓存中必然就要移除一个。游戏进行中需要读取的内存多了,如果大量的都无法从缓存中拿到,就要重走内存读取在放入L1缓存的流程,势必效率就很差。称之为Cache miss。
Entity Component System(ECS) :实体组件系统 ,做的事情就是将传统游戏对象杂乱无序的组件归类,将相同组件整齐排列在内存中。当我们开始遍历相同组件时并不需要一个个从内存中读取,可以指定一个长度一次性全读进来放入缓存中。
ECS中我们以块的形式进行保存,每个块有16K字节,每个块也整齐的排列在一起。
接着就轮到Job System出场了,任务系统中会找到自己关心的组件依次遍历它们,执行自己的任务。
任务之间是可以并行的,这样我们就可以充分利用CPU的多核心,并行任务了。其实就是多线程,多线程中数据很可能出现竞争的情况,所以传统开发需要程序员手动对某个数据“加锁“ 然而Unity实现了一套更好的方案,开发者可以不用写加解锁的代码,通过提供这样的标签[OnlyRead] [OnlyWrite] 系统会自动处理,并且保证数据不会被竞争改写。原理大概是[OnlyRead] 只读数据线程访问是完全安全的,[OnlyWrite] 可写数据则比较危险,需要做好数据的同步。
最后轮到Burst编译器出场了,它会编译Job中的代码提升它的性能。
当项目中使用DOTS后应该怎么做?
当项目中使用DOTS后应该怎么做? 1.解决一个特定问题
2.绕过技术障碍
3.一点点来
4. 对DOTS目前的限制和DOTS未来的功能有清晰的了解。 说人话的意思就是目前DOTS也还在开发中,大家要时刻关注DOTS最新更新的信息,才能正确的使用DOTS(2019.3目前DOTS已经趋于稳定,但是讲这个PPT的时候DOTS还并不太稳定)
DOTS和传统开发确实有些区别,你需要仔细思考你的逻辑和数据。 不过作者说的这个图片梗,作为Chinese确实Get不到,哈哈哈~~
重点来了,让我们开始集成DOTS进你的游戏中。
假如,我们有3个脚本,PlayerMovementAndLook.cs 负责处理角色的移动、旋转、生命值、EnemyBehaviour.cs负责敌人的移动、生命值、ProjectileBehaviour.cs负责导弹的移动、生命值。
这种做法是不太好的,聪明的你仔细想想。
3个脚本中、速度、生命值、刚体、被重复了3份,这将直接导致每个脚本都需要将这三个对象保存起来,内存长度会更长,显然缓存命中率不友好。而且毫无复用性可言,如果现在新加一个魔法值比变量,主角、怪物的类中都需要添加这个新变量,代码的维护性更加麻烦。
再来看看代码中的方法,由于敌人需要旋转、移动两个功能,但是导弹只需要移动功能,被迫只能将重复的逻辑分别写在两个类中。
在来看看敌人和主角的碰撞,依然有部分代码是重复的,被迫还是得写两份。
ECS的核心就是将红色部分和黄色部分代码分成两个系统,一个系统负责减血、另一个系统负责处理死亡,这样代码的利用率就非常高。还是上面的代码,如果我们写了减血系统和死亡系统后,如果有一天需要添加一个加血功能,我们并不需要修改减血系统和死亡系统的代码,直接添加一个加血系统就可以,这将大幅度提升代码的利用率。 下面我们打开AngryBots_ECS工程。 我们需要将传统的Prefab制作的子弹变成实体对象,打开子弹Prefab挂载的ProjectileBehaviour.cs 添加IConvertGameObjectToEntity接口。
然后将原本写在一起的speed 、lifeTime变量保存在不同的组件MoveForward、MoveSpeed、TimeToLive中,将原本序列化在Prefab面板上的值拷贝进实体组件中。分开保存一方面可以起到组件的复用性,另一方面通过增加缓存的命中率。
加载的时候,通过GameObjectConversionUtility将Prefab转换成Entity对象。
有了Entity对象后就可以通过EntityManager对它实例化了,并且绑定需要的实体组件。
每发射一发子弹时就可以在Entity Debugger窗口中看见实体对象以及实体组件信息了,当选择某个实体组件右侧面板中会显示它的序列化信息。
通过设置totalAmout可以同时实例化多个实体,并且在循环中对每个实体组件设置坐标信息。
修改了发射频率,6W个子弹毫无压力!
接着我们需要添加怪物了,怪物需要始终朝向主角的逻辑。我们来将怪物朝向和旋转放到Job多线程中完成。IJobForEach就是只去遍历保存Translation位置和Rotation旋转的组件,然后在Execute方法中执行怪物的朝向。float3 playerPosition表示主角的坐标,需要外部传进来。
在OnUpdate中首先判断主角有没有死,如果没有死亡就开始执行Job任务,Job.Schedule表示异步执行。
TurnTowardsPlayerSystem的TurnJob中遍历了同时包含Translation位置和Rotation旋转的组件敌人,但是子弹和主角也包含了这个两个组件,所以有时候我们需要在Job中进行区分。 我们可以创建一个空的组件EnemyTag,绑定给怪物。
RequireComponentTag就表示遍历的实体组件,必须前置包含EnemtyTag组件,[BurstCompile]表示开始Burst编译器,加载Job代码的执行效率
完美!
2000多个敌人·实体
2.7W颗子弹实体
发射子弹的瞬超过60FPS毫无压力!
原文链接:https://connect.unity.com/p/zhuan-huan-ni-de-you-xi-dao-dots-yi?app=true (内含PPT)
欢迎戳上方原文链接,下载Unity官方技术社区app,在线技术答疑,发现更多资源干货!