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

420 lines
16 KiB
Markdown
Raw Permalink 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.
# 41 · VFX 架构与 URP 2D 光照指南
> **命名空间** `BaseGames.VFX`
> **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md)
> **依赖** `BaseGames.Core`GlobalObjectPool· `BaseGames.Combat`HitFxType· URP 2D Renderer · FeelMMF_Player
> **关联** 07_FeedbackSystemMMF_Player 集成)· 04_CombatSystem §2HitFxType· 27_PerformanceBudgetGuide §5VFX 性能预算)
---
## 目录
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
{
/// <summary>
/// ParticleSystem 专用对象池。
/// 粒子播放完成后自动回池,无需调用方手动归还。
/// </summary>
public class VFXPool : MonoBehaviour
{
// 每种 VFX Prefab 对应独立的子池
private Dictionary<AssetReferenceGameObject, Queue<ParticleSystem>> _pools = new();
/// <summary>在世界坐标 <paramref name="position"/> 播放一次特效。</summary>
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<ParticleSystem> 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<ParticleSystem>();
}
}
}
```
### 预热Pool Warm-up
`GameManager.OnGameplayStarted` 时预热高频 VFX
```csharp
// VFXCatalogSO 中声明需要预热的 VFX 及预热数量
[System.Serializable]
public struct VFXWarmupEntry
{
public AssetReferenceGameObject vfxRef;
public int warmupCount; // 默认建议 35
}
```
---
## 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<HitFxType, AssetReferenceGameObject> _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
{
/// <summary>
/// 挂在 SpriteRenderer 同 GameObject 上。
/// 受击时将 Sprite 替换为纯白色一帧,随后恢复原始颜色。
/// </summary>
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 映射到 PaletteTexture64×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 受此层光照 |
| 57 | _预留_ | 未来扩展 |
**规则**:地形 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=0Boss 战开始时 blend 到 1.0
├── Volume_Death Priority=20 Weight=0玩家死亡时瞬间 blend 到 1.0
└── Volume_Victory Priority=10 Weight=0Boss 击败时 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*