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

18 KiB
Raw Permalink Blame History

53 · 武器系统

命名空间 BaseGames.Combat
所属文档集 ← 返回索引 · 总览
依赖 BaseGames.PlayerFormController / PlayerCombat· BaseGames.EquipmentEquipmentManager


目录

  1. 系统总览
  2. WeaponSO — 武器数据
  3. WeaponManager — 武器切换管理
  4. 形态与武器绑定
  5. PlayerCombat 适配
  6. 护符武器覆盖WeaponOverrideEffect
  7. 三形态武器规格表
  8. 武器 VFX 与音效
  9. SaveData 集成
  10. 编辑器友好设计

1. 系统总览

武器模组Weapon 封装了角色在某一形态下的全部近战攻击数据:连击动画片段、每段伤害来源 SO、HitBox 尺寸偏移、武器特效配置。

三种形态(天魂 / 地魂 / 命魂的攻击手感、动作组、HitBox 形状完全不同,因此武器以 ScriptableObject 形式独立于形态,通过 FormSO.defaultWeapon 绑定,并由 WeaponManager 在形态切换时自动激活。

武器系统职责:
  ├─ WeaponSO         → 武器数据 SO动画片段 + 伤害来源 + HitBox + VFX
  ├─ WeaponManager    → 运行时武器激活、形态联动切换、护符覆盖管理
  ├─ FormSO           → defaultWeapon 字段(每形态默认武器,替代旧 FormAttackConfig
  └─ WeaponOverrideEffect → ICharmEffect 实现,允许护符替换特定形态的武器

与旧版 FormAttackConfig 的关系
FormAttackConfig 内联结构体已废弃,其所有字段(动画片段 + hitBoxSizeOverride迁移进 WeaponSO,并新增每段伤害来源引用与武器特效配置。


2. WeaponSO — 武器数据

namespace BaseGames.Combat
{
    /// <summary>
    /// 武器数据 SO。封装一种攻击模组的全部属性。
    /// 每种形态默认绑定一把武器;护符可在运行时替换。
    /// </summary>
    [CreateAssetMenu(menuName = "Combat/Weapon")]
    public class WeaponSO : ScriptableObject
    {
        [Header("基础信息")]
        public string      weaponId;       // 全局唯一 ID如 "Weapon_SkyBlade"
        public string      displayName;    // "天裂刃"
        public Sprite      icon;           // 武器图标(装备 UI 可选展示)
        public WeaponType  weaponType;     // 武器类型(见枚举)

        [Header("连击动画")]
        public ClipTransition attack1Clip;
        public ClipTransition attack2Clip;
        public ClipTransition attack3Clip;
        public ClipTransition airAttackClip;
        public ClipTransition upAttackClip;
        public ClipTransition downAttackClip;

        [Header("伤害来源(每段独立)")]
        public DamageSourceSO attack1Source;
        public DamageSourceSO attack2Source;
        public DamageSourceSO attack3Source;
        public DamageSourceSO airAttackSource;
        public DamageSourceSO upAttackSource;
        public DamageSourceSO downAttackSource;

        [Header("HitBox 配置")]
        /// <summary>零向量 = 使用 PlayerCombat 中的默认尺寸</summary>
        public Vector2 hitBoxSizeOverride;
        public Vector2 hitBoxOffsetOverride;

        [Header("武器特效")]
        public WeaponVFXConfig vfxConfig;
    }

    public enum WeaponType
    {
        SkyBlade,    // 天魂:裂空刃(高频轻击)
        EarthHammer, // 地魂:地震锤(低频重击,范围大)
        LifeScythe,  // 命魂:命镰(穿透、直线斩)
        Custom,      // 护符替换或未来扩展武器
    }

    [Serializable]
    public class WeaponVFXConfig
    {
        [Tooltip("切换到此武器时播放的特效(形态切换音效/粒子)")]
        public FeedbackPresetSO onEquipFeedback;

        [Tooltip("武器挥斩拖尾 Prefab无 = 不显示拖尾)")]
        public GameObject weaponTrailPrefab;

        public Color trailColor = Color.white;

        [Tooltip("攻击命中时的特效类型(覆盖 DamageSourceSO.HitFxTypenull = 不覆盖)")]
        public HitFxType? hitFxOverride;
    }
}

资产存放路径Assets/ScriptableObjects/Combat/Weapons/
命名规范Weapon_{Name}.asset,如 Weapon_SkyBlade.asset


3. WeaponManager — 武器切换管理

WeaponManager 常驻 Player GameObject监听 FormController.OnFormChanged 并自动切换活动武器。支持护符 override见 §6

namespace BaseGames.Combat
{
    public class WeaponManager : MonoBehaviour
    {
        [SerializeField] FormController _formController;

        /// <summary>当前激活的武器 SO。PlayerCombat 从这里读取攻击数据。</summary>
        public WeaponSO ActiveWeapon { get; private set; }

        /// <summary>武器切换时广播PlayerCombat / VFX 监听)</summary>
        public event Action<WeaponSO> OnWeaponChanged;

        // 护符注入的武器覆盖Key = FormSO.formIdValue = 替换武器
        readonly Dictionary<string, WeaponSO> _overrides = new();

        void Awake()
        {
            // 初始化:激活第一个形态对应的武器
            if (_formController.CurrentForm != null)
                ApplyWeapon(_formController.CurrentForm);
        }

        void OnEnable()  => _formController.OnFormChanged += HandleFormChanged;
        void OnDisable() => _formController.OnFormChanged -= HandleFormChanged;

        void HandleFormChanged() => ApplyWeapon(_formController.CurrentForm);

        void ApplyWeapon(FormSO form)
        {
            // 护符 override 优先;若无 override使用形态默认武器
            WeaponSO next = _overrides.TryGetValue(form.formId, out var ov)
                ? ov
                : form.defaultWeapon;

            if (next == ActiveWeapon) return;
            ActiveWeapon = next;
            OnWeaponChanged?.Invoke(next);
            next?.vfxConfig.onEquipFeedback?.PlayFeedbacks(gameObject);
        }

        // ─────────────── 护符 Override API由 WeaponOverrideEffect 调用)───────────────

        /// <summary>为指定形态设置武器覆盖。若 formId 为空,覆盖所有形态。</summary>
        public void SetOverride(string formId, WeaponSO weapon)
        {
            if (string.IsNullOrEmpty(formId))
            {
                // 覆盖所有形态
                foreach (var form in _formController.AllForms)
                    _overrides[form.formId] = weapon;
            }
            else
            {
                _overrides[formId] = weapon;
            }
            // 若当前形态受影响,立即刷新
            ApplyWeapon(_formController.CurrentForm);
        }

        /// <summary>移除指定形态的武器覆盖,恢复默认武器。</summary>
        public void ClearOverride(string formId)
        {
            if (string.IsNullOrEmpty(formId))
            {
                foreach (var form in _formController.AllForms)
                    _overrides.Remove(form.formId);
            }
            else
            {
                _overrides.Remove(formId);
            }
            ApplyWeapon(_formController.CurrentForm);
        }
    }
}

FormController.AllForms 需新增只读属性(返回 _config.forms),供 WeaponManager 在全形态 override 时枚举。


4. 形态与武器绑定

FormSOattackConfig: FormAttackConfig(旧内联结构体)已替换为 defaultWeapon: WeaponSO

[CreateAssetMenu(menuName = "Player/Form")]
public class FormSO : ScriptableObject
{
    [Header("形态标识")]
    public string  formId;
    public string  displayName;
    public Sprite  formIcon;
    public Color   formAccentColor;

    [Header("默认武器")]
    public WeaponSO defaultWeapon;     // ← 替代旧 FormAttackConfig

    [Header("技能配置")]
    public FormSkillSO soulSkill;
    public FormSkillSO spiritSkill1;
    public FormSkillSO spiritSkill2;
}

形态切换流程(完整时序):

玩家按形态切换键
        │
        ▼
FormController.SwitchForm(FormSO newForm)
        │  ① CurrentForm = newForm
        │  ② _onFormChanged.Raise(newForm)
        │  ③ OnFormChanged?.Invoke()
        │
        ├──► WeaponManager.HandleFormChanged()
        │       → ApplyWeapon(newForm)
        │       → ActiveWeapon = overrides[formId] ?? newForm.defaultWeapon
        │       → OnWeaponChanged?.Invoke(newWeapon)
        │
        ├──► PlayerCombat.OnWeaponChanged(newWeapon)
        │       → 刷新动画片段引用
        │       → 刷新 HitBox DamageSource 引用
        │       → ResetCombo()
        │
        └──► VFXManager拖尾颜色换色等

5. PlayerCombat 适配

PlayerCombatWeaponManager.ActiveWeapon 读取当前武器数据,替代旧的 FormController.CurrentForm.AttackConfig

public class PlayerCombat : MonoBehaviour
{
    [SerializeField] WeaponManager _weaponManager;
    // ... 其余字段不变 ...

    void OnEnable()
    {
        _weaponManager.OnWeaponChanged += RefreshWeaponData;
        RefreshWeaponData(_weaponManager.ActiveWeapon);
    }

    void OnDisable() => _weaponManager.OnWeaponChanged -= RefreshWeaponData;

    void RefreshWeaponData(WeaponSO weapon)
    {
        if (weapon == null) return;
        // 将连击动画片段缓存到本地
        _attack1Clip    = weapon.attack1Clip;
        _attack2Clip    = weapon.attack2Clip;
        _attack3Clip    = weapon.attack3Clip;
        _airAttackClip  = weapon.airAttackClip;
        _upAttackClip   = weapon.upAttackClip;
        _downAttackClip = weapon.downAttackClip;
        // 刷新 HitBox DamageSource
        _hitBoxGround.SetDamageSource(weapon.attack1Source); // 初始段;各段在 AttackState 切换时更新
        // 刷新 HitBox 尺寸
        if (weapon.hitBoxSizeOverride != Vector2.zero)
            _hitBoxGround.SetSizeOverride(weapon.hitBoxSizeOverride, weapon.hitBoxOffsetOverride);
        else
            _hitBoxGround.ClearSizeOverride();
        // 重置连击
        ResetCombo();
    }

    // AttackState 每段攻击开始时调用,切换当前段 DamageSource
    public void SetComboSegmentSource(ComboState segment)
    {
        WeaponSO w = _weaponManager.ActiveWeapon;
        DamageSourceSO src = segment switch
        {
            ComboState.Attack1    => w.attack1Source,
            ComboState.Attack2    => w.attack2Source,
            ComboState.Attack3    => w.attack3Source,
            ComboState.AirAttack  => w.airAttackSource,
            ComboState.UpAttack   => w.upAttackSource,
            ComboState.DownAttack => w.downAttackSource,
            _                     => null,
        };
        if (src != null) _hitBoxGround.SetDamageSource(src);
    }
}

6. 护符武器覆盖WeaponOverrideEffect

护符可通过 WeaponOverrideEffect 将某形态的武器整体替换为另一把 WeaponSO

namespace BaseGames.Equipment
{
    [Serializable]
    public class WeaponOverrideEffect : ICharmEffect
    {
        [Tooltip("目标形态 ID留空 = 所有形态)")]
        public string    targetFormId;

        [Tooltip("替换武器 SO")]
        public WeaponSO  replacementWeapon;

        public void OnEquip(EquipmentContext ctx)
            => ctx.WeaponMgr.SetOverride(targetFormId, replacementWeapon);

        public void OnUnequip(EquipmentContext ctx)
            => ctx.WeaponMgr.ClearOverride(targetFormId);

        public string GetEffectDescription()
        {
            string formStr = string.IsNullOrEmpty(targetFormId) ? "所有形态" : targetFormId;
            string wName   = replacementWeapon != null ? replacementWeapon.displayName : "null";
            return $"{formStr}的武器替换为 [{wName}]";
        }
    }
}

EquipmentContext 需新增 WeaponManager WeaponMgr 字段,EquipmentManager.Awake() 中同步初始化(见 17_EquipmentSystem §3)。

配置示例

// 护符:【命魂刃灵符】(将命魂形态的武器替换为高速短刃)
targetFormId        = "LifeForm"
replacementWeapon   = Weapon_ShadowDagger

// 护符:【混沌铸兵符】(所有形态均使用地魂锤)
targetFormId        = ""   // 空 = 全部形态
replacementWeapon   = Weapon_EarthHammer

冲突规则:若多个护符同时对同一形态设置武器覆盖,后装备的护符生效Dictionary 键覆盖语义)。与 SkillSlotOverride 的优先级机制独立,互不影响。


7. 三形态武器规格表

形态 武器 ID WeaponType 动作风格 Attack3 特征 HitBox 大小
天魂 Weapon_SkyBlade SkyBlade 高频轻击,水平连斩 前冲斩击(带位移) 标准(无覆盖)
地魂 Weapon_EarthHammer EarthHammer 低频重击,下砸范围 地震踩击AOE ×1.4,高 ×0.8
命魂 Weapon_LifeScythe LifeScythe 穿透型直线斩 长距离水平穿刺 ×1.8,高 ×0.6

伤害来源资产路径

Assets/ScriptableObjects/Combat/DamageSources/Player/
  ├─ SkyBlade/
  │   ├─ DS_Sky_Attack1.asset    (BaseDamage 4, ×1.0)
  │   ├─ DS_Sky_Attack2.asset    (BaseDamage 4, ×1.0)
  │   ├─ DS_Sky_Attack3.asset    (BaseDamage 6, ×1.5, KnockbackForce 10)
  │   ├─ DS_Sky_Air.asset        (BaseDamage 4, ×1.0)
  │   ├─ DS_Sky_Up.asset         (BaseDamage 5, ×1.0)
  │   └─ DS_Sky_Down.asset       (BaseDamage 5, ×1.0, CanPogo)
  ├─ EarthHammer/
  │   ├─ DS_Earth_Attack1.asset  (BaseDamage 7, ×1.0, HitStun 0.4s)
  │   ├─ DS_Earth_Attack2.asset  (BaseDamage 7, ×1.0)
  │   ├─ DS_Earth_Attack3.asset  (BaseDamage 12, ×2.0, AOE, HitStun 0.6s)
  │   ├─ DS_Earth_Air.asset      (BaseDamage 7, ×1.0)
  │   ├─ DS_Earth_Up.asset       (BaseDamage 8, ×1.0)
  │   └─ DS_Earth_Down.asset     (BaseDamage 8, ×1.0, CanPogo)
  └─ LifeScythe/
      ├─ DS_Life_Attack1.asset   (BaseDamage 5, ×1.0, IsProjectile-like 穿透)
      ├─ DS_Life_Attack2.asset   (BaseDamage 5, ×1.0)
      ├─ DS_Life_Attack3.asset   (BaseDamage 9, ×1.8, 长范围)
      ├─ DS_Life_Air.asset       (BaseDamage 5, ×1.0)
      ├─ DS_Life_Up.asset        (BaseDamage 6, ×1.0)
      └─ DS_Life_Down.asset      (BaseDamage 6, ×1.0, CanPogo)

平衡参考:地魂单击伤害最高但攻速最慢;天魂伤害最低但攻速最快;命魂居中,胜在范围。三形态 DPS攻速×伤害接近。


8. 武器 VFX 与音效

武器拖尾

  • WeaponVFXConfig.weaponTrailPrefab:挂在武器锚点(WeaponTrailAnchor 子节点)下,形态切换时重新实例化
  • trailColor:与 FormSO.formAccentColor 保持一致(天魂青色 / 地魂橙色 / 命魂紫色)
  • 拖尾在攻击动画帧内激活,DisableHitBox AnimationEvent 同时关闭

切换音效

  • onEquipFeedback:一个 FeedbackPresetSO,包含刀鸣/切音 SFX + 短暂 MMF 屏幕冲击
  • WeaponManager.ApplyWeapon() 末尾调用 PlayFeedbacks(),切换形态时自动触发

命中特效优先级

vfxConfig.hitFxOverride != null
    → 使用 WeaponSO 指定的 HitFxType覆盖 DamageSourceSO.HitFxType
else
    → 使用 DamageSourceSO.HitFxType

HitFxType 由 HitFXSpawner(见 41_VFXArchitecture §3)处理。


9. SaveData 集成

武器系统无需单独存档字段

  • 当前武器由 FormController.CurrentForm + 装备的护符组合决定
  • FormController.CurrentForm.formId 已写入 SaveData31_SaveDataSchema_Unified §4
  • 护符 override 在存档加载时随护符 OnEquip 回调自动重建
  • WeaponManagerAwake 时按当前形态初始化活动武器SavePoint 恢复后无需额外逻辑

10. 编辑器友好设计

WeaponSO 自定义 Inspector

┌─ Weapon_SkyBlade ─────────────────────────────────────────────┐
│  [图标]  天裂刃    Type: SkyBlade                             │
│  ────────────────── 连击动画 ────────────────────────         │
│  Attack1: [Sky_A1]  Attack2: [Sky_A2]  Attack3: [Sky_A3]    │
│  Air:     [Sky_Air] Up:      [Sky_Up]  Down:    [Sky_Dn]    │
│  ────────────────── 伤害来源 ────────────────────────         │
│  A1: DS_Sky_Attack1  A2: DS_Sky_Attack2  A3: DS_Sky_Attack3  │
│  Air: DS_Sky_Air     Up:  DS_Sky_Up      Down: DS_Sky_Down   │
│  ────────────────── HitBox ──────────────────────────         │
│  Size Override: (0, 0) ← 零向量 = 使用默认尺寸               │
│  ────────────────── VFX ─────────────────────────────         │
│  OnEquip Feedback: [FB_SkyEquip]                              │
│  Trail Prefab: [SkyBlade_Trail]  Trail Color: ████ (青色)    │
└───────────────────────────────────────────────────────────────┘

Weapon-Form 绑定验证

FormConfigSO Custom Inspector 在发现 FormSO.defaultWeapon == null 时显示红色警告:

⚠ FormSO "SkyForm" defaultWeapon 未绑定!攻击无法正常工作。

WeaponManager Scene Gizmo

在 Scene 视图的玩家头顶显示 ActiveWeapon.weaponId(仅 Play Mode / 暂停时),便于运行时调试武器切换状态。