Files
zeling_v2/Docs/Design/27_PerformanceBudgetGuide.md
2026-05-08 11:04:00 +08:00

376 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 27 · 性能预算指南
> **适用范围** 全部系统
> **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md)
---
## 目录
1. [帧时间预算](#1-帧时间预算)
2. [GC 分配预算](#2-gc-分配预算)
3. [每平台性能目标](#3-每平台性能目标)
4. [对象池容量公式](#4-对象池容量公式)
5. [物理配置规范](#5-物理配置规范)
6. [动画导入配置规范](#6-动画导入配置规范)
7. [UI Canvas 批处理规范](#7-ui-canvas-批处理规范)
8. [音频预算](#8-音频预算)
9. [Profiler 使用规范](#9-profiler-使用规范)
10. [性能检查清单PR 门禁)](#10-性能检查清单pr-门禁)
---
## 1. 帧时间预算
### 目标帧率
| 平台 | 目标帧率 | 帧时间预算 |
|------|---------|----------|
| PC高配| 144 fps | 6.9 ms |
| PC低配| 60 fps | 16.6 ms |
| Steam Deck | 60 fps | 16.6 ms |
| Nintendo Switch未来| 30 fps | 33.3 ms |
**主要基准**:以 **60 fps / 16.6 ms** 为核心优化目标。
### 帧时间分配16.6 ms
```
16.6 ms 总预算分配:
├─ Unity 引擎开销渲染、物理、GC ≤ 5.0 ms
│ ├─ Physics2D.FixedUpdate ≤ 1.5 ms
│ ├─ Camera + Culling ≤ 1.0 ms
│ └─ 渲染Draw Call + Sort ≤ 2.5 ms
├─ 游戏逻辑 ≤ 6.0 ms
│ ├─ PlayerController + States ≤ 1.0 ms
│ ├─ EnemyAIBehaviorDesigner ≤ 2.0 ms≤ 20 个活跃敌人)
│ ├─ EnemyNavAgentPathBerserker≤ 1.0 ms
│ ├─ AnimancerComponent 驱动 ≤ 1.0 ms
│ └─ 其他系统SO 事件/UI/音频) ≤ 1.0 ms
├─ Feel / MMFeedbacks ≤ 1.5 ms
└─ 余量 ≈ 4.1 ms
```
### 超时警报阈值
| 系统 | Profiler Marker | 警报阈值 |
|------|----------------|---------|
| `PlayerController` | `[BaseGames] Player.Update` | > 1 ms |
| `EnemyAI全部` | `[BaseGames] Enemies.AI` | > 2 ms |
| `GlobalObjectPool.Prewarm` | `[BaseGames] Pool.Prewarm` | > 50 ms仅启动一次|
| `SaveManager.Save` | `[BaseGames] Save.Write` | > 10 ms异步写入主线程触发|
---
## 2. GC 分配预算
### 限制
| 范围 | 限制 | 说明 |
|------|------|------|
| **每帧 GC 分配(游戏进行中)** | ≤ 256 KB | 超出可能导致 GC Pause |
| **单次 GC Pause如需触发** | ≤ 1 ms | 使用 Incremental GC |
| **游戏启动时(预热阶段)** | 不限制 | 仅启动一次,不影响游玩 |
| **场景加载时(加载画面覆盖)** | 不限制 | 加载画面覆盖,不可见 |
### Unity 设置
```
Project Settings → Player → Other Settings:
✅ Use Incremental GC
GC Incremental Time Slice: 1 ms
Project Settings → Quality:
✅ Enable V-Sync或手动 Application.targetFrameRate = 60
```
### 常见 GC 热点及替代方案
| 反模式 | 替代方案 |
|--------|---------|
| `GetComponent<T>()` 在 Update 中 | `Awake` 缓存引用 |
| `string` 拼接/Format 在热路径 | 预分配 `StringBuilder``string.Format` 仅在非热路径 |
| LINQ`.Where().ToList()`)在 Update | 手写 `for` 循环 + 预分配 `List<T>` |
| 事件 `+= lambda` 每帧注册 | `OnEnable`/`OnDisable` 订阅/取消 |
| `new Vector2/3()` 频繁在 Update | 复用局部变量,不 new |
| `Physics2D.RaycastAll()` | `Physics2D.RaycastNonAlloc()` + 预分配数组 |
| Coroutine `yield return new WaitForSeconds()` 每帧 | 缓存 `WaitForSeconds` 实例(`static readonly`|
### 推荐 Profiler 内存标记
```csharp
// 每个系统的 Update/FixedUpdate 包裹 ProfilerMarker
static readonly ProfilerMarker s_Marker = new ProfilerMarker("[BaseGames] System.Update");
void Update()
{
using var _ = s_Marker.Auto();
// 逻辑
}
```
---
## 3. 每平台性能目标
### 渲染
| 指标 | PC 低配 目标 | Steam Deck 目标 |
|------|------------|----------------|
| Draw Calls / 帧 | ≤ 150 | ≤ 100 |
| Batches / 帧 | ≤ 80 | ≤ 60 |
| SetPass Calls / 帧 | ≤ 20 | ≤ 15 |
| Sprite Atlas 最大尺寸 | 2048×2048 | 1024×1024 |
| 像素填充率(全屏四边形)| — | 避免超过 3 层透明叠加 |
### 活跃对象上限
| 对象类型 | 同屏上限 | 超出时行为 |
|---------|---------|----------|
| 活跃敌人 | 20 | 超出范围的敌人进入"休眠"(禁用 AI保留碰撞|
| 活跃弹射物 | 64 | 对象池:超出上限时最老的 Projectile 自动回收 |
| 活跃粒子系统 | 32 | Feel MMF_Player 内置最大粒子预算 |
| 活跃 MMF_Player | 16 | 低优先级 Feedback 在帧预算紧张时跳过 |
---
## 4. 对象池容量公式
### 弹射物对象池
```
池容量 = 最大活跃敌人数 × 每敌人最大弹射物数 × 齐射数 × 安全余量
= 20 × 4 × 2 × 1.5
= 240取整为 256
```
| 参数 | 值 | 说明 |
|------|----|------|
| 最大活跃敌人 | 20 | 见活跃对象上限 |
| 每敌人最大弹射物 | 4 | Boss 弹幕模式最大同时弹射物 |
| 齐射数 | 2 | 一轮连发的发射次数 |
| 安全余量 | 1.5 | 防止池枯竭 |
| **推荐池容量** | **256** | 向上取 2 的幂次 |
### 粒子/特效对象池
```
池容量 = 弹射物命中率估计 × 命中特效数量 × 并发系数
= 64活跃弹射物× 0.3(命中率) × 2特效种类
= 38取整为 48
```
### 敌人对象池
- Boss 敌人:不使用对象池(唯一实例,场景卸载时销毁)
- 普通敌人:`预热数量 = 房间最大敌人数 × 2`(支持密集战斗)
### 池枯竭策略
```csharp
// GlobalObjectPool 当池空时的回退策略
public enum PoolExhaustPolicy
{
ReturnNull, // 返回 null调用方需处理推荐用于弹射物
RecycleOldest, // 强制回收最老实例(推荐,用于粒子特效)
ExpandPool, // 动态扩容(仅允许在加载/过渡场景时使用)
}
```
---
## 5. 物理配置规范
### Fixed Timestep
```
Project Settings → Time:
Fixed Timestep: 0.02 50 Hz即 50 次/秒 FixedUpdate
Maximum Allowed Timestep: 0.1(防止 Spiral of Death
```
**不要使用默认 0.02 以外的值**除非特别测试验证。60fps 游戏中 FixedUpdate 每帧约调用 0.83 次(略低于每帧 1 次),游戏逻辑不依赖精确的 FixedUpdate 频率。
### Physics2D 配置
```
Project Settings → Physics 2D:
Velocity Iterations: 6默认 8降低以减少 CPU 开销)
Position Iterations: 2默认 3
Queries Hit Triggers: 仅在需要时开启(逐 Collider 使用 ContactFilter2D
Layer Collision Matrix: 精确配置,禁用不需要的层对碰撞
```
### 推荐碰撞层矩阵
| 层 | Player | Enemy | Projectile | Wall | OneWay | Trigger |
|----|--------|-------|-----------|------|--------|---------|
| **Player** | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ |
| **Enemy** | ✗ | ✗ | ✓ | ✓ | ✓ | ✗ |
| **Projectile** | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ |
| **HitBox** | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ |
| **HurtBox** | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
> HitBox/HurtBox 仅与对应目标层碰撞,减少不必要的碰撞查询。
### Rigidbody2D 最佳实践
- 玩家/敌人:`CollisionDetectionMode = Continuous`(防止穿墙)
- 弹射物:`CollisionDetectionMode = Continuous`(高速物体必须)
- 静态平台:使用 `StaticRigidbody2D` 或仅 `Collider2D`(无 Rigidbody2D
---
## 6. 动画导入配置规范
### 帧率策略
| 动画类型 | 推荐帧率 | 说明 |
|---------|---------|------|
| Idle待机循环| 12 fps | 动作缓慢,低帧率不明显 |
| Run奔跑循环| 20 fps | 需要流畅感 |
| Jump / Fall | 12 fps | 空中动作简单 |
| Attack精确帧数| 60 fps | HitBox 需要精确到帧 |
| Boss 攻击预警 | 60 fps | 电报动作需要精确控制 |
| UI 动画 | 30 fps | 界面元素不需要极高帧率 |
| Cutscene | 24 fps | 电影帧率 |
> Animancer 支持动画混合,帧率差异在混合时自动处理,无需担心不同帧率动画之间的转换问题。
### 导入设置
```
AnimationClip Import:
✅ Resample Curves → true提升采样精度
Loop Time → 循环动画 true一次性动画 false
Compression → OptimalUnity 自动选择最佳压缩)
Anim. Compression → Keyframe Reduction减少关键帧存储
Error (Position/Rotation/Scale): 0.5 / 0.5 / 0.5(默认即可)
```
---
## 7. UI Canvas 批处理规范
### Canvas 分层策略
| Canvas | 更新频率 | 推荐类型 |
|--------|---------|---------|
| 主 HUDHP/Soul/Geo| 按需(事件驱动)| Screen Space - Camera |
| Boss HP 条 | 按需 | 与主 HUD 同 Canvas |
| 小地图 | 按需 | 独立 Canvas复杂更新|
| 暂停菜单 | 静态(打开时)| Screen Space - Overlay |
| 对话 UI | 按需 | 独立 Canvas |
| 加载画面 | 静态 | 独立 Canvas最高层级|
### Draw Call 限制
| 规则 | 值 |
|------|-----|
| 单个 Canvas 最大 Draw Calls | 8 |
| 共享 Sprite Atlas 尺寸 | 512×512HUD/ 1024×1024全屏 UI|
| 透明 UI 元素叠加层数 | ≤ 4 层(超出会导致 Overdraw|
### 避免 Canvas 脏标记
- 动态数值HP、Geo使用 `TextMeshPro`,仅在数值变化时更新 `text` 属性
- 动画元素Blood flash、Soul 充能)使用独立子 Canvas 隔离脏标记
- 不在 Update 中每帧刷新 UI 文本
---
## 8. 音频预算
| 指标 | 限制 |
|------|------|
| 同时播放的 AudioSource 数量 | ≤ 32平台限制通常为 32~256保守取低值|
| BGM 同时层数(自适应音乐)| ≤ 4 层(探索基础层 + 3 个叠加层)|
| 单个 SFX 最大并发实例 | ≤ 4同一 SFX 类型,超出时最老实例停止)|
| 音频剪辑内存预算 | ≤ 50 MB所有已加载音频资产总大小|
### 音频导入规范
| 类型 | 格式 | Load Type | Compression |
|------|------|-----------|-------------|
| BGM长曲| `.ogg` | Streaming | Vorbis Q50 |
| SFX短促| `.wav` | Decompress on Load | PCM |
| Ambient环境| `.ogg` | Streaming | Vorbis Q40 |
| UI 点击音效 | `.wav` | Decompress on Load | PCM |
---
## 9. Profiler 使用规范
### 必须加 ProfilerMarker 的位置
```csharp
using Unity.Profiling;
// 格式:[BaseGames] 命名空间.类名.方法名
static readonly ProfilerMarker s_PlayerUpdate = new("[BaseGames] Player.Update");
static readonly ProfilerMarker s_EnemyAIUpdate = new("[BaseGames] Enemies.AI.Update");
static readonly ProfilerMarker s_PoolPrewarm = new("[BaseGames] Pool.Prewarm");
static readonly ProfilerMarker s_SaveWrite = new("[BaseGames] Save.Write");
static readonly ProfilerMarker s_SaveRead = new("[BaseGames] Save.Read");
static readonly ProfilerMarker s_SceneLoad = new("[BaseGames] Scene.Load");
static readonly ProfilerMarker s_DialogueParse = new("[BaseGames] Dialogue.Parse");
```
### 性能分析 SOP每里程碑
1. **深度模式录制 300 帧**在最复杂场景Boss 战 + 大量弹射物 + Feedback
2. **关注热点**`CPU Usage` 视图,过滤 `[BaseGames]` 标签,检查超时项
3. **GC 检查**`Memory` 视图,确认每帧 GC 分配 ≤ 256 KB
4. **物理热点**`Physics 2D` 检查 `ContactCount`,确认碰撞层矩阵设置正确
5. **渲染**`Frame Debugger` 检查 Draw Call确认 Atlas 打包正确
### 内存快照 SOP
- 每个主要场景加载后各取一次内存快照
- 对比两次快照的内存增量确认无场景加载内存泄漏Addressable Handle 是否全部释放)
---
## 10. 性能检查清单PR 门禁)
提交 PR 前必须自查以下所有项,全部通过方可合并:
### 代码层
- [ ] Update / FixedUpdate 中无 `GetComponent<T>()``FindObjectOfType<T>()`
- [ ] Update / FixedUpdate 中无 `new` 分配(除 `ValueType struct` 栈分配外)
- [ ] LINQ 仅在初始化路径使用,运行时热路径不使用
- [ ] 所有 Coroutine `WaitForSeconds` 使用 `static readonly` 缓存
- [ ] `Physics2D.RaycastAll` / `OverlapCircleAll` 替换为 `NonAlloc` 版本
- [ ] 新增的 MonoBehaviour 有 ProfilerMarker 包裹(若在热路径)
### 对象池
- [ ] 新增弹射物/粒子使用 `GlobalObjectPool` 而非 `Instantiate`/`Destroy`
- [ ] 池容量根据 §4 公式计算并配置在 SO 中
- [ ] 池枯竭策略已选择并测试
### 物理
- [ ] 新增 Collider 的碰撞层已在 Layer Collision Matrix 中配置
- [ ] 高速移动对象(弹射物/玩家冲刺)使用 `Continuous` 碰撞检测
### UI
- [ ] 新增 UI 组件不在 Update 中每帧刷新 Text
- [ ] Canvas 层级合理(不频繁更新的元素不与频繁更新的元素同 Canvas
### 音频
- [ ] 新增 SFX 格式为 `.wav`BGM 格式为 `.ogg`
- [ ] 高频 SFX脚步/攻击)设置了并发实例上限
### Addressables
- [ ] `LoadAssetAsync` 的 Handle 在合适时机 `Release`
- [ ] 场景卸载时 Handle 统一释放(无泄漏)