Files
zeling_v2/Docs/Architecture/20_ShieldModule.md
2026-05-12 15:34:08 +08:00

388 lines
13 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.
# 20 · 护盾模块Shield Module
> **命名空间** `BaseGames.Combat`
> **程序集** `BaseGames.Combat``Assets/Scripts/Combat/`
> **依赖** `BaseGames.Core.Events` · `BaseGames.Combat`DamageInfo · HurtBox· `BaseGames.UI`HUDController
> **Design 来源** [30_ShieldMechanicsSystem](../Design/30_ShieldMechanicsSystem.md)
---
## 目录
1. [模块职责](#1-模块职责)
2. [伤害管道修正](#2-伤害管道修正)
3. [ShieldConfigSO](#3-shieldconfigso)
4. [ShieldComponent](#4-shieldcomponent)
5. [IShieldable 接口](#5-ishieldable-接口)
6. [护盾恢复机制](#6-护盾恢复机制)
7. [护盾 UI 集成](#7-护盾-ui-集成)
8. [弹反集成P1](#8-弹反集成p1)
9. [SaveData 集成](#9-savedata-集成)
10. [事件频道](#10-事件频道)
---
## 1. 模块职责
护盾是独立于玩家 HP 之外的**第二道防御层**
```
伤害输入(来自 HurtBox
ShieldComponent.AbsorbDamage(incomingDamage)
├─ 护盾耐久 > 0 且未破碎
│ ├─ 吸收 = damage × DamageAbsorptionRatio
│ ├─ 扣除护盾耐久
│ ├─ 耐久归零 → 触发护盾破碎EVT_ShieldBroken
│ └─ 返回穿透量 → HurtBox 直接调用 _damageable.TakeDamage(passInfo)
└─ 护盾已破碎或耐久 = 0
└─ HurtBox 走原始伤害流程(直接 TakeDamage
```
**零耦合**`ShieldComponent.AbsorbDamage()` 只返回穿透量,不持有 `PlayerStats` 引用。穿透伤害由 `HurtBox.ReceiveDamage()` 负责转交给 `IDamageable.TakeDamage()`(见 §2
---
## 2. 伤害管道修正
`HurtBox.ReceiveDamage()` 需要在调用 `IDamageable.TakeDamage` 之前检查护盾:
```csharp
// HurtBox.cs修改 06_CombatModule §2 的实现)
public void ReceiveDamage(DamageInfo info)
{
if (!_isActive) return;
if (_shieldable != null && _shieldable.HasShield)
{
// 护盾优先拦截AbsorbDamage 返回穿透伤害剧量
int passThrough = _shieldable.AbsorbDamage(info.Amount);
if (passThrough > 0)
{
var passInfo = info;
passInfo.Amount = passThrough;
_damageable?.TakeDamage(passInfo);
}
// ShieldComponent 内部已通过事件频道更新 ShieldBarUI
return;
}
// 无护盾或已穿透,走原始流程
_damageable?.TakeDamage(info);
_onDamageDealt.Raise(info); // EVT_DamageDealtAnalyticsManager
_onHitConfirmed.Raise(new HitInfo { DamageInfo = info, HitPoint = transform.position });
}
```
`PlayerController` 在 Awake 时将 `ShieldComponent` 注入 `HurtBox._shieldable`
```csharp
// PlayerController.Awake()
_hurtBox.SetShieldable(_shieldComponent);
```
---
## 3. ShieldConfigSO
```csharp
namespace BaseGames.Combat
{
[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 = 完全吸收
[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("弹反加成P1")]
[Range(0f, 1f)]
public float ParryRestoreRatio = 0.3f; // 成功格挡时恢复护盾耐久比例
}
}
```
**资产路径**`Assets/ScriptableObjects/Player/Shield_Config.asset`
---
## 4. ShieldComponent
```csharp
namespace BaseGames.Combat
{
/// <summary>
/// 挂在 PlayerController 子节点 [Shield] 上。
/// 在 HurtBox 和 IDamageablePlayerStats之间担当拦截层。
/// </summary>
public class ShieldComponent : MonoBehaviour, IShieldable
{
// ── Inspector ───────────────────────────────────────
[SerializeField] ShieldConfigSO _config;
[SerializeField] IntEventChannelSO _onShieldHPChanged; // 广播当前耐久整数
[SerializeField] VoidEventChannelSO _onShieldBrokenChannel;
[SerializeField] VoidEventChannelSO _onShieldRestoredChannel;
[SerializeField] DifficultyChangedEventChannel _onDifficultyChanged;
// ── Runtime State ────────────────────────────────────
int _currentShieldHP;
bool _isBroken;
float _timeSinceLastHit;
float _brokenTimer;
// ── IShieldable ─────────────────────────────────────
public bool HasShield => _currentShieldHP > 0 && !_isBroken;
public int CurrentShieldHP => _currentShieldHP;
public int MaxShieldHP => _config.MaxShieldHP;
void Awake() => _currentShieldHP = _config.MaxShieldHP;
void OnEnable()
{
_onDifficultyChanged.OnEventRaised += OnDifficultyChanged;
}
void OnDisable()
{
_onDifficultyChanged.OnEventRaised -= OnDifficultyChanged;
}
void Update()
{
if (_isBroken)
{
_brokenTimer += Time.deltaTime;
if (_brokenTimer >= _config.BrokenPenaltyDuration)
{
_isBroken = false;
_brokenTimer = 0f;
}
return;
}
if (_currentShieldHP < _config.MaxShieldHP)
{
_timeSinceLastHit += Time.deltaTime;
if (_timeSinceLastHit >= _config.RechargeDelay)
{
_currentShieldHP = Mathf.Min(
_config.MaxShieldHP,
_currentShieldHP + Mathf.RoundToInt(_config.RechargeRate * Time.deltaTime)
);
}
}
}
/// <summary>
/// 护盾拦截伤害。由 HurtBox.ReceiveDamage 调用。
/// 返回剩余穿透伤害値0 = 完全吸收)。
/// 通过 EVT_ShieldHPChanged 更新 ShieldBarUI不直接修改 DamageInfo。
/// </summary>
public int AbsorbDamage(int incomingDamage)
{
_timeSinceLastHit = 0f;
int absorbed = Mathf.RoundToInt(incomingDamage * _config.DamageAbsorptionRatio);
int passThrough = incomingDamage - absorbed;
_currentShieldHP -= absorbed;
if (_currentShieldHP <= 0)
{
// 护盾破碎:多余伤害穿透
passThrough += Mathf.Abs(_currentShieldHP);
_currentShieldHP = 0;
_isBroken = true;
_brokenTimer = 0f;
_onShieldBrokenChannel.Raise();
}
_onShieldHPChanged.Raise(_currentShieldHP); // 更新 ShieldBarUI
return passThrough;
}
/// <summary>存档点激活时调用(若配置允许)。</summary>
public void FullRecharge()
{
if (!_config.FullRechargeOnSavePoint) return;
_currentShieldHP = _config.MaxShieldHP;
_isBroken = false;
_brokenTimer = 0f;
_onShieldRestoredChannel.Raise();
}
/// <summary>存档加载时恢复护盾状态。由 PlayerController.LoadFromSaveData() 调用。</summary>
public void SetShieldHP(int hp, bool isBroken)
{
_currentShieldHP = Mathf.Clamp(hp, 0, _config.MaxShieldHP);
_isBroken = isBroken;
_brokenTimer = 0f;
_timeSinceLastHit = 0f;
}
/// <summary>弹反成功时恢复护盾P1。</summary>
public void OnParrySuccess()
{
int restore = Mathf.RoundToInt(_config.MaxShieldHP * _config.ParryRestoreRatio);
_currentShieldHP = Mathf.Min(_config.MaxShieldHP, _currentShieldHP + restore);
}
void OnDifficultyChanged(DifficultyScalerSO scaler)
{
// 难度变更时按比例调整护盾(可选,若 ShieldConfigSO 有难度钩子字段则使用)
}
}
}
```
---
## 5. IShieldable 接口
```csharp
namespace BaseGames.Combat
{
/// <summary>
/// 可拥有护盾的实体接口。HurtBox 持有此接口引用,在受击时优先检查护盾。
/// </summary>
public interface IShieldable
{
bool HasShield { get; }
int CurrentShieldHP { get; }
int MaxShieldHP { get; }
/// <summary>拦截 incomingDamage返回剩余穿透伤害0 = 完全吸收)。</summary>
int AbsorbDamage(int incomingDamage);
void FullRecharge();
void OnParrySuccess();
}
}
```
---
## 6. 护盾恢复机制
| 时机 | 行为 |
|------|------|
| 最后一次受击后 `RechargeDelay` 秒 | 开始按 `RechargeRate/s` 线性恢复 |
| 护盾破碎后 `BrokenPenaltyDuration` 秒 | 破碎状态结束,恢复计时重新开始 |
| 激活存档点 | `SavePoint.Activate()``ShieldComponent.FullRecharge()`(若配置为 true |
| 弹反成功 | `ParrySystem.OnParrySuccess``ShieldComponent.OnParrySuccess()` |
---
## 7. 护盾 UI 集成
护盾 UI 显示在 HUD 的 HP 条上方(或并列):
```csharp
// HUDController 中订阅护盾变化
// ShieldBarUI 组件,与 HP Bar 类似设计
public class ShieldBarUI : MonoBehaviour
{
[SerializeField] Image _fill;
[SerializeField] ShieldComponent _shield;
[SerializeField] GameObject _brokenIndicator; // 护盾破碎时显示红色图标
[Header("Event Channels")]
[SerializeField] IntEventChannelSO _onShieldHPChanged; // 订阅耐久变化
[SerializeField] VoidEventChannelSO _onShieldBrokenChannel;
[SerializeField] VoidEventChannelSO _onShieldRestoredChannel;
void OnEnable()
{
_onShieldHPChanged.OnEventRaised += RefreshFill;
_onShieldBrokenChannel.OnEventRaised += ShowBroken;
_onShieldRestoredChannel.OnEventRaised += HideBroken;
}
void OnDisable()
{
_onShieldHPChanged.OnEventRaised -= RefreshFill;
_onShieldBrokenChannel.OnEventRaised -= ShowBroken;
_onShieldRestoredChannel.OnEventRaised -= HideBroken;
}
private void RefreshFill(int currentHP)
{
_fill.fillAmount = _shield.MaxShieldHP > 0
? (float)currentHP / _shield.MaxShieldHP : 0f;
}
private void ShowBroken() => _brokenIndicator.SetActive(true);
private void HideBroken() => _brokenIndicator.SetActive(false);
}
```
---
## 8. 弹反集成P1
弹反成功时,`ParrySystem.HandleSuccessfulParry()` 末尾调用(见 06_CombatModule §8
```csharp
if (_controller.TryGetComponent<ShieldComponent>(out var shield))
shield.OnParrySuccess();
```
此处 `HandleSuccessfulParry()``ParrySystem` 中处理弹反成功后统一逻辑的方法,同时负责广播 `_onParrySuccess``ParryInfoEventChannelSO`)事件、奖励灵力、触发子弹时间。`ShieldComponent.OnParrySuccess()` 通过直接调用(而非事件订阅)接收通知,以保证执行顺序。
---
## 9. SaveData 集成
`PlayerSaveData` 中新增字段:
```csharp
public int ShieldHP; // 当前护盾耐久(-1 = 使用最大值,即满护盾)
public bool ShieldIsBroken; // 是否处于破碎状态
```
**加载时**`PlayerController.LoadFromSaveData`
```csharp
if (saveData.ShieldHP >= 0)
_shield.SetShieldHP(saveData.ShieldHP, saveData.ShieldIsBroken);
else
_shield.FullRecharge();
```
---
## 10. 事件频道
| 频道 SO | Payload | 发布者 | 订阅者 |
|--------|---------|--------|--------|
| `EVT_ShieldHPChanged` | `int`(当前耐久值) | `ShieldComponent` | `ShieldBarUI`(更新护盾条填充) |
| `EVT_ShieldBroken` | void | `ShieldComponent` | `PlayerFeedback`(破碎音效/特效)、`HUDController`(护盾破碎 UI |
| `EVT_ShieldRestored` | void | `ShieldComponent` | `HUDController`(护盾恢复 UI |
## Player Prefab 层级更新
```
[Player]
├── PlayerController.cs
│ └── [SerializeField] ShieldComponent _shield ← 新增
├── [Combat]
│ ├── HurtBox.cs
│ │ └── [SerializeField] IShieldable _shieldable ← 由 PlayerController.Awake() 注入
│ └── HitBox.cs
└── [Shield] ← 新增子节点
└── ShieldComponent.cs
```