- 用户指南
- 资源商店 (Asset Store)
- 资源服务器 (Asset Server)(仅限团队许可证)
- 缓存服务器(仅限团队许可证)
- 幕后场景
- 创建游戏
- 运行时实例化预设 (Prefabs)
- 变换 (Transforms)
- 物理
- 添加随机的游戏元素
- 粒子系统(Particle Systems)
- Mecanim 动画系统
- 旧动画系统
- 导航网格 (Navmesh) 和寻路 (Pathfinding)(仅限专业版 (Pro))
- Sound (音频侦听器)
- 游戏界面元素
- 多玩家联网游戏
- iOS 开发入门
- Android 开发入门
- Blackberry 10 开发入门
- Metro:入门指南
- 本地客户端开发入门
- FAQ
- Advanced
- Vector Cookbook
- 资源包(仅限专业版)
- Graphics Features
- 资源数据库 (AssetDatabase)
- 构建播放器管道
- 分析器(仅限专业版)
- 光照贴图快速入门
- 遮挡剔除(仅限专业版)
- 相机使用技巧
- 运行时加载资源
- 通过脚本修改源资源
- 用程序生成网格几何体
- 富文本
- 在 Unity 工程 (Project) 中使用 Mono DLL
- 事件函数的执行顺序
- 移动优化实用指南
- Unity XCode 工程结构
- 优化图形性能
- 减少文件大小
- 理解自动内存管理
- 平台依赖编译
- 泛型函数
- 调试
- 插件(专业版/移动版特有功能)
- 文本场景文件格式(仅限专业版)
- 流媒体资源
- 启动时运行编辑器脚本代码
- 网络模拟
- VisualStudio C 集成
- 分析
- 检查更新
- 安装多版本 Unity
- 故障排除
- Unity 中的阴影
- Unity 中的 IME
- 对集成显卡进行优化
- 网络播放器 (Web Player) 部署
- 使用网络播放器中的信任链系统
移动优化实用指南 — 优化脚本
本节介绍了如何优化游戏使用的实际脚本和方法,同时也详细说明了介绍了优化能够发挥作用的原因,以及为何在某些情况下应用优化可以带来诸多益处。
分析器 (Profiler) 才是王道(Unity 专业版)
事实上,并不存在这样一个供勾选的复选框清单,可以确保工程顺利运行。如需优化运行缓慢的工程,必须通过分析找到占用过多时间的特定“罪犯”。在没有进行性能分析,或没有充分理解分析器提供 的结果的前提下优化,无疑是盲目的优化。
因此,如果想要技术要求非常严苛的游戏在移动平台上运行,可能需要使用 Unity 专业版的分析器 (Profiler)。
Indie 怎么样?
您可以使用内部分析器判断哪些过程导致游戏运行缓慢,是物理、脚本还是渲染,但您不能再深入探讨详细的脚本和方法,找到问题的根源所在。但是,通过在游戏中构建可以启用和禁用某些功能的交换器,可以明显缩小问题根源的范围。例如,如果删除敌人角色的 AI 脚本,且帧速率变成两倍,您将知道,是脚本或是脚本带入游戏中的其他方面必须进行优化。唯一的问题,是在找到问题之前,必须尝试不同的方法。
如需了解移动设备性能分析的更多详细信息,请参阅性能分析部分。
通过设计优化
尝试开发从一开始就能快速运行的游戏具有很大的风险,因为必须权衡像在没有优化的情况下一样,游戏照样快速运行所花费的时间,和由于游戏太慢在稍后必须对其进行删除或替换所花费的时间。就此而言,开发人员对硬件必须具备敏锐的直觉和丰富的知识,特别是由于每种游戏都与众不同,一种游戏的关键优化对另一种游戏而言可能完全不起作用。
对象池 (Object Pooling)
在优化脚本方法简介中,我们将对象池 (Object Pooling) 作为同时具有合理的游戏设置和良好的代码设计的示例。将对象池用作短暂的对象比创建并摧毁这些对象更加快速,因为它让内存分配更加简单,并清除了动态内存分配占用和垃圾收集 (Garbage Collection,或称 GC)。
内存分配
function showhide(link) { var theDiv = document.getElementById(link.id+"_div"); if (theDiv) { var curState = theDiv.style.display != 'none'; theDiv.style.display = (curState ? 'none' : 'block'); var str = (curState ? "+ Show [" : "- Hide [") + link.title + (curState ? "] +" : "] -"); link.innerHTML = str; } } + Show [自动内存管理的简单阐释] +在 Unity 编写的脚本使用自动内存管理。基本上所有脚本语言均是如此。与此相反,C 和 C++ 等低级别语言则使用手动内存分配,程序设计员可以直接从内存地址读取和编写,因此,他负责清除其创建的所有对象。例如,如果在 C++ 中创建对象,那么在完成之后,必须手动解除对象占据的内存分配。在脚本语言中使用 objectReference = null;
即可。
请注意:如果拥有一个像 GameObject myGameObject;
或者 var myGameObject : GameObject;
一样的对象变量,写入 myGameObject = null;
时,为什么不会摧毁这个对象?
- 游戏对象依然被 Unity 引用。因为 Unity 必须保持对对象的引用,才能进行绘制、更新等。调用
Destroy(myGameObject);
可清除参考并删除对象。
但是,如果创建一个 Unity 一无所知的对象,例如,没有继承任何东西的类实例(相比之下,大部分类或“脚本组件”都继承自 MonoBehaviour),然后设置其参考变量为空值,事实上,就您的脚本和 Unity 而言,对象已经丢失;它们不能访问且不会再看到对象,但它依然保存在内存中。在一段时间之后,垃圾收集器 (Garbage Collector) 将运行并清除内存中没有任何引用的所有数据。之所以能够做到这一点,是因为在场景后,每个内存块的参考数量都一致处于追踪之下。这也是脚本语言之所以比 C++ 慢的一个原因。
在 Unity 编写的脚本使用自动内存管理。基本上所有脚本语言均是如此。与此相反,C 和 C++ 等低级别语言则使用手动内存分配,程序设计员可以直接从内存地址读取和编写,因此,他负责清除其创建的所有对象。例如,如果在 C++ 中创建对象,那么在完成之后,必须手动解除对象占据的内存分配。在脚本语言中使用 objectReference = null;
即可。
请注意:如果拥有一个像 GameObject myGameObject;
或者 var myGameObject : GameObject;
一样的对象变量,写入 myGameObject = null;
时,为什么不会摧毁这个对象?
- 游戏对象依然被 Unity 引用。因为 Unity 必须保持对对象的引用,才能进行绘制、更新等。调用
Destroy(myGameObject);
可清除参考并删除对象。
但是,如果创建一个 Unity 一无所知的对象,例如,没有继承任何东西的类实例(相比之下,大部分类或“脚本组件”都继承自 MonoBehaviour),然后设置其参考变量为空值,事实上,就您的脚本和 Unity 而言,对象已经丢失;它们不能访问且不会再看到对象,但它依然保存在内存中。在一段时间之后,垃圾收集器 (Garbage Collector) 将运行并清除内存中没有任何引用的所有数据。之所以能够做到这一点,是因为在场景后,每个内存块的参考数量都一致处于追踪之下。这也是脚本语言之所以比 C++ 慢的一个原因。
- 了解更多有关自动内存管理和垃圾收集器的信息。
如何避免分配内存
每创建一个对象都将分配内存。您经常会在代码中创建对象,甚至自己都没有发现。
- Debug.Log("boo" + "hoo"); 创建一个对象。
- 在处理大量字符串时,使用 System.String.Empty 而不是 ""。
- 快速模式 GUI (UnityGUI) 很慢,考虑到性能时,在任何时间都不应使用。
- 类和结构的不同之处:
类是目标,起引用作用。如 Foo 是一个类,并且
Foo foo = new Foo(); MyFunction(foo);
那么,MyFunction 将收到对分配在堆上的原始 Foo 对象的引用。在 MyFunction 内对 foo 作出的任何更改都可以在任何引用 foo 的位置看到。
结构是数据,具有相同的功能。如果 Foo 是结构并且
Foo foo = new Foo(); MyFunction(foo);
那么,MyFunction 将收到 foo 的副本。foo 既不在堆上分配,也不会作为垃圾收集。如果 MyFunction 修改 foo 的副本,其他 foo 不会受到任何影响。
类是目标,起引用作用。如 Foo 是一个类,并且
Foo foo = new Foo(); MyFunction(foo);
那么,MyFunction 将收到对分配在堆上的原始 Foo 对象的引用。在 MyFunction 内对 foo 作出的任何更改都可以在任何引用 foo 的位置看到。
结构是数据,具有相同的功能。如果 Foo 是结构并且
Foo foo = new Foo(); MyFunction(foo);
那么,MyFunction 将收到 foo 的副本。foo 既不在堆上分配,也不会作为垃圾收集。如果 MyFunction 修改 foo 的副本,其他 foo 不会受到任何影响。
- 保存时间很长的对象应为类,而短暂存在的对象应为结构。Vector3 可能是最著名的结构。如果它是一个类的话,运行的速度会明显更慢。
为什么对象池 (Object Pooling) 更快?
非常重要的一点是,经常使用实例化和摧毁 (Instantiate and Destroy) 给垃圾收集器 (Garbage Collector) 带来了大量工作,这可能在游戏设置中导致“故障“。按照自动内存管理页面的介绍,还有其他的方式可以处理实例化和摧毁 (Instantiate and Destroy) 面临的一般性能故障,如在没有任何运行时手动触发垃圾收集器;或经常触发垃圾收集器,阻止未使用内存的大量积压。
另一个原因是,在特定的预设 (prefab) 首次实例化时,某些额外的数据会加载至 RAM,或者纹理、网格需要上传至 GPU。这也可能导致故障,而有了对象池,这些过程将在关卡加载时发生,而不是游戏设置时。
想象一下,一个木偶操纵师拥有一个容量无限大的箱子,里面装了大量木偶,每次剧本需要一个角色上场时,他都会在箱子外面得到一个新木偶的复制品,而每次角色在舞台上退出时,他都将抛弃当前的复制品。对象池就相当于在表演开始之前将所有木偶从箱子里面取出来,在不需要出场的时候,将木偶放在舞台后面的桌子上。
为什么对象池 (Object Pooling) 会更慢
一个问题是,对象池的创建将减少可用作其他目的的堆内存;因此,如果将分配内存放置在刚刚创建的对象池上,可能需要更加频繁地触发垃圾收集器。不仅如此,每次收集的速度也将更慢,因为收集所花费的时间会随着活跃对象的数量而增加。了解了这些问题之后,应该可以很明显地发现如果分配过大的对象池,或在其包含的对象无需使用的一段时间内保持其有效,都将使性能受到影响。此外,很多类型的对象并不是非常适合对象池。例如,游戏可能包含持续相当长一段时间的法术效果,或出现大量敌人,但是只能随着游戏的继续而逐个杀死。在这样的情况下,对象池的性能开销大大超过了它所带来的益处,因而不应使用。
实施
下面将两种简单炮弹的脚本做了并列比较,其中一种使用实例化 (Instantiation),另一种使用对象池 (Object Pooling)。
+ Show [对象池示例] +// GunWithInstantiate.js #pragma strict var prefab : ProjectileWithInstantiate; var power = 10.0; function Update () { if(Input.GetButtonDown("Fire1")) { var instance : ProjectileWithInstantiate = Instantiate(prefab, transform.position, transform.rotation); instance.velocity = transform.forward * power; } } | // GunWithObjectPooling.js #pragma strict var prefab : ProjectileWithObjectPooling; var maximumInstanceCount = 10; var power = 10.0; private var instances : ProjectileWithObjectPooling[]; static var stackPosition = Vector3(-9999, -9999, -9999); function Start () { instances = new ProjectileWithObjectPooling[maximumInstanceCount]; for(var i = 0; i < maximumInstanceCount; i++) { // place the pile of unused objects somewhere far off the map instances[i] = Instantiate(prefab, stackPosition, Quaternion.identity); // disable by default, these objects are not active yet. instances[i].enabled = false; } } function Update () { if(Input.GetButtonDown("Fire1")) { var instance : ProjectileWithObjectPooling = GetNextAvailiableInstance(); if(instance != null) { instance.Initialize(transform, power); } } } function GetNextAvailiableInstance () : ProjectileWithObjectPooling { for(var i = 0; i < maximumInstanceCount; i++) { if(!instances[i].enabled) return instances[i]; } return null; } |
// ProjectileWithInstantiate.js #pragma strict var gravity = 10.0; var drag = 0.01; var lifetime = 10.0; var velocity : Vector3; private var timer = 0.0; function Update () { velocity -= velocity * drag * Time.deltaTime; velocity -= Vector3.up * gravity * Time.deltaTime; transform.position += velocity * Time.deltaTime; timer += Time.deltaTime; if(timer > lifetime) { Destroy(gameObject); } } | // ProjectileWithObjectPooling.js #pragma strict var gravity = 10.0; var drag = 0.01; var lifetime = 10.0; var velocity : Vector3; private var timer = 0.0; function Initialize(parent : Transform, speed : float) { transform.position = parent.position; transform.rotation = parent.rotation; velocity = parent.forward * speed; timer = 0; enabled = true; } function Update () { velocity -= velocity * drag * Time.deltaTime; velocity -= Vector3.up * gravity * Time.deltaTime; transform.position += velocity * Time.deltaTime; timer += Time.deltaTime; if(timer > lifetime) { transform.position = GunWithObjectPooling.stackPosition; enabled = false; } } |
// GunWithInstantiate.js #pragma strict var prefab : ProjectileWithInstantiate; var power = 10.0; function Update () { if(Input.GetButtonDown("Fire1")) { var instance : ProjectileWithInstantiate = Instantiate(prefab, transform.position, transform.rotation); instance.velocity = transform.forward * power; } } | // GunWithObjectPooling.js #pragma strict var prefab : ProjectileWithObjectPooling; var maximumInstanceCount = 10; var power = 10.0; private var instances : ProjectileWithObjectPooling[]; static var stackPosition = Vector3(-9999, -9999, -9999); function Start () { instances = new ProjectileWithObjectPooling[maximumInstanceCount]; for(var i = 0; i < maximumInstanceCount; i++) { // place the pile of unused objects somewhere far off the map instances[i] = Instantiate(prefab, stackPosition, Quaternion.identity); // disable by default, these objects are not active yet. instances[i].enabled = false; } } function Update () { if(Input.GetButtonDown("Fire1")) { var instance : ProjectileWithObjectPooling = GetNextAvailiableInstance(); if(instance != null) { instance.Initialize(transform, power); } } } function GetNextAvailiableInstance () : ProjectileWithObjectPooling { for(var i = 0; i < maximumInstanceCount; i++) { if(!instances[i].enabled) return instances[i]; } return null; } |
// ProjectileWithInstantiate.js #pragma strict var gravity = 10.0; var drag = 0.01; var lifetime = 10.0; var velocity : Vector3; private var timer = 0.0; function Update () { velocity -= velocity * drag * Time.deltaTime; velocity -= Vector3.up * gravity * Time.deltaTime; transform.position += velocity * Time.deltaTime; timer += Time.deltaTime; if(timer > lifetime) { Destroy(gameObject); } } | // ProjectileWithObjectPooling.js #pragma strict var gravity = 10.0; var drag = 0.01; var lifetime = 10.0; var velocity : Vector3; private var timer = 0.0; function Initialize(parent : Transform, speed : float) { transform.position = parent.position; transform.rotation = parent.rotation; velocity = parent.forward * speed; timer = 0; enabled = true; } function Update () { velocity -= velocity * drag * Time.deltaTime; velocity -= Vector3.up * gravity * Time.deltaTime; transform.position += velocity * Time.deltaTime; timer += Time.deltaTime; if(timer > lifetime) { transform.position = GunWithObjectPooling.stackPosition; enabled = false; } } |
当然,对于复杂的大型游戏,可能需要制定一个适用于所有预设 (prefab) 的通用解决方案。
另一个示例:Coin Party!
脚本编写方法部分给出的“大量旋转效果、动态光照、可即刻在屏幕上收集的金币”示例可用来演示如何使用脚本代码、粒子系统 (Particle System) 等 Unity 组件以及自定义着色器创造炫目的游戏效果,而不会给稍显落后的移动硬件带来沉重负担。
想象一下,如果这种效果运用于二维侧面滚动游戏中,游戏拥有大量掉落的金币、弹跳和旋转效果。这些金币由点光照动态照亮。我们希望捕捉金币上闪烁的光线,让游戏更加夺人眼球。
如果拥有强大的硬件,可以使用标准方法解决这一问题。让每个金币成为一个对象,在前端使用顶点光照或延时光照着色,然后将光晕作为图像效果添加至顶端,获得闪闪发光的金币,并将光照效果混合至周围区域。
但是大量对象可能导致移动硬件卡机,光晕效果更将无法实现。那么,我们应该怎样做?
动画子画面粒子系统
如果想要显示大量对象,它们均以相同的方式移动且玩家不会仔细检查,那么使用粒子系统可以很快渲染大量对象。下面是这一技术的几种常规应用:
- 收藏品或金币
- 飞行的碎片
- 一大群简单的敌人
- 欢呼的人群
- 大量爆弹和爆炸
有一个叫 Sprite Packer 的简单的编辑器扩展可以促进动画子画面粒子系统的创建。它可以将对象帧渲染成纹理,然后将其作为粒子系统上的动画子画面表。而对于我们的示例,将在旋转的金币上使用。
参考实施
Sprite Packer 工程包含一个示例,演示这一确切问题的解决方案。
它使用不同种类的资源家族,可以在低计算预算的前提下获得眼花缭乱的效果。
- 控制脚本
- 由 SpritePacker 输出创建的专用纹理
- 专用着色器将与控制脚本和纹理紧密连接。
示例中还包含一个自述文件,介绍系统的工作原理和方式,并概括了确定需要哪些特性以及如何实现这些特性的过程。此文件如下所示:
+ Show [Coin Party 自述] +这个问题被定义为“大量旋转效果、动态光照、可立即在屏幕上收集的金币”。
最简单的方法是实例化一堆金币预设 (prefab) 副本,但是相反,我们将使用粒子渲染金币。这种方法将带来一系列必须克服的挑战。
- 粒子系统没有可视角度,这将成为一个问题。
- 假设相机位于右上方,而金币沿着 Y-轴旋转。
- 使用通过 SpritePacker 打包的动画纹理,创建金币旋转的错觉。
- 这将带来一个新的问题:所有硬币都以同样的速度和方向旋转,过于单调
- 我们自己记录旋转和寿命,然后在脚本中“渲染”旋转至粒子寿命,以修复这一问题。
- 法线也是一个问题,因为粒子没有法线,并且我们需要实时照明。
- 在由 Sprite Packer 生成的每个动画帧内,为金币正面生成单个法向向量。
- 根据上个步骤中获取的法向向量,在脚本中对每个粒子应用 Blinn-Phong 照明。
- 将结果作为颜色应用至粒子。
- 在着色器中分别处理金币的正面和边缘。
- 带来一个新的问题:着色器如何知道边缘的位置,以及边缘可以看见的部分?
- 不能使用 UV,它们已经在动画中使用。
- 使用纹理贴图。
- Y-位置需要与金币相对。
- 需要二进制的"正面 (on face)" 和 "边缘 (on rim)"。
- 我们不想引入其他纹理,更多的纹理读取也就意味着更多的纹理内存。
- 将所需信息与一个通道结合,并用它替换一种纹理颜色通道。
- 现在,金币颜色错误!我们应该怎样做?
- 使用着色器重建缺失的通道,作为余下两个通道的组合。
- 带来一个新的问题:着色器如何知道边缘的位置,以及边缘可以看见的部分?
- 假设我们想要金币上闪耀的光芒更加耀眼。发布过程对于移动设备来说过于昂贵。
- 创建另一个粒子系统,并设计更加柔软光亮的硬币动画。
- 只有在对应的金币颜色超级明亮时为光晕着色。
- 不能在每一帧对所有金币进行光晕渲染,这会扼杀填充率。
- 每帧重新设定光晕,仅放置亮度 > 0 的光晕。
- 物理是一个问题,收集金币也是一个问题 — 粒子不能进行良好的碰撞。
- 是否可以使用内置的粒子碰撞?
- 相反,将碰撞写入脚本即可。
- 最后,我们碰到了另一个问题 — 脚本发挥了很大的作用,但运行缓慢!
- 性能与有效金币的数量呈线性比例!
- 限制最大金币。此项有助于实现我们的目标:100 个金币,2 种光照,可在移动设备上快速运行。
- 性能与有效金币的数量呈线性比例!
- 进一步优化还可尝试其他方法:
- 将游戏世界分割成组块,为每个组块中的每个旋转帧计算光照条件,而不是分别为每个金币计算光照。
- 用作查找表,以金币位置和金币旋转作为指数。
- 位置使用双线性插值,增加保真度。
- 对查找表进行稀疏更新,或者彻底使用静态查找表。
- 为此使用 Light Probe?
- 使用法线贴图粒子,而不是在脚本中计算光照?
- 使用"显示法线 (Display Normals)" 着色器烘焙法线帧动画。
- 限制光照数量。
- 修复运行缓慢的脚本问题。
- 将游戏世界分割成组块,为每个组块中的每个旋转帧计算光照条件,而不是分别为每个金币计算光照。
这个问题被定义为“大量旋转效果、动态光照、可立即在屏幕上收集的金币”。
最简单的方法是实例化一堆金币预设 (prefab) 副本,但是相反,我们将使用粒子渲染金币。这种方法将带来一系列必须克服的挑战。
- 粒子系统没有可视角度,这将成为一个问题。
- 假设相机位于右上方,而金币沿着 Y-轴旋转。
- 使用通过 SpritePacker 打包的动画纹理,创建金币旋转的错觉。
- 这将带来一个新的问题:所有硬币都以同样的速度和方向旋转,过于单调
- 我们自己记录旋转和寿命,然后在脚本中“渲染”旋转至粒子寿命,以修复这一问题。
- 法线也是一个问题,因为粒子没有法线,并且我们需要实时照明。
- 在由 Sprite Packer 生成的每个动画帧内,为金币正面生成单个法向向量。
- 根据上个步骤中获取的法向向量,在脚本中对每个粒子应用 Blinn-Phong 照明。
- 将结果作为颜色应用至粒子。
- 在着色器中分别处理金币的正面和边缘。
- 带来一个新的问题:着色器如何知道边缘的位置,以及边缘可以看见的部分?
- 不能使用 UV,它们已经在动画中使用。
- 使用纹理贴图。
- Y-位置需要与金币相对。
- 需要二进制的"正面 (on face)" 和 "边缘 (on rim)"。
- 我们不想引入其他纹理,更多的纹理读取也就意味着更多的纹理内存。
- 将所需信息与一个通道结合,并用它替换一种纹理颜色通道。
- 现在,金币颜色错误!我们应该怎样做?
- 使用着色器重建缺失的通道,作为余下两个通道的组合。
- 带来一个新的问题:着色器如何知道边缘的位置,以及边缘可以看见的部分?
- 假设我们想要金币上闪耀的光芒更加耀眼。发布过程对于移动设备来说过于昂贵。
- 创建另一个粒子系统,并设计更加柔软光亮的硬币动画。
- 只有在对应的金币颜色超级明亮时为光晕着色。
- 不能在每一帧对所有金币进行光晕渲染,这会扼杀填充率。
- 每帧重新设定光晕,仅放置亮度 > 0 的光晕。
- 物理是一个问题,收集金币也是一个问题 — 粒子不能进行良好的碰撞。
- 是否可以使用内置的粒子碰撞?
- 相反,将碰撞写入脚本即可。
- 最后,我们碰到了另一个问题 — 脚本发挥了很大的作用,但运行缓慢!
- 性能与有效金币的数量呈线性比例!
- 限制最大金币。此项有助于实现我们的目标:100 个金币,2 种光照,可在移动设备上快速运行。
- 性能与有效金币的数量呈线性比例!
- 进一步优化还可尝试其他方法:
- 将游戏世界分割成组块,为每个组块中的每个旋转帧计算光照条件,而不是分别为每个金币计算光照。
- 用作查找表,以金币位置和金币旋转作为指数。
- 位置使用双线性插值,增加保真度。
- 对查找表进行稀疏更新,或者彻底使用静态查找表。
- 为此使用 Light Probe?
- 使用法线贴图粒子,而不是在脚本中计算光照?
- 使用"显示法线 (Display Normals)" 着色器烘焙法线帧动画。
- 限制光照数量。
- 修复运行缓慢的脚本问题。
- 将游戏世界分割成组块,为每个组块中的每个旋转帧计算光照条件,而不是分别为每个金币计算光照。
这个示例的最终目标或者“故事寓意”是,如果存在游戏真正需要的东西,并且它在通过常规方法实现时引起延迟,这并不意味着它无法实施,而是意味着您必须在自己运行更快的系统中投入一些工作。
同时管理数以千计目标的技术
这些特定的脚本优化可应用于包含成千上万个动态目标的环境中。将这些技术应用到游戏的所有脚本是一个可怕的想法;对于要在运行时处理大量对象或数据的大型脚本,它们应作为工具和设计指南使用。
- 避免或尽量少在大型数据集上进行 O(n2) 运算
在计算机科学中,Order 是一种运算,表示为 O(n),它代表一种方式,也就是随着其应用至 (n) 的对象数量增加,必须评估的运算次数也随之增加。
例如,考虑基本排序算法。我有 n 个数字,希望从小到大排序。
void sort(int[] arr) { int i, j, newValue; for (i = 1; i < arr.Length; i++) { // record newValue = arr[i]; //shift everything that is larger to the right j = i; while (j > 0 && arr[j - 1] > newValue) { arr[j] = arr[j - 1]; j--; } // place recorded value to the left of large values arr[j] = newValue; } }
非常重要的部分是这里有两处循环,一个套着另一个。
for (i = 1; i < arr.Length; i++) { ... j = i; while (j > 0 && arr[j - 1] > newValue) { ... j--; } }
我们假设使用这个算法最困难的情况:输入的数字已排序,但是顺序相反。在这种情况下,最里面的循环将运行 j 次。通常,i 将从 1 开始到 arr.Length-1,j 将是 arr.Length/2。按照 O(n),arr.Length 就是我们的 n,因此,总的来说,最里面的循环需运行 n*n/2 次或 n2/2 次。但是就 O(n) 而言,我们抛弃了所有常量,如 1/2,因为我们希望探讨运算次数增加的方式,而不是运算的实际次数。因此,算法为 O(n2)。如果数据集为大型数据集,那么运算顺序具有非常重要的作用,因为运算次数将以指数方式剧增。
O(n2) 运算在游戏内的一个示例是 100 个敌人,每个敌人的 AI 都要考虑到其他每一个敌人的移动。将贴图分割成单元,并将每个敌人的运动记录在最近的单元中,然后每个敌人都从距离最近的几个单元中采样。这样可能更快。这就是一种 O(n) 运算。
在计算机科学中,Order 是一种运算,表示为 O(n),它代表一种方式,也就是随着其应用至 (n) 的对象数量增加,必须评估的运算次数也随之增加。
例如,考虑基本排序算法。我有 n 个数字,希望从小到大排序。
void sort(int[] arr) { int i, j, newValue; for (i = 1; i < arr.Length; i++) { // record newValue = arr[i]; //shift everything that is larger to the right j = i; while (j > 0 && arr[j - 1] > newValue) { arr[j] = arr[j - 1]; j--; } // place recorded value to the left of large values arr[j] = newValue; } }
非常重要的部分是这里有两处循环,一个套着另一个。
for (i = 1; i < arr.Length; i++) { ... j = i; while (j > 0 && arr[j - 1] > newValue) { ... j--; } }
我们假设使用这个算法最困难的情况:输入的数字已排序,但是顺序相反。在这种情况下,最里面的循环将运行 j 次。通常,i 将从 1 开始到 arr.Length-1,j 将是 arr.Length/2。按照 O(n),arr.Length 就是我们的 n,因此,总的来说,最里面的循环需运行 n*n/2 次或 n2/2 次。但是就 O(n) 而言,我们抛弃了所有常量,如 1/2,因为我们希望探讨运算次数增加的方式,而不是运算的实际次数。因此,算法为 O(n2)。如果数据集为大型数据集,那么运算顺序具有非常重要的作用,因为运算次数将以指数方式剧增。
O(n2) 运算在游戏内的一个示例是 100 个敌人,每个敌人的 AI 都要考虑到其他每一个敌人的移动。将贴图分割成单元,并将每个敌人的运动记录在最近的单元中,然后每个敌人都从距离最近的几个单元中采样。这样可能更快。这就是一种 O(n) 运算。
- 缓存引用,而不是执行不必要的搜索。
如果游戏中有 100 个敌人,他们都向玩家移动。
// EnemyAI.js var speed = 5.0; function Update () { transform.LookAt(GameObject.FindWithTag("Player").transform); // this would be even worse: //transform.LookAt(FindObjectOfType(Player).transform); transform.position += transform.forward * speed * Time.deltaTime; }
如果有大量敌人同时移动的话,游戏可能非常缓慢。鲜为人知的事实:MonoBehaviour 中的所有组件访问器 (accessors),如 变换 (transform)、渲染器 (renderer) 和 音频 (audio) 之类的数据,都相当于其 GetComponent(Transform) 对应物,并且事实上它们的运行稍显缓慢。游戏对象 (GameObject).FindWithTag 已经过优化,但是某些情况下,这个脚本可能运行缓慢,如在内部循环中,或在大量实例上运行的脚本中。
该脚本还有一个更好的版本。
// EnemyAI.js var speed = 5.0; private var myTransform : Transform; private var playerTransform : Transform; function Start () { myTransform = transform; playerTransform = GameObject.FindWithTag("Player").transform; } function Update () { myTransform.LookAt(playerTransform); myTransform.position += myTransform.forward * speed * Time.deltaTime; }
如果游戏中有 100 个敌人,他们都向玩家移动。
// EnemyAI.js var speed = 5.0; function Update () { transform.LookAt(GameObject.FindWithTag("Player").transform); // this would be even worse: //transform.LookAt(FindObjectOfType(Player).transform); transform.position += transform.forward * speed * Time.deltaTime; }
如果有大量敌人同时移动的话,游戏可能非常缓慢。鲜为人知的事实:MonoBehaviour 中的所有组件访问器 (accessors),如 变换 (transform)、渲染器 (renderer) 和 音频 (audio) 之类的数据,都相当于其 GetComponent(Transform) 对应物,并且事实上它们的运行稍显缓慢。游戏对象 (GameObject).FindWithTag 已经过优化,但是某些情况下,这个脚本可能运行缓慢,如在内部循环中,或在大量实例上运行的脚本中。
该脚本还有一个更好的版本。
// EnemyAI.js var speed = 5.0; private var myTransform : Transform; private var playerTransform : Transform; function Start () { myTransform = transform; playerTransform = GameObject.FindWithTag("Player").transform; } function Update () { myTransform.LookAt(playerTransform); myTransform.position += myTransform.forward * speed * Time.deltaTime; }
- 最小化代价不菲的数学函数
超越函数 (Transcendental functions)(Mathf.Sin、Mathf.Pow 等)、除法、平方根所花费的时间约为乘法的 100 倍。(从大局来说,它们几乎不花费时间,但是,如果您每帧调用数千次,时间将可以累积。)
这方面最常见的示例是向量规格化。如果您正在反复规格化同一个向量,那么请考虑规格化一次,然后将结果缓存,以便日后使用。
如果您使用向量长度并同时规划化此向量,用向量乘以长度的倒数,而不要使用 .normalized 属性,这样可以更快取得规格化的向量。
如果您正在对比距离,您无需对比实际距离。相反,可以通过使用 .sqrMagnitude 属性并保存一至两个平方根,以此对比距离的平方。
另外,如果您正在反复除以常量 c,您可以乘以它的倒数。首先用 1.0/c 计算这个常量的倒数。
超越函数 (Transcendental functions)(Mathf.Sin、Mathf.Pow 等)、除法、平方根所花费的时间约为乘法的 100 倍。(从大局来说,它们几乎不花费时间,但是,如果您每帧调用数千次,时间将可以累积。)
这方面最常见的示例是向量规格化。如果您正在反复规格化同一个向量,那么请考虑规格化一次,然后将结果缓存,以便日后使用。
如果您使用向量长度并同时规划化此向量,用向量乘以长度的倒数,而不要使用 .normalized 属性,这样可以更快取得规格化的向量。
如果您正在对比距离,您无需对比实际距离。相反,可以通过使用 .sqrMagnitude 属性并保存一至两个平方根,以此对比距离的平方。
另外,如果您正在反复除以常量 c,您可以乘以它的倒数。首先用 1.0/c 计算这个常量的倒数。
- 仅在少数情况下执行费时的运算,如 Physics.Raycast()
如果必须执行费时的运算,可以通过减少使用频率并缓存结果,优化这一运算。例如,使用 Raycast 的爆弹脚本:
// Bullet.js var speed = 5.0; function FixedUpdate () { var distanceThisFrame = speed * Time.fixedDeltaTime; var hit : RaycastHit; // every frame, we cast a ray forward from where we are to where we will be next frame if(Physics.Raycast(transform.position, transform.forward, hit, distanceThisFrame)) { // Do hit } else { transform.position += transform.forward * distanceThisFrame; } }
可以使用 Update 替换 FixedUpdate,并使用 deltaTime 替换 fixedDeltaTime,即刻改进脚本。FixedUpdate 是指物理更新,它比帧更新更为频繁。但是,只需每 n 秒进行一次光线投射便可更进一步。n 值更小,瞬时分辨率更大,而 n 值更大,性能将更佳。如果目标更大更慢,在时间映频混扰 (temporal aliasing) 发生之前可使用更大的 n 值。(出现延迟,玩家击中目标,但在目标 n 秒前出现的位置发生爆炸,或者玩家击中目标,但是爆弹顺利通过)。
// BulletOptimized.js var speed = 5.0; var interval = 0.4; // this is 'n', in seconds. private var begin : Vector3; private var timer = 0.0; private var hasHit = false; private var timeTillImpact = 0.0; private var hit : RaycastHit; // set up initial interval function Start () { begin = transform.position; timer = interval+1; } function Update () { // don't allow an interval smaller than the frame. var usedInterval = interval; if(Time.deltaTime > usedInterval) usedInterval = Time.deltaTime; // every interval, we cast a ray forward from where we were at the start of this interval // to where we will be at the start of the next interval if(!hasHit && timer >= usedInterval) { timer = 0; var distanceThisInterval = speed * usedInterval; if(Physics.Raycast(begin, transform.forward, hit, distanceThisInterval)) { hasHit = true; if(speed != 0) timeTillImpact = hit.distance / speed; } begin += transform.forward * distanceThisInterval; } timer += Time.deltaTime; // after the Raycast hit something, wait until the bullet has traveled // about as far as the ray traveled to do the actual hit if(hasHit && timer > timeTillImpact) { // Do hit } else { transform.position += transform.forward * speed * Time.deltaTime; } }
如果必须执行费时的运算,可以通过减少使用频率并缓存结果,优化这一运算。例如,使用 Raycast 的爆弹脚本:
// Bullet.js var speed = 5.0; function FixedUpdate () { var distanceThisFrame = speed * Time.fixedDeltaTime; var hit : RaycastHit; // every frame, we cast a ray forward from where we are to where we will be next frame if(Physics.Raycast(transform.position, transform.forward, hit, distanceThisFrame)) { // Do hit } else { transform.position += transform.forward * distanceThisFrame; } }
可以使用 Update 替换 FixedUpdate,并使用 deltaTime 替换 fixedDeltaTime,即刻改进脚本。FixedUpdate 是指物理更新,它比帧更新更为频繁。但是,只需每 n 秒进行一次光线投射便可更进一步。n 值更小,瞬时分辨率更大,而 n 值更大,性能将更佳。如果目标更大更慢,在时间映频混扰 (temporal aliasing) 发生之前可使用更大的 n 值。(出现延迟,玩家击中目标,但在目标 n 秒前出现的位置发生爆炸,或者玩家击中目标,但是爆弹顺利通过)。
// BulletOptimized.js var speed = 5.0; var interval = 0.4; // this is 'n', in seconds. private var begin : Vector3; private var timer = 0.0; private var hasHit = false; private var timeTillImpact = 0.0; private var hit : RaycastHit; // set up initial interval function Start () { begin = transform.position; timer = interval+1; } function Update () { // don't allow an interval smaller than the frame. var usedInterval = interval; if(Time.deltaTime > usedInterval) usedInterval = Time.deltaTime; // every interval, we cast a ray forward from where we were at the start of this interval // to where we will be at the start of the next interval if(!hasHit && timer >= usedInterval) { timer = 0; var distanceThisInterval = speed * usedInterval; if(Physics.Raycast(begin, transform.forward, hit, distanceThisInterval)) { hasHit = true; if(speed != 0) timeTillImpact = hit.distance / speed; } begin += transform.forward * distanceThisInterval; } timer += Time.deltaTime; // after the Raycast hit something, wait until the bullet has traveled // about as far as the ray traveled to do the actual hit if(hasHit && timer > timeTillImpact) { // Do hit } else { transform.position += transform.forward * speed * Time.deltaTime; } }
- 内部循环尽量避免调用堆栈开销
仅调用本身开销非常少的函数。如果每帧调用 x = Mathf.Abs(x) 数千次,最好使用 x = (x > 0 ? x : -x); 取而代之。
仅调用本身开销非常少的函数。如果每帧调用 x = Mathf.Abs(x) 数千次,最好使用 x = (x > 0 ? x : -x); 取而代之。
优化物理性能
Unity 使用的 NVIDIA PhysX 物理引擎可在移动设备上使用,但是,移动平台比桌面平台更容易达到硬件的性能限制。
下面是优化物理提示,以便在移动设备上取得更佳性能:-
- 可以调整固定时间步 (Fixed Timestep) 设置(位于时间管理器 (Time manager) 中),减少物理更新占用的时间。增加时间步将以物理精确度为代价,减少 CPU 开销。通常,更低的精确度是提高速度可接受的折中方案。
- 在时间管理器 (Time manager) 中设置最大允许时间步 (Maximum Allowed Timestep) 为 8-10fps 之间,以便在最坏的情况下限制物理所花费的时间。
- 网格碰撞体比原始碰撞体拥有更高的性能开销,因此,请谨慎使用。通常可使用支持原始碰撞体的子对象来接近网格形状。子碰撞体作为单一复合碰撞体,由父碰撞体的刚体统一控制。
- 对立体对象而言,车轮碰撞体并非严格意义上的碰撞体,但是它们依然拥有较高的 CPU 开销。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论