# 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 │ ├─ EnemyAI(BehaviorDesigner) ≤ 2.0 ms(≤ 20 个活跃敌人) │ ├─ EnemyNavAgent(PathBerserker)≤ 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()` 在 Update 中 | `Awake` 缓存引用 | | `string` 拼接/Format 在热路径 | 预分配 `StringBuilder` 或 `string.Format` 仅在非热路径 | | LINQ(`.Where().ToList()`)在 Update | 手写 `for` 循环 + 预分配 `List` | | 事件 `+= 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 → Optimal(Unity 自动选择最佳压缩) Anim. Compression → Keyframe Reduction(减少关键帧存储) Error (Position/Rotation/Scale): 0.5 / 0.5 / 0.5(默认即可) ``` --- ## 7. UI Canvas 批处理规范 ### Canvas 分层策略 | Canvas | 更新频率 | 推荐类型 | |--------|---------|---------| | 主 HUD(HP/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×512(HUD)/ 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()`、`FindObjectOfType()` - [ ] 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 统一释放(无泄漏)