# 41 · VFX 架构与 URP 2D 光照指南 > **命名空间** `BaseGames.VFX` > **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md) > **依赖** `BaseGames.Core`(GlobalObjectPool)· `BaseGames.Combat`(HitFxType)· URP 2D Renderer · Feel(MMF_Player) > **关联** 07_FeedbackSystem(MMF_Player 集成)· 04_CombatSystem §2(HitFxType)· 27_PerformanceBudgetGuide §5(VFX 性能预算) --- ## 目录 1. [系统总览](#1-系统总览) 2. [VFX 分类与生命周期策略](#2-vfx-分类与生命周期策略) 3. [VFXPool — 粒子对象池](#3-vfxpool--粒子对象池) 4. [HitFX — 命中特效路由](#4-hitfx--命中特效路由) 5. [伤害闪白(HurtFlash)](#5-伤害闪白hurtflash) 6. [形态切换调色板变换(PaletteSwap)](#6-形态切换调色板变换paletteswap) 7. [URP 2D 光照架构](#7-urp-2d-光照架构) 8. [后处理 Volume 分区管理](#8-后处理-volume-分区管理) 9. [区域 Shader 效果(Environmental Shader)](#9-区域-shader-效果environmental-shader) 10. [VFX 性能约束](#10-vfx-性能约束) 11. [编辑器友好设计](#11-编辑器友好设计) --- ## 1. 系统总览 VFX 架构回答两类问题: 1. **何时、如何生成粒子特效**,且不产生 GC spike 2. **如何通过光照/后处理塑造区域氛围**,不破坏像素风格统一性 ``` VFX 系统职责: ├─ VFXPool → 粒子对象池(复用 ParticleSystem 实例) ├─ VFXCatalogSO → 命中类型 → VFX Prefab 的映射字典 ├─ HitFXSpawner → 监听战斗命中事件,从 VFXPool 取粒子播放 ├─ HurtFlashController → SpriteRenderer Material 属性块闪白 ├─ PaletteSwapSystem → 形态切换时玩家精灵调色板替换 ├─ LightLayerManager → 区域进出时切换 Light Layer 配置 └─ PostProcessManager → Volume Blend Weight 平滑过渡 ``` **零耦合原则**:`HitFXSpawner` 监听 `OnHitConfirmed` 事件频道,不直接与战斗系统耦合。 --- ## 2. VFX 分类与生命周期策略 | 分类 | 示例 | 生命周期策略 | 说明 | |------|------|------------|------| | **命中特效** | 刀击火花、魔法爆炸 | 对象池(VFXPool)| 频繁触发,必须池化 | | **状态特效** | 持续燃烧、中毒气泡 | Prefab 跟随目标 Transform | 随目标销毁,不需要池 | | **环境特效** | 灰尘、落叶、水泡 | 场景内预放置,常驻播放 | 不产生运行时 Instantiate | | **能力特效** | 冲刺残影、双跳光圈 | 对象池(VFXPool)| 中等频率,建议池化 | | **死亡特效** | 敌人爆裂、Boss 死亡烟花 | Addressables.InstantiateAsync | 低频但复杂,不值得池化 | | **UI 特效** | 升级闪光、货币飞入 | Timeline Signal / MMF_Player | 在 UI Canvas 子层用 UI Particle | **原则**:帧内可能多次触发(命中、脚步等)→ 池化;每关卡最多触发数次 → Addressables 按需加载。 --- ## 3. VFXPool — 粒子对象池 `VFXPool` 是 `GlobalObjectPool`(见 13_ProjectileSystem §6)的上层封装,针对 `ParticleSystem` 定制: ```csharp namespace BaseGames.VFX { /// /// ParticleSystem 专用对象池。 /// 粒子播放完成后自动回池,无需调用方手动归还。 /// public class VFXPool : MonoBehaviour { // 每种 VFX Prefab 对应独立的子池 private Dictionary> _pools = new(); /// 在世界坐标 播放一次特效。 public async UniTaskVoid Play(AssetReferenceGameObject vfxRef, Vector3 position, Quaternion rotation = default) { var ps = await GetOrCreate(vfxRef); ps.transform.SetPositionAndRotation(position, rotation); ps.gameObject.SetActive(true); ps.Play(); // 自动回池:等待粒子系统完成 await UniTask.WaitUntil(() => !ps.IsAlive(true)); ps.gameObject.SetActive(false); _pools[vfxRef].Enqueue(ps); } async UniTask GetOrCreate(AssetReferenceGameObject vfxRef) { if (_pools.TryGetValue(vfxRef, out var queue) && queue.Count > 0) return queue.Dequeue(); var go = await Addressables.InstantiateAsync(vfxRef, transform).Task; return go.GetComponent(); } } } ``` ### 预热(Pool Warm-up) 在 `GameManager.OnGameplayStarted` 时预热高频 VFX: ```csharp // VFXCatalogSO 中声明需要预热的 VFX 及预热数量 [System.Serializable] public struct VFXWarmupEntry { public AssetReferenceGameObject vfxRef; public int warmupCount; // 默认建议 3–5 } ``` --- ## 4. HitFX — 命中特效路由 `HitFXSpawner` 监听 `OnHitConfirmed` 频道,根据 `DamageInfo.HitFxType` 路由到对应 VFX: ### VFXCatalogSO ```csharp [CreateAssetMenu(menuName = "VFX/VFXCatalog")] public class VFXCatalogSO : ScriptableObject { [Serializable] public struct Entry { public HitFxType hitFxType; public AssetReferenceGameObject vfxPrefabRef; } [SerializeField] Entry[] _entries; private Dictionary _map; public void Initialize() { _map = new(); foreach (var e in _entries) _map[e.hitFxType] = e.vfxPrefabRef; } public bool TryGet(HitFxType type, out AssetReferenceGameObject vfxRef) => _map.TryGetValue(type, out vfxRef); } ``` ### HitFxType(与 04_CombatSystem §2 对应) | 枚举值 | 特效 | 建议粒子数 | |-------|------|----------| | `Spark` | 金属碰撞火花 | ≤ 8 | | `Blood` | 敌人受击血迹 | ≤ 12 | | `Magic` | 法术命中光环 | ≤ 15 | | `Crit` | 暴击大爆炸 | ≤ 20 | | `Void` | 深渊伤害涟漪 | ≤ 10 | | `Heal` | 治疗绿光粒子 | ≤ 8 | | `Parry` | 弹反白光爆散 | ≤ 20 | --- ## 5. 伤害闪白(HurtFlash) 受击闪白通过 **Material Property Block** 实现,不修改 Material 本身(避免额外 Draw Call): ```csharp namespace BaseGames.VFX { /// /// 挂在 SpriteRenderer 同 GameObject 上。 /// 受击时将 Sprite 替换为纯白色一帧,随后恢复原始颜色。 /// public class HurtFlashController : MonoBehaviour { [SerializeField] SpriteRenderer _renderer; [SerializeField] float _flashDuration = 0.08f; // 秒 [SerializeField] Color _flashColor = Color.white; MaterialPropertyBlock _mpb; static readonly int _colorId = Shader.PropertyToID("_Color"); void Awake() => _mpb = new MaterialPropertyBlock(); public async UniTaskVoid Flash(CancellationToken ct = default) { _renderer.GetPropertyBlock(_mpb); _mpb.SetColor(_colorId, _flashColor); _renderer.SetPropertyBlock(_mpb); await UniTask.Delay( TimeSpan.FromSeconds(_flashDuration), cancellationToken: ct); _mpb.SetColor(_colorId, Color.white); // 恢复原始颜色乘数 _renderer.SetPropertyBlock(_mpb); } } } ``` **触发路径**:`HurtBox.ReceiveDamage` → 发布 `OnEntityHurt` 频道 → `HurtFlashController.Flash()` --- ## 6. 形态切换调色板变换(PaletteSwap) 玩家在**天魂 / 地魂 / 命魂**形态间切换时,Sprite 颜色方案改变,使用 **Shader Graph** 中的 Palette Swap 节点实现,避免维护多套 Sprite 资产: ### Shader Graph 实现要点 ``` PaletteSwap.shadergraph 关键节点: Sample Texture 2D (Main Sprite) → UV 映射到 PaletteTexture(64×1 横条,每列一个色阶) → 输出最终颜色 PaletteTextureSO: 每个形态一个 64×1 Texture(或 Sprite Sheet 一行): Row 0 (天魂): 蓝白色调 Row 1 (地魂): 赤褐色调 Row 2 (命魂): 翠绿色调 ``` ### 运行时切换 ```csharp public class PlayerPaletteSwap : MonoBehaviour { [SerializeField] SpriteRenderer _renderer; [SerializeField] PaletteSwapCatalogSO _catalog; // FormId → Texture2D 映射 static readonly int _paletteTex = Shader.PropertyToID("_PaletteTexture"); MaterialPropertyBlock _mpb; void Awake() => _mpb = new(); // 监听 OnFormSwitched 事件频道调用 public void OnFormChanged(string formId) { if (!_catalog.TryGet(formId, out var tex)) return; _renderer.GetPropertyBlock(_mpb); _mpb.SetTexture(_paletteTex, tex); _renderer.SetPropertyBlock(_mpb); } } ``` --- ## 7. URP 2D 光照架构 ### 光照层(Light Layer)规范 URP 2D Renderer 支持最多 **8 个光照层**,分配如下: | Light Layer | 名称 | 用途 | |------------|------|------| | 0 | `Default` | 全局环境光(所有对象默认受光)| | 1 | `Environment` | 蜡烛、火把、发光水晶等环境光源 | | 2 | `PlayerLight` | 玩家携带光源(如灵力发光) | | 3 | `EnemyLight` | Boss 爆炸闪光、特殊敌人发光体 | | 4 | `FXOnly` | 仅特效 ParticleSystem 受此层光照 | | 5–7 | _预留_ | 未来扩展 | **规则**:地形 Tilemap 仅勾选 Layer 0;粒子特效 Renderer 勾选 Layer 0 + 4;敌人勾选 Layer 0 + 3。 ### Global Light 2D 配置 每个区域(Region)的 **Persistent 常驻场景**中维护一个 Global Light 2D: | 区域 | 颜色 | 强度 | 说明 | |------|------|------|------| | Forest(扎根森林)| `#C8E8D0`(淡绿)| 0.8 | 明亮、生机盎然 | | Cave(腐蚀洞穴)| `#1A0A2E`(深紫)| 0.2 | 黑暗、压抑 | | Ruins(坍塌废墟)| `#3D3028`(暖褐)| 0.5 | 残阳余晖 | | Abyss(深渊裂隙)| `#000820`(极暗蓝)| 0.1 | 近乎全黑 | | Core(核心熔炉)| `#4A1800`(暗红橙)| 0.6 | 熔岩红光 | **切换策略**:进入新区域时,`RegionLightController`(监听 `OnRegionEntered` 频道)用 `DOTween` 在 1.5 秒内插值 `GlobalLight2D.color` 和 `intensity`,避免突兀切换。 ### Shadow Caster 2D 规范 - **地形 Tilemap**:挂 `ShadowCaster2D`(Composite),`usesPathPoints = true`,自动生成阴影形状 - **动态物件**(敌人/玩家):不挂 `ShadowCaster2D`(2D 精灵角色使用 Normal Map 而非实时阴影) - **环境道具**(箱子、柱子):可选挂 `ShadowCaster2D`,仅在 P1 关卡填充时添加 --- ## 8. 后处理 Volume 分区管理 ### Volume 结构 ``` Persistent 场景: [PostProcess] ├── Volume_Default Priority=0 Weight=1.0(始终生效基础 Profile) ├── Volume_Underwater Priority=10 Weight=0(进水时 blend 到 1.0) ├── Volume_BossArena Priority=10 Weight=0(Boss 战开始时 blend 到 1.0) ├── Volume_Death Priority=20 Weight=0(玩家死亡时瞬间 blend 到 1.0) └── Volume_Victory Priority=10 Weight=0(Boss 击败时 blend 到 1.0) ``` ### Profile 参数速查 | Volume | Bloom | Color Grading | Vignette | Chromatic Aberration | |--------|-------|--------------|----------|---------------------| | Default | Intensity 0.3 | 正常 | 0.2 | 关闭 | | Underwater | 0.1 | 青绿 Filter -0.3 | 0.45 | 0.4 | | BossArena | 0.5 | 饱和度 +20% | 0.35 | 0.15 | | Death | 0 | 去饱和度 -80% | 0.7(黑色)| 0.8 | | Victory | 0.8(白色)| 亮度 +0.4 | 0 | 0 | ### PostProcessManager ```csharp public class PostProcessManager : MonoBehaviour { [SerializeField] Volume _underwater; [SerializeField] Volume _bossArena; [SerializeField] Volume _death; [SerializeField] Volume _victory; // 通过 DOTween 平滑 blend Weight public void BlendTo(PostProcessState state, float duration = 0.4f) { Volume target = state switch { PostProcessState.Underwater => _underwater, PostProcessState.BossArena => _bossArena, PostProcessState.Death => _death, PostProcessState.Victory => _victory, _ => null }; // 先关闭所有非默认 Volume foreach (var v in new[] { _underwater, _bossArena, _death, _victory }) DOTween.To(() => v.weight, x => v.weight = x, 0f, duration); if (target != null) DOTween.To(() => target.weight, x => target.weight = x, 1f, duration) .SetAutoKill(true) // Tween 完成后自动回池,禁止保留无用 Tween 对象 .SetLink(gameObject); // 绑定到容器 GameObject,对象销毁时自动 Kill } } > **DOTween 全局规范**: > - 所有 `DOTween.To()` 调用均**必须**链接 `.SetAutoKill(true)`(默认开启时可省略,关闭时**必须**显式填写) > - 挂在 MonoBehaviour 上的 Tween 必须链接 `.SetLink(gameObject)` 或在 `OnDestroy` 中手动 `DOTween.Kill(target)` > - 禁止全局 `DOTween.KillAll()`(会杀死所有 Tween,包括 Feel 内部的 Tween) > - Tween 序列(Sequence)使用后调用 `.SetAutoKill(true).Play()` 启动,不保留引用即丢弃 ``` --- ## 9. 区域 Shader 效果(Environmental Shader) 像素风 Metroidvania 常见区域 Shader 效果规范: | 效果 | Shader 方案 | 挂载位置 | |------|------------|---------| | 水体流动 | `WaterFlow.shadergraph`(UV 偏移 + 正弦波扰动)| Tilemap_Liquid 的 Material | | 岩浆涌动 | `LavaFlow.shadergraph`(橙红色 + 亮度闪烁)| Tilemap_Lava Material | | 发光水晶 | `Emission.shadergraph`(自发光强度受 sin(time) 调制)| 水晶 Sprite Material | | 死亡区域迷雾 | `FogEffect.shadergraph`(Noise 纹理 + Alpha)| Tilemap_Background Overlay | | 伤害闪白 | `HurtFlash`(`_Color` 属性块,见 §5)| 角色/敌人 Sprite Material | | 调色板替换 | `PaletteSwap.shadergraph`(见 §6)| 玩家 Sprite Material | **禁止在游戏逻辑中直接 `Material.SetXxx()`**,一律通过 `MaterialPropertyBlock` 修改,避免 Material 实例化产生 GC。 --- ## 10. VFX 性能约束 > 参见 27_PerformanceBudgetGuide §5;以下为 VFX 专项补充。 | 约束项 | 限制 | 备注 | |--------|------|------| | 同屏活跃粒子总数 | ≤ 500 | 超过时 Profile 会看到 GPU 帧时上升 | | 单次命中特效粒子数 | ≤ 20 | HitFxType 表格已约束 | | 同屏 Light 2D 数量 | ≤ 16 | 超过时触发 URP 2D 光照降级 | | Shadow Caster 2D 数量 | ≤ 32 | Composite 模式下 1 Tilemap = 1 | | 新增 VFX Prefab | 必须通过 VFXPool 统一管理 | PR Review 清单检查项 | | `Material.SetXxx()` 直接调用 | 禁止 | 一律用 MaterialPropertyBlock | | `Instantiate(ParticleSystem)` 运行时调用 | 禁止 | 一律从 VFXPool.Play() 取 | --- ## 11. 编辑器友好设计 ### VFXCatalog Custom Inspector ``` VFXCatalogSO Inspector: ┌─ [命中类型 → VFX 映射] │ HitFxType | VFX Prefab Ref | 预热数量 │ ───────────────────────────────────────────── │ Spark | [vfx_hit_spark] | 5 │ Blood | [vfx_hit_blood] | 5 │ Magic | [vfx_hit_magic] | 3 │ ... └─ [▶ 全部预热] ← Editor Only 按钮(调用 WarmupAll) ``` ### Scene View Gizmos - `LiquidZone` 光照区域:半透明色块填充(颜色对应液体类型) - `VFXPool` 当前各子池使用率:在 Debug Overlay 中显示(见 09_EditorExtensions §4) ### 快速创建工具 `菜单 → BaseGames/VFX/Create HitFX Prefab` → 自动创建包含正确 Layer、Sort Order、Auto Stop 配置的 ParticleSystem Prefab 模板,需设计师填充 Particle Shape / Color Over Lifetime 即可。 --- *文档版本 1.0 · 2025*