Unity3D 官方移动游戏优化指南5.编程和代码架构

Posted Kaitiren

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity3D 官方移动游戏优化指南5.编程和代码架构相关的知识,希望对你有一定的参考价值。

Unity PlayerLoop 包含与游戏引擎核心交互的函数。这种树状结构包括许多处理初始化和每帧更新的系统。所有脚本都将依赖该 PlayerLoop 来创建游戏玩法。

在进行性能分析时,可以看到项目的所有用户代码都位于 PlayerLoop 下(编辑器组件位于 EditorLoop 下)。

自定义脚本、设置和图形会显著影响每一帧的计算和在屏幕上渲染的时间。

可以按照以下提示和技巧优化脚本。

了解Unity Playerloop

确保了解 Unity 的帧循环的执行顺序。每个 Unity 脚本都将按预定顺序运行多个事件函数。您应该了解 Awake、Start、Update 及其他创建脚本生命周期的函数之间的区别。

有关事件函数的具体执行顺序,请参阅脚本生命周期流程图

了解 PlayerLoop 和脚本的生命周期。

尽可能减少每帧运行的代码

考虑代码是否必须每一帧都运行。将不必要的逻辑移出 Update、 LateUpdate 和 FixedUpdate。可在这些事件函数中方便地放置必须每帧更新的代码,但应提取出任何不需要以这种频率更新的逻辑。尽可能只在情况发生改变时才执行逻辑。

如果确实 需要使用 Update,可以考虑每 n 帧运行一次代码。这是一种应用时间切片 (将繁重的工作负载分布到多个帧的常用技术)的方法。在下面的示例中,我们每三帧运行一次 ExampleExpensiveFunction:

private int interval = 3;

void Update()

{

    if (Time.frameCount % interval == 0)

    {

         ExampleExpensiveFunction();

     }

}

避免在 Start/Awake 中处理复杂逻辑

加载第一个场景时,将为每个对象调用以下函数 :

  • Awake
  • OnEnable
  • Start

在应用程序渲染其第一帧之前,避免在这些函数中处理代价高昂的逻辑。否则,可能会增加不必要的加载时间。

有关加载第一个场景的详细信息,请参阅事件函数的执行顺序

避免空Unity 事件

即使是空的 MonoBehaviour 也需要资源,因此应删除空的 Update 或 LateUpdate 方法。

如果使用这些方法进行测试,请使用预处理器指令 :

#if UNITY_EDITOR

void Update()
{

}

#endif

这样,您可以在编辑器中随意使用 Update 进行测试,而不会在构建版本中遗留不必要的开销。

删除调试日志语句

日志语句(尤其是在 Update、LateUpdate 或 FixedUpdate 中)可能会降低性能。在进行构建之前,请禁用日志语句。

为了更轻松地执行该操作,可以考虑配合预处理指令创建一个 Conditional 属性。例如,创建如下所示的自定义类 :

public static class Logging

{

   [System.Diagnostics.Conditional(“ENABLE_LOG”)]

   static public void Log(object message)

   {

     UnityEngine.Debug.Log(message);

   }

}

添加自定义预处理器指令可对脚本进行分区。

使用自定义类生成日志消息。如果在 Player Settings 中禁用 ENABLE_LOG 预处理器,您的所有 Log 语句都会立即消失。

使用哈希值而不是字符串参数

Unity 不使用字符串名称对 Animator、Material 和 Shader 属性进行内部寻址。为了加快速度,所有属性名称都经过哈希处理为属性 ID,实际上是这些 ID 用于寻址属性。

每当在 Animator、Material 或 Shader 上使用 Set 或 Get 方法时,请使用整数值方法而非字符串值方法。字符串方法只执行字符串哈希处理,然后将经过哈希处理的 ID 转发给整数值方法。

对于 Animator 属性名称,使用 Animator.StringToHash,对于 Material 和 Shader 属性名称,使用 Shader.PropertyToID

选择正确的数据结构

随着每一帧迭代成千上万次,所选择的数据结构可能存在累积效应会导致高效或低效。是否使用 List、Array 或 Dictionary 来处理集合会更合理?在 C# 中,请以 MSDN 数据结构指南作为常规指南来选择正确的数据结构。

避免在运行时添加组件

在运行时调用 AddComponent 需要一些开销。每当在运行时添加组件时,Unity 都必须检查是否有重复项或是否需要其他组件。

对设置好所需组建的预制件进行实例化,通常性能表现更优异。

缓存游戏对象和组件

GameObject.Find、GameObject.GetComponent 和 Camera.main( 在 2020.2之前的版本中)可能开销较大,应避免在 Update 方法中调用它们。而应在 Start 中调用它们,并且缓存相应结果。

例如,下面演示低效使用重复的 GetComponent 调用 :

void Update()

{

   Renderer myRenderer = GetComponent<Renderer>();

   ExampleFunction(myRenderer);

}

如果函数结果已缓存,就可以仅调用 GetComponent 一次。缓存的结果可以在 Update 中重用,无需再调用 GetComponent。

private Renderer myRenderer; void Start() 
{ 
   myRenderer = GetComponent<Renderer>();
} 


void Update() 
{
   ExampleFunction(myRenderer);
}

使用对象池

Instantiate 和 Destroy 可能生成垃圾和垃圾收集 (GC) 尖峰,通常是一个缓慢的过程。请不要频繁初始化和销毁游戏对象(例如,从枪射出子弹),而应使用预分配的对象,这样可以重用和回收。

在本示例中,ObjectPool 创建 20 个 PlayerLaser 实例以供重用。

PlayerLaser 对象的池处于不活动状态,已准备好可以发射。

在 CPU 尖峰不那么明显的时候,在游戏中某个点(如在菜单屏幕出现时)创建可重用的实例。通过集合跟踪对象“池”。在游戏过程中,只是在需要时启用下一个可用实例,禁用对象而不是销毁对象,然后将其返回池。

这样可减少项目中托管分配的数量,可以防止垃圾收集问题。

在此了解如何在 Unity 中创建简单对象池系统。

使用ScriptableObject

在 ScriptableObject 中而不是 MonoBehaviour 中存储不变的值或设置。ScriptableObject 这种资源只需设置一次就可以在项目中一直使用。它不能直接附加到游戏对象。

在 ScriptableObject 中创建字段来存储值或设置,然后在 Monobehaviour 中引用该 ScriptableObject。

在此示例中,名为 Inventory 的 ScriptableObject 为各种游戏对象保存设置。

使用 ScriptableObject 的这些字段可以防止每次使用该 Monobehaviour 实例化对象时出现不必要的数据重复。

观看此 ScriptableObject 简介教程了解 ScriptableObject 可以如何帮助项目开发。也可以在此查找文档

以上是关于Unity3D 官方移动游戏优化指南5.编程和代码架构的主要内容,如果未能解决你的问题,请参考以下文章

Unity3D 官方移动游戏优化指南 摘要

Unity3D 官方移动游戏优化指南3.内存

Unity3D 官方移动游戏优化指南1.简介

Unity3D 官方移动游戏优化指南7.资源

Unity3D 官方移动游戏优化指南8.图形和 GPU 优化

Unity3D 官方移动游戏优化指南8.图形和 GPU 优化