32.性能优化-CPU-脚本-Update
32.1 知识点
知识回顾
知识回顾一:移除空的 Update 函数
若在脚本中定义了生命周期函数但未写任何逻辑,它们仍会被 Unity 按帧调用,带来额外开销。因此要避免写空的生命周期函数,尤其是每帧调用的:Update、LateUpdate、FixedUpdate、OnGUI。
知识回顾二:需要在 Update 中频繁使用的组件应该缓存
在 Unity 中反复对同一对象调用 GetComponent 是常见错误;对常用组件应用泛型方式获取一次并缓存,后续直接使用字段。应避免在 Update 中调用:GetComponent、Find 等查找组件/对象的 API。
降低执行频率
我们常在 Update 中重复执行某些逻辑;若超出实际需要的频率重复调用,会造成性能浪费。例如:怪物 AI 需要不断朝玩家寻路并移动,若在 Update 里每帧都做寻路,会造成不必要的消耗。
可以通过降低执行频率来避免:最常用的方式是自定义更新间隔,用计时变量累加 Time.deltaTime,达到间隔后再执行一次复杂逻辑,并将计时清零。
// 间隔多久更新一次(秒)
private float _updateTime = 0.25f;
// 用于计时的变量
private float _timer;
void Update()
{
// 把原本每帧都执行的复杂逻辑,改为每隔一段时间执行一次;具体间隔根据实际情况设定
_timer += Time.deltaTime;
if (_timer >= _updateTime)
{
MonsterAILogic();
_timer = 0;
}
}
// 假设是执行怪物逻辑的函数
void MonsterAILogic()
{
// 主要处理寻路逻辑
}
减少复杂计算
应尽量减少在 Update 中进行复杂数学计算。对于不变的计算结果,应在初始化阶段(Awake、Start)就计算好并存成字段,Update 中直接使用该字段,避免每帧重复计算。
事件驱动代替每帧检测
用观察者模式取代在 Update 中每帧轮询状态:当状态变化时通过事件通知,而不是每帧检测。可参考 Unity 基础框架中的事件中心模块实现。
分帧执行
对大批量对象的更新做分帧处理:每帧只处理其中一部分对象,避免单帧内一次性处理过多导致卡顿。例如用列表记录所有待更新对象,用索引记录「当前处理到哪一位」,每帧只推进固定个数,索引到头再从头开始。
// 假设是存储所有游戏中怪物的容器
private List<Object> monsters = new List<Object>();
// 当前处理到第几个怪物(用于分帧)
private int _index = 0;
// 每帧最多更新多少个怪物
private int _monsterNums = 10;
void Update()
{
for (int i = 0; i < _monsterNums; i++)
{
if (_index >= monsters.Count)
_index = 0;
// 假设这里是让容器中的某个怪物执行更新逻辑
monsters[_index].GetType();
_index++;
}
}
自定义 Update 管理器(统一管理 Update 更新)
在 Unity 中,若每个继承 MonoBehaviour 的脚本都独立拥有自己的 Update,当这类对象很多时,会因引擎底层机制(C++ 调用 C# 的跨语言开销、内存访问方式等)带来额外性能开销。若对 Update 进行统一管理,只在一个地方执行「所有需要每帧更新的逻辑」,可以明显降低开销;需要每帧更新的对象越多,统一管理器的优势越明显(尤其在中大型场景中)。
常见做法:通过一个统一更新管理器,集中管理所有需要 Update 的对象或回调;场景中只需一个 MonoBehaviour 持有唯一的 Update,在该 Update 内遍历并调用所有注册的更新逻辑。实现方式可以是:用事件记录所有 Update 回调,或用列表记录需要更新的对象,在管理器内统一调用。可参考 Unity 基础框架中的公共 Mono 模块。
32.2 知识点代码
Lesson32_性能优化_CPU_脚本_Update.cs
using System.Collections.Generic;
using UnityEngine;
public class Lesson32_性能优化_CPU_脚本_Update : MonoBehaviour
{
// 间隔多久更新一次(秒)
private float _updateTime = 0.25f;
// 用于计时的变量
private float _timer;
// 假设是存储所有游戏中怪物的容器
private List<Object> monsters = new List<Object>();
// 当前处理到第几个怪物(用于分帧)
private int _index = 0;
// 每帧最多更新多少个怪物
private int _monsterNums = 10;
void Start()
{
#region 知识回顾一 移除空的Update函数
/*
* 定义了生命周期函数但未写逻辑,仍会带来额外开销。
* 应避免写空的生命周期函数,尤其是 Update、LateUpdate、FixedUpdate、OnGUI。
*/
#endregion
#region 知识回顾二 需要在Update中频繁使用的组件应该缓存
/*
* 反复 GetComponent 是常见错误;常用组件应缓存。
* 应避免在 Update 中调用 GetComponent、Find 等查找 API。
*/
#endregion
#region 知识点一 降低执行频率
/*
* Update 中超出需要的频率重复调用会造成浪费(如每帧寻路)。
* 可通过自定义更新间隔:_timer += Time.deltaTime,达到间隔再执行逻辑并清零。
*/
#endregion
#region 知识点二 减少复杂计算
/*
* 尽量减少在 Update 中做复杂数学计算。
* 不变的结果应在 Awake/Start 中算好,Update 中直接使用。
*/
#endregion
#region 知识点三 事件驱动代替每帧检测
/* 用观察者模式取代 Update 中每帧状态检测,可参考事件中心模块 */
#endregion
#region 知识点四 分帧执行
/* 大批量对象更新做分帧处理,每帧只处理一部分,避免单帧卡顿 */
#endregion
#region 知识点五 自定义Update管理器(统一管理Update更新)
/*
* 每个 MonoBehaviour 独立 Update 在对象多时会有跨语言、内存访问等开销。
* 统一管理:一个 Update 内执行所有注册的更新逻辑,可大幅提升性能。
* 可用事件或列表记录回调/对象,参考公共 Mono 模块。
*/
#endregion
}
void Update()
{
// 降低执行频率:每隔 _updateTime 秒执行一次怪物逻辑
_timer += Time.deltaTime;
if (_timer >= _updateTime)
{
MonsterAILogic();
_timer = 0;
}
// 分帧执行:每帧只更新 _monsterNums 个怪物
for (int i = 0; i < _monsterNums; i++)
{
if (_index >= monsters.Count)
_index = 0;
monsters[_index].GetType();
_index++;
}
}
// 假设是执行怪物逻辑的函数
void MonsterAILogic()
{
// 主要处理寻路逻辑
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com