chore: initial commit

This commit is contained in:
2026-05-08 11:04:00 +08:00
commit f55d2a57c3
6278 changed files with 866081 additions and 0 deletions

View File

@@ -0,0 +1,472 @@
# 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
```