渲染管线与音频系统的 GC 分析
篇章:08-核心技术-性能优化篇 |阅读时间:约 45 分钟
一、引言
Unity 的渲染管线、粒子系统、物理引擎和音频系统是游戏运行时的核心子系统。这些系统在底层由 C++ 实现,但通过 C# 脚本层交互时,边界处的数据传递和回调机制可能产生大量 GC 分配。
系统性地分析各子系统的 GC 触发点,并提供针对性的优化方案,是 Unity 性能优化的关键。本章将逐一分析粒子系统、物理引擎、渲染管线、音频系统和动画系统的 GC 行为,提供完整的优化清单。
在展开之前先把本章的定位说清楚。08-08 Unity API 篇分析了通用 API 的 GC 陷阱,本章聚焦各子系统的专用 API。08-07 Span 篇分析的零分配技术可以应用于本章的粒子数据处理。
二、粒子系统的 GC 陷阱
2.1 粒子系统 GC 分配的深层分析
粒子系统是 Unity 中 GC 分配的高发区,因为粒子操作通常每帧执行。理解粒子系统的 GC 行为需要从 Unity 的底层架构入手。
Unity 的粒子系统由 C++ 端的 ParticleSystem 引擎驱动,C# 端的ParticleSystem类只是一个包装器。当调用GetParticles时,C++ 引擎将粒子数据拷贝到 C# 数组中。这个拷贝过程本身是高效的(使用memcpy),但数组的分配是不可避免的——除非使用预分配缓冲区。
粒子系统产生 GC 分配的主要场景包括:
- GetParticles 输出数组:每次调用需要输出数组,如果数组未预分配则产生分配。
- particleCount 属性:在较旧 Unity 版本中可能触发内部缓存重建。
- module 属性访问:每次访问
ps.trigger、ps.main等 module 属性时,Unity 可能创建新的包装器对象。 - OnParticleTrigger 回调:回调中如果创建新 List 或数组,每帧触发分配。
- Emit 参数对象:使用
Emit(count)时 Unity 内部创建临时参数对象。
这些分配单独看可能只有几十字节,但在一个有 10 个粒子场景面中,每帧累积的 GC 分配可能达到数百字节,在 60 FPS 下每秒产生 48KB 的 GC 分配。
2.2 ParticleSystem API 分配
粒子系统是 Unity 中 GC 分配的高发区,因为粒子操作通常每帧执行:
// ❌ ParticleSystem.GetParticles 需要预分配数组 // 但 particleCount 属性访问本身可能触发内部分配 ParticleSystem ps = GetComponent<ParticleSystem>(); ParticleSystem.Particle[] particles = new ParticleSystem.Particle[ps.particleCount]; int count = ps.GetParticles(particles); // 写入预分配数组 // particleCount 的分配风险: // - 在较旧 Unity 版本中,particleCount 可能触发内部缓存重建 // - 在 Unity 2020+ 中已优化,但仍建议缓存 // ✅ 预分配粒子缓冲区:在 Awake 中分配一次 private ParticleSystem.Particle[] _particleBuffer = new ParticleSystem.Particle[1024]; private ParticleSystem _ps; void Awake() { _ps = GetComponent<ParticleSystem>(); } void Update() { // 复用数组,零分配 int count = _ps.GetParticles(_particleBuffer); for (int i = 0; i < count; i++) { // 修改粒子属性 _particleBuffer[i].velocity += Vector3.up * 0.1f; } // 写回修改 _ps.SetParticles(_particleBuffer, count); }关键要点:GetParticles需要预分配数组作为输出缓冲区。数组大小应根据粒子系统的最大粒子数预分配。如果粒子数动态变化,使用ArrayPool或双缓冲策略。
2.2 ParticleSystem.TrailModule 与 TriggerModule
// ❌ 频繁访问 ParticleSystem 的 module 属性可能分配 // var trigger = ps.trigger; // 每次访问可能创建新的 ParticleSystem.TriggerModule 包装器 // trigger.SetCollider(0, someCollider); // ✅ 缓存 module 引用 private ParticleSystem.TriggerModule _triggerModule; void Awake() { _triggerModule = GetComponent<ParticleSystem>().trigger; _triggerModule.enabled = true; _triggerModule.collider0 = someCollider; } // ❌ OnParticleTrigger 回调中分配列表 void OnParticleTrigger() { // ❌ 分配 List // List<ParticleSystem.Particle> triggered = new List<ParticleSystem.Particle>(); // ps.GetTriggerParticles(ParticleSystemTriggerEventType.Enter, triggered); // ✅ 复用 List _triggeredBuffer.Clear(); _ps.GetTriggerParticles(ParticleSystemTriggerEventType.Enter, _triggeredBuffer); } private List<ParticleSystem.Particle> _triggeredBuffer = new(64);2.3 粒子碰撞回调
// OnParticleCollision 每次回调可能分配 private readonly List<ParticleCollisionEvent> _collisionBuffer = new(64); void OnParticleCollision(GameObject other) { // ✅ 使用 NonAlloc 版本获取碰撞事件 int count = ParticlePhysicsExtensions.GetCollisionEvents(other, _collisionBuffer); for (int i = 0; i < count; i++) { var evt = _collisionBuffer[i]; // evt.intersection: 碰撞点 // evt.normal: 碰撞法线 // evt.velocity: 碰撞速度 // evt.internalCollisionImpulse: 碰撞冲量 } }2.4 ParticleSystem.Emit 分配
// ❌ ParticleSystem.Emit 每次调用可能分配 ps.Emit(1); // 发射 1 个粒子 // ❌ ParticleSystem.Emit 批量发射 ps.Emit(100); // 发射 100 个粒子 // ✅ 预配置发射参数,减少每帧分配 private ParticleSystem.EmitParameters _emitParams; void Init() { _emitParams.velocityOverSpace = new ParticleSystem.MinMaxGradient( new Vector3(0, 5, 0), new Vector3(0, 5, 0)); _emitParams.startColor = Color.white; } void Emit() { ps.Emit(_emitParams, 10); // 复用参数,减少分配 }3.0 物理引擎 GC 行为的架构视角
物理引擎的 GC 行为与粒子系统有本质不同。粒子系统的 GC 主要来自 C# 数组分配,而物理引擎的 GC 主要来自碰撞检测结果的数组返回。
Unity 的物理引擎(PhysX)在 C++ 端执行碰撞检测,检测到的碰撞结果通过 C# API 暴露。Physics.OverlapSphere等方法在内部分配数组来存储碰撞结果,这是因为 C# 调用方不知道需要多大的数组来存储结果。
NonAlloc版本的设计思路是:调用方预分配数组,C++ 引擎将结果写入预分配数组。这消除了每次调用的数组分配,但要求调用方正确管理缓冲区大小。
3.1 OnCollisionEnter / OnTriggerEnter
// ✅ Collision 是 struct,通过参数传递,无分配 void OnCollisionEnter(Collision collision) { // ❌ collision.contacts 返回 ContactPoint[] — 分配! // ContactPoint[] contacts = collision.contacts; // ✅ 使用 GetContact(i) 逐个获取,无分配 int contactCount = collision.contactCount; for (int i = 0; i < contactCount; i++) { ContactPoint contact = collision.GetContact(i); // struct 返回,无分配 // contact.point: 碰撞点 // contact.normal: 碰撞法线 // contact.thisCollider: 本碰撞体 // contact.otherCollider: 对方碰撞体 } } // ✅ Collider 是引用类型,但通过参数传递无分配 void OnTriggerEnter(Collider other) { if (other.CompareTag("Enemy")) // 无分配 { // 处理 } }3.2 物理材质与摩擦
// ❌ collider.material 访问可能分配 PhysicMaterial 实例 PhysicMaterial mat = collider.material; // 可能分配 // ✅ 缓存物理材质引用 private PhysicMaterial? _physicMat; void Awake() { _physicMat = collider.sharedMaterial; }3.3 Rigidbody API
// ✅ 无分配的 Rigidbody API // rb.velocity // Vector3 值返回 // rb.angularVelocity // Vector3 值返回 // rb.position // Vector3 值返回 // rb.mass // float 返回 // rb.drag // float 返回 // ❌ rb.SweepTestAll 返回数组 // RaycastHit[] hits = rb.SweepTestAll(direction, distance); // 分配! // ✅ rb.SweepTest 单个测试,无分配 if (rb.SweepTest(direction, out RaycastHit hit, distance)) { // 无分配 }3.4 Joint API
// ❌ Joint 的属性访问可能分配 // Joint joint = GetComponent<Joint>(); // var limits = joint.limits; // 可能分配 // ✅ 缓存 Joint 配置 private ConfigurableJoint _configJoint; void Awake() { _configJoint = GetComponent<ConfigurableJoint>(); // 在 Awake 中配置,运行时不修改 }4.0 渲染管线 GC 行为的架构视角
渲染管线的 GC 行为与粒子系统和物理引擎有显著不同。渲染管线的大部分 GC 分配来自材质实例化和渲染命令的创建,而不是数据数组。
Unity 的渲染管线分为固定功能管线(FFP)、内置渲染管线(URP)和可编程渲染管线(SRP)。不同管线对 GC 的影响不同:
- FFP:材质实例化是最主要的 GC 来源。每帧调用
renderer.material会为每个 Renderer 创建新材质实例。 - URP:在 FFP 基础上增加了 CommandBuffer 和 RenderPass 的分配。
- SRP:提供了最大的灵活性,但也引入了更多的 GC 风险点(如自定义 RenderPass 的创建)。
理解渲染管线的 GC 行为需要从渲染调用的角度分析:每个 DrawCall 可能触发材质实例化,每个 RenderPass 可能触发 CommandBuffer 创建。
4.1 Renderer 相关分配
// ❌ renderer.materials 返回 Material[] 数组 // Material[] mats = renderer.materials; // 分配数组 + 每个 Material 引用 // ✅ 缓存 sharedMaterials private Material[]? _sharedMats; void Awake() { _sharedMats = renderer.sharedMaterials; } // ✅ 使用 MaterialPropertyBlock 修改材质属性 private MaterialPropertyBlock? _propBlock; private static readonly int s_colorId = Shader.PropertyToID("_BaseColor"); void SetColor(Color color) { _propBlock ??= new MaterialPropertyBlock(); _propBlock.SetColor(s_colorId, color); renderer.SetPropertyBlock(_propBlock); }4.2 Light 相关分配
// ✅ 大部分 Light 属性是值类型,无分配 light.intensity = 1f; // float light.color = Color.white; // Color struct light.range = 10f; // float light.type = LightType.Point; // enum // ⚠️ LightmapData 访问 // LightmapData[] lightmaps = LightmapSettings.lightmaps; // 分配数组 // ✅ 缓存 private LightmapData[]? _cachedLightmaps; void Awake() { _cachedLightmaps = LightmapSettings.lightmaps; }4.3 Graphics.DrawMesh / DrawMeshInstanced
// ❌ DrawMeshInstanced 需要矩阵数组 // Matrix4x4[] matrices = new Matrix4x4[1000]; // 分配 // Graphics.DrawMeshInstanced(mesh, 0, material, matrices); // ✅ 使用 DrawMeshInstancedIndirect + ComputeBuffer private ComputeBuffer? _matrixBuffer; private Matrix4x4[] _matrixData; // CPU 端数据 void Init() { _matrixData = new Matrix4x4[1000]; _matrixBuffer = new ComputeBuffer(1000, sizeof(float) * 16); } void Render() { // 直接写入 ComputeBuffer,无 C# 数组分配 _matrixBuffer!.SetData(_matrixData); Graphics.DrawMeshInstancedIndirect( mesh, 0, material, bounds, _matrixBuffer); } void OnDestroy() { _matrixBuffer?.Dispose(); }4.4 CommandBuffer 与 SRP
// ✅ CommandBuffer 操作通常无 GC(除了创建时) var cmd = new CommandBuffer { name = "CustomPass" }; cmd.SetRenderTarget(renderTexture); cmd.ClearRenderTarget(true, true, Color.black); cmd.DrawMesh(mesh, matrix, material); Graphics.ExecuteCommandBuffer(cmd); cmd.Release(); // 释放(不归还池) // ⚠️ CommandBuffer 本身分配,应复用 private static CommandBuffer? _sharedCmdBuffer; void Init() { _sharedCmdBuffer = new CommandBuffer { name = "SharedPass" }; _sharedCmdBuffer.SetRenderTarget(renderTexture); // 预配置命令... } void Render() { Graphics.ExecuteCommandBuffer(_sharedCmdBuffer); }4.5 RenderTexture 分配
// ❌ 每帧创建 RenderTexture,严重 GC void Update() { var rt = new RenderTexture(Screen.width, Screen.height, 24); // 使用 rt... rt.Release(); } // ✅ 预分配 RenderTexture private RenderTexture? _rt; void Awake() { _rt = new RenderTexture(Screen.width, Screen.height, 24); _rt.enableRandomWrite = true; _rt.Create(); } void Update() { // 复用 _rt }5.0 音频系统 GC 行为的架构视角
Unity 的音频系统由 FMOD 或 Unity Audio 后端驱动,C# 端的AudioSource类是 C++ 音频引擎的包装器。音频系统的 GC 行为相对简单,主要分配来自音频数据的数组传递。
音频系统产生 GC 分配的主要场景:
- GetOutputData/GetSpectrumData:需要 float[] 作为输出缓冲区,如果每次调用都创建新数组则产生分配。
- AudioClip.Create:动态创建 AudioClip 时分配音频缓冲区。
- AudioMixer 参数获取:
GetFloat本身无分配,但频繁调用有 CPU 开销。
音频系统的 GC 优化相对简单——主要是预分配缓冲区。但音频分析通常在后台线程或较低频率执行,所以 GC 影响通常小于粒子系统和物理引擎。
5.1 AudioSource API
// ✅ 无分配的 AudioSource API // audioSource.volume = 1f; // float // audioSource.pitch = 1f; // float // audioSource.time = 0f; // float // audioSource.isPlaying // bool // audioSource.clip // AudioClip 引用 // ❌ audioSource.GetOutputData 需要预分配 float[] // float[] samples = new float[1024]; // audioSource.GetOutputData(samples, 0); // 需要预分配数组 // ✅ 预分配采样缓冲区 private float[] _sampleBuffer = new float[1024]; void AnalyzeAudio() { audioSource.GetOutputData(_sampleBuffer, 0); // 复用缓冲区 // 分析 _sampleBuffer... } // ❌ GetSpectrumData 同样需要预分配 // float[] spectrum = new float[512]; // audioSource.GetSpectrumData(spectrum, 0, FFTWindow.Blackman); // ✅ 预分配 private float[] _spectrumBuffer = new float[512]; void AnalyzeSpectrum() { audioSource.GetSpectrumData(_spectrumBuffer, 0, FFTWindow.Blackman); }5.2 AudioClip 动态创建
// ❌ AudioClip.Create 每次分配 // AudioClip clip = AudioClip.Create("Dynamic", 44100, 1, 44100, false); // ✅ 复用 AudioClip private AudioClip? _reusableClip; void Init() { _reusableClip = AudioClip.Create( "Reusable", 44100, 1, 44100, false, OnAudioRead); } // 回调:填充音频数据 void OnAudioRead(float[] data) { // 直接填充 data,零分配 for (int i = 0; i < data.Length; i++) { data[i] = Mathf.Sin(i * 0.01f) * 0.5f; } } // ✅ 使用 SetData 预分配的 Clip void UpdateAudio(float[] newData) { _reusableClip!.SetData(newData, 0); // 复用 Clip }5.3 AudioSettings 与回调
// ⚠️ AudioSettings.OnAudioConfigurationChanged 回调 void OnAudioConfigChanged(bool deviceChanged) { // 重新初始化音频系统 // 此回调本身无分配,但内部操作可能分配 } // ✅ 注册一次 void OnEnable() { AudioSettings.OnAudioConfigurationChanged += OnAudioConfigChanged; } void OnDisable() { AudioSettings.OnAudioConfigurationChanged -= OnAudioConfigChanged; }5.4 AudioMixer 分配
// ❌ AudioMixer.GetFloat 返回 bool,但参数是 out float // 本身无分配,但频繁调用有 CPU 开销 bool result = _audioMixer.GetFloat("MasterVolume", out float volume); // ✅ 缓存 AudioMixer 引用 private AudioMixer? _mixer; void Awake() { _mixer = GetComponent<AudioMixer>(); } // ❌ AudioMixer.SetFloat 无分配 _mixer?.SetFloat("MasterVolume", newVolume);5.5 音频系统 GC 优化的工程实践
音频系统的 GC 优化虽然相对简单,但在某些场景下仍然需要特别注意。以下是音频系统 GC 优化的工程实践建议:
- 音频分析频率:音频频谱分析通常不需要每帧执行。降低分析频率(如每秒 10 次)可以显著减少 GC 分配。
- 音频线程安全:
AudioSource的 API 调用通常需要在主线程执行。如果在后台线程分析音频数据,需要使用AudioSettings.OnAudioConfigurationChanged等机制同步。 - 音频对象池:对于动态创建/销毁的音频源(如爆炸音效),使用 AudioSource 对象池可以避免 AudioClip 的重复创建。
六、动画系统的 GC 分析
6.1 Animator API
// ✅ 无分配的 Animator API // animator.speed = 1f; // float // animator.applyRootMotion = false; // bool // animator.isInitialized // bool // ✅ Animator.StringToHash 缓存 static readonly int s_speedHash = Animator.StringToHash("Speed"); static readonly int s_attackHash = Animator.StringToHash("Attack"); static readonly int s_deadHash = Animator.StringToHash("Dead"); void Update() { animator.SetFloat(s_speedHash, _currentSpeed); // 使用缓存 Hash,无分配 animator.SetTrigger(s_attackHash); } // ❌ 使用字符串参数 // animator.SetFloat("Speed", _currentSpeed); // 可能分配字符串6.2 Animation Event 回调
// ⚠️ AnimationEvent 通过反射调用 // 在 Animation Event 中调用的方法名通过字符串匹配 // 本身无分配,但如果方法内部有分配则每帧触发 // ✅ Animation Event 方法保持零分配 void OnAnimationEvent() { // 避免在此方法内分配 // ✅ 使用预分配的缓冲区 }6.3 AnimationClip 加载
// ❌ Resources.Load 每帧调用,分配 // AnimationClip clip = Resources.Load<AnimationClip>("Animations/Idle"); // ✅ 缓存引用 private AnimationClip? _idleClip; void Awake() { _idleClip = Resources.Load<AnimationClip>("Animations/Idle"); } // ⚠️ AnimationClip 本身较大,加载时分配较多内存 // 应在初始化阶段加载,运行时复用6.5 动画系统 GC 优化的工程实践
动画系统的 GC 优化相对简单,主要关注 Animator 参数的 Hash 缓存。以下是动画系统 GC 优化的工程实践建议:
- Hash 缓存:所有 Animator 参数(SetFloat、SetInteger、SetBool、SetTrigger)都应该使用缓存的 Hash 值,避免字符串查找。
- Animation Event 零分配:Animation Event 回调方法中避免分配,使用预分配缓冲区。
- AnimationClip 预加载:所有需要的 AnimationClip 应该在初始化阶段加载,避免运行时加载。
七、各子系统 GC 触发点汇总
7.1 粒子系统
| API | 分配 | 优化 |
|---|---|---|
ps.particleCount | 可能 | 缓存 |
ps.GetParticles(new[]) | 数组 | 预分配缓冲区 |
GetTriggerParticles(new List) | List | 复用 List |
GetCollisionEvents(new List) | List | 复用 List |
trigger.SetCollider | 可能 | 预配置 |
7.2 物理引擎
| API | 分配 | 优化 |
|---|---|---|
collision.contacts | ContactPoint[] | GetContact(i) |
Physics.OverlapSphere | Collider[] | NonAlloc 版本 |
Physics.RaycastAll | RaycastHit[] | NonAlloc 版本 |
rb.SweepTestAll | RaycastHit[] | SweepTest单个 |
collider.material | 可能 | 缓存引用 |
7.3 渲染管线
| API | 分配 | 优化 |
|---|---|---|
renderer.materials | Material[] | 缓存/PropertyBlock |
renderer.material | Material 实例 | sharedMaterial |
LightmapSettings.lightmaps | LightmapData[] | 缓存 |
DrawMeshInstanced | Matrix4x4[] | DrawMeshInstancedIndirect |
Shader.PropertyToID | 0 | 缓存为 static |
new RenderTexture() | RenderTexture | 预分配 |
new CommandBuffer() | CommandBuffer | 复用 |
7.4 音频系统
| API | 分配 | 优化 |
|---|---|---|
GetOutputData(new[]) | float[] | 预分配缓冲区 |
GetSpectrumData(new[]) | float[] | 预分配缓冲区 |
AudioClip.Create | AudioClip | 复用 Clip |
AudioClip.SetData | 0 | 安全 |
AudioMixer.GetFloat | 0 | 缓存 Mixer 引用 |
7.5 动画系统
| API | 分配 | 优化 |
|---|---|---|
animator.SetFloat("name") | 可能 | 缓存 Hash |
animator.SetTrigger("name") | 可能 | 缓存 Hash |
animator.StringToHash | 0 | 缓存为 static |
AnimationEvent回调 | 方法内分配 | 保持零分配 |
八、实战:零分配粒子碰撞处理
public class ParticleCollisionHandler : MonoBehaviour { private readonly List<ParticleCollisionEvent> _collisionEvents = new(64); private readonly Collider[] _overlapBuffer = new Collider[32]; private ParticleSystem _ps; private Transform _transform; void Awake() { _ps = GetComponent<ParticleSystem>(); _transform = transform; } void OnParticleCollision(GameObject other) { // 零分配获取碰撞事件 int count = ParticlePhysicsExtensions.GetCollisionEvents(other, _collisionEvents); for (int i = 0; i < count; i++) { var evt = _collisionEvents[i]; Vector3 point = evt.intersection; Vector3 normal = evt.normal; // 零分配范围查询 int overlapCount = Physics.OverlapSphereNonAlloc( point, 2f, _overlapBuffer); for (int j = 0; j < overlapCount; j++) { var col = _overlapBuffer[j]; if (col.TryGetComponent<Damageable>(out var damageable)) { damageable.TakeDamage(10f, point, normal); } } } } }九、各子系统 GC 优化的工程化实践
9.1 子系统初始化模式
为每个子系统建立统一的初始化模式:
public class SubsystemInitializer : MonoBehaviour { void Awake() { // 粒子系统:预分配缓冲区 InitParticleSystem(); // 物理:预分配查询缓冲区 InitPhysicsBuffers(); // 渲染:预分配 RenderTexture/CommandBuffer InitRendering(); // 音频:预分配采样缓冲区 InitAudio(); // 动画:缓存 Hash InitAnimation(); } void InitParticleSystem() { /* ... */ } void InitPhysicsBuffers() { /* ... */ } void InitRendering() { /* ... */ } void InitAudio() { /* ... */ } void InitAnimation() { /* ... */ } }9.2 子系统 GC 监控
public class SubsystemGCMonitor : MonoBehaviour { // 各子系统的 GC 分配监控 private ProfilerRecorder _particleAlloc; private ProfilerRecorder _physicsAlloc; private ProfilerRecorder _renderAlloc; private ProfilerRecorder _audioAlloc; void OnEnable() { _particleAlloc = ProfilerRecorder.StartNew( "ParticleSystem", "GC.Alloc", 1); _physicsAlloc = ProfilerRecorder.StartNew( "Physics", "GC.Alloc", 1); _renderAlloc = ProfilerRecorder.StartNew( "Rendering", "GC.Alloc", 1); _audioAlloc = ProfilerRecorder.StartNew( "Audio", "GC.Alloc", 1); } void OnDisable() { _particleAlloc?.Dispose(); _physicsAlloc?.Dispose(); _renderAlloc?.Dispose(); _audioAlloc?.Dispose(); } }十、总结与实践建议
10.1 核心要点回顾
渲染管线与音频系统的 GC 优化要点:
- 粒子系统预分配缓冲区:GetParticles/GetCollisionEvents 复用。
- 物理引擎用 NonAlloc:避免数组返回版本。
- collision.GetContact(i) 替代 contacts:零分配获取碰撞点。
- MaterialPropertyBlock 替代 material:避免材质实例化。
- DrawMeshInstancedIndirect 替代 DrawMeshInstanced:ComputeBuffer 零分配。
- 音频采样预分配缓冲区:GetOutputData/GetSpectrumData 复用。
- Animator 参数用 Hash 缓存:StringToHash 缓存为 static。
- AudioClip 复用:SetData 替代 Create。
- RenderTexture/CommandBuffer 预分配:避免每帧创建。
- AnimationClip 初始化加载:运行时复用。
10.2 实践建议
- 子系统初始化时预分配所有缓冲区:粒子、物理、渲染、音频。
- 建立子系统 GC 监控:使用 ProfilerRecorder 跟踪各子系统分配。
- 定期性能审计:检查各子系统的 GC 分配趋势。
- Unity 版本更新时检查 API 变化:新 Unity 版本可能优化或引入新的分配。
- 使用 DOTS/ECS 替代高频循环:NativeArray + Burst 编译器实现零分配。
10.3 子系统 GC 优化决策树
使用 Unity 子系统? │ ├─ 粒子系统? │ ├─ 每帧读取粒子?→ GetParticles + 预分配缓冲区 │ ├─ 处理触发事件?→ GetTriggerParticles + 复用 List │ └─ 处理碰撞事件?→ GetCollisionEvents + 复用 List │ ├─ 物理引擎? │ ├─ 范围查询?→ NonAlloc 版本 │ ├─ 射线检测?→ RaycastNonAlloc │ └─ 碰撞接触点?→ GetContact(i) │ ├─ 渲染管线? │ ├─ 修改材质?→ MaterialPropertyBlock │ ├─ 批量渲染?→ DrawMeshInstancedIndirect │ ├─ 后处理?→ 预分配 RenderTexture │ └─ 自定义 pass?→ 复用 CommandBuffer │ ├─ 音频系统? │ ├─ 频谱分析?→ 预分配 float[] │ ├─ 动态音频?→ 复用 AudioClip │ └─ AudioMixer?→ 缓存引用 │ └─ 动画系统? └─ Animator 参数?→ 缓存 Hash