模拟游戏如何优化?《医院计划》开发者分享

【Gamelook专稿,转载请注明出处】

Gamelook报道/经营模拟游戏是当下比较热门的独立游戏品类,其中一些作品还加入了让玩家创作内容的模式,比如Oxymoron Games在去年底发行的《医院计划(Project Hospital)》,就在Steam平台获得了80%以上的好评。

这是一款建造经营类游戏,玩家们在游戏里可以同时成为一流的医生、有抱负的建筑师和成功的管理者,你可以设计属于你自己的医院,调整每一个细节或者使用预置模型直接行医。为了经营医院,你要联系不同的保险公司获得更多带有特殊医疗条件的病人,执行检查、实验室测试,并使用各种设备解决各种诊断难题,送病人住院,用手术和其他治疗手段帮助病人痊愈。那么,作为独立团队,如何处理这样一款复杂的游戏呢?Oxymoron工作室共同创始人Jan Benes最近在博客中分享了他们的一些优化技巧和心得,想要做建造经营类游戏的同行们可以参考:

《医院计划》试玩视频(时长26分钟)

《医院计划》有着该品类游戏的所有经典特色:玩家们创造的动态内容、大量活跃的角色和物体以及可拓展的UI系统。想要在不同设备上完美运行这款游戏,需要大量的努力,而且也是非著名案例“凌迟”(a death by a thousand cuts,即每件事都不致命,但累积起来却可以造成严重后果)的最佳案例,所以需要大量细节步骤,解决很多特殊问题,也需要在工具上投入大量时间。

性能指标:我们真正要实现的是什么?

在研发的初期,我们设定了想要支持的场景,达到的性能以及硬件需求等目标。

我们的目标是可以至少100个全动画角色同屏展示,最高同屏可展示300个角色,做一个100X100图块地图,最高四层。并且希望,哪怕是在集成显卡上,也能在最高帧率下实现1080P效果,这本身并不难实现,因为CPU才是最主要的因素,尤其是随着亿元的增长。现代的集成显卡已经比较强大,只有到2560X1440分辨率的时候才会显得吃力。

对于简易模组支持,游戏里大多数据是开放的,这就意味着为了一些打包文件而牺牲了部分性能,但这似乎不会有太大的影响,唯一的问题可能就是加载时间更长些。

图形

由于《医院计划》是一款经典的2D三维不透明游戏,你可以想象所有东西都是从后往前渲染的,在Unity引擎里,每一个图形物体都需要设置正确的Z值(或者说是到摄像头的距离)。在可能的情况下,不互动的物体会被放到不同的层面,比如不同楼层与物体和角色都是独立的。

在垂直渲染的场景中,所有的几何图形都是用C#动态创作的,所以需要重建的几何图形频率是两个最重要部分的其中之一,第二个部分就是绘制调用(Draw Call)次数。

Draw Call

不管物体有多么简单,单个物体的绘制都要占用一帧,这是最主要的限制之一,尤其是在低端设备上(再加上Unity本身也有额外占用),最明显的解决方案就是尽可能在一次绘制调用中批处理更多的图形物体,但这么做会带来一些有趣的结果,比如可以批处理的物体和摄像头的距离相同,所以其他图形也可以在前面或者后面被恰当地渲染。

这里列举一些数字:在一个96X96地图上,,理论上你可以放置9216个物体,这可能需要9216次draw call,批处理之后,调用次数降低到了192次。

然而,在现实条件下,这会变得更复杂一些,因为只有相同纹理的物体才能被批处理,所以结果并没有那么乐观,不过,这种方法仍然是可行的。

大多数的批处理都是手动操作的,这样可以控制结果,我们还是用Unity的动态批处理作为“最后手段”解决方案,但这种方法有利也有弊,它的确可以降低调用次数,但每帧都会多占用性能,而且有些情况下不可预测。比如两个与摄像头等距重叠的精灵图会以不同顺序被渲染在不同帧,这就会产生抖动,而手动批处理是不会有这种情况的。

多楼层

让玩家们能够建造多层建筑增加了很多的复杂性,但却可以对性能带来惊人的帮助。在活跃楼层,只有角色和武器需要动画和渲染,医院内活跃楼层上下所有的东西都可以被隐藏。

着色器

《医院计划》使用了相对简单的定制化着色器,只有颜色替换等少数功能,比如角色着色器可以最多替换5种颜色(在着色器代码中使用condition),而且这种做法的代价相对昂贵,不过,由于角色所占屏幕的空间不大,所以通常不会带来太大问题。这么做也是非常值得的,因为不限制角色的衣服颜色也给游戏环境和玩家角色带来了多样性。

我们还快速学会了避免设置着色器参数,并且尽量使用顶点色。

纹理质量

一个比较有趣的信息是,我们在《医院计划》里没有使用任何的纹理压缩,在矢量图形中特定纹理压缩之后看起来的效果很差。

为了给内存低于1GB的设备节约GPU性能,我们自动把游戏内纹理分辨率减半(UI界面不变),所以游戏内纹理的质量是比较低的,而UI纹理保持了原本的分辨率。

优化CPU性能:多线程

虽然Unity的脚本逻辑基本上是单线程的,但你始终可以选择直接用C#运行多线程。在游戏逻辑方面,这可能不是一个好方法,但在某些形式的任务系统(Job System)里,通常有一些非临界时间任务可以从单独线程运行中受益,在我们的案例中,我们为两种功能使用了多线程:

寻路任务,尤其是大地图和布局不佳的环境,寻路往往需要数百毫秒,因此这个任务非常适合从主线程剥离出来,平行任务的数量需要考虑电脑硬件上的线程数量。

光照地图也是通过单独线程更新的,但一次只更新一层楼,这并不是一个重要的系统,而且房间里的自动灯光会随着缓慢更新而淡出。

动画制作

我们在研发很早期的时候就决定使用2D骨骼动画系统,在考虑了当时所有的动画软件之后,我们最后决定使用我在几年前做的一个简单系统(当时是业余爱好项目),来满足《医院计划》里的特殊需求,基本上就是简单的支持角色变化的Spine动画。和用C#运行Spine一样,很明显这种方法比使用原生代码的代价更高,所以在研发期间我们做了多次优化,幸运的是结构(rig)都非常简单,每个角色大概有20个骨骼。

比较无厘头的事实:当访问单个骨骼的转换时,最重要的一点是从地图查找切换至数组中的简单索引。

除了不给摄像头之外的角色做动画之外,另一个技巧就是藏在主UI窗口背后的角色也不需要做动画,不幸的是,由于使用了半透明UI,我们在最终版本中没能使用该技巧。

缓存

在可能的条件下,只有当影响数值的变化时,我们才会尝试运行更为苛刻的计算,最好的例子可能是房间和电梯,当玩家们放置一个电梯或者墙体的时候,我们会运行泛洪填充算法,它可以标记电梯和房间可以使用哪些图块,这在随后会加速寻路,而且可以用于向玩家展示哪个房间目前是不可用的。

分散、延迟的更新

某些情况下隔段时间运行特定更新是有意义的,我们使用的方法有以下几个:

有些更新只需要在每帧在角色的一部分运行,比如病人一半的行动脚本只在比较奇怪的帧更新,另一半则在普通帧更新(同时动画和动作都可以流畅运行)。

在特定情况下,尤其是角色限制但需要调用比较大量代码时,更新只在特定时间内运行,比如每秒一次。

最昂贵、有时候也最普通的口令是每个病人的检查评估,这里需要衡量很多因素,比如一个部门里的哪个职员正在忙、哪个设备目前被预约。这个信息对于所有的病人也是不寻常的,因为他们的主治医师和他们的语言能力也是有影响的。有时候你需要检查的东西有很多,所以更新只在少数帧运行,而且在下一帧继续。

结论与心得

优化一款带有大量互动元素的建造模拟游戏是个持续的过程,通过使用Unity里的工具集,并且解决最糟糕的障碍,成为了我在研发过程中的常规任务。

虽然游戏研发总有提高的空间,但我们对于目前的结果是满意的,游戏的运行符合我们原定的目标,玩家们也经常给游戏做模组,极大超出了原有角色的限制。

或许值得一提的是,即便是和我曾经从事过的3A游戏相比,我觉得《医院计划》也是我见过最复杂的玩法逻辑,所以很多问题是特定项目才出现的,不过,预留足够的时间做优化是任何项目都需要做的。

本文为GameLook原创文章,如若转载,请注明出处:http://www.gamelook.com.cn/2019/07/364503

关注微信