hz-unity-code-review

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Unity Code Review for Meta Quest

面向Meta Quest的Unity代码评审

When to Use

使用场景

Use this skill when reviewing Unity C# code or project settings that target Meta Quest headsets. This includes:
  • Reviewing scripts for VR performance issues
  • Checking rendering pipeline configuration and settings
  • Ensuring adherence to Quest-specific best practices
  • Identifying common VR development pitfalls
  • Validating input handling for controllers, hands, and eye tracking
  • Auditing memory usage and GC allocation patterns
当你评审面向Meta Quest头显的Unity C#代码或项目设置时,可以使用本技能,包括:
  • 检查脚本中的VR性能问题
  • 验证渲染管线配置与设置
  • 确保遵循Quest专属的最佳实践
  • 识别常见VR开发陷阱
  • 验证控制器、手部及眼动追踪的输入处理逻辑
  • 审核内存使用与GC分配模式

Key Review Areas

核心评审领域

1. Rendering Pipeline Configuration

1. 渲染管线配置

Quest applications must use the Universal Render Pipeline (URP) with specific settings optimized for mobile VR. The Built-in Render Pipeline is not recommended for new Quest projects.
Critical settings to verify:
  • Single-pass multiview must be enabled (Player Settings > XR Plug-in Management > Oculus > Stereo Rendering Mode)
  • Vulkan should be the primary graphics API
  • Linear color space is required for correct lighting
  • HDR should be disabled in URP asset settings
  • Post-processing should be minimal or disabled
Quest应用必须使用针对移动VR优化的Universal Render Pipeline (URP)。不建议在新的Quest项目中使用内置渲染管线。
需验证的关键设置:
  • 单通道多视图必须启用(Player Settings > XR Plug-in Management > Oculus > Stereo Rendering Mode)
  • Vulkan应设为首选图形API
  • 线性色彩空间是正确光照的必要条件
  • 应在URP资源设置中禁用HDR
  • 应尽量减少或禁用后期处理

2. Draw Call Budgets and Batching

2. 绘制调用预算与批处理

Quest has draw call budgets that vary by workload complexity. Every draw call has CPU overhead that directly impacts frame timing.
MetricQuest 2 / Quest ProQuest 3 / Quest 3S
Draw calls (busy simulation)80-200200-300
Draw calls (medium simulation)200-300400-600
Draw calls (light simulation)400-600700-1000
Triangles per frame750K-1M1M-2M
SetPass calls< 50< 80
Enable and verify:
  • Static batching for non-moving geometry
  • GPU instancing for repeated objects
  • SRP batcher for URP materials
  • Dynamic batching for small meshes (< 300 vertices)
Quest的绘制调用预算会因工作负载复杂度而异。每个绘制调用都会产生CPU开销,直接影响帧时序。
指标Quest 2 / Quest ProQuest 3 / Quest 3S
绘制调用(高负载场景)80-200200-300
绘制调用(中等负载场景)200-300400-600
绘制调用(轻负载场景)400-600700-1000
每帧三角形数量750K-1M1M-2M
SetPass调用< 50< 80
启用并验证以下批处理方式:
  • 针对非移动几何体的静态批处理
  • 针对重复对象的GPU实例化
  • 针对URP材质的SRP批处理
  • 针对小型网格(< 300顶点)的动态批处理

3. Shader Complexity

3. Shader复杂度

Mobile GPUs on Quest cannot handle desktop-class shaders. Review all materials for:
  • Use of URP/Lit or URP/Simple Lit instead of Standard shader
  • Custom shaders that minimize texture samples and ALU operations
  • Avoidance of real-time shadows where possible (bake instead)
  • No screen-space effects (SSAO, SSR, screen-space shadows)
Quest的移动GPU无法处理桌面级Shader。需检查所有材质:
  • 使用URP/Lit或URP/Simple Lit替代Standard shader
  • 自定义Shader应尽量减少纹理采样与ALU操作
  • 尽可能避免实时阴影(改用烘焙阴影)
  • 不使用屏幕空间特效(SSAO、SSR、屏幕空间阴影)

4. Memory Management

4. 内存管理

GC allocations cause frame hitches and must be eliminated from hot paths.
csharp
// BAD: Allocates every frame
void Update() {
    string label = "Score: " + score.ToString();
    var enemies = FindObjectsOfType<Enemy>();
    var filtered = enemies.Where(e => e.IsAlive).ToList();
}

// GOOD: Zero allocations in Update
private StringBuilder _sb = new StringBuilder(32);
private List<Enemy> _enemyCache = new List<Enemy>();
private Enemy[] _enemyArray;

void Start() {
    _enemyArray = FindObjectsOfType<Enemy>();
}

void Update() {
    _sb.Clear();
    _sb.Append("Score: ");
    _sb.Append(score);
}
GC分配会导致帧卡顿,必须从热点路径中消除。
csharp
// BAD: Allocates every frame
void Update() {
    string label = "Score: " + score.ToString();
    var enemies = FindObjectsOfType<Enemy>();
    var filtered = enemies.Where(e => e.IsAlive).ToList();
}

// GOOD: Zero allocations in Update
private StringBuilder _sb = new StringBuilder(32);
private List<Enemy> _enemyCache = new List<Enemy>();
private Enemy[] _enemyArray;

void Start() {
    _enemyArray = FindObjectsOfType<Enemy>();
}

void Update() {
    _sb.Clear();
    _sb.Append("Score: ");
    _sb.Append(score);
}

5. Input Handling

5. 输入处理

Quest supports multiple input modalities. Code should handle:
  • Controllers: Use Unity's Input System Package for new projects (recommended);
    OVRInput
    is maintained for legacy support
  • Hand tracking:
    OVRHand
    and
    OVRSkeleton
    for hand pose data
  • Eye tracking:
    OVREyeGaze
    (Quest Pro / Quest 3, requires permission)
  • Graceful switching between controller and hand tracking modes
Quest支持多种输入方式,代码应处理:
  • Controllers: 新项目建议使用Unity's Input System Package;
    OVRInput
    为遗留项目提供支持
  • Hand tracking: 使用
    OVRHand
    和
    OVRSkeleton
    获取手部姿态数据
  • Eye tracking:
    OVREyeGaze
    (适用于Quest Pro / Quest 3,需权限)
  • Graceful switching between controller and hand tracking modes

6. Physics Configuration

6. 物理配置

Physics simulation is expensive on mobile. Review for:
  • Physics timestep set to match target frame rate (72/90/120 Hz)
  • Simplified collision meshes (use primitives, not mesh colliders)
  • Reduced solver iterations (4-6 is usually sufficient)
  • Layer-based collision matrix to minimize pair checks
  • Rigidbody sleep thresholds configured appropriately
物理模拟在移动设备上开销较大,需检查:
  • 物理时间步长设置为匹配目标帧率(72/90/120 Hz)
  • 使用简化的碰撞网格(使用基本几何体,而非网格碰撞器)
  • 减少求解器迭代次数(通常4-6次足够)
  • 使用基于层的碰撞矩阵以减少配对检查
  • 合理配置刚体休眠阈值

7. Audio Setup

7. 音频设置

Audio is often overlooked but can impact performance:
  • Compress audio clips (Vorbis for music, ADPCM for short SFX)
  • Use streaming load type for clips longer than 1 second
  • Limit simultaneous audio sources (target < 16)
  • Spatialize audio using Meta's audio SDK for 3D positioning
音频常被忽略,但会影响性能:
  • 压缩音频剪辑(音乐使用Vorbis,短音效使用ADPCM)
  • 时长超过1秒的剪辑使用流式加载类型
  • 限制同时播放的音频源数量(目标 < 16)
  • 使用Meta的音频SDK实现3D定位的空间化音频

Quick Review Checklist

快速评审检查表

AreaTargetNotes
Draw calls80-200 (busy) to 400-600 (light)Use batching, instancing, atlasing
Triangles750K-1M/frameUse LODs, occlusion culling
Texture resolutionMax 2K, 4K sparinglyASTC compression required
ShaderURP mobile shadersNo Standard shader, no screen-space effects
Rendering modeSingle-pass multiviewMust be enabled in XR settings
FFREnabled (High or HighTop)Fixed foveated rendering reduces edge fragment cost
MSAA4x quality / 2x perfFree on tile-based GPU when configured correctly
Target frame rate72 Hz minimum90 Hz recommended, 120 Hz for smooth experiences
GC allocations0 B/frame in steady stateNo allocations in Update/LateUpdate/FixedUpdate
Audio sources< 16 simultaneousUse pooling for audio sources
领域目标说明
绘制调用80-200(高负载)至400-600(轻负载)使用批处理、实例化、图集
三角形数量750K-1M/帧使用LODs、遮挡剔除
纹理分辨率最大2K,谨慎使用4K必须使用ASTC压缩
ShaderURP移动shaders禁止使用Standard shader及屏幕空间特效
渲染模式单通道多视图必须在XR设置中启用
FFR启用(High或HighTop)固定注视点渲染可降低边缘片段开销
MSAA4x画质 / 2x性能配置正确时,基于瓦片的GPU可免费获得该效果
目标帧率最低72 Hz推荐90 Hz,120 Hz可获得更流畅体验
GC分配稳态下每帧0 BUpdate/LateUpdate/FixedUpdate中无分配
音频源< 16个同时播放使用对象池管理音频源

What to Look For in Code

代码中需关注的问题

GC-Heavy Patterns

高GC分配模式

csharp
// Flag these patterns in code review:
Camera.main                          // Calls FindWithTag internally
GameObject.Find("name")             // Linear search every call
GetComponent<T>() in Update         // Cache the result
new List<T>() in Update             // Allocates on heap
string + string in Update           // Creates new string objects
foreach on non-List collections     // Enumerator allocation
LINQ queries (.Where, .Select)      // Multiple allocations
Boxing (int -> object)              // Heap allocation
delegate/lambda in hot paths        // Closure allocation
csharp
// Flag these patterns in code review:
Camera.main                          // Calls FindWithTag internally
GameObject.Find("name")             // Linear search every call
GetComponent<T>() in Update         // Cache the result
new List<T>() in Update             // Allocates on heap
string + string in Update           // Creates new string objects
foreach on non-List collections     // Enumerator allocation
LINQ queries (.Where, .Select)      // Multiple allocations
Boxing (int -> object)              // Heap allocation
delegate/lambda in hot paths        // Closure allocation

Update() Misuse

Update()误用

csharp
// BAD: Empty Update still has overhead
void Update() { }

// BAD: Logic that doesn't need per-frame execution
void Update() {
    SavePlayerPrefs();  // Should be event-driven
}

// GOOD: Use events, coroutines, or InvokeRepeating for non-per-frame logic
void OnScoreChanged(int newScore) {
    UpdateScoreUI(newScore);
}
csharp
// BAD: Empty Update still has overhead
void Update() { }

// BAD: Logic that doesn't need per-frame execution
void Update() {
    SavePlayerPrefs();  // Should be event-driven
}

// GOOD: Use events, coroutines, or InvokeRepeating for non-per-frame logic
void OnScoreChanged(int newScore) {
    UpdateScoreUI(newScore);
}

Camera.main Anti-Pattern

Camera.main反模式

csharp
// BAD: Camera.main uses FindWithTag internally
void Update() {
    transform.LookAt(Camera.main.transform);
}

// GOOD: Cache the reference
private Transform _cameraTransform;

void Start() {
    _cameraTransform = Camera.main.transform;
}

void Update() {
    transform.LookAt(_cameraTransform);
}
csharp
// BAD: Camera.main uses FindWithTag internally
void Update() {
    transform.LookAt(Camera.main.transform);
}

// GOOD: Cache the reference
private Transform _cameraTransform;

void Start() {
    _cameraTransform = Camera.main.transform;
}

void Update() {
    transform.LookAt(_cameraTransform);
}

Find Calls in Hot Paths

热点路径中的Find调用

csharp
// BAD: Expensive search every frame
void Update() {
    var player = GameObject.FindWithTag("Player");
    var rb = player.GetComponent<Rigidbody>();
}

// GOOD: Cache in Awake/Start or use dependency injection
private Rigidbody _playerRb;

void Awake() {
    _playerRb = GameObject.FindWithTag("Player").GetComponent<Rigidbody>();
}
csharp
// BAD: Expensive search every frame
void Update() {
    var player = GameObject.FindWithTag("Player");
    var rb = player.GetComponent<Rigidbody>();
}

// GOOD: Cache in Awake/Start or use dependency injection
private Rigidbody _playerRb;

void Awake() {
    _playerRb = GameObject.FindWithTag("Player").GetComponent<Rigidbody>();
}

Using metavr for Validation

使用metavr进行验证

You can use the
metavr
tool to validate builds and check device-side behavior. Invoke via
metavr <args>
(published as the npm package
metavr
; if
metavr
is not on PATH, run
npx -y metavr <args>
) — no install required.
bash
undefined
你可以使用
metavr
工具验证构建包并检查设备端行为。通过
metavr <args>
调用(作为npm包
metavr
发布;若
metavr
不在PATH中,可运行
npx -y metavr <args>
)——无需安装。
bash
undefined

Check connected Quest device

Check connected Quest device

metavr device list
metavr device list

Install and run a build

Install and run a build

metavr app install path/to/build.apk metavr app launch com.company.app
metavr app install path/to/build.apk metavr app launch com.company.app

Check device logs for errors

Check device logs for errors

metavr adb logcat --tag Unity
metavr adb logcat --tag Unity

Monitor GPU performance

Monitor GPU performance

metavr perf capture

Use device-side profiling to validate that code review findings translate to real performance improvements.
metavr perf capture

使用设备端分析工具验证代码评审发现的问题是否转化为实际性能提升。

Reference Documents

参考文档

For detailed guidance on specific topics, see the following reference documents:
  • Performance Checklist — comprehensive performance targets and optimization strategies
  • Rendering Best Practices — Quest-specific rendering configuration and shader guidelines
  • Input Handling — controller, hand tracking, and eye tracking implementation
  • Common Pitfalls — frequently encountered mistakes and their fixes
如需特定主题的详细指导,请查看以下参考文档:
  • Performance Checklist —— 全面的性能目标与优化策略
  • Rendering Best Practices —— Quest专属的渲染配置与Shader指南
  • Input Handling —— 控制器、手部追踪及眼动追踪的实现方法
  • Common Pitfalls —— 常见错误及其修复方案