# 30 · 护盾力学系统
> **命名空间** `BaseGames.Player.Shield`
> **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md)
> **依赖** `BaseGames.Core.Events` · `BaseGames.Player`(PlayerStats · PlayerCombat)· `BaseGames.UI`
---
## 目录
1. [系统总览](#1-系统总览)
2. [ShieldConfigSO — 护盾配置](#2-shieldconfigso--护盾配置)
3. [ShieldComponent — 护盾运行时](#3-shieldcomponent--护盾运行时)
4. [伤害拦截流程](#4-伤害拦截流程)
5. [护盾恢复机制](#5-护盾恢复机制)
6. [护盾破碎反馈](#6-护盾破碎反馈)
7. [格挡系统集成(Parry,P1)](#7-格挡系统集成parryp1)
8. [护盾 UI 指示器](#8-护盾-ui-指示器)
9. [SaveData 集成](#9-savedata-集成)
10. [编辑器友好设计](#10-编辑器友好设计)
---
## 1. 系统总览
护盾是独立于玩家 HP 之外的**第二道防御层**:护盾耐久 > 0 时,受到的伤害优先由护盾吸收(可配置吸收比例);护盾耐久归零触发**护盾破碎**(短暂无法重新充能);一段时间无受击后护盾开始**自动回充**。
```
伤害输入
│
▼
ShieldComponent.AbsorbDamage()
├─ 护盾耐久 > 0 ──→ 吸收(全量或比例)+ 扣护盾耐久
│ └─ 耐久归零 → 触发护盾破碎
└─ 护盾已破碎 ──→ 直接穿透到 PlayerStats.TakeDamage()
```
**设计约束**:
- `ShieldComponent` 不直接调用 `PlayerStats`,而是通过 `DamageAbsorbedEvent` / `DamagePassedThroughEvent` 事件频道传递结果,由 `PlayerStats` 监听处理。
- 护盾耐久、护盾破碎状态持久化至 `SaveData`(使护盾在存档点完全恢复成为可配置选项)。
---
## 2. ShieldConfigSO — 护盾配置
```csharp
[CreateAssetMenu(menuName = "Player/ShieldConfig")]
public class ShieldConfigSO : ScriptableObject
{
[Header("耐久")]
[Min(1)] public int MaxShieldHP = 60; // 护盾最大耐久
[Range(0f, 1f)]
public float DamageAbsorptionRatio = 1.0f; // 1.0 = 完全吸收,0.5 = 吸收一半
[Header("恢复")]
[Min(0f)] public float RechargeDelay = 2.5f; // 最后一次受击后多少秒开始恢复
[Min(0f)] public float RechargeRate = 20f; // 每秒恢复护盾耐久点数
public bool FullRechargeOnSavePoint = true; // 激活存档点时护盾是否立即满值
[Header("破碎惩罚")]
[Min(0f)] public float BrokenPenaltyDuration = 3.0f; // 护盾破碎后无法恢复的时间
[Header("格挡恢复(Parry,P1 功能)")]
[Range(0f, 1f)]
public float ParryRestoreRatio = 0.3f; // 成功格挡时恢复护盾耐久比例
}
```
**资产存放路径**:`Assets/ScriptableObjects/Player/Shield_Config.asset`
---
## 3. ShieldComponent — 护盾运行时
```csharp
namespace BaseGames.Player.Shield
{
[DefaultExecutionOrder(-40)]
public class ShieldComponent : MonoBehaviour
{
// ── 配置 ─────────────────────────────────────────────────
[SerializeField] ShieldConfigSO _config;
[SerializeField] VoidEventChannelSO _onSavePointActivated;
[SerializeField] IntEventChannelSO _onShieldHPChanged; // 发布:当前耐久
[SerializeField] VoidEventChannelSO _onShieldBroken; // 发布
[SerializeField] VoidEventChannelSO _onShieldRestored; // 发布(完全恢复)
[SerializeField] IntEventChannelSO _onDamagePassedThrough; // 发布:穿透伤害量
// ── 状态 ─────────────────────────────────────────────────
public int CurrentShieldHP { get; private set; }
public int MaxShieldHP => _config.MaxShieldHP;
public bool IsShieldBroken { get; private set; }
public float ShieldRatio => (float)CurrentShieldHP / MaxShieldHP; // 0~1
float _timeSinceLastHit;
float _brokenTimer;
void OnEnable()
{
CurrentShieldHP = _config.MaxShieldHP;
_onSavePointActivated.OnEventRaised += OnSavePoint;
}
void OnDisable()
{
_onSavePointActivated.OnEventRaised -= OnSavePoint;
}
void Update()
{
if (IsShieldBroken)
{
_brokenTimer += Time.deltaTime;
if (_brokenTimer >= _config.BrokenPenaltyDuration)
RecoverFromBroken();
return;
}
// 延迟回充
_timeSinceLastHit += Time.deltaTime;
if (_timeSinceLastHit >= _config.RechargeDelay && CurrentShieldHP < MaxShieldHP)
{
int restored = Mathf.CeilToInt(_config.RechargeRate * Time.deltaTime);
int prev = CurrentShieldHP;
CurrentShieldHP = Mathf.Min(CurrentShieldHP + restored, MaxShieldHP);
if (prev != CurrentShieldHP)
{
_onShieldHPChanged.Raise(CurrentShieldHP);
if (CurrentShieldHP == MaxShieldHP)
_onShieldRestored.Raise();
}
}
}
///
/// 外部(PlayerCombat)调用,处理一次伤害输入。
/// 返回实际穿透到 PlayerStats 的伤害量(护盾吸收后的余量)。
///
public int AbsorbDamage(int incomingDamage)
{
if (IsShieldBroken)
{
_onDamagePassedThrough.Raise(incomingDamage);
return incomingDamage;
}
int absorb = Mathf.CeilToInt(incomingDamage * _config.DamageAbsorptionRatio);
absorb = Mathf.Min(absorb, CurrentShieldHP); // 不能超过当前耐久
int passThrough = incomingDamage - absorb;
CurrentShieldHP -= absorb;
_timeSinceLastHit = 0f;
_onShieldHPChanged.Raise(CurrentShieldHP);
if (CurrentShieldHP <= 0)
TriggerShieldBreak();
if (passThrough > 0)
_onDamagePassedThrough.Raise(passThrough);
return passThrough;
}
/// 格挡成功时恢复部分护盾(Parry 系统调用)
public void OnParrySuccess()
{
if (IsShieldBroken) return;
int restore = Mathf.RoundToInt(MaxShieldHP * _config.ParryRestoreRatio);
int prev = CurrentShieldHP;
CurrentShieldHP = Mathf.Min(CurrentShieldHP + restore, MaxShieldHP);
if (prev != CurrentShieldHP)
_onShieldHPChanged.Raise(CurrentShieldHP);
}
void TriggerShieldBreak()
{
CurrentShieldHP = 0;
IsShieldBroken = true;
_brokenTimer = 0f;
_onShieldBroken.Raise();
}
void RecoverFromBroken()
{
IsShieldBroken = false;
_timeSinceLastHit = _config.RechargeDelay; // 立即开始回充
}
void OnSavePoint()
{
if (!_config.FullRechargeOnSavePoint) return;
CurrentShieldHP = MaxShieldHP;
IsShieldBroken = false;
_brokenTimer = 0f;
_timeSinceLastHit = 0f;
_onShieldHPChanged.Raise(CurrentShieldHP);
}
}
}
```
---
## 4. 伤害拦截流程
`PlayerCombat.TakeHit()` 中,`ShieldComponent` 是第一道过滤层:
```csharp
// PlayerCombat.TakeHit(HitInfo info)
int remainingDamage = _shield != null
? _shield.AbsorbDamage(info.Damage)
: info.Damage;
if (remainingDamage > 0)
_playerStats.TakeDamage(remainingDamage);
```
### 吸收比例说明
| `DamageAbsorptionRatio` | 行为 |
|------------------------|------|
| 1.0(默认)| 护盾耐久充足时完全吸收,无溢出伤害到 HP |
| 0.5 | 护盾承担 50% 伤害,剩余 50% 直接扣 HP |
| 0.0 | 护盾仅作为计数器,所有伤害直接到 HP |
> 建议起始配置:`DamageAbsorptionRatio = 1.0`,配合较低的 `MaxShieldHP`(60 点,约等于 3 发普通攻击)。
---
## 5. 护盾恢复机制
```
最后一次受击
│
▼
_timeSinceLastHit 开始计时
│
▼
达到 RechargeDelay(2.5s)后
│
▼
每帧回充 RechargeRate × deltaTime(20 点/秒)
│
▼
恢复至 MaxShieldHP → 触发 OnShieldRestored(UI 播放满值动画)
```
**破碎惩罚**:护盾破碎后 `BrokenPenaltyDuration`(3.0s)内不进入回充计时,期间 UI 显示"破碎"状态(红色闪烁)。
---
## 6. 护盾破碎反馈
通过 **Feel** 的 MMFeedbacks 播放破碎效果:
```csharp
// ShieldFeedbackReceiver : MonoBehaviour(挂载在玩家对象)
[SerializeField] VoidEventChannelSO _onShieldBroken;
[SerializeField] MMFeedbacks _shieldBreakFeedback; // 音效 + 屏幕震动 + 粒子
[SerializeField] MMFeedbacks _shieldRestoredFeedback;// 恢复满值时的轻微提示音
void OnEnable()
{
_onShieldBroken.OnEventRaised += () => _shieldBreakFeedback.PlayFeedbacks();
_onShieldRestored.OnEventRaised += () => _shieldRestoredFeedback.PlayFeedbacks();
}
```
### 破碎效果清单
| 效果 | 参数 |
|------|------|
| 音效(破碎)| SFX_Shield_Break,音量 0.9,间距 ±0.05 半音 |
| 屏幕震动 | 振幅 0.15,持续 0.25s(MMCameraShake)|
| 粒子爆裂 | ShieldBreakVFX(蓝色碎片,持续 0.4s)|
| 玩家闪白 | 持续 0.1s(SpriteRenderer 颜色 tint)|
---
## 7. 格挡系统集成(Parry,P1)
格挡(Parry)是 P1 规划功能,与护盾的交互设计如下:
```
玩家在攻击前摇帧内按下 [Guard] 键
│
▼
ParrySystem.TryParry(HitInfo info)
├─ 成功格挡 → 反弹/无伤 + ShieldComponent.OnParrySuccess()
│ (恢复 30% 护盾耐久,视觉反馈)
└─ 格挡失败 → 护盾吸收正常伤害
```
**格挡条件**(P1 具体值 TBD):
- 格挡窗口:攻击动画前摇的最后 3 帧(约 50ms @ 60fps)
- 成功格挡动画:`Anim_Parry_Success`(Animancer 状态切换)
- 成功格挡音效:`SFX_Parry`(高频金属碰撞音)
---
## 8. 护盾 UI 指示器
护盾耐久以**独立小条**显示于 HP 条正下方:
```
┌───────────────────────────────────┐
│ ♥ ♥ ♥ ♥ ♥ HP Bar │
│ ▪▪▪▪▪▪▪▪▪ Shield Bar(蓝色) │
└───────────────────────────────────┘
```
### ShieldBarUI.cs
```csharp
namespace BaseGames.UI
{
public class ShieldBarUI : MonoBehaviour
{
[SerializeField] IntEventChannelSO _onShieldHPChanged;
[SerializeField] VoidEventChannelSO _onShieldBroken;
[SerializeField] VoidEventChannelSO _onShieldRestored;
UIDocument _doc;
VisualElement _fill;
Label _valueLabel;
int _maxHP;
void OnEnable()
{
_doc = GetComponent();
_fill = _doc.rootVisualElement.Q("ShieldFill");
_valueLabel = _doc.rootVisualElement.Q