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

473 lines
15 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.
# 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. [格挡系统集成ParryP1](#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("格挡恢复ParryP1 功能)")]
[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();
}
}
}
/// <summary>
/// 外部PlayerCombat调用处理一次伤害输入。
/// 返回实际穿透到 PlayerStats 的伤害量(护盾吸收后的余量)。
/// </summary>
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;
}
/// <summary>格挡成功时恢复部分护盾Parry 系统调用)</summary>
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 开始计时
达到 RechargeDelay2.5s)后
每帧回充 RechargeRate × deltaTime20 点/秒)
恢复至 MaxShieldHP → 触发 OnShieldRestoredUI 播放满值动画)
```
**破碎惩罚**:护盾破碎后 `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.25sMMCameraShake|
| 粒子爆裂 | ShieldBreakVFX蓝色碎片持续 0.4s|
| 玩家闪白 | 持续 0.1sSpriteRenderer 颜色 tint|
---
## 7. 格挡系统集成ParryP1
格挡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<UIDocument>();
_fill = _doc.rootVisualElement.Q("ShieldFill");
_valueLabel = _doc.rootVisualElement.Q<Label>("ShieldValue");
_maxHP = FindFirstObjectByType<ShieldComponent>().MaxShieldHP;
_onShieldHPChanged.OnEventRaised += OnHPChanged;
_onShieldBroken.OnEventRaised += OnBroken;
_onShieldRestored.OnEventRaised += OnRestored;
}
void OnDisable()
{
_onShieldHPChanged.OnEventRaised -= OnHPChanged;
_onShieldBroken.OnEventRaised -= OnBroken;
_onShieldRestored.OnEventRaised -= OnRestored;
}
void OnHPChanged(int current)
{
float ratio = (float)current / _maxHP;
_fill.style.width = Length.Percent(ratio * 100f);
_valueLabel.text = current.ToString();
_fill.RemoveFromClassList("shield-broken");
}
void OnBroken()
{
_fill.AddToClassList("shield-broken"); // USS: 红色闪烁动画
}
void OnRestored()
{
_fill.AddToClassList("shield-full-flash"); // USS: 短暂白色高亮
// 500ms 后移除
_fill.schedule.Execute(() =>
_fill.RemoveFromClassList("shield-full-flash")).StartingIn(500);
}
}
}
```
### USS 样式(护盾条)
```css
/* Assets/UI/Styles/ShieldBar.uss */
#ShieldFill {
background-color: rgb(80, 160, 230);
height: 6px;
border-radius: 3px;
transition-property: width;
transition-duration: 0.1s;
}
.shield-broken {
background-color: rgb(200, 60, 60);
animation-name: shield-blink;
animation-duration: 0.4s;
animation-iteration-count: 3;
}
.shield-full-flash {
background-color: white;
}
@keyframes shield-blink {
0% { opacity: 1; }
50% { opacity: 0.2; }
100% { opacity: 1; }
}
```
---
## 9. SaveData 集成
```json
"shield": {
"currentHP": 60,
"isBroken": false
}
```
```csharp
// ShieldComponent
public ShieldSaveData GetSaveData() => new ShieldSaveData
{
CurrentHP = CurrentShieldHP,
IsBroken = IsShieldBroken,
};
public void LoadSaveData(ShieldSaveData data)
{
CurrentShieldHP = data.CurrentHP;
IsShieldBroken = data.IsBroken;
_brokenTimer = 0f;
_onShieldHPChanged.Raise(CurrentShieldHP);
}
```
> 通常在激活存档点时护盾全满(`FullRechargeOnSavePoint = true`),因此存档数据中的护盾值主要用于**中途存档**(如检查点)。
---
## 10. 编辑器友好设计
### ShieldConfigSO 自定义 Inspector
```csharp
[CustomEditor(typeof(ShieldConfigSO))]
public class ShieldConfigSOEditor : Editor
{
public override VisualElement CreateInspectorGUI()
{
var root = new VisualElement();
InspectorElement.FillDefaultInspector(root, serializedObject, this);
var cfg = (ShieldConfigSO)target;
var stats = new Foldout { text = "护盾估算" };
float fillTime = cfg.MaxShieldHP / Mathf.Max(cfg.RechargeRate, 0.01f);
stats.Add(new Label($"满值充能时间(无受击): {fillTime:F1} 秒"));
stats.Add(new Label($"破碎冷却后开始恢复: {cfg.BrokenPenaltyDuration:F1} + {cfg.RechargeDelay:F1} = {cfg.BrokenPenaltyDuration + cfg.RechargeDelay:F1} 秒"));
stats.Add(new Label($"吸收比例: {cfg.DamageAbsorptionRatio * 100:F0}%"));
root.Add(stats);
return root;
}
}
```
### 场景 Gizmos
```csharp
#if UNITY_EDITOR
void OnDrawGizmosSelected()
{
// 护盾状态可视化
var color = IsShieldBroken ? Color.red : new Color(0.3f, 0.7f, 1f, 0.5f);
Gizmos.color = color;
float r = 0.7f * ShieldRatio;
Gizmos.DrawWireSphere(transform.position, r);
UnityEditor.Handles.Label(transform.position + Vector3.up * 1.0f,
$"Shield {CurrentShieldHP}/{MaxShieldHP}");
}
#endif
```