13 KiB
20 · 护盾模块(Shield Module)
命名空间
BaseGames.Combat
程序集BaseGames.Combat(Assets/Scripts/Combat/)
依赖BaseGames.Core.Events·BaseGames.Combat(DamageInfo · HurtBox)·BaseGames.UI(HUDController)
Design 来源 30_ShieldMechanicsSystem
目录
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 之前检查护盾:
// 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_DamageDealt(AnalyticsManager)
_onHitConfirmed.Raise(new HitInfo { DamageInfo = info, HitPoint = transform.position });
}
PlayerController 在 Awake 时将 ShieldComponent 注入 HurtBox._shieldable:
// PlayerController.Awake()
_hurtBox.SetShieldable(_shieldComponent);
3. ShieldConfigSO
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
namespace BaseGames.Combat
{
/// <summary>
/// 挂在 PlayerController 子节点 [Shield] 上。
/// 在 HurtBox 和 IDamageable(PlayerStats)之间担当拦截层。
/// </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 接口
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 条上方(或并列):
// 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):
if (_controller.TryGetComponent<ShieldComponent>(out var shield))
shield.OnParrySuccess();
此处 HandleSuccessfulParry() 是 ParrySystem 中处理弹反成功后统一逻辑的方法,同时负责广播 _onParrySuccess(ParryInfoEventChannelSO)事件、奖励灵力、触发子弹时间。ShieldComponent.OnParrySuccess() 通过直接调用(而非事件订阅)接收通知,以保证执行顺序。
9. SaveData 集成
PlayerSaveData 中新增字段:
public int ShieldHP; // 当前护盾耐久(-1 = 使用最大值,即满护盾)
public bool ShieldIsBroken; // 是否处于破碎状态
加载时(PlayerController.LoadFromSaveData):
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