标签:碰撞 有一个 操作 实战 标签 内存 地图 阶段 节点
如何在立项前做好客户端的技术选型?腾讯资深开发工程师给你答案,一起来看这篇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,并且人物比较小,所以动作幅度和寻路状态机械一点反而好看,但是世界地图是功能性的,虽然建筑和资源点都是基于网格的,但是我们计算路径的时候却不能使用网格,会影响到行军的时长和路径。因为行军是由服务器计算的,所以这块我们的打算也是制作一个世界地图的寻路系统库,然后丢到服务器去跑,也就是说功能是客户端做,但是丢在服务器去运行,是不是很酷。
收尾
选型是个很大的课题,这篇文章只讲了技术部分的方案,后面会针对各种技术细节做探讨,以及讲解项目中遇到的实际问题从什么维度去思考解决方案,但在这之前还需要先讲一下客户端的目录分布。看看一个实际的大项目是怎么在几十个人之间合作有序,各司其职的。
标签:碰撞 有一个 操作 实战 标签 内存 地图 阶段 节点
原文地址:https://www.cnblogs.com/dyf214/p/13020605.html