Unity性能优化


Unity优化总结

1、最快获取组件方法

在unity中有三种获取组件方法,分别是GetComponent()、GetComponent(typeof(T))、GetComponent(string),其中速度由快到慢分别是GetComponent()、GetComponent(typeof(T))、GetComponent(string)

2、移除相应的空方法

移除MonoBehaviour自带的空方法。例如Awake(), Start(), Update()、FixedUpdate(),优化方法可以写工具使用正则表达式查到MonoBehaviour脚本中没有用到的空方法,例如void\sUpdate\s?(\s*?)\s*?\n*?{\n*?\s*?}

3、缓存组件引用

重复并且多次使用的组件可以在初始化的地方使用全局变量进行缓存

4、Update重复调用频率过高优化

Update定时器间隔优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private float _aiProcessDelay = 0.2f;  

private float _timer = 0.0f;

void Update() {

  _timer += Time.deltaTime;

if (_timer > _aiProcessDelay){
ProcessAI();
_timer -= _aiProcessDelay;
}

}

携程模拟Update

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Start() {  
StartCoroutine(ProcessAICoroutine ());

}

IEnumerator ProcessAICoroutine () {

  while (true) {

    ProcessAI();

    yield return new WaitForSeconds(_aiProcessDelay);

}

}

InvokeRepeating()代替携程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void Start()  
{
InvokeRepeating("ProcessAI", 0f, _aiProcessDelay);
}
```

## 5、更快的GameObject空引用检查

``` C#
if (!System.Object.ReferenceEquals(gameObject, null)) {
  // do stuff with gameObject
}使用比 使用if (gameObject != null) {
  // do stuff with gameObject
}快一点

6、避免从GameObjects检索字符串属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
例如,下面的代码将在循环的每次迭代期间导致额外的内存分配
for (int i = 0; i < listOfObjects.Count; ++i) {
  if (listOfObjects[i].tag == "Player") {
    // do something with this object
  }
}

优化方法是使用CompareTag 代替tag ,例如下面的例子
void Update() {
  int numTests = 10000000;
  if (Input.GetKeyDown(KeyCode.Alpha1)) {
    for(int i = 0; i < numTests; ++i) {
      if (gameObject.tag == "Player") {
        // do stuff
      }
    }
  }

优化代码如下:
  if (Input.GetKeyDown(KeyCode.Alpha2)) {
    for(int i = 0; i < numTests; ++i) {
      if (gameObject.CompareTag ("Player")) {
        // do stuff
      }
    }
  }
}

7、避免在 运行时重新设置transform的父子关系

8、考虑transform的缓存变化,尽量使用localPosition, localRotation,localScale进行transform的相关操作,并且尽量使用缓存的方法并且在帧的末尾进行相关重新设置,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
private bool _positionChanged;
private Vector3 _newPosition;
public void SetPosition(Vector3 position) {
  _newPosition = position;
  _positionChanged = true;
}
void FixedUpdate() {
  if (_positionChanged) {
    transform.position = _newPosition;
    _positionChanged = false;
  }
}

9、根据对象的可见性禁用对象

由于可见性回调必须与渲染管道通信,因此GameObject必须附加可渲染组件,如MeshRenender或SkinnedMeshRenender。我们必须确保要从其接收可见性回调的组件也附加到与可渲染对象相同的GameObject实例,并且不是父或子GameObject;否则,它们将不会被调用。

1
2
void OnBecameVisible() { enabled = true; }
void OnBecameInvisible() { enabled = false; }

10、按距离禁用对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[SerializeField] GameObject _target;
[SerializeField] float _maxDistance;
[SerializeField] int _coroutineFrameDelay;
void Start() {
  StartCoroutine(DisableAtADistance());
}
IEnumerator DisableAtADistance() {
  while(true) {
    float distSqrd = (transform.position -
_target.transform.position).sqrMagnitude;
    if (distSqrd < _maxDistance * _maxDistance) {
      enabled = true;
    } else {
      enabled = false;
    }
    for (int i = 0; i < _coroutineFrameDelay; ++i) {
      yield return new WaitForEndOfFrame();
}
  }
}

11、在距离上使用距离平方

1
2
3
4
5
6
7
8
9
10
11
12
float distance = (transform.position –
other.transform.position).Distance();
if (distance < targetDistance) {
  // do stuff
}

float distanceSqrd = (transform.position –
other.transform.position).sqrMagnitude;
if (distanceSqrd < (targetDistance * targetDistance)) {
  // do stuff
}

12、动态批处理

  • 所有网格实例必须使用相同的材质引用
  • 仅对ParticleSystem和MeshRenender组件进行动态批处理。不能批处理SkinnedMeshRenender组件(对于动画角色)和所有其他可渲染组件类型
  • 每个网格最多只能有300个顶点;但是,着色器使用的顶点属性总数不得大于900。这意味着对于复杂着色器,每个网格的最大顶点数可能小于300(有关详细信息,请参见顶点属性部分)
  • 对象不得包含变换上的镜像(即,不能将比例为正的游戏对象A和比例为负的游戏对象B批处理在一起)
  • 网格实例应该引用相同的光照贴图文件。
  • 材质的着色器不应依赖于多个过程
  • 网格实例不得接收实时阴影
  • 整个批次的网格索引总数有一个上限,该上限因使用的图形API和平台而异,约为32,000-64,000个索引(有关详细信息,请查看文档/前面提到的博客文章)

13、优化UGUI

  • 使用更多的Canvas,而不是整个游戏就只有一个canvas画布,当一个canvas画布下UI元素多的时候,UI元素出现dirty的时候回导致UI元素重新生成网格,然后发出绘制调用。

  • 动静分离Canvas画布,
    静态:静态UI元素是那些永远不会改变的元素;背景图像、标签等就是很好的例子
    附带动态:动态元素是那些可以改变的元素,其中附带动态对象是那些仅响应某件事(如UI按钮按下或悬停动作)而改变的UI元素
    连续动态:连续动态对象是那些定期更新的UI元素,比如动画元素
    对于UI的任何给定部分,我们应该尝试将这三个组中的UI元素拆分到三个不同的画布中,因为这将最小化重新生成过程中浪费的工作量。

  • 禁用非交互元素的光线投射目标

  • 通过禁用父画布组件隐藏UI元素

  • 避免动画制作组件,Unity的Animator组件从未打算与其最新版本的UI系统一起使用,它们与它的交互是一种天真的实现。在每一帧中,动画制作人都会更改UI元素的属性,这些属性会导致它们的布局被弄脏,并导致重新生成大量内部UI信息。我们应该避免完全使用动画师,而应该自己进行补间,或者使用旨在进行此类操作的实用资产。

  • 画布模式为WorldSpace以及ScreeSpaceCamera的时候记得要设置摄像机

  • 不要使用Alpha隐藏UI元素,呈现颜色属性中Alpha值为0的UI元素仍将导致发出绘制调用。我们应该倾向于更改UI元素的IsActive属性,以便在必要时将其隐藏。另一种选择是通过CanvasGroup组件使用画布组,该组件可用于控制其下所有子元素的Alpha透明度。将Canvas Group的Alpha值设置为0将剔除其子对象,因此不会发出绘制调用。

  • 优化ScrollRects,ScrollRect组件是用于滚动浏览其他UI元素列表的UI元素,在移动应用程序中非常常见。不幸的是,由于画布需要定期重新生成这些元素,因此这些元素的性能随大小的变化很差。我们可以做几件事来提高ScrollRect组件的性能。下面让我们来看一下其中的一些,第一、确保使用RectMask2D,只需放置深度值低于ScrollRect元素的其他UI元素,就可以创建滚动UI行为。但是,这是不好的做法,因为在ScrollRect中不会发生剔除,并且需要为ScrollRect移动的每个帧重新生成每个元素。如果还没有,我们应该使用RectMask2D组件来裁剪和剔除不可见的子对象。该组件创建一个空间区域,由此,如果其中的任何子UI元素位于RectMask2D组件的边界之外,则它们将被剔除。与渲染太多不可见对象所节省的成本相比,确定是否剔除对象的成本通常是值得的。第二、为ScrollRects禁用Pixel Perfect,Pixel Perfect是画布组件上的一个设置,它强制其子UI元素与屏幕上的像素直接对齐。这通常是艺术和设计的要求,因为UI元素看起来会比禁用时清晰得多。虽然这种对齐行为是一项相对昂贵的操作,但实际上,我们的大部分UI都必须启用它,以保持清晰明了。但是,对于动画和快速移动的对象,由于涉及的运动可能有些毫无意义。第三、手动停止ScrollRect运动,画布将始终需要重新生成整个ScrollRect元素,即使每帧的速度移动了零点几个像素。一旦检测到其速度低于特定阈值,就可以使用ScrollRect.ocity和ScrollRect.StopMovement()手动冻结其运动。这可以大大降低再生的频率

  • 使用空的UIText元素进行全屏交互,大多数UI中的一个常见实现是激活一个覆盖整个屏幕的大型透明可交互元素,强制玩家在继续操作之前处理弹出窗口,同时仍然允许玩家看到它背后正在发生的事情(作为一种不会完全剥夺玩家游戏体验的方法)。这通常是使用UIImage元素完成的,但不幸的是,这可能会中断批处理操作,并且在移动设备上透明度可能是一个问题,解决此问题的一种棘手方法是使用未定义字体或文本的UIText元素。这将创建一个不需要生成任何可呈现信息的元素,并且只处理交互的边界框检查。

  • 官方文档优化教程https://learn.unity.com/tutorial/optimizing-unity-ui

 

15、Shader优化

  • 使用较小的数据类型可以比使用较大的数据类型更快地进行计算(特别是在移动平台上),因此我们可以尝试的第一个调整是用较小的版本(如Half(16位,浮点)或甚至FIXED(12位,定点))替换我们的Float数据类型(32位,浮点)。

  • 禁用不必要的功能,也许我们可以通过简单地禁用不重要的着色器功能来节省成本。着色器是否真的需要透明度、Z写、Alpha测试和/或Alpha混合?调整这些设置或删除这些功能会让我们在不损失太多图形保真度的情况下获得理想的效果吗?做出这样的改变是节省填充率成本的好方法。

  • 删除不必要的输入数据,有时,编写着色器的过程涉及在场景中编辑代码和查看代码的大量来回试验。此过程的典型结果是,一旦获得了所需的效果,着色器早期开发时所需的输入数据现在就会变成多余的绒毛,并且很容易忘记在该过程拖了很长一段时间时所做的更改。但是,这些冗余数据值可能会耗费GPU宝贵的时间,因为即使着色器没有显式使用它们,也必须从内存中提取它们。因此,我们应该仔细检查着色器,以确保它们的所有输入几何体、顶点和碎片数据都被实际使用。

  • 仅公开必要的变量,将不必要的变量从着色器暴露给附带的材质可能代价很高,因为GPU不能假定这些值是常量,这意味着编译器无法编译掉这些值。每次传递时必须从CPU推送此数据,因为可以随时通过材质对象的方法(如SetColor()和SetFloat())修改这些数据。如果我们发现,在项目接近尾声时,我们始终对这些变量使用相同的值,则应将它们替换为着色器中的常量,以移除这种多余的运行时工作负荷。唯一的代价是模糊可能是关键的图形效果参数,所以这应该在过程的后期完成。

  • 降低数学复杂性,复杂的数学运算会严重阻碍渲染过程,所以我们应该尽我们所能来限制损害。通过预计算复杂数学函数输出并将其作为浮点数据放置在纹理文件中,完全可以存储复杂数学函数输出的贴图。毕竟,纹理文件只是一个巨大的浮点值斑点,可以用三个维度快速索引:x、y和颜色(RGBA)。


文章作者: MiKiNuo
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 MiKiNuo !
  目录