hz-unity-code-review
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseUnity 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.
| Metric | Quest 2 / Quest Pro | Quest 3 / Quest 3S |
|---|---|---|
| Draw calls (busy simulation) | 80-200 | 200-300 |
| Draw calls (medium simulation) | 200-300 | 400-600 |
| Draw calls (light simulation) | 400-600 | 700-1000 |
| Triangles per frame | 750K-1M | 1M-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 Pro | Quest 3 / Quest 3S |
|---|---|---|
| 绘制调用(高负载场景) | 80-200 | 200-300 |
| 绘制调用(中等负载场景) | 200-300 | 400-600 |
| 绘制调用(轻负载场景) | 400-600 | 700-1000 |
| 每帧三角形数量 | 750K-1M | 1M-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); is maintained for legacy support
OVRInput - Hand tracking: and
OVRHandfor hand pose dataOVRSkeleton - Eye tracking: (Quest Pro / Quest 3, requires permission)
OVREyeGaze - Graceful switching between controller and hand tracking modes
Quest支持多种输入方式,代码应处理:
- Controllers: 新项目建议使用Unity's Input System Package;为遗留项目提供支持
OVRInput - Hand tracking: 使用和
OVRHand获取手部姿态数据OVRSkeleton - Eye tracking: (适用于Quest Pro / Quest 3,需权限)
OVREyeGaze - 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
快速评审检查表
| Area | Target | Notes |
|---|---|---|
| Draw calls | 80-200 (busy) to 400-600 (light) | Use batching, instancing, atlasing |
| Triangles | 750K-1M/frame | Use LODs, occlusion culling |
| Texture resolution | Max 2K, 4K sparingly | ASTC compression required |
| Shader | URP mobile shaders | No Standard shader, no screen-space effects |
| Rendering mode | Single-pass multiview | Must be enabled in XR settings |
| FFR | Enabled (High or HighTop) | Fixed foveated rendering reduces edge fragment cost |
| MSAA | 4x quality / 2x perf | Free on tile-based GPU when configured correctly |
| Target frame rate | 72 Hz minimum | 90 Hz recommended, 120 Hz for smooth experiences |
| GC allocations | 0 B/frame in steady state | No allocations in Update/LateUpdate/FixedUpdate |
| Audio sources | < 16 simultaneous | Use pooling for audio sources |
| 领域 | 目标 | 说明 |
|---|---|---|
| 绘制调用 | 80-200(高负载)至400-600(轻负载) | 使用批处理、实例化、图集 |
| 三角形数量 | 750K-1M/帧 | 使用LODs、遮挡剔除 |
| 纹理分辨率 | 最大2K,谨慎使用4K | 必须使用ASTC压缩 |
| Shader | URP移动shaders | 禁止使用Standard shader及屏幕空间特效 |
| 渲染模式 | 单通道多视图 | 必须在XR设置中启用 |
| FFR | 启用(High或HighTop) | 固定注视点渲染可降低边缘片段开销 |
| MSAA | 4x画质 / 2x性能 | 配置正确时,基于瓦片的GPU可免费获得该效果 |
| 目标帧率 | 最低72 Hz | 推荐90 Hz,120 Hz可获得更流畅体验 |
| GC分配 | 稳态下每帧0 B | Update/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 allocationcsharp
// 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 allocationUpdate() 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 tool to validate builds and check device-side behavior. Invoke via (published as the npm package ; if is not on PATH, run ) — no install required.
metavrmetavr <args>metavrmetavrnpx -y metavr <args>bash
undefined你可以使用工具验证构建包并检查设备端行为。通过调用(作为npm包发布;若不在PATH中,可运行)——无需安装。
metavrmetavr <args>metavrmetavrnpx -y metavr <args>bash
undefinedCheck 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 —— 常见错误及其修复方案