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

470 lines
18 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.
# 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
{
/// <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
```csharp
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. 形态与武器绑定
`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 / 暂停时),便于运行时调试武器切换状态。