Unity项目技术方案Dots架构方案简介
Posted Peter_Gao_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity项目技术方案Dots架构方案简介相关的知识,希望对你有一定的参考价值。
DOTS全称是Data-Oriented Tech Stack,翻译过来就是多线程式数据导向型技术堆栈(DOTS),它由任务系统(Job System)、实体组件系统(ECS)、Burst Compiler编译器三部分组成。
ECS + JobSystem + BurstCompile = 高性能 + 多线程 + 编译层面优化
DOTS保证相同类型组件在内存中都是顺序排列,极大程度增加缓存的命中率,此外配合任务系统(Job System)让开发者无需头疼多线程同时访问数据需要手动加解锁的麻烦,最终加持Burst Compiler让性能飞起来。
ECS、JobSystem、Burst,这三个组件是可以相互独立使用,并不是说使用一个这三个必须同时用,你可以任意选择其中一个来进行使用,用于不同的应用场景。
如果说我们需要使用JobSystem,其实它跟ECS没有太大的关系,你可以在ECS里面用,也可以不在ECS里面用,只要是需要并行计算的地方都可以使用。
Burst也一样,它也不需要配合ECS使用,不需要跟并行计算捆绑使用,它的作用仅仅是对于一些复杂的计算密集的东西去进行编译器优化,来达到性能提升。
只要是计算密集型的东西,都可以使用Burst,同步方法也是可以的。
关于ECS,一个比较大的误解,可能大家会觉得用ECS之后,所有东西都可以用ECS来写,就会想UI的业务逻辑怎么用ECS实现。大可不必,并不是说用ECS,所有东西全部都要用ECS来做,而是大家可以根据项目需求选择其中适合那部分来用ECS去写,剩下的部分还是使用传统的面向对象的方式去写,没有任何问题,只要用代码稍微结合一下就可以了。
在《黑暗之潮》当中利用ECS的例子,我们通过ECS渲染了大量的怪物。我们游戏里面怪物通常有一个特点,一组怪由几名精英配合一两种大量的存在的爪牙组成的,大家可以看到右面的图只有三种怪,如果说用默认的SkinMeshRenderer的话,就有一个非常严重的问题,没有办法合批了,画面上面有多少个怪,有多少个DrawCall而且Animator开销也不小,还有一个问题,GameObject为.Instantiate开销也是比较大的,如果说我要同时刷出来三四十只怪的话,肯定会卡顿,用ECS就能比较好的解决这三个问题。
使用ECS先把整个动画信息去烘焙到这么一张动画贴图上面,在GPU当中进行蒙皮操作,我们再通过JobSystem和Burst实现视锥剔除和动画系统的更新,最后我们再在面向对象那块业务逻辑那块控制ECS Enity就可以了。也就是说ECS的部分,我们只是提供渲染的和动作的结构,其他部分业务逻辑还是完全用面向对象去实现的,相当于各取所长。
用ECS最大的好处就是性能。
首先第一个,因为我们采用了GPU蒙皮,整个DrawCall的数量下降到有几种怪就是几个DrawCall,这个就非常好了。实例化也是非常快,ECS基本上就是无感的,在极端机上消耗,即便同时刷一千只怪也不足1毫秒,借助Burst力量类似于视锥剔除这些计算量比较大的操作,在低端机上也是可以忽略不计的。
大家可以看到下面的截图,演示我们整个动画更新阶段,也是同样在骁龙450 SoC上测的,100只怪左右的情况,动画整个更新过程只用了0.008毫秒,这就是忽略不计,根本不需要考虑的一个量级。通过ECS,我们画面上怪物的渲染完全取决于GPU本身的渲染性能,CPU的开销完全不需要去考虑了,所以也不会出现卡顿。
第二个,我们通过Jobsystem去实现了怪物击飞的效果,大家可以看到这个怪物被打下悬崖,它如果说碰到墙壁必须要被墙壁挡下来,需要进行一些物理运算,如果直接使用Unity的Ragdoll也就是布娃娃系统,它的物理计算非常复杂,对于低端机会造成比较大的性能负担。我们把这个过程稍微简化了一下,所有的这些怪物在被击飞的时候,使用的是预先制作好的动画,我们只需要计算它的运行轨迹就行了。
我们首先用Job去并行计算这些怪物的分析轨迹,再通过Unity提供的多线程Raycast方法去进行射线检测来判断它是否撞到墙或者碰到地面了。最后如果说我们还有一些非ECS了对象,我们可以在计算完毕之后再通过一个单独的Job把这个所有GameObject的位置给同步一下就可以了。
第三个,我们通过Burst实现的就是射线技能,这个东西看上去很简单,实际上需要对整个场景以及所有的怪物和其他对象产生交互。射线打到墙上能够实时产生反映,我们这个东西需要每帧对整个场景进行射线检测,整个计算过程实际上是开销比较大的。通过Burst我们相当于把这个东西做成了一个Job,通过Job.Run的方法去直接进行调用,就是在当前这个线程进行的操作。
使用起来跟一个静态方法没有太大的差别,还有像大家看到的这个技能,会有大量的子弹,对这些子弹我们同样需要进行运行轨迹的计算。通过Burst非常有效的把这两个计算开销降的非常低,Burst开启之后,它的性能提升基本能上百倍,通过刚才也提到Job.Run的方式实现同步调用,我们在整个计算流程当中不需要开额外的线程,直接在当前线程,单个静态方法直接调用就可以了,也是非常方便的。
大家可以看一下开启和不开启Burst效果的差别,左边是开启,右边是不开启,我们在一个计算体系化模型工具中测试,左边只用241毫秒,右边用了20毫,真是一百倍的差别。而不是说它用了多线程所以更快,大家可以看到每个线程都快了100倍,如果算总耗时,这边用了143秒,这边只用了1秒钟,如果把所有线程的时间加起来,就是100倍的差别,效果非常明显。
什么项目用Dots?
具有大量相同类型运算的项目,例如RTS、SLG、我的世界类型方块沙盒。
Dots这套解决方案,暂时只能对CPU进行优化,GPU方向期待Unity后续计划。
Burst目前还是Preview,正式项目中使用可能会有很多坑 。
https://aladdin.blog.csdn.net/article/details/105081530
Unity DOTS技术浅析_IT善男信女鬼的博客-CSDN博客
[Unity 活动]-Unity DOTS技术详解 - 宣雨松_哔哩哔哩_bilibili
ILRuntime作者林若峰分享:次世代手游渲染怎么做 - GameRes游资网
[Unity 活动]-游戏专场|Unity 2019 新特性在次时代手游 《黑暗之潮》中的应用经验及技术分享_哔哩哔哩_bilibili
Unity2019 Dots初试_Hello Captain-CSDN博客
Unity DOTS学习教程汇总_学无止境的专栏-CSDN博客
一种类ECS设计范式的介绍 链接: https://pan.baidu.com/s/1hH3YwgPvB9m39_e3HzlAcA 提取码: 5wwm
Unity手游实战:从0开始SLG—客户端技术选型
如何在立项前做好客户端的技术选型?腾讯资深开发工程师给你答案,一起来看这篇Unity手游实战:从0开始SLG—客户端技术选型。
项目背景
所谓选型,我认为就是为了实现某(些)个需求或者解决某(些)个问题所使用的解决方案。它可能是一个技术方案,也可能是一个管理方案,也可以是一个软件、工具或者是流程规范。
这篇的主题是技术选型,所以主要会分析项目客户端部分的技术解决方案。那么做选型分析之前就要先收集需求,分析需求,搞清楚我们的项目需要什么,达到什么效果,实现什么功能。除了项目本身章程和范围之外,还要看同项目的其他部门分工和合作方案(比如服务器的技术对接,美术资源的规范流程以及部分效果的实现对接),在这之外还需要考虑公司的大环境,比如资源的支持程度,项目的编制和人员的支持力度,开发环境甚至是市场和法律条件。
世界地图
我们现在的项目是一个沙盘类型的SLG。主要玩法也是世界地图的资源掠夺。这是比较经典的SLG玩法,包括《列王纷争》《王国纪元》《乱世王者》《真龙霸业》等等国内外数据良好的游戏都是这种核心玩法。大地图很大,一个大服甚至会有几十万的地形数据。在地形编辑、行军寻路(需要支持关隘和高地)、服务器数据同步等诸多地方都会有比较大的挑战。
主城
接下来是城市发展。这部分和市面上大多数的同类型游戏设计都不一样了。大部分的沙盘类游戏都是采用静态城市的布局策略,即每个建筑的坑都留好了,你达到等级之后只要点击坑位建造指定的兵营、伐木场、训练营等建筑就好了。
而且建筑本身也只是一个功能系统的入口,并不会有建筑和建筑之间产能影响,亦或是按照自己的习惯建造建筑和道路。我们的建筑后期加起来会有140多个,每个建筑都是可以自由移动和布局,在这个功能点上更像《部落冲突》的表现形式。
但是和它不同的是,我们的主城不会参与战斗,所以也不会有防御性的建筑,取而代之的是服务器性的建筑,比如水井、医院、教堂、公园等等这些服务器性的建筑会影响到生产建筑的产能,所以相同等级的情况下,合理的布局会让你的产能超出别人一截。另外我们的道路也是可以自定义编辑的,和道路相连的建筑也会有加成。如果有人玩过《城市:天际线》应该能够更明白一些主城的玩法模型。但是和《天际线》相比我们又没有那么复杂的计算和影响,毕竟人家是PC上的纯单机模拟养成类型。
除了建筑和道路的的自由编辑之外,NPC也是主城的主要功能。NPC会有10几种,每种的AI都不一样,并且要求能够在两个完全紧连的建筑缝隙中穿插和移动,还要考虑道路优先。
城市会有自定的保存模板,还要有可破坏和不可破坏的装饰机制等等。
所以主城的难点除了实现各个功能之外,还需要解决100多个建筑+几十个NPC+场景本身和UI部分的所有性能消耗。
战斗
战斗之前也有说过,需要支持同屏500+的单位同时战斗,这些单位每个都是独立的个体。这表示,每个AI都需要有自己的AI机制和独立的动作表现。一个单位大概会有5-6种动作,小型单位600+面,大型单位1000+面。这对GPU和CPU的压力都非常的大。
战斗还需要支持录像回放,并且在任何设备任何时候播放出来的结果和过程都要一致。
战斗需要支持倍速功能。
如果有人熟悉《全面战争》系列会比较容易理解我们战斗模式。不过和全战不同的是,我们的士兵在出战之后就不能手动控制了,毕竟是移动游戏,太复杂了伤害玩家。。。不过,其实还是可以手动释放英雄技能的。【手动滑稽】
三块重点内容分析完成之后,技术方案就需要根据需求去挑选了。用一句概括游戏就是:轻经营、重策略的沙盘SLG。
技术选型
嗯,下面就正式入活了。
引擎版本
技术选型要服务于产品。但在挑选技术方案之前还要做一件事情,引擎版本的选择。
早在2018年末,我们就收到了谷歌商店上架APP强制要求64位版本的需求,具体强制时间在19年8月1日。当时和Unity团队沟通的时候,反馈是必须2018以上的版本才能支持64位(不过一段时间之后又说2017.3也可以)。加上当时手里有Unity2018.3的引擎源码,所以就把版本定在了Unity2018.3(不过未来可能会升级到2019,里面有分帧GC的功能我会比较感兴趣)。
版本选定之后,就开始真正的技术选型了,这里我大致罗列了一下,其中有些是框架方向,有些是工具插件,有些是设计思路。但总体还是囊括了客户端该有的技术部分。
Sproto
网络游戏,首先要考虑的是如何与服务器进行通信。作为SLG类型,对于响应速度需求并不会像FPS或者MOBA类型那么的强烈。所以就挑选了TCP的方式进行连接。然后,使用了Sproto作为协议的载体进行消息传递和RPC封装。
TCP的部分就不用过多讲解了,做网络游戏都会接触和了解。这里讲一下Sproto。但是在讲Sproto之前呢,还必须先拓展另外一个东西:skynet。
skynet是云风大神创建的开源服务器框架,使用C和Lua结合的技术搭建的基于Actor模式的引擎。这里不会拓展讲解skynet的技术细节,有兴趣的可以去看下我同事对于skynet的源码赏析。
回到刚才SProto的问题上来,Skynet本来是支持PB(proto buffer)的。但是只支持2.X的版本,并且已经不再维护了。出于优化的目的,skynet使用了一套自定义的格式Sproto。它其实是基于proto的改良,将proto里的冗余表达进行了简化,让它更满足于skynet在Lua端的性能表现。那么我们综合考虑下来也是选取了sproto的方式进行协议传输。
这其实又涉及到一个问题,Sproto其实是设计个skynet用的,但是客户端用的是Unity,开发语言是C#,肯定不能直接使用。不过没关系,我央求了服务端大佬给我们写了C#的转译工具,可以将Sproto的描述文件转为CS文件,然后再写了一套序列化和反序列化工具,呃~可以像PB一样正常序列化了。
一般客户端关心数据分为两个部分,一个部分来自于服务器端,另一个部分来自于策划配置表。现在网络端搞定了,数据表怎么办呢?对,我又去央求了我们的服务端大佬,给我们写了一个excel转Sproto的工具(过程非常复杂。。嗯先把Excel转成Lua格式,再Lua转成Sproto的描述文件,再把描述文件转为CS),这样我们的策划数据也搞定了。
GPUSkin+GPUInstance
我们的战斗场景需要显示500+的单位,每个单位携带自己独立的AI和动作。大型单位约有100面,小型约600面。那么同屏显示之后,CPU和GPU都面临巨大的性能压力。用小米5S做过一次测试,当使用skinmesh的时候,4000单位的帧率就只能到20了,换了GPUSkin方案,8000个单位仍然能够保持50帧。这部分的选型是为了解决同屏渲染压力。
ECS
与传统的面向对象的编程理念不一样,ECS(Entity-Component-System)是面向数据的编程思想。如果不理解概念的可以自己先去翻阅下资料,也可以等后面讲技术细节的时候再去了解。这里简单的类比一下帮助理解。就好比Unity的开发模式,一个GameOject可以理解为一个Entity,单独放在场景里它什么都不是。如果你给它绑定了一个Text组件,那么它马上就会变成一个Text 组件;如果绑定一个Button组件那么它就是一个Button。那么这个时候你可以理解为Unity就是一个EC的思想。至于为什么引入S的概念就是为了解决耦合和数据冗余。让一个Component里只有数据而没有方法,所有的方法都写在System。让数据在内存里的排布更加紧密,增加缓存命中率,特别善于处理大批量的数据。
同时,因为数据和系统分离,那么做回放的时候数据非常便于保存。这又符合了我们常规的逻辑和表现分离的设定,所以这套机制完美契合了我们战斗需求。配合GPUSkin和GPUInstance既优化了性能,又能实现回放和解耦,同时还会带来另外一个优势,逻辑和表现分离。
我们还做了一个大胆的尝试,将逻辑和表现分离之后,将逻辑层接入到服务器中(服务器是基于Actor的,所以扩展一个战斗服很容易),客户端则既跑逻辑又跑表现。这样带来的好处就是,只要我们给定的输入一致,因为逻辑是一套,跑出来的结果也必定一致。所以世界离线战斗的时候我们调用服务器秒算结果,PVE副本的时候,客户端展现战斗过程,非常美妙。
XLua
Lua在客户端集成的主要作用还是用来解决热更新问题的,它带来了便捷的同时当然也带来了性能问题。一般来说,Lua和C#的性能差距在40倍左右。移动开发一路走来有很多Lua相关的框架,比如toLua,uLua,slua,Xlua等。
所以有的时候就会想,有没有既可以实现热更新又能提高性能的方法,那么Xlua就是这种。开发用C#,热更新修复用XLua。当然这也不是完全免费的,取而代之的是要在开发的过程中做好各种标识,增加了开发管理难度同时包的代码段会增长很多。
说点题外话,移动游戏刚起步的阶段,除了Lua之外确实没有更好的热更新手段。所以大家才考虑将Lua接入到开发中,甚至一度接管项目的整体外围开发。但是现在除了Lua之外,也还有很多其他方式可以做到热更新,比如腾讯的潘多拉。当然项目的开发过程中要使用防御性编程是肯定的,除了做好各项QA验收之外,还要对每个功能做出屏蔽入口,甚至在一些运营活动上做好模板参数,可以通过快速调节参数就能变成另外一个活动。
我们使用XLua的想法也会趋近于这个思维。平时开发都会在C#上,但是仍然会在Lua层面维护一整套的功能系统,让Lua层面有能力解决大部分的突发情况和新增需求,但是这仅仅是一个后备手段。所有一切还是以C#为主,哪怕是上线阶段用lua修了某些问题,那么再下一个版本里也会把功能修复到C#层面,并从lua层移除。
UGUI
这个其实现在可选择性不是很大。目前能与之一战的是NGUI和FairyGUI。NGUI和UGUI是一个爸爸,但是在层级处理方面十分复杂,对于一些新手小朋友的理解尚不友好,不像UGUI保证在一个Canvas下能按照树状层级显示。FairyGUI是一个第三方的GUI,它需要接入SDK。并且它自己内部保证接入了SDK会在不同平台表现一致。这对于可能需要转引擎(COCOS转Unity之类的)的项目可能更好,但是我们并不会转所以并不需要。
Wwise
Wwise是一个音效框架,其实这里能选择的余地不大,基本就是fmode和Wwise两种。但是近几年fmode有些没落,操作、性能和工具链都跟不上了,以前可是一枝独秀。
GCloud
GCloud是腾讯云产品的一种,起初是为了服务内部游戏产品所孵化出的统一平台。
国内游戏常用的游戏内语音,电台等都可以接入这个实现。另外功能还覆盖了游戏更新,区服导航,微端puffer等游戏内常用的功能设定。
这一套接入起来真真儿是极好的,为手游的几个难搞部分提供了统一化的服务,后台的操作也是极其简单,有兴趣的可以去官网了解。
Addressable Asset System
这套东西是我目前极力推荐的,它起于2018版本(预览版),在2019已经是正式版本功能,提供了一套极其强大的资源打包和加载的管理方案。
以往我们的资源打包方案都需要自己去实现,诸如在编辑器下使用编辑器接口,在实机状态下打包成bundle形式加载,然后还需要我们自己去收集和管理资源的依赖关系,维护自定义的资源列表,而这套统统帮我们做好了,并且提供了可视化的界面操作,管理资源妈妈再也不用为我费心了。
依稀记得4.x的版本,要做资源管理需要自己指定目录或者资源,然后根据是否是依赖项的方式调用打包的API。甚至如果做资源更新,你需要自己维护一份资源列表,自己自定义MD5值比对差异,如果需要告知用户下载的资源大小,你还要自己统计单个资源的大小,汇总告知玩家。
5.X的时候,资源管理做过一次大的升级,让每个资源都带有Asset Bundle标签,这样在Unity的工程目录就可以通过自定义标注资源的方式标识资源,并且在生成的每个bundle的同时为bundle生成一个manifest文件,用来标识该bundle的内容和依赖项等大概长这样:
在运行时进行资源加载的时候也是先加载这个文件查找依赖项,递归加载直至完成。比起4.x之后肯定是好了很多,但是仍然是极度的麻烦。
现在是一个这样的可视化面板,所有资源都可以通过拖拽完成,另外代码里也提供了完整的加载方案,让你在编辑器和真机的都不用关心资源格式只使用同一个接口调用就好。
Tiled
Tiled是一个老牌的基于瓦片的2D编辑器。功能非常之强大,以至于我就不在这里讲述它的强大之处了。
其实Unity2017之后也针对性的提供了tileMap功能组件,用于给2D游戏提供一些周边辅助。甚至在github上还提供了扩展笔刷和Demo来支撑。但尽管如此,它在功能实现上还是不如Tiled来的快捷。
另外我们的世界地图非常之大,有几十万格,所以单用模型或者地形去刷就太耗费资源了。所以这里会选用2D的方式来展现世界地图的地形,至于地图上的奇观、主城、资源点、玩家部队、怪物等等就用3D的形式去展现。
Tiled编辑器生成的格式Unity并不能直接用,所以还需要借助一些插件,这个我们放在后面去讲解。
TimeLine
这个很简单,是2017以后提供的一套线性编辑工具,我们有可能会在剧情,镜头等方面使用它,另外它和cinemachine是一对CP,成对出现。
TextMeshPro
TMP是早在5.x就存在的一个优秀插件,后来因为表现过于优异被吸收为Unity正式功能。我们都知道UGUI对于字体计算上非常的耗时。同时UGUI的渲染原理也决定了对于一些经常变动的UI节点有着较大的性能问题。所以对于一些战斗飘字,小地图、聊天等变更频繁功能来说,UGUI表现是非常糟糕的。另外UGUI对于字体的内存处理上面也是有比较大的问题的,当字号不一致或者差异大的时候,内存消耗严重,这在聊天功能里表现尤其显著。
另外还有一个问题是聊天图文混排,这个在NGUI里做的比较好,但是UGUI本身却不支持,不过没关系TMP支持!
除了支持图文混排之外,它还支持各种富文本,超链接、类似平方的上标,化学表达式的下标、 各种文字效果比如打字机或者遮罩等等非常强大。
并且最重要的是,它可以和UGUI完全混用,甚至直接替代UGUI里的Text、DropDown、Inputfield等使用到文字的组件。
嗯,它除了处理文字之外,我们的血条,建筑头顶图标等各种HUD也可以用它表现,是不是非常惊奇!
A*PathFinding
这个就是前段时间翻译的的A*PathFinding 教程系列。之前的总结篇也有对这个插件做过总结,总的来说这是一个非常非常值得推崇的插件。不仅仅在于它的功能强大,也在于它的软件架构,和文档教程支持程度。是一个教科书般的第三方库。
我们的战斗其实并没有用到寻路模块,但是在表现层需要做动态规避。因为对于逻辑层(服务器运算的时候)来说,单位是没有碰撞和体积的,但是对于客户端来说,我们肯定不能让单位全部重叠在一起,这就使用到了A*插件的动态规避(RVO)。
主城部分因为涉及到经营,那么就必须模拟大量的NPC行为,有NPC就要有各种寻路和目标表现,比如一个送牛奶的农夫,去公园玩耍的孩子,送货的、送酒的,去市政厅办事的,去医院看病的,城里巡逻的等等。那么寻路这块就极为重要。
世界地图这块我们也涉及到行军,因为我们会考虑做关隘和高地,所以需要使用到分层寻路。另外与主城的NPC表现不一样的是,主城是装饰性的NPC,并且人物比较小,所以动作幅度和寻路状态机械一点反而好看,但是世界地图是功能性的,虽然建筑和资源点都是基于网格的,但是我们计算路径的时候却不能使用网格,会影响到行军的时长和路径。因为行军是由服务器计算的,所以这块我们的打算也是制作一个世界地图的寻路系统库,然后丢到服务器去跑,也就是说功能是客户端做,但是丢在服务器去运行,是不是很酷。
收尾
选型是个很大的课题,这篇文章只讲了技术部分的方案,后面会针对各种技术细节做探讨,以及讲解项目中遇到的实际问题从什么维度去思考解决方案,但在这之前还需要先讲一下客户端的目录分布。看看一个实际的大项目是怎么在几十个人之间合作有序,各司其职的。
以上是关于Unity项目技术方案Dots架构方案简介的主要内容,如果未能解决你的问题,请参考以下文章
Unity开源项目精选Entitas:Unity DOTS的先行者