因为爱情有晴天片尾曲游戏世界可以提供另一个人生的诠释空间,从它诞生起就受到了人们的喜爱,从单机游戏到网络游戏,从PC游戏到移动游戏,游戏陪人们度过了一个又一个难熬的碎片时间,成为人们生活中不可或缺的一部分。相信每一个从事游戏行业的人,每一个热爱玩游戏的人,每一个学习code的人都有这样一个梦想,那就是能自己开发出一款好玩游戏。
在雷火,每年校招入职的新人,都会以团队的形式,从0开始,开发一款成熟可玩的游戏,相信这对每一个热爱游戏的人都是难以拒绝的诱惑,也将会是踏入游戏行业大门最难忘的记忆。
想要从头开始开发一款游戏吗?那么如何打造游戏世界的浮光掠影,如何给游戏世界赋予灵魂,这里笔者便陪大家一起从0开始,“云开发”一款小游戏吧! (๑•̀ㅂ•́) ✧
首先我们要定位一下,我们想做什么类型的游戏,确定需求以及对游戏产品的一个最终画像。然后根据需求做一定的技术选型。
比如我们是要做2D还是3D游戏、要做FPS还是MOBA、是做MMO以及SLG等,每种类型的游戏实现起来的方式和细节也是完全不同的,更多的是选择自己喜欢玩的类型或者自己擅长的类型,那么我们今天就选一个咱们雷火最拿手的MMORPG(大型多人在线角色扮演)类型作为切入点来讲讲吧。
工欲善其事,必先利其器。首先我们要挑选一个适合我们游戏的游戏引擎,比如2D游戏我们可以选择Cocos2d-x或者CocosCreator来做,3D游戏我们可能选择Unity3D或者UE会更为合适。我们可以根据自己的技术栈选择一个擅长的引擎。也可以追求最好的表现,选择渲染效果最好的引擎。从笔者个人的喜好以及市场占有率来看,咱们的Mini MMORPG就使用Unity3d来做吧!
选择好游戏引擎之后,我们可以对游戏主体进行拆分,因为一个游戏的工程量实在是太庞大,拆分成一个个功能模块,然后将其整合。
网络通信主要是实现两个比较重要的点,一个是如何做实现客户端和服务器的通信,还有一个是游戏对象的同步方式。
这里针对我自己对游戏的理解,做了一下网络通信协议选择和游戏的一个适用性对比,这个也不绝对,例如一款MOBA游戏,你可以用TCP做也可以用UDP实现,也能够混用,例如战斗数据传输用UDP,其他用TCP。每个协议优缺点都比较明显,我们可以通过一些策略来弥补缺点,因此协议的选择需要根据具体游戏的实际情况来。
那么咱们Mini MMORPG的话,其实选择TCP和UDP都可以,主要是需要针对协议的一些特点做一些适配,比如在延迟高的时候,为了不让玩家感受到高延迟,可以做一些客户端的行为预测(延时补偿策略),在没有收到服务器返回的指令前,先按照客户端对这个对象的一个行为预测策略,去模拟他的行为。等服务器结果发来之后,再做一个插值运算平滑地过渡到服务器的位置上。
不同的游戏适用于不同的同步方式,例如MMORPG游戏必须是状态同步的,因为逻辑相对复杂且庞大,且MMORPG的客户端承载有限,并不能把整个游戏的实体全部展现出来,比如很远的山和人物。例如MOBA、RTS、FPS类型游戏更适合帧同步一些,因为这类游戏对客户端表现要求“及时反馈”,且同一场战斗中对象不会特别多,在客户端的承载范围内。但是这些也都不绝对,例如很多MOBA和FPS游戏也是用状态同步方式实现的,好处就是能够有效地防止作弊和外挂。
我们的MiniGame可以选择一个能够实现上述两点的网络插件,这里我们选择Photon Unity Network(PUN)插件来做开发示例,这是一个采用UDP协议+帧同步的一个支持房间对战的服务器框架,可以利用他便捷地实现我们MiniGame所需要的网络模块功能。
但是这套网络底层是不开源的,因此我们也可以自己从头写一套服务器框架来做,这样可定制化程度更高,感兴趣且有能力的同学不妨一试。
Unity也为我们提供了一套Socket库,可以用Socke来简单实现一套基础的网络通信流程
关于上述代码缓冲区的设计,可以看这个示意图来进行理解,同样发送数据的逻辑,也是通过将要发送的数据加入发送缓冲区来发送。
具体什么时候把缓冲区的数据发送到服务器、什么时候接收缓冲区的数据,是由TCP底层策略来实现的,Unity应用层可以不需要关注。
有很多管理类需要在游戏中有唯一实例,用来管理游戏中的某一部分的需求,因此我们要用C#实现一个单例模式基类,用来实现这些管理类
实现好管理类基类后,我们可以开始写其他的一些管理类,来继承这个类就好了。
我们的网络框架使用PhotonServer服务器引擎来实现,同步框架示意图如下,类似于帧同步的一种网络同步方式:
主客户端PlayerX向我们这个房间的GameServer发送了一个事件,然后GameServer将这个事件,同步给该房间内的所有其他客户端。也就是主客户端是一切行为的发起源,服务器只是做转发。
PhotonServer的安装和下载以及服务器配置,可以参考下面链接,或者网上找一找相关资料。
首先根据PhotonServer的框架设计里,每一个需要联网(需要同步给别的玩家)的对象,都需要挂一个PhotonView脚本
根据参数可以看出,PhotonView组件主要用于管理和区别服务器对象,服务器根据组件的ViewID作为唯一标识符,以区别同一预制体(prefab)生成的不同实例。
订阅和发布模式是游戏里必不可少的一个设计模式,因为当游戏模块越来越多之后,为了程序的可扩展性、低耦合性,需要能够高效地进行一些消息和数据的传递。
从上面的一个示例可以看出,这种设计模式下,界面和逻辑层是完全分离的,通过消息的一个订阅和发布来完成一些事件的传递,将程序模块与模块之间的耦合性降到最低。
策划表是游戏必不可少的一部分,游戏里的一些设置、数值设定等都是策划配置在一个策划表的,一般都是用功能强大且通用的Excel。
但是我们Unity代码是无法直接读Excel中的数据的,因此需要工具将Excel中的表转成Unity可以解析的一些文本格式,例如Lua的Table,Json格式,XML格式等,这部分我们可以直接用前辈们造好的轮子,就不用自己去写了。
转成对应格式后,由于我们需要在游戏中实时使用这些策划数据,因此我们要创建一个DesignManager类,来解析这些数据并且缓存起来,便于我们开发时使用。
通过这样的设计,将数据的解析和外部调用逻辑进行分离,所有数据通过策划表缓存中取,扩展性好且方便使用,但是如果需要考虑热更新,那么就需要别的方案了。因为策划数据已经被我们缓存了,直接更新策划表是没有作用的。
MMORPG游戏核心就是技能机制,而技能是一个很宽泛的一个设计,这里我们只讲几个比较常规常用,大家比较好理解的技能来做实现,以及如何设计一个技能框架。
首先还是用我们面向对象的编程思想,设计一个Skill基类,实现一些所有技能都需要的通用行为
除了上述的AOE技能,我们还可以依次实现上面定义的一些技能或者我们自己想做的一些技能,关键就是要做好技能的可扩展性。
另外还需要一个SkillManager管理类来全局管理这些技能的创建、更新、销毁。
我们的资源最终会打包成AssetBundle,我们在Editor模式下可以直接在我们的资源目录内加载对应的资源,如果在发布平台,那么我们需要加载的是ab包文件。
在游戏开发初期,可以不太关注资源更新部分,可以把更多的时间和经历放在打磨游戏细节上。但如果之后这个游戏要上线长期运营,那么就离不开资源的热更新。Unity3d提供一套资产管理解决方案,可以将Unity项目内资产按一定策略打包成AssetBundle文件,可以通过将游戏的资源打包成AssetBundle,并且将每个文件的版本信息以及md5值单独存成一个配置文件,并且生成一个版本号,放在服务器上来进行动态热更新。
按照上述流程,就可以实现我们的资源动态更新了。资源动态更新的好处有很多,例如缩小包体的大小,动态更新版本(无需再走提审流程),热修复一些bug等。
资源更新的这个更新的流程不仅仅适用于Unity,其他游戏引擎基本也都是同样的解决方案,比如Cocos2d-x官方的AssetManager资源更新解决方案。思路基本大同小异,实现细节上会有一些差异化,可以根据我们项目的特点和需求自定义。
在进行UI开发之前,首先要搞清楚UI是如何渲染在我们屏幕上的。实际上UI在Unity中也是通过构建网格然后通过UI摄像机渲染到我们屏幕上的,和其他3D资产一样也是需要通过构建DrawCall的,因此科学地搭建UI界面也是提升性能必须要做的一件事。
根据上图可以看到NGUI的Drawcall构建是按照Widget的深度来进行排序构建,当他们的材质或者贴图不一样时,就会单独构建一个Drawcall来渲染
根据这个可以看出,我们可以尽量将同材质的对象放到一个Panel下构建,就可以减少UI渲染的Drawcall了。
例如上面说的Drawcall的合并和构建代码在UIPanel.cs的一个合并设计,就在NGUI的FillAllDrawCalls函数中,也是NGUI框架中的一个核心逻辑了。
UIPanel组件会把这个Panel需要构建多少个DrawCall以及每个DrawCall绘制的内容和三角形数都显示在面板上,供我们调整,尽可能的减少DrawCall。根据上图显示,
UI开发上还有很多有趣的地方,可以实现很多很炫酷的效果,大家有兴趣的话这也是一个值得深入学习的方向。
上面讲了UI类要如何设计,这里讲一下UIManager类要如何管理这些界面
在我们游戏中可以分为很多状态,状态的分类可以有多种依据,例如以当前的场景为加载依据(登录、加载、世界场景、副本场景等等),将游戏分成多个状态,好处就是当我们游戏状态发生改变的时候,可以做一些事件的统一处理。例如当我们从副本场景切回到世界场景,要对副本场景里的一些资源做统一的清理操作以及对世界场景做一些预加载操作,有状态这个标记我们就能清楚地知道“什么时候应该做什么事”。
通过状态,来控制游戏的进程推进,通过状态接口来规范状态的实现和操作,实现了一个扩展性强的简易游戏状态机。
上面我们设计的管理类都是继承Singleton的单例类,完全与引擎的MonoBehavior解耦,但由于我们一定需要有一个Update类(例如Skill的生命周期、资源的更新),因此我们必须要有一个继承MonoBehavior的类,我们就叫Main类好了,顾名思义就全局管理所有Manager的类
3D的MMORPG游戏中,玩家一般的操作自由度相对较高,一般包含行走、跳跃、加速跑、攀爬、攻击、防御等行为,这些操作都是需要程序来一一实现的,这些操作将最直接地影响玩家的体验,因此除了开发功能,更多的时间需要用到参数调试上,比如玩家的移动速度,攻击动作前摇,视角的旋转范围和速度等,也就是我们玩家所说的“手感”。
我们先给玩家加上一个PlayerController脚本,用来实现玩家的各种操作
另外我们给玩家加上一个Unity自带的CharacterController组件来帮我们实现一些角色运动的功能
我们在Unity的InputSetting先配置一下操作快捷键,例如空格键代表跳跃。
摄像机拍摄到的东西就是玩家能看到的东西,因此摄像机的位置决定了屏幕渲染的内容。 同样摄像机的拍摄角度,镜头可旋转的范围,镜头旋转的速度等也会影响玩家的直接感官,没有调整好的话甚至会引起头晕,因此“摄像机体验”也是MMO开发的重点研究对象。
给我们游戏的主摄像机挂上一个用来实现摄像机跟随的脚本,跟随的Target设置成我们Player身上的一个Transform(MyPosition)
MMORPG战斗模式可以分为多种,这个主要看游戏设定的核心玩法是什么样的。我们的MiniMMO的话,就先实现一个最基础的近战战斗。
这儿我们只是实现了一下最简单的普通攻击,实际开发中要达到很好的攻击手感,要做的和计算的东西远不止于此,这里只是提供一个设计思路,后续大家有兴趣的可以深入研究一下。
游戏有状态机,我们的游戏主角、怪物也需要有一个状态机,来控制玩家的各种状态。
有限状态机属于设计模式范畴的一种设计,为的也是用扩展性更好的代码实现玩家的各种状态之间的切换,如:walk、run、Idle、Attack、Dead等。如果简单的做切换的话,可能就是大量的if else逻辑,可读性不好且扩展性差,代码耦合度太高。
但这个动画状态机局限于控制动画的状态切换,因此游戏内对象的自定义状态需要我们自己去实现。
通过上述方式,扩展玩家的状态,然后通过FSM.Enter进入对应的状态,而进入对应状态之后的方法处理逻辑,我们已经全部通过FSM类封装好了,就不需要再关注这个部分了。这样设计的好处就是避免了大量的if else判断,而且扩展起来非常方便,再添加一个子类即可。
为了更好的了解插件的接入和开发,我们尝试接入一个破碎插件,可以做一些游戏内的物件破碎的效果,例如我们MMORPG里,可以使用技能去打一下物件,击碎之后会爆出一些道具。
2.下载后,双击运行,导入我们项目(建议每个插件单独用一个文件夹管理,放入Plugins文件夹里)
导入后就可以写代码了,先创建一个我们需要破碎的对象,然后通过插件将他分块(分成很多破碎块)
具体这个破碎效果如何实现的,有兴趣的可以看看这个插件的源码,先是将一个GameObject拆分成了很多个子物体(碎块),然后爆破的时候通过一定的物理计算来实现破碎的一个效果。
我们可以在Unity的Build Setting中切换到对应平台进行开发,然后进行对应平台的打包。
打包之后,我们会生成以上两部分文件,一部分是可运行的exe文件,一部分是我们的游戏库数据(包括Mono库等),两个文件必须在同一个目录下才可以运行。
当项目到了一定规模,打包时间会很长,因此肯定不能单纯地靠人工去打包。另我们需要写一些脚本来实现自动化的打包,下面简单写一些伪代码,仅提供一些思路,网上也有不少文章有自动化打包相关的详细解析,也可以学习一下。
打包后可以先将游戏运行到我们真机上,然后用我们的UnityProfiler连上,进行性能上的检测
关于UnityProfiler具体如何去采样以及真机调试,可以学习一下:
2.左边这一列加入我们需要监测的内容,例如我们要对CPU,GPU,Memory同时采样。
3.开始运行游戏,底部面板就会显示,我们游戏运行时的具体情况,包括每个函数每个模块的具体CostTime和CPU Cost等。
4.分析采样结果,如上图采样下来,DockArea的OnGUI函数占比比较高,因为我在游戏里加了很多测试调试的GUI,这部分效率是非常低的,正式发布的时候可以将他们去掉。
另外NGUI的UIPanel.LateUpdate占比也比较高,我们也可以看一下我们代码里实现得是否有问题。
我们也可以自己加一些测试代码,来验证一下UnityProfiler能否采样到我们人为制造的性能瓶颈
可以看到,光GameManager.Update就占用了37%的CPU,非常精准。因此我们在打包前后,都可以针对我们游戏的性能进行一次采样,并且针对性地做一些优化。
上一点,我们说到,可以根据我们通过UnityProfiler对我们工程的分析,针对一些性能瓶颈进行优化
虽然我们的小项目,没有什么内存压力,但是如果是大型MMORPG,内存问题永远是优化的一大重点,因为场景大、对象多、资源多,如何尽可能的减少内存占用就很重要了
我们需要根据Profiler中的内存占用堆栈针对性地去做对应的优化,可以从几个方面入手:
资源:比如我们的一些资源加载了不用了记得要销毁;避免一次性加载太多资源(内存峰值);单个资源不要太大(例如模型、地图、纹理图集);
脚本:Update内不频繁申请和释放内存,减少内存碎片的产生,且如果出现bug容易导致内存溢出;减少函数(栈空间);使用优质算法;
插件:有一些插件的使用时也可能会占不少内存,可以根据堆栈进行优化或者裁剪一些不必要的内容。
同样,在Profiler中可以看到CPU的每个函数占比,也是需要去选择性的优化,主要也是从几个方面入手:
减少GC:避免产生各种临时对象(例如string拼接)、碎片化内存(装箱拆箱)等
降低DrawCall:可以从我们UI、特效上,对一些纹理图片进行合批;也可以把一些模型例如山体岩石树木等做一些DrawCall Batch等。
在Unity2018之后的版本,推出了JobSystem等一些多线程方案的支持,也可以减少我们游戏主线程的CPU占用。
我们MiniMMO涉及渲染相关的东西几乎没有,但是成熟的重度游戏往往对于渲染质量和效率要求是极高的,因此GPU上的优化也是突破性能瓶颈的重要手段。
篇幅有限,很多实现细节都没有很好地写清楚,希望游戏er们见谅,这篇文章主要是从框架的涉及到模块分解再到一些功能的具体实现。主要是给那些想自己做游戏,或者刚接触游戏开发、想了解一款成熟的游戏是怎么开发的同学看的。文章所有东西都是根据自己的理解,自己做的一个Demo所产出,可能有一些错误,也希望大家在评论区指出。
另外,咱们的雷火Mini项目,会比这个简陋的Demo好上10倍甚至百倍,还在等什么~快来和我们一起创造游戏行业未来吧!ヽ(✿゚▽゚)ノ
|