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

16 KiB
Raw Permalink Blame History

41 · VFX 架构与 URP 2D 光照指南

命名空间 BaseGames.VFX
所属文档集 ← 返回索引 · 总览
依赖 BaseGames.CoreGlobalObjectPool· BaseGames.CombatHitFxType· URP 2D Renderer · FeelMMF_Player
关联 07_FeedbackSystemMMF_Player 集成)· 04_CombatSystem §2HitFxType· 27_PerformanceBudgetGuide §5VFX 性能预算)


目录

  1. 系统总览
  2. VFX 分类与生命周期策略
  3. VFXPool — 粒子对象池
  4. HitFX — 命中特效路由
  5. 伤害闪白HurtFlash
  6. 形态切换调色板变换PaletteSwap
  7. URP 2D 光照架构
  8. 后处理 Volume 分区管理
  9. 区域 Shader 效果Environmental Shader
  10. VFX 性能约束
  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 — 粒子对象池

VFXPoolGlobalObjectPool(见 13_ProjectileSystem §6的上层封装针对 ParticleSystem 定制:

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

// VFXCatalogSO 中声明需要预热的 VFX 及预热数量
[System.Serializable]
public struct VFXWarmupEntry
{
    public AssetReferenceGameObject vfxRef;
    public int warmupCount;   // 默认建议 35
}

4. HitFX — 命中特效路由

HitFXSpawner 监听 OnHitConfirmed 频道,根据 DamageInfo.HitFxType 路由到对应 VFX

VFXCatalogSO

[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

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 (命魂): 翠绿色调

运行时切换

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 配置

每个区域RegionPersistent 常驻场景中维护一个 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.colorintensity,避免突兀切换。

Shadow Caster 2D 规范

  • 地形 Tilemap:挂 ShadowCaster2DCompositeusesPathPoints = true,自动生成阴影形状
  • 动态物件(敌人/玩家):不挂 ShadowCaster2D2D 精灵角色使用 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

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.shadergraphUV 偏移 + 正弦波扰动) Tilemap_Liquid 的 Material
岩浆涌动 LavaFlow.shadergraph(橙红色 + 亮度闪烁) Tilemap_Lava Material
发光水晶 Emission.shadergraph(自发光强度受 sin(time) 调制) 水晶 Sprite Material
死亡区域迷雾 FogEffect.shadergraphNoise 纹理 + 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