Notifications
Article
模拟经营游戏《Project Hospital》的优化经验
Updated a month ago
672
9
本文将由《Project Hospital》的开发者Jan Benes分享该项目的优化经验。
由独立工作室Oxymoron Games使用Unity打造的《Project Hospital》(中文名:《医院计划》)是一款模拟经营类游戏。它具备该类型游戏的特点,包括:由玩家创建的动态场景,大量的活动角色和物体以及可扩展的UI系统。
让《Project Hospital》在不同硬件上运行需要不少努力,而且这也是“积小流以成江海”的典型性能优化案例,正是我们处理了许多小步骤,解决了大量具体问题,花费很多时间进行性能分析,才有最后性能卓越的提升。

性能目标

在早期开发阶段中,我们为将要支持的大型场景设定了主要性能目标以及硬件要求。
我们的目标是至少在一个屏幕上同时显示100个活动的动态角色。《Project Hospital》共有300个活动角色,大小约为100 x 100的瓦片地图,最多有4个楼层。
我们希望游戏以合适的帧率运行在1080P的画面中,即使在集成显卡上也有这样的效果。由于CPU是此项目标的主要影响因素,所以这种效果并不难实现。随着医院的扩大,集成显卡在2560 x 1440的高分辨率下才开始比较吃力。
为了实现简单的模组支持,游戏中的大部分数据都是开放的。这意味着和打包文件相比会牺牲一些性能,但这不会造成太大影响,只会稍微延长加载时间。

图形

由于《Project Hospital》是一款典型的2D等距游戏,所有内容都是从后向前渲染。在Unity中,这种渲染方式通过设置图形对象上正确的Z值或摄像机距离来表现。
在可能的情况下,不相互交互的对象会组织到不同图层,例如:地板和物体及角色是相互独立的。
等距渲染场景中的所有几何体都是使用C#代码动态创建的,因此对于图形性能而言,几何体重构的频率是重要部分之一,另一个重要部分是Draw Call的数量。

Draw Call

无论对象的简单程度如何,一帧中绘制的独立对象数量都是主要的限制,特别是在低端的硬件上,而Unity本身也有额外的开销。最好的解决方案是将更多图形对象批处理到单个Draw Call中。
这样得到一些有趣的结果,例如:由于支持批处理的对象是和摄像机有相同距离的对象,因此其它图形会得到正确的渲染。
下面列举几个数字:在96 x 96的贴图上,我们理论上可以放9216个对象,也就是要进行9216次Draw Call,在批处理后,Draw Call减少为192次。
在实际环境下,情况会变得更加复杂,因为只有具有相同的纹理的对象才可以进行批处理,所以得到结果的优化程度不那么高,然而这种方法依旧是很有效的。
大多数批处理通过手工完成,从而对结果进行控制。我们也使用了Unity的动态批处理作为后备方案,但这种方案是一把双刃剑。
Unity的动态批处理确实可以帮助减少Draw Call的数量,但是每帧都有额外性能开销,在某些情况下,性能开销会难以预测。
例如:与摄像机距离相同的二个重叠精灵会在不同帧以不同顺序进行渲染,这会造成精灵闪烁现象,手工进行的批处理则不会出现这种现象。

多楼层建筑

允许玩家构建多楼层建筑会增加很多复杂度,但是对性能有所帮助。
只有在活动楼层和户外环境的角色和物体需要动画和渲染,医院中活动楼层之下或之上的内容都可以隐藏起来。

着色器

《Project Hospital》使用相对简单的自定义着色器,利用了颜色替换等多个技巧。
例如:通过在着色器代码中使用条件,角色着色器可以使用最多5种颜色替代。它的性能开销较大,但因为角色很少占据屏幕的大量空间,所以这不是我们需要担心的问题。
我们也学会了如何避免设置着色器参数,转而使用顶点颜色。

纹理质量

有趣的是,我们没有在《Project Hospital》中使用任何纹理压缩。因为如果对游戏中存在矢量风格的图形进行压缩,有一些纹理会看起来非常糟糕。
为了节省系统上的GPU内存,使它小于1GB,除了用户界面的纹理外,我们会自动减小游戏内纹理大小为一半的分辨率,我们可以将选项设置为“texture quality : low”(纹理质量:低)。UI纹理会保持原始分辨率。

多线程处理

虽然Unity脚本逻辑基本上是单线程的,但我们可以选择使用C#代码运行多线程。对于游戏逻辑而言,这可能不是一种合理的方法,但是有些非时间相关的任务可以受益于以Job System形式在各个单独的线程上运行。
在这款游戏中,我们把多线程用于二个功能:
  • 寻路作业,特别是在大型地图上,由于糟糕的布局可能需要数百毫秒处理,因此它是从主线程移除的理想选择。并行作业的数量会考虑到机器上硬件线程的数量。
  • 光照贴图也会在单独的线程上更新,但每次仅更新一个楼层,它不是一个重要的系统,房间中的自动灯光会以较慢的更新速度淡出。

动画

在开发阶段早期,我们就决定使用2D骨架动画系统。在考虑当时不同的动画软件后,我们最后修改和使用了几年前开发的简单系统,使它符合《Project Hospital》的特别用例,你可以把它看作直接支持创建角色变体的简单样条曲线。
类似于样条曲线,该工具系统使用C#运行时,显然这比原生代码的性能开销更大,因此我们在开发期间进行了多轮优化。幸运的是绑定非常简单,每个角色仅有20个骨骼。
最重要的部分是在访问单独骨骼的Transform时,从地图查询切换为简单数组索引的过程。
除了不使摄像机视图外的角色有动态效果,另一个和动画相关的技巧是让主UI窗口背后的角色也不需要有动态效果,然而切换为半透明UI后,我们无法在最终版本使用这个技巧。

缓存

在可能的情况下,我们会尝试仅在有影响数值的改动时,运行有特别要求的计算。
最好例子大概是房间和电梯,在角色放置电梯或建起墙面时,我们会运行楼层填充算法,它会标记可以访问电梯和房间的瓦片。这样会加快寻路过程,向玩家展示哪些房间目前无法访问。

分散和延迟的更新

在一些情况下,我们会偶尔运行一次特定更新,我们使用了下面的方法。
一些更新每帧只可以在一部分角色运行,因此会出现这样的情况:一半病人的行为脚本仅在奇数帧更新,另一半会在偶数帧更新,而动画和移动会流畅的运行。
在特别状态下,有时角色闲置时会调用开销较大的代码,例如:员工检查过程需要填充和寻找可用设备,该过程仅在特定时期完成,例如:每秒进行一次。
性能开销最大且最常见的调用是每个病人可以进行哪些检查的估算。我们需要评估很多因素,例如:哪个部门的员工目前处于忙碌状态,哪些设备目前已被预订。
因为他们的指定医生和谈话技能也有影响,所以这些信息对所有病人来说并不常见。游戏中可能有多个可用检查需要执行,因此更新只在每帧进行几次,而且会持续到下一帧。

其它经验

优化具有大量不同交互部分的游戏是一个持续的过程,使用Unity的性能分析器能够解决性能影响较大的部分问题。游戏按照我们原来的目标运行,玩家可以给游戏添加模组,使游戏内容超过原始角色限制。
相对于我参与过的其他AAA级游戏项目,《Project Hospital》具有最复杂的游戏逻辑,因此很多问题是和项目相关的。
最后要建议大家:不管是什么项目,一定根据游戏的复杂性预留出足够的时间进行优化。

结语

《Project Hospital》的优化经验为大家介绍到这里,更多Unity项目的优化经验分享,尽在Unity官方微信:Unity官方平台。
Tags:优化
Unity China
644
Comments
朱海林
a month ago
受益匪浅,挺好的
0
东风不败
a month ago
从考试考试考试
0
李四
a month ago
学习了
0
g
guohua
Staff
a month ago
test
guohuagood
good
2
g
guohua
Staff
a month ago
test
good
1