# 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*