# 53 · 武器系统
> **命名空间** `BaseGames.Combat`
> **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md)
> **依赖** `BaseGames.Player`(FormController / PlayerCombat)· `BaseGames.Equipment`(EquipmentManager)
---
## 目录
1. [系统总览](#1-系统总览)
2. [WeaponSO — 武器数据](#2-weaponso--武器数据)
3. [WeaponManager — 武器切换管理](#3-weaponmanager--武器切换管理)
4. [形态与武器绑定](#4-形态与武器绑定)
5. [PlayerCombat 适配](#5-playercombat-适配)
6. [护符武器覆盖(WeaponOverrideEffect)](#6-护符武器覆盖weaponoverrideeffect)
7. [三形态武器规格表](#7-三形态武器规格表)
8. [武器 VFX 与音效](#8-武器-vfx-与音效)
9. [SaveData 集成](#9-savedata-集成)
10. [编辑器友好设计](#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 — 武器数据
```csharp
namespace BaseGames.Combat
{
///
/// 武器数据 SO。封装一种攻击模组的全部属性。
/// 每种形态默认绑定一把武器;护符可在运行时替换。
///
[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 配置")]
/// 零向量 = 使用 PlayerCombat 中的默认尺寸
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.HitFxType,null = 不覆盖)")]
public HitFxType? hitFxOverride;
}
}
```
**资产存放路径**:`Assets/ScriptableObjects/Combat/Weapons/`
**命名规范**:`Weapon_{Name}.asset`,如 `Weapon_SkyBlade.asset`
---
## 3. WeaponManager — 武器切换管理
`WeaponManager` 常驻 Player GameObject,监听 `FormController.OnFormChanged` 并自动切换活动武器。支持护符 override(见 §6)。
```csharp
namespace BaseGames.Combat
{
public class WeaponManager : MonoBehaviour
{
[SerializeField] FormController _formController;
/// 当前激活的武器 SO。PlayerCombat 从这里读取攻击数据。
public WeaponSO ActiveWeapon { get; private set; }
/// 武器切换时广播(PlayerCombat / VFX 监听)
public event Action OnWeaponChanged;
// 护符注入的武器覆盖:Key = FormSO.formId,Value = 替换武器
readonly Dictionary _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 调用)───────────────
/// 为指定形态设置武器覆盖。若 formId 为空,覆盖所有形态。
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);
}
/// 移除指定形态的武器覆盖,恢复默认武器。
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. 形态与武器绑定
`FormSO` 的 `attackConfig: FormAttackConfig`(旧内联结构体)**已替换为** `defaultWeapon: WeaponSO`:
```csharp
[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 适配
`PlayerCombat` 从 `WeaponManager.ActiveWeapon` 读取当前武器数据,替代旧的 `FormController.CurrentForm.AttackConfig`:
```csharp
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`:
```csharp
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](./17_EquipmentSystem.md#3-icharmeffect--效果接口))。
**配置示例**:
```
// 护符:【命魂刃灵符】(将命魂形态的武器替换为高速短刃)
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](./41_VFXArchitecture.md))处理。
---
## 9. SaveData 集成
武器系统**无需单独存档字段**:
- 当前武器由 `FormController.CurrentForm` + 装备的护符组合决定
- `FormController.CurrentForm.formId` 已写入 SaveData(见 [31_SaveDataSchema_Unified §4](./31_SaveDataSchema_Unified.md))
- 护符 override 在存档加载时随护符 `OnEquip` 回调自动重建
- `WeaponManager` 在 `Awake` 时按当前形态初始化活动武器,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 / 暂停时),便于运行时调试武器切换状态。