使用Unity 3D优化游戏运行性能的经验
流畅的游戏玩法依赖于流畅的帧率,我们即将推出的动作平台游戏《Shadow Blade》,将在标准iPhone和iPad设备上实现每秒60帧作为重要目标。以下是我们在紧凑的优化过程中,提升游戏运行性能并实现目标帧率时需要考虑的事项。
当基本游戏功能完成后,就需要确保游戏运行表现达标。我们衡量游戏运行表现的基本工具是Unity内置分析器以及Xcode分析工具。使用Unity分析器分析设备上的运行代码是一项非常实用的功能。
我们总结了为将目标设备的帧率控制在60fps而进行的衡量、调整、再衡量过程中的相关经验。

一、合理调用“垃圾回收器”(Garbage Collector,简称GC)
由于我们具有C/C++游戏编程背景,不太习惯无用单元收集程序的特定行为。自动清理不用的内存,刚开始看似不错,但很快会发现分析器经常显示CPU负荷过大,原因是垃圾回收器正在收集垃圾内存,这在移动设备上问题更为突出。我们需要跟进内存分配,并尽量避免产生垃圾内存,主要操作如下:
- 移除代码中的字符串连接:字符串连接会给GC留下大量垃圾,应避免在代码中使用。
- 用“for”循环替代“foreach”循环:每个“foreach”循环的每次迭代会生成24字节的垃圾内存。例如,一个简单的循环迭代10次就会产生240字节的垃圾内存,所以建议使用简单的“for”循环。
- 更改检查游戏对象标签的方法:使用“if (go.CompareTag (“Enemy”))”代替“if (go.tag == “Enemy”)”。在内部循环中调用对象分配的标签属性并拷贝额外内存是糟糕的做法。
- 使用对象库:为所有动态游戏对象制作和使用库,这样在游戏运行时不会动态分配任何东西,不需要时将所有东西放回库中。
- 避免使用LINQ命令:LINQ命令通常会分配中间缓存器,容易生成垃圾内存。
二、谨慎处理高级脚本和本地引擎C++代码之间的通信开销
所有使用Unity3D编写的游戏玩法代码都是脚本代码,在我们的项目中是使用Mono执行时间处理的C#代码。与引擎数据通信需要调用本地引擎代码,这会产生开销,因此应尽量减少游戏代码中的此类调用。具体方法如下:
- 缓存对象转换需求:在游戏玩法代码的一个帧中缓存某一对象的转换需求,一次仅向引擎发送一个请求,以减少调用开销。这种模式不仅适用于移动和旋转对象。
- 本地缓存元件引用:将引用本地缓存到元件中,减少每次在游戏对象中使用 “GetComponent” 获取元件引用的需求,这也是减少调用本地引擎代码的一种方式。
三、优化物理效果
- 最小化物理模拟时间步:在我们的项目中,物理模拟时间步不能低于16毫秒。
- 减少角色控制器移动命令调用:移动角色控制器同步发生,每次调用都会消耗大量性能。我们的做法是缓存每帧的移动请求,仅运用一次。
- 避免依赖“ControllerColliderHit”回调函数:这些回调函数处理速度较慢,应修改代码避免依赖它们。
- 根据设备性能选择合适的模型:对于性能较弱的设备,使用skinned mesh代替physics cloth。合理调整cloth参数,找到美学与运行表现的平衡点。
- 谨慎使用ragdolls:在物理模拟过程中,尽量不使用ragdolls,仅在必要时启用。
- 谨慎评估触发器的“onInside”回调函数:在项目中,尽量在不依赖它们的情况下模拟逻辑。
- 使用层次代替标签:为对象分配层次和标签并查询特定对象时,涉及碰撞逻辑,层次在运行表现上更具优势,能实现更快的物理计算和更少的无用内存分配。
- 避免使用Mesh对撞机:Mesh对撞机对性能影响较大,应避免使用。
- 最小化碰撞检测请求:减少碰撞检测请求(如ray casts和sphere checks),并尽量从每次检查中获取更多信息。
四、优化AI代码性能
我们使用AI敌人来阻拦忍者英雄并进行对战。以下是与AI性能问题相关的建议: AI逻辑(如能见度检查等)会生成大量物理查询,可以将AI更新循环设置低于图像更新循环,以减少CPU负荷。
五、关闭不必要的元素以提升性能
没有发生特殊情况时,说明性能良好。这是我们关闭一切不必要之物的基本原则。我们的项目是侧边横向卷轴动作游戏,因此当物体不具有可视性时,可以关闭许多动态关卡物体。具体方法如下:
- 使用细节层次定制关卡:关闭远处敌人的AI。
- 关闭移动平台和障碍的物理碰撞机:当它们远离可视范围时。
- 利用“动画挑选”系统:关闭未被渲染对象的动画。
- 禁用关卡内的粒子系统:使用同样的禁用机制。
六、减少Unity回调函数
应尽量减少Unity回调函数,即使是空白的回调函数也会造成性能损失,没有必要将其留在代码库中(尤其是在大量代码重写和重构时)。
七、发挥美术人员的作用
在程序员为提高帧率绞尽脑汁时,美术人员往往能发挥重要作用。具体操作如下:
- 共享游戏对象材料:让游戏对象材料在Unity中处于静止状态并绑定在一起,简化绘图调用,这是呈现良好移动运行性能的重要因素。
- 使用纹理地图集:纹理地图集对UI元素尤其有用。
- 合理压缩纹理:方形纹理以及两者功率的合理压缩是必要步骤。
- 简化背景:移除远处背景的网格,将其转化为简单的2D位面。
- 利用光照图:光照图具有重要价值。
- 优化顶点:在一些关口移除额外顶点。
- 使用合理的纹理mip标准:这在让不同分辨率的设备呈现良好帧率时尤为重要。
- 结合网格:美术人员可以进行网格结合操作。
- 共享动画:动画师应尽力让不同角色共享动画。
- 平衡美学与性能:找到美学/性能之间的平衡,需要对许多粒子效果进行迭代,减少发射器数量并尽量减少透明度需求是一大挑战。
八、减少内存使用
使用大内存会对性能产生负面影响,在我们的项目中,iPod因超过内存上限多次崩溃。游戏中最耗内存的是纹理。具体措施如下:
- 根据设备选择纹理大小:不同设备使用不同的纹理大小,尤其是UI和大型背景中的纹理。《Shadow Blade》使用通用型模板,在启动时检测设备大小和分辨率,载入不同资产。
- 避免未使用资产载入内存:确保未使用的资产不会载入内存,找出仅被一个预制件实例引用且从未完全载入内存中实例化的资产。
- 去除网格中的额外多边形:减少网格中的额外多边形,降低内存使用。
- 重建资产生命周期管理:调整主菜单资产、关卡资产、游戏音乐等的加载/卸载时间和有效期限。
- 定制对象库:每个关卡根据其动态对象需求定制特定对象库,并根据最小内存需求进行优化。对象库在开发过程中可以灵活包含大量对象,但在明确游戏对象需求后应具体设置。
- 压缩声音文件:保持声音文件在内存中的压缩状态。
加强游戏运行性能是一个漫长而具有挑战性的过程,游戏开发社区分享的大量知识以及Unity提供的出色分析工具,为《Shadow Blade》实现目标运行性能提供了极大帮助。