# 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