420 lines
16 KiB
Markdown
420 lines
16 KiB
Markdown
# 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
|
||
{
|
||
/// <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; // 默认建议 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<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 映射到 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*
|