Unity优化——脚本优化策略1

简介: Unity优化——脚本优化策略1

一、最快方法获取组件


GetComponent()方法有一些变体,它们的性能消耗不同,因此要谨慎地调用该方法的最高版本。


3个可用的重载版本是GetComponent(string),GetComponent<T>()和GetComponent(typeof(T))。由于这些方法每年都做一些优化,因此最高效的版本取决于所使用的Unity版本。在Unity5的所有后续版本中,最好使用GetComponent<T>()变体。


通过测试最终发现,GetComponent<T>()比GetComponent(typeof(T))快一点点,而GetComponent(string)明显比另外两个方法慢得多。因此,我们应该确保永远都不用GetComponent(string)方法。除非万不得已。


二、移除空的回调定义


Unity中编写脚本的主要意义是在从MonoBehaviour继承的类中编写回调函数,Unity会在必要时调用它们。最常用的4个回调是Awake()、Start()、Update()和FixedUpdate()。

在第一次创建MonoBehaviour时调用Awake()。Start()在Awake()之后不久,但在第一个Update()之前调用。在场景初始化期间,每个MonoBehaviour组件的Awake()回调在Start()回调之前调用。


之后,每次渲染管线呈现一个新图像时,都会重复调用Update()。


最后,在物理引擎更新之前,调用FixedUpdate(),以固定时间间隔调用。


MonoBehaviour在场景中第一次实例化时,Unity会将任何定义好的回调添加到一个函数指针列表中,在关键时刻调用这个列表。然而,即使函数体是空的,Unity也会将其添加入列表。核心Unity引擎没有意识到这些函数体是空的,它只知道方法已经定义,而它必须获取方法,然后在必要时调用它。如果将这些空定义分散在整个代码库中,那么将浪费少量CPU。


解决方法很简单;删除空的回调定义。但在可扩展的代码库中查找这样的空定义可能比较困难,但如果使用一些基本的正则表达式(简称regex),应该能够相对容易地找到空的回调定义


下面的regex表达式应搜索出代码中的空Update()定义:

void\s*Update\s*?\(\s*?\)\s*?\n*?{\n*?\s*?\}

这个regex检查Update()回调的标准方法定义,同时包含可能分布在整个方法定义中的多于空白和换行符。


当然,上面的做法也可以查找非样板的Unity回调,例如OnGUI()、OnEndable()、OnDestroy()和LateUpdate()。唯一的区别是在新样本中自定义了Start()和Update()。


在Unity脚本中,性能问题最常见来源时执行以下操作,而误用Update()回调

  • 反复计算很少或从不改变的值
  • 太多的组件计算一个可以共享的结果
  • 执行工作的频率远超必要值


下面来谈谈直接解决这些问题的一些提示


三、缓存组件引用


在Unity中编写脚本时,反复计算一个值是常见的错误,特别是在使用GetComponent()方法时。如果实在Update()里面,那么这个问题将会更加严重。更好的方法时在初始化过程中获取所需数据的引用,并将其保存,直到需要它们为止。


以这种方式缓存组件引用,就不必在每次需要它们时重新获取,每次都会节省一些CPU开销。代价是少量的额外内存消耗。


同样的技巧也适用于在运行时决定计算的任何数据块。不需要要求CCPU在每次执行Update()时都重新计算相同的值,因为可以将它存储在内存中,供将来参考。


四、共享计算输出


让多个对象共享某些计算结果,可节省性能开销;当然,只有这些计算都生成相同的结果,才有效。这种情形通常很容易发现,但是重构起来很困难,因此利用这种情况将非常依赖于实现方案。


示例包括在场景中找到对象,从文件中读取数据,解析数据(如XML或JSON),在大列表或深层的信息字典中找到内容,为一组AI对象计算路径,复杂的数学轨迹,光线追踪等。


每次执行一个昂贵的操作时,考虑是否从多个位置调用它,但总是得到相同的输出。如果是这样,重构就是明智的。最大的成本通常只是牺牲了一点代码的简洁性,尽管传递值可能会造成一些额外的开销。


请注意,通常很容易养成在基类中隐藏大型复杂函数的习惯,然后定义使用该函数的派生类,完全忘记了该函数的开销,因为我们很少再次查看改代码。最好使用Unity Profiler来指出,这个昂贵的函数可能调用了多少次,像往常一样,不要预先优化那些函数,除非已经证明这是一个性能问题。无论他有多昂贵,只要不超出性能限制,它就不是真正的性能问题。


五、Update、Coroutines和InvokeRepeating


另一个很容易养成的习惯是在Update()回调中以超出需要的频率重复调用某段代码。例如,开始情况如下:

void Update(){
   processAI();
}


processAI()可能是个复杂的任务,在每一帧中都调用了ProcessAI()。如果这个活动占用了太多的帧率预算,且任务完成的频率低于没有明显缺陷的每一帧,那么提高性能的一个好方法就是简单地减少ProcessAI()的调用频率。

 void Update()
    {
        _timer += _timer.deltaTime;
        if(_timer >= _aiProcessDelay)
        {
            ProcessAI();
            _timer-=_aiProcessDelay;
        }
    }


这样改进减少了Update()的回调总成本,需要一些额外的内存来存储浮点数据。但最终Unity仍要调用一个空的回调函数。


这个函数是一个完美的示例,可以将它转换成协程,利用其延迟的调用属性。协程通常用于编写短事件序列的脚本,可以是一次性的,也可以是重复的操作。它们不应该与线程混淆,线程以并发方式在完全不同的CPU内核上运行,而且多个线程可以同时运行。相反,协程以顺序的方式在主线程上运行,这样在任何给定时刻都只处理一个协程,每个协程通过yield语句决定何时暂停和继续。下面的代码说明可以协程形式重写以上的Update()回调。

 void Update()
    {
        StartCorountine(ProcessAICoroutine);
    }
    IEnumerator ProcessAICoroutine()
    {
        while (true)
        {
            ProcessAI();
            yeld return new WaitForSeconds(_aiProcessDelay);
        }
    }

这种方法的好处是,这个函数只调用_aiProcessDelay值指示的次数,在此之前它一直处于空闲状态,从而减少对大多数帧的性能影响。然而,这种方法有其缺点。


首先,与标准函数调用相比,启动协程会带来额外的开销成本(大约是标准函数调用的三倍),还会分配一些内存,将当前状态存储在内存中,直到下一次调用它。这种额外的开销也不是一次性的成本,因为协程经常不断地调用yield,这会一次又一次地造成相同的开销成本,所以需要确保降低频率的好处大于此成本。


其次,一旦初始化,协程的运行独立于MonoBehaviour组件中Uptdate()回调的触发,不管组件是否禁用,都将继续调用协程。如果执行大量的GameObject构建和析构操作,写成可能会显得很笨拙。


再次,协程会在包含它的GameObject变成不活动的那一刻自动停止,不管出于什么原因(无论它被设置为不活动的还是它的一个父对象被设置为不活动的)。如果GameObject再次设置为活动的,协程不会自动重新启动。


最后,将方法转换为协程,可减少大部分帧中的性能损失,但如果方法体的单次调用突破了帧率预算,则无论该方法的调用次数怎么少,都将超出预算。因此,这种方法最适用于如下情况:即由于在给定的帧中调用该方法的次数太多而导致帧率超出预算,而不是因为该方法本身太昂贵。这种情况下,我们别无选择,只能深入研究并改进方法本身的性能,或者减少其他任务的成本,将时间让给该方法,来完成其工作。


在生成协程时,有几种可用的yield类型。WaitForSeconds容易理解;协程在yield语句上暂停指定的秒数。但是,它并不是一个精确的计时器,所以当这个yield类型恢复执行时,可能会有少量的变化。


WaitForSecondsRealTime是另一个选项,与WaitForSeconds的唯一区别是,它使用未缩放的时间。WaitForSeconds与缩放的时间进行比较,后者收到全局Time.timeScale属性的影响。而WaitForSecondsRealTime则不是,因此,如果要调整时间缩放值,请注意使用哪种yield类型。


还有WaitForEndOfFrame选项,它在下一个Update()结束时继续,还有WaitForFixedUpdate,它在下一个FixedUpdate()结束时继续。最后,Unity5.3引入了WaitUntil和WaitWhile,在这两个函数中,提供了一个委托函数,协程根据给定的委托返回true或false分别暂停或继续。请注意,为这些yield类型提供的委托将对每个Update()执行一次,直到它们返回停止它们所需的布尔值,因此它们非常类似于在while循环过程中使用WaitForEndOfFrame的协程。当然,同样重要的是,所提供的委托函数执行起来不会太昂贵。


委托函数是C#中非常有用的结构,允许将本地方法作为参数传递给其他方法,通常用于回调


某些Update()回调的编写方式可以简化为简单的协程,这些协程总是在其中一种类型上调用yield,但应该注意前面提供的缺点。协程很难调试,因此它们不遵循正常的执行流程;在调用栈上没有调用者。可以直接指责为什么协程在给定的时间触发,如果协程执行复杂的任务,与其他子系统交互,就会导致一些很难察觉的缺陷,因为他们在其他代码不希望的时刻触发,这些缺陷也往往是及其难重现的类型。如果希望使用协程,最好使它们尽可能简单,且独立于其他复杂的子系统。


事实上,如果在上面的实例中协程很简单,可以归结为一个while循环,总是在WaitForSeconds或WaitForSecondsRealTime上调用yield,则通常可以替换成InvokeRepeating()调用,它的建立更简单,开销成本略小。下面的代码在功能上与前面使用协程定期调用ProcessAI()方法的实现方案相同:

  void Start()
    {
        InvokeRepeating("ProcessAI", Of, _aiProcessDelay);
    }

InvokeRepeating()和协程之间的一个重要区别是,InvokeRepeating()完全独立于MonoBehaviour和GameObject的状态。停止InvokeRepeating()调用的两种方法:


  • 调用CancelInvode(),它停止给定的MonoBehaviour发起的所有InvokeRepeating()回调
  • 销毁关联的MonoBehaviour或它的父GameObject。禁用MonoBehaviour或GameObject都不会停止InvokeRepeating()


相关文章
|
7月前
|
大数据 API 图形学
Unity优化——批处理的优势
Unity优化——批处理的优势
215 0
|
7月前
|
存储 人工智能 Java
Unity优化——脚本优化策略4
Unity优化——脚本优化策略4
120 0
|
5月前
|
存储 设计模式 监控
运用Unity Profiler定位内存泄漏并实施对象池管理优化内存使用
【7月更文第10天】在Unity游戏开发中,内存管理是至关重要的一个环节。内存泄漏不仅会导致游戏运行缓慢、卡顿,严重时甚至会引发崩溃。Unity Profiler作为一个强大的性能分析工具,能够帮助开发者深入理解应用程序的内存使用情况,从而定位并解决内存泄漏问题。同时,通过实施对象池管理策略,可以显著优化内存使用,提高游戏性能。本文将结合代码示例,详细介绍如何利用Unity Profiler定位内存泄漏,并实施对象池来优化内存使用。
345 0
|
3月前
|
设计模式 存储 人工智能
深度解析Unity游戏开发:从零构建可扩展与可维护的游戏架构,让你的游戏项目在模块化设计、脚本对象运用及状态模式处理中焕发新生,实现高效迭代与团队协作的完美平衡之路
【9月更文挑战第1天】游戏开发中的架构设计是项目成功的关键。良好的架构能提升开发效率并确保项目的长期可维护性和可扩展性。在使用Unity引擎时,合理的架构尤为重要。本文探讨了如何在Unity中实现可扩展且易维护的游戏架构,包括模块化设计、使用脚本对象管理数据、应用设计模式(如状态模式)及采用MVC/MVVM架构模式。通过这些方法,可以显著提高开发效率和游戏质量。例如,模块化设计将游戏拆分为独立模块。
213 3
|
4月前
|
开发者 图形学 iOS开发
掌握Unity的跨平台部署与发布秘籍,让你的游戏作品在多个平台上大放异彩——从基础设置到高级优化,深入解析一站式游戏开发解决方案的每一个细节,带你领略高效发布流程的魅力所在
【8月更文挑战第31天】跨平台游戏开发是当今游戏产业的热点,尤其在移动设备普及的背景下更为重要。作为领先的游戏开发引擎,Unity以其卓越的跨平台支持能力脱颖而出,能够将游戏轻松部署至iOS、Android、PC、Mac、Web及游戏主机等多个平台。本文通过杂文形式探讨Unity在各平台的部署与发布策略,并提供具体实例,涵盖项目设置、性能优化、打包流程及发布前准备等关键环节,助力开发者充分利用Unity的强大功能,实现多平台游戏开发。
125 0
|
4月前
|
图形学 C# 开发者
全面掌握Unity游戏开发核心技术:C#脚本编程从入门到精通——详解生命周期方法、事件处理与面向对象设计,助你打造高效稳定的互动娱乐体验
【8月更文挑战第31天】Unity 是一款强大的游戏开发平台,支持多种编程语言,其中 C# 最为常用。本文介绍 C# 在 Unity 中的应用,涵盖脚本生命周期、常用函数、事件处理及面向对象编程等核心概念。通过具体示例,展示如何编写有效的 C# 脚本,包括 Start、Update 和 LateUpdate 等生命周期方法,以及碰撞检测和类继承等高级技巧,帮助开发者掌握 Unity 脚本编程基础,提升游戏开发效率。
100 0
|
4月前
|
开发者 图形学 UED
深度解析Unity游戏开发中的性能瓶颈与优化方案:从资源管理到代码执行,全方位提升你的游戏流畅度,让玩家体验飞跃性的顺滑——不止是技巧,更是艺术的追求
【8月更文挑战第31天】《Unity性能优化实战:让你的游戏流畅如飞》详细介绍了Unity游戏性能优化的关键技巧,涵盖资源管理、代码优化、场景管理和内存管理等方面。通过具体示例,如纹理打包、异步加载、协程使用及LOD技术,帮助开发者打造高效流畅的游戏体验。文中提供了实用代码片段,助力减少内存消耗、提升渲染效率,确保游戏运行丝滑顺畅。性能优化是一个持续过程,需不断测试调整以达最佳效果。
104 0
|
6月前
|
人工智能 图形学
【unity小技巧】使用动画状态机脚本实现一个简单3d敌人AI功能
【unity小技巧】使用动画状态机脚本实现一个简单3d敌人AI功能
64 0
|
6月前
|
人工智能 定位技术 图形学
【Unity小技巧】一个脚本实现控制3D远程/近战敌人AI
【Unity小技巧】一个脚本实现控制3D远程/近战敌人AI
59 0
|
6月前
|
自然语言处理 图形学
【unity实战】一个通用的FPS枪支不同武器射击控制脚本
【unity实战】一个通用的FPS枪支不同武器射击控制脚本
106 0