程序丨祖龙技术总监王远明:Unity 开发2D游戏的诀窍

本文为祖龙技术总监王远明在Unite大会上的演讲,主要分享祖龙娱乐在《梦幻诛仙》开发方面使用Unity的心得和体会。

关于Unite
Unite大会是由Unity举办的全球开发者大会,至今已有10年的历史。Unite现已成为游戏行业,VR/AR行业中最具有权威性和影响力的活动。

2372757-3e050b884a78895c

王远明2001年加入洪恩祖龙工作室,参与负责开发《大秦悍将》、《血战上海滩》等多部单机游戏;于2003年加入网易,参与《天下贰》(现名天下三)的研发;2013年加入完美世界,对CryEngine进行二次开发用于端游《完美世界2》项目;之后于2014年加入祖龙娱乐,设计并实现了《六龙争霸》项目的底层架构;2016年参与《梦幻诛仙手游》的研发。

王远明:大家下午好,今天很高兴来到上海跟大家分享一下祖龙娱乐在《梦幻诛仙》开发方面使用Unity的心得和体会。《梦幻诛仙》有些人可能没有玩过,简单做一下介绍,《梦幻诛仙》是组龙娱乐开发的一款重度2D回合制手游,是亲密回合社交手游,采用2D+3D技术,最多支持40个人战斗。

我们为什么选择Unity,这个项目前后做了两年的时间,这是手游里面时间比较长的周期。开始是用一个2D的引擎,后来我们觉得有必要用一些更加3D化的一个方式来表现它。因为3D的引擎在表现力上是比较丰富的,能够用Unity达到2D的视角,还能有一些相机的变化,比如在战斗的时候,可能有一个相机的旋转,这种表现力会更丰富。而且我们在六龙争霸以来积累了丰富的经验,六龙争霸在国内可以说是第一款Unity+Lua的游戏,我们考虑如何能够把以前的技术、框架能够继承起来,这样的话能够更大限度的得到技术的复用。

第一个是技术关键点:我们如何用一个Unity来开发一个2D的游戏,Unity有自己的2D模块,这个2D模块是那种纯粹的像横版过关的方式可能不太适合梦幻诛仙游戏的模式,这方面我们需要做一些特殊的处理。
第二个方面是我们的引擎架构,这个架构也是六龙的架构,祖龙很多款游戏,包括刚上不久的权力与荣耀都是这种框架。
第三方面是性能优化。

一、Unity开发2D游戏的关键点

分为六个方面讲一下技术关键点,这些都是和2D游戏相关,并不是全3D的方式。2D的游戏,其实是一个类似于窗口化的坐标系统,X轴的坐标是横向,向右,Y是冲下的。但是Unity里面是3D的坐标,当我们在屏幕上点一个坐标,这个人物决定朝那个方向走的时候,其实我们在数据方面,都是用2D的方式来获取坐标,我们用3D只做表现,我们需要通过一种方式,能够把屏幕上点的点,还原到3D里面去,可以让这个人物在3D场景当中走,用一个屏幕到一个点的变换,这样变换得到一个纯粹Unity的3D空间。

这个2D世界里面的坐标,X是一样的,但是Y是反的。我们怎么样把这个模型放在一个合适的3D位置,能够让它看起来和2D的背景是重合,我们这里有一个3D的坐标变换。我们乘上一个系数,这个1/72,比如横向一千米,纵向是一千米的话,这个可以通过适当的调整,不是一个绝对的数字,每个项目不一样,乘上系数,就可以把这个坐标缩小,而不至于那么大。这个角色看上去在屏幕往上跑,往下跑,但是在Unity里面是水平面,是这样走的。这个从2D空间来讲是上下,所以这里写了一个worldY,是在世界坐标的位置,这个先变换成2D坐标,然后转换成3D,我们这样是斜看过去,我们屏幕是这样的方式。当在这个屏幕上挪动了这样一个距离的话,是在水平面是这样,是横向的距离除以一个cos角度,这个角度和相机俯视角的Sin值是一样的,有这样一个除的计算。

有了坐标的变化关系之后,我们需要设置2D的相机参数,我们先看一下下面的图,会有一个直观的理解。这个白色的框是一个正投影相机,后面是一个2D的场景,这个场景是通过若干张图平铺的方式拼起来,在随着玩家的移动,会不断地卸载和加载,右下角是一个预览图。

正投影相机的viewport的尺寸就是当初在做2D场景的时候,你所能看到的范围。相机的正投影的尺寸的值等于viewport的高度除以2,这个尺寸为什么除2,是因为Unity的含义就是高度,视窗高度的一半,这样设置以后就没有问题了。就是这样的一个效果。

3D的相机是这样斜着看过去的,俯仰角是20度,这个视窗的宽度和高度,乘以一个坐标系数,投影的尺寸是等于高度除2,相机俯仰角度是20度。最终显示效果就是这样的一个效果图。当你在屏幕上点击一个位置,然后我们就根据刚才所计算到的先从屏幕转换到2D的空间,从2D的空间转化到3D的空间,人物可以按照水平的位置来移动,你看上去的话,这个人物就从下面走到上面了。

粒子特效,我们分为两种,场景特效和战斗特效。2D场景里面的粒子特效是通过场景的正投影相机去渲染的,我们看看左上角水池子那个光效是一个场景特效,场景特效是在左边有一个发白的区域,那个特效是场景特效,采用正投影,我们放特效会使用一个工具,我们自己开发的工具把位置能够精准地对齐,这样就是2D的场景里面光效就是这样的效果。

3D的战斗特效,采用斜的3D正投影去渲染,和角色是在一起的,纯粹是3D的光效,只是通过相机的设置,能把最终两个相机重合在一起,这是粒子特效方面。

美术规范,我们做梦幻诛仙,主角模型是3500到4000,骨骼不超过40根,有三个material,一个是头发,一个装备,一个是皮肤,贴补是256乘256,shader正常的就不说了,不透明的,比如这个会跑到房子的后面,纯粹的3D游戏会被房子挡住。我们在做2D游戏,预先保存了遮挡关系,每走一个点的时候会读出来,发现这个点是不是被遮挡,如果被遮挡,我们会换一个半透明的shader,这个shader很简单,采用两个pass,第一个是写深度,第二个pass会做一个混合,这样就不会看到整个身体前胸贴后背这种感觉。还有融化的shader,是打死之后,这个人慢慢融化掉,然后配合一个动画飞掉,就感觉被击碎的效果。使用熔化贴图的一个通道乘以一个系数,这个系数在变,乘上之后,数值会慢慢变大,然后我们通过这个值,做一个像素discard,就有一个慢慢消失的过程。

二、2D游戏架构

现在讲一下引擎的架构是C#、C++和Lua的三个部分,C#是做功能引擎的模块,没有游戏逻辑,只是做功能模块,这个模块是跟Unity相关,C++也是做引擎功能,但是和Unity没有关系,比如我们做虚拟的文件系统,我们采用的是类似于端游的方式,然后虚拟文件系统,以及包括我们的Lua的代码全部集成在这里,做一个插件。Lua做所有的逻辑,有很多人说,它的效率很低,包括今天看王者荣耀也说,用C++的方式做,他们担心Lua做效率会低,但是以它那种同屏人数是没有那么多,没有梦幻这么多,是没有必要担心这个问题,如果用c#和c++,热更新方面会比较困难。

接下来是加载模块,LUA、特效、声音,每个文件都会被打成一个assetBundle,某个模型文件依赖关系展开之后可能有20个文件,会打成20个包,这些包的关系我们自己来维护,每一个都会记录,依赖哪些bundle,想要创建bundle时,我们会看这个bundle依赖于哪些bundle,然后依次打开依赖的bundle,这样可以完全打散,采用端游的方式,哪一个文件修改了,就只更新那一个文件,这样可以做到更新包的最小。这个方式在现在unity5.5、5.6的版本下做了改进。不采用从内存创建bundle而是用文件+偏移的方式。我们现在都改用这种方式,这种方式对内存的使用比较小。可能打开祖龙做游戏的包,可以看到很多PNG,这个PNG是虚拟的包,里面包含了若干个小的文件。这一部分加载虚拟文件系统是写在C++插件里面的。

第二个模块是Lua,我们没有用市面上现在比较流行的框架,因为我们当时做六龙的时候还没有这些框架,但是我们又想做成全部Lua化的,这个Lua模块,能够把几乎所有的逻辑都做在里面。现在有很多游戏公司,跟我沟通这块,他们虽然也用了各种各样的Lua,但是他们很多都是只有界面的逻辑用Lua,很多其他的逻辑还是用c#,当lua和c#进行数据交换,甚至代码调用时就很麻烦,互相读取的时候跨越这个边界是比较慢,也不方便,他们现在已经写成这个样子,就没办法改了。一开始的话,最好把所有的逻辑都Lua化。 原来能做到95%,其中5%没有做到,是因为我们更新的模块,比如我们发现patch更新,弹出来的窗口,你有一个更新,需不需要去更新,这块我们没有做,当时做的比较匆忙,现在把这块也挪到lua里面,我们Lua可以接管所有逻辑。

特效和声音,这块比较简单,是对于Unity接口封装,让逻辑程序员在Lua里面很方便播放声音和特效,我们内部做类型种类的控制,以及声音的音量的控制,比如某一类音效的改变和音效关闭。

定时器,用了lua之后会觉得,不要每帧在lua里做事情,那样会比较慢。Lua做的事情是事件触发的,比如玩家按下了一个按纽,或者有一个倒计时,这些事件不是每帧都有,使用Timer做就可以了,这个用在Lua里面,我想几秒钟以后做什么事情,没有每帧都去执行Lua。

三、Unity 2D游戏性能优化

性能优化,性能优化有一些是比较常规的方法,还会讲一下我们自己遇到的一些比较特殊的情况,分这么几个方面讲。我觉得最重要的优化,你一开始的时候,可能在架构上面,就可能需要把这些东西弄好,Lua是逻辑的黏合剂,不适合做费时费力的事情,费时费力的事情都是模块化的东西,放在引擎里实现。这些单独的模块,其实没有任何的意义,不会有一个完整的逻辑。那么我们需要用Lua把这些串起来,串起来的方式怎样去组合,哪个调哪个,这些方式在Lua里,改变了lua,游戏的行为和外观就发生了变化。所以不要觉得用Lua就会一定慢,这个不会慢的。

因为我原来在网易也干过好多年,网易现在很多游戏,用python做脚本,只要把这块设计好,这块完全可以做到,不用担心脚本的效率。C++和C#做引擎相关的事情,我所知道的一些公司,他们在开发的时候,包括做一些内部测试的时候,他们的加载的模块用的是Resources.Load,这样一来没办法做热更新。他们觉得没关系,上线之后,再改成资源用AssetBundle加载。最好是一开始做测试的时候,至少在做第一次测试的时候,就要把代码就写成最终的样子,而不是临时拼凑,以后再改,在这种临时代码的情况下做优化,其实是没有意义,因为这个代码以后可能会被改了。
GUI是比较传统的方式了,就不一一说了。贴图其实这个第二点和第三点是我在跟其他同行交流的时候,他们可能没有太注意。我们打开这个Mipmaps的选项,渲染之后,可以重点观察红色区域,红色区域表示这个贴图单位位置是挤压,其实是没有必要使用那么大贴图的,或者说,可以调整UV,使它在这张图取得像素少一点,原来我们游戏角色的脸,怎么调都不是很清楚,都有点模糊,我们对比了其他的游戏之后,我们发现,其实我们不是说图用小了,而是图用太大了,原来用的是64乘64,后来我们把它改成32乘32,这个脸就清晰多了。

第三点根据这个机器的配置,我们动态调整一下贴图的尺寸,决定用第一级,还是用第二级还是第三级,我们在优化上分三个档次,高中低,我再说一下这个分级的问题。原来我们用CPU核数和内存分档,现在我们发现这个不太准。最近我们改成了根据显卡的型号,因为现在市场占有率最高的手机Vivo,oppo,比如有一个R9S,那个各项指标都很好,内存也很好,CPU也很强,但是我们发现它的崩溃率很高,后来查了一下,显存很差,显存只有不到300兆,所以只看CPU是不准确的,现在采用显卡的参数分成高中低三档。中档和低档会降一下贴图精度,这样比较省内存,显存也比较省。

声音模块我们在Unity的接口之上分装了一些接口,主要分类型,每种类型同时能够播多少个,如果是达到上限的话,会把很久没有使用的声音asset清除掉。语音便条的网络优化,现在有很多游戏有发语音的功能,点击图标能听到对方说的话,原来用讯飞,我们发现上行和下行的流量都很大,我们还要把语音数据压缩之后,放在我们的语言服务器上,这样别人点那个图标才能下载下来播放,这样就相当于3倍的流量了,之后我们先在本地用Unity的功能先录音,录完音之后,压成opus的格式,或者speex的格式,然后直接传到语音服务器上,服务器拿到它之后,调用翻译的接口,语音服务器既做翻译又做存储,网络流量就一份,这样流量消耗就比较少了。

特效也是分总量控制的,因为一个特效特别复杂,这种重度游戏表现力是很强的,一个特效可能包含有二三十个组件,这个组件有些是很耗的,在我们特效全开的时候,或者高档机上,我们会把它全部打开。但是有些中低端的机器,我们希望有选择性在这二十多个组件里面会有一些被disabled,我们让美术去挂接一个组件,这个组件自己来决定说,我在开中档特效的时候,这个特效的组件需不需要生效,我们通过修改组件的属性,让它播放的时候,能够有一部分是有效,有一部分是无效的,这个就是特效的分级。

网络消息流,我们做这样的优化,就是说我们根据玩家AOI的范围,当玩家周围的人密集的时候,会有一些选择性的发一些消息流,而不是所有的都发出来,这样的话会动态调整,角色的属性也有一些是有优先级的,可能离远的时候服务器就不发了,只是很近的时候,才会去把这些属性广播给周围的玩家。第二个是缓存服务器的消息,当我们游戏在加载的时候,进度条在走,网络消息也有,既要处理网络消息,又有很多的IO,这个时候会影响加载速度,我们希望场景很快能加载出来,我们就在加载场景的时候,先把收到的网络消息全部缓存起来,加载完成后再分摊到若干帧里执行,这样会缓解客户端的压力。

说到消息还要说一点,很多朋友跟我沟通的时候,他们说网络包的处理是在C#里面处理或者在C++,我们现在所有消息解包发送全部在Lua去做,按照现在项目的情况来讲,我觉得是可行的,不会因为它成为项目的效率的瓶颈,只有一些很费,很耗时的网络消息包,才把它派发到C#去做,这样比较灵活,我们可以实时更新lua,决定哪些消息是需要转到C#里面去做的

展开阅读全文

没有更多推荐了,返回首页