chore: initial commit
This commit is contained in:
680
Docs/Design/04_CombatSystem.md
Normal file
680
Docs/Design/04_CombatSystem.md
Normal file
@@ -0,0 +1,680 @@
|
||||
# 04 · 战斗系统
|
||||
|
||||
> **命名空间** `BaseGames.Combat`
|
||||
> **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md)
|
||||
> **依赖** `BaseGames.Player` · `BaseGames.Enemies` · `BaseGames.Feedback`
|
||||
> **参见** [54_PoiseSystem](./54_PoiseSystem.md) — 霸体 / 打断等级系统
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [系统总览](#1-系统总览)
|
||||
2. [DamageInfo — 伤害信息结构](#2-damageinfo--伤害信息结构)
|
||||
3. [HitBox — 攻击判定](#3-hitbox--攻击判定)
|
||||
4. [HurtBox — 受击响应](#4-hurtbox--受击响应)
|
||||
5. [伤害流水线](#5-伤害流水线)
|
||||
6. [击退计算](#6-击退计算)
|
||||
7. [连击链与攻击属性](#7-连击链与攻击属性)
|
||||
8. [伤害层级与 Layer 矩阵](#8-伤害层级与-layer-矩阵)
|
||||
9. [事件频道集成](#9-事件频道集成)
|
||||
10. [CombatConfigSO](#10-combatconfigso)
|
||||
11. [编辑器友好设计](#11-编辑器友好设计)
|
||||
12. [状态效果系统(StatusEffect / DoT)](#12-状态效果系统statuseffect--dot)
|
||||
13. [拼刀系统(Nail Clash)](#13-拼刀系统nail-clash)
|
||||
|
||||
---
|
||||
|
||||
## 1. 系统总览
|
||||
|
||||
战斗系统的核心原则是**数据驱动 + 物理解耦**:
|
||||
|
||||
- 伤害以 `DamageInfo` 结构体传递,不通过直接引用获取对象
|
||||
- `HitBox` 和 `HurtBox` 通过 `Collider2D` 触发器交互,互不直接引用对方 Owner
|
||||
- 伤害路由全部通过 `OnHitConfirmed` 事件频道广播
|
||||
- 击退方向、硬直时长、格挡属性全部封装在 `DamageInfo` 中
|
||||
|
||||
```
|
||||
攻击方 受击方
|
||||
────── ──────
|
||||
AttackState
|
||||
└─ AnimationEvent: EnableHitBox
|
||||
└─ HitBox.Activate()
|
||||
└─ OnTriggerEnter2D(HurtBox)
|
||||
└─ HurtBox.ReceiveDamage(DamageInfo)
|
||||
├─ 本地响应(HP扣减、硬直)
|
||||
└─ 广播 OnHitConfirmed 频道
|
||||
├─ PlayerStats.AddSoul(+10)
|
||||
├─ EnemyFeedback.OnHit()
|
||||
└─ (其他侦听者)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. DamageInfo — 伤害信息结构
|
||||
|
||||
`DamageInfo` 是**只读值类型**(Struct),在整个伤害流水线中传递,不可修改:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `RawDamage` | `int` | 基础伤害量 |
|
||||
| `FinalDamage` | `int` | 经过加成/减免后的最终伤害(由 HurtBox 计算)|
|
||||
| `KnockbackDirection` | `Vector2` | 归一化击退方向 |
|
||||
| `KnockbackForce` | `float` | 击退力度 |
|
||||
| `HitStunDuration` | `float` | 受击硬直时长(秒)|
|
||||
| `DamageType` | `DamageType` | 伤害类型(Normal / Fire / Poison / True)|
|
||||
| `DamageFlags` | `DamageFlags` | 属性标记(BitFlags,见下方)|
|
||||
| `SourcePosition` | `Vector2` | 攻击来源世界位置(用于计算击退方向)|
|
||||
| `SourceLayer` | `int` | 发起攻击的 GameObject Layer(敌人/玩家/环境)|
|
||||
| `HitFxType` | `HitFxType` | 期望产生的命中特效类型(Spark / Blood / Magic 等)|
|
||||
| `BreakLevel` | `BreakLevel` | 此次攻击的打断等级(决定能打断多高的霸体,见 [54_PoiseSystem](./54_PoiseSystem.md))|
|
||||
| `SourceId` | `string` | 攻击来源 ID(DamageSourceSO.sourceId),用于 PoiseOverrideTable 精细规则查询 |
|
||||
|
||||
### DamageFlags(位标记)
|
||||
|
||||
| Flag | 说明 |
|
||||
|------|------|
|
||||
| `Unblockable` | 无法被弹反、无法被格挡 |
|
||||
| `CanBeParried` | 可被弹反 |
|
||||
| `IgnoreIFrame` | 忽略无敌帧 |
|
||||
| `PerfectParryOnly` | 只能被完美弹反(高难度攻击)|
|
||||
| `IsProjectile` | 来自弹射物(影响部分弹反判定)|
|
||||
| `CanClash` | 近战武器攻击可参与拼刀检测;弹射物、环境伤害、DoT 无此标记 |
|
||||
| `ForceBreak` | 强制打断目标当前动作,无视霸体等级(`Unbreakable` 除外)|
|
||||
| `NoKnockback` | 打断时不施加击退,仅进入 HurtState 硬直 |
|
||||
|
||||
### DamageType 说明
|
||||
|
||||
| 类型 | 说明 |
|
||||
|------|------|
|
||||
| `Normal` | 物理攻击,可弹反 |
|
||||
| `True` | 真实伤害,绕过所有减免,不可弹反 |
|
||||
| `Fire` | 持续灼烧(P1)|
|
||||
| `Poison` | 持续中毒(P1)|
|
||||
|
||||
---
|
||||
|
||||
## 3. HitBox — 攻击判定
|
||||
|
||||
`HitBox` 是挂在攻击者身上的**触发器组件**,在 AnimationEvent 驱动下激活/关闭:
|
||||
|
||||
### 组件结构
|
||||
|
||||
```
|
||||
[HitBox_Ground]
|
||||
├── Collider2D (BoxCollider2D,IsTrigger = true)
|
||||
│ └── Layer: "PlayerAttack" 或 "EnemyAttack"
|
||||
├── HitBox.cs
|
||||
│ ├── _damageSource: DamageSourceSO ← 攻击配置(基础伤害/属性)
|
||||
│ ├── _attackerTransform: Transform ← 用于计算击退方向
|
||||
│ ├── _hitCooldown: float ← 同一 HitBox 对同一目标的命中冷却(防多帧重复)
|
||||
│ └── _canClash: bool ← 是否参与拼刀检测(近战武器 = true,弹射物/环境 = false)
|
||||
└── (由 PlayerCombat 调用 Activate/Deactivate)
|
||||
```
|
||||
|
||||
### HitBox 工作流程
|
||||
|
||||
1. `AttackState` 初始化时,将攻击属性注入 `HitBox`(`SetDamageSource(DamageSourceSO)`)
|
||||
2. AnimationEvent `EnableHitBox` 触发 → `HitBox.Activate()`(碰撞体 enabled = true)
|
||||
3. `OnTriggerEnter2D` 检测碰撞层:
|
||||
- **HurtBox Layer** → 构建 `DamageInfo`,传递给 `HurtBox.ReceiveDamage(DamageInfo)`
|
||||
- **对立 HitBox Layer**(PlayerAttack ↔ EnemyAttack)且对方 `_canClash == true` → 路由到 `ClashResolver.ResolveClash(this, other)`,中止伤害处理
|
||||
4. 记录命中目标,在冷却时间内不重复命中
|
||||
5. AnimationEvent `DisableHitBox` 触发 → `HitBox.Deactivate()`(碰撞体 enabled = false)
|
||||
|
||||
### DamageSourceSO — 攻击属性配置
|
||||
|
||||
每种攻击有独立 `DamageSourceSO`,存放于 `Assets/ScriptableObjects/Combat/DamageSources/`:
|
||||
|
||||
| 字段 | 类型 | 示例值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `BaseDamage` | `int` | 5 | 基础伤害 |
|
||||
| `DamageMultiplier` | `float` | 1.0 | 连击倍率(Attack3 = 2.0)|
|
||||
| `KnockbackForce` | `float` | 8.0 | 击退力度 |
|
||||
| `HitStunDuration` | `float` | 0.3s | 目标硬直时长 |
|
||||
| `DamageType` | `DamageType` | Normal | 伤害类型 |
|
||||
| `DamageFlags` | `DamageFlags` | CanBeParried | 属性标记 |
|
||||
| `HitFxType` | `HitFxType` | Spark | 命中特效 |
|
||||
| `breakLevel` | `BreakLevel` | Light | 此攻击的霸体打断等级(见 [54_PoiseSystem](./54_PoiseSystem.md))|
|
||||
| `forceBreak` | `bool` | false | 强制打断(无视霸体等级,`Unbreakable` 除外)|
|
||||
|
||||
---
|
||||
|
||||
## 4. HurtBox — 受击响应
|
||||
|
||||
`HurtBox` 挂在可受击单位身上,接收 `DamageInfo` 并触发本地响应:
|
||||
|
||||
### 组件结构
|
||||
|
||||
```
|
||||
[HurtBox]
|
||||
├── Collider2D (BoxCollider2D,IsTrigger = true)
|
||||
│ └── Layer: "PlayerHurtBox" 或 "EnemyHurtBox"
|
||||
├── HurtBox.cs
|
||||
│ ├── _owner: MonoBehaviour ← 宿主(PlayerController 或 EnemyBase)
|
||||
│ ├── _defenseStat: int ← 防御减免(0 = 无减免)
|
||||
│ └── UnityEvent<DamageInfo> OnHurt ← 本地事件,供 Inspector 直接绑定反馈
|
||||
└── 调用链: HurtBox → _owner.TakeDamage(DamageInfo)
|
||||
```
|
||||
|
||||
### 伤害减免计算
|
||||
|
||||
$$\text{FinalDamage} = \max(1,\ \text{RawDamage} - \text{DefenseStat})$$
|
||||
|
||||
> `True` 类型伤害跳过此计算,`FinalDamage = RawDamage`
|
||||
|
||||
---
|
||||
|
||||
## 5. 伤害流水线
|
||||
|
||||
```
|
||||
AnimationEvent: EnableHitBox
|
||||
│
|
||||
▼
|
||||
HitBox.OnTriggerEnter2D(HurtBox)
|
||||
│
|
||||
├─ 构建 DamageInfo
|
||||
│ ├─ RawDamage = DamageSourceSO.BaseDamage × DamageMultiplier
|
||||
│ ├─ Direction = (HurtBox.position - Attacker.position).normalized
|
||||
│ ├─ Knockback = DamageSourceSO.KnockbackForce
|
||||
│ └─ HitStun = DamageSourceSO.HitStunDuration
|
||||
│
|
||||
▼
|
||||
HurtBox.ReceiveDamage(DamageInfo info)
|
||||
│
|
||||
├─ [检查无敌帧] → 若 IsInvincible && !IgnoreIFrame → 直接返回
|
||||
├─ [检查弹反] → 若 CanBeParried && ParrySystem.IsInParryWindow → 触发弹反流程
|
||||
├─ [计算最终伤害] FinalDamage = max(1, RawDamage - DefenseStat)
|
||||
├─ [霸体打断检查] → 查询 PoiseOverrideTable / ForceBreak / 数值比较 → 决定是否打断(见 54_PoiseSystem §5)
|
||||
├─ 调用 Owner.TakeDamage(FinalDamage)
|
||||
├─ 广播 OnHitConfirmed 事件频道(DamageInfoEventChannelSO)
|
||||
├─ 若打断:Owner.ApplyKnockback(info) + Owner.ForceState(HurtState)
|
||||
├─ 若不打断:Owner.PlayHitFlash()(仅视觉反馈,动作不中断)
|
||||
└─ 触发本地 UnityEvent<DamageInfo> OnHurt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 击退计算
|
||||
|
||||
击退方向由 `DamageInfo.SourcePosition` 和受击位置共同决定:
|
||||
|
||||
$$\vec{Knockback} = \text{normalize}(\text{HurtPos} - \text{SourcePos}) \times \text{KnockbackForce}$$
|
||||
|
||||
**特殊情况处理**:
|
||||
|
||||
| 情况 | 处理方式 |
|
||||
|------|---------|
|
||||
| 玩家被正下方攻击(如地刺)| 方向强制为 `(0, 1)` 向上 |
|
||||
| 玩家贴墙被打 | 水平分量减为 50%,保留垂直分量 |
|
||||
| Boss 击退 | 使用 `DamageInfo` 中的固定方向,不动态计算 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 连击链与攻击属性
|
||||
|
||||
### 攻击属性 SO 资产列表
|
||||
|
||||
存放于 `Assets/ScriptableObjects/Combat/DamageSources/Player/`:
|
||||
|
||||
| 资产名 | BaseDamage | Multiplier | KnockbackForce | Flags |
|
||||
|--------|-----------|-----------|---------------|-------|
|
||||
| `DS_Player_Attack1.asset` | 5 | ×1.0 | 5 | CanBeParried |
|
||||
| `DS_Player_Attack2.asset` | 5 | ×1.0 | 5 | CanBeParried |
|
||||
| `DS_Player_Attack3.asset` | 5 | ×2.0 | 10 | CanBeParried |
|
||||
| `DS_Player_AirAttack.asset` | 5 | ×1.0 | 6 | CanBeParried |
|
||||
| `DS_Player_UpAttack.asset` | 5 | ×1.0 | 8 | CanBeParried |
|
||||
| `DS_Player_ParryCounter.asset` | 5 | ×3.0 | 15 | Unblockable |
|
||||
|
||||
> 弹反反击 `ParryCounter` 标记为 `Unblockable`,不可再被弹反,且击退力度最大。
|
||||
|
||||
### 连击命中音效与特效
|
||||
|
||||
`HitFxType` 枚举控制命中时产生的特效类型,由 `PlayerFeedback` 中对应 `MMF_Player` 响应:
|
||||
|
||||
| HitFxType | 特效 | 音效 | 说明 |
|
||||
|-----------|------|------|------|
|
||||
| `Spark` | 金属火花 Particle | SFX_Hit_Spark | 金属敌人 |
|
||||
| `Slash` | 挥斩光轨特效 | SFX_Hit_Slash | 通用普攻 |
|
||||
| `Heavy` | 大范围冲击波 | SFX_Hit_Heavy | Attack3 / ParryCounter |
|
||||
| `Magic` | 魔法闪光 | SFX_Hit_Magic | 魔法攻击(P1)|
|
||||
|
||||
---
|
||||
|
||||
## 8. 伤害层级与 Layer 矩阵
|
||||
|
||||
**Physics 2D Layer Collision Matrix 配置**(ProjectSettings → Physics2D):
|
||||
|
||||
| 攻击方 Layer | 可命中 Layer | 不可命中 Layer |
|
||||
|-------------|------------|--------------|
|
||||
| `PlayerAttack` | `EnemyHurtBox`, `BreakableObject`, `EnemyAttack`(拼刀)| `PlayerHurtBox`, `PlayerAttack` |
|
||||
| `EnemyAttack` | `PlayerHurtBox`, `PlayerAttack`(拼刀)| `EnemyHurtBox`, `EnemyAttack` |
|
||||
| `Projectile_Player` | `EnemyHurtBox`, `Wall`, `BreakableObject` | `PlayerHurtBox` |
|
||||
| `Projectile_Enemy` | `PlayerHurtBox`, `Wall` | `EnemyHurtBox` |
|
||||
| `Environment` | `PlayerHurtBox`, `EnemyHurtBox` | — |
|
||||
|
||||
---
|
||||
|
||||
## 9. 事件频道集成
|
||||
|
||||
战斗系统发布/订阅的 SO 事件频道:
|
||||
|
||||
| 频道资产 | 类型 | 发布方 | 主要订阅方 |
|
||||
|---------|------|--------|----------|
|
||||
| `OnHitConfirmed.asset` | `DamageInfoEventChannelSO` | `HurtBox` | `PlayerStats`(+Soul)、`FeedbackSystem`(特效)、`UI`(伤害数字)|
|
||||
| `OnPlayerHPChanged.asset` | `IntEventChannelSO` | `PlayerStats` | `HUD`(血条)、`PlayerFeedback`(受伤反馈)|
|
||||
| `OnEnemyDied.asset` | `TransformEventChannelSO` | `EnemyBase` | `WorldSystem`(掉落物)、`FeedbackSystem`(死亡特效)|
|
||||
| `OnParrySuccess.asset` | `DamageInfoEventChannelSO` | `ParrySystem` | `PlayerStats`(+33 Soul)、`PlayerFeedback`(弹反反馈)、`EnemySystem`(Stagger)|
|
||||
| `OnNailClash.asset` | `VoidEventChannelSO` | `ClashResolver` | `PlayerFeedback`(拼刀特效/音效)、`EnemyFeedback`(拼刀反馈)、`CameraSystem`(轻微 Impulse)|
|
||||
|
||||
---
|
||||
|
||||
## 10. CombatConfigSO
|
||||
|
||||
存放于 `Assets/ScriptableObjects/Config/Combat/CombatConfigSO.asset`:
|
||||
|
||||
| 参数 | 类型 | 推荐值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `HitCooldownPerTarget` | `float` | 0.05s | HitBox 对同一目标命中冷却 |
|
||||
| `ComboWindowDuration` | `float` | 0.5s | 连击窗口(攻击动画结束后)|
|
||||
| `ComboResetDelay` | `float` | 0.3s | 连击链完成后重置延迟 |
|
||||
| `SoulOnHitEnemy` | `int` | 10 | 命中普通敌人获得 Soul |
|
||||
| `SoulOnHitBoss` | `int` | 5 | 命中 Boss 获得 Soul |
|
||||
| `HitFreezeFrames` | `int` | 2 | 命中暂停帧数(Hitstop,以帧为单位)|
|
||||
| `HitFreezeDuration` | `float` | 0.033s | 命中暂停时长(约 2 帧)|
|
||||
|
||||
---
|
||||
|
||||
## 11. 编辑器友好设计
|
||||
|
||||
### HitBox Scene 视图 Gizmos
|
||||
|
||||
- **激活时**:绘制橙色实心矩形(不透明度 50%)
|
||||
- **未激活时**:绘制橙色虚线矩形(不透明度 20%)
|
||||
- 鼠标悬停时,显示浮窗:`BaseDamage: 5 | KnockbackForce: 8 | Flags: CanBeParried`
|
||||
|
||||
### HurtBox Scene 视图 Gizmos
|
||||
|
||||
- **正常状态**:绘制绿色半透明矩形
|
||||
- **无敌帧中**:绘制黄色闪烁矩形
|
||||
- **受击时**:绘制红色矩形(持续 0.1s)
|
||||
|
||||
### DamageSourceSO 自定义 Inspector
|
||||
|
||||
| 区域 | 内容 |
|
||||
|------|------|
|
||||
| 基础伤害预览 | 显示 `BaseDamage × Multiplier = FinalDamage` 计算结果 |
|
||||
| 击退预览 | 绘制箭头示意击退方向和力度 |
|
||||
| 属性标记 | 颜色标签显示 DamageFlags |
|
||||
| 伤害测试 | Play Mode 下"模拟命中"按钮,立即触发一次伤害流水线 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 状态效果系统(StatusEffect / DoT)
|
||||
|
||||
> **命名空间** `BaseGames.Combat.StatusEffects`
|
||||
|
||||
状态效果(DoT,Damage over Time)让 DamageType 中的 `Fire`、`Poison` 等属性真正生效,为战斗增加策略层次。
|
||||
|
||||
### 12.1 StatusEffect 基类
|
||||
|
||||
```csharp
|
||||
namespace BaseGames.Combat.StatusEffects
|
||||
{
|
||||
public abstract class StatusEffect
|
||||
{
|
||||
public abstract StatusEffectType EffectType { get; }
|
||||
public abstract int MaxStacks { get; } // 最大叠加层数(1 = 不可叠加)
|
||||
|
||||
public int StackCount { get; protected set; } = 1;
|
||||
public float Duration { get; protected set; } // 当前剩余持续时间
|
||||
public float TickInterval { get; protected set; } // 每次 Tick 的间隔秒数
|
||||
float _tickTimer;
|
||||
|
||||
protected StatusEffectManager Owner; // 宿主(PlayerStats 或 EnemyStats)
|
||||
|
||||
public virtual void OnApply(StatusEffectManager owner)
|
||||
{
|
||||
Owner = owner;
|
||||
Duration = GetBaseDuration();
|
||||
}
|
||||
|
||||
public virtual void OnStack() // 已有时再次施加 → 刷新/叠层
|
||||
{
|
||||
Duration = GetBaseDuration(); // 默认刷新持续时间
|
||||
StackCount = Mathf.Min(StackCount + 1, MaxStacks);
|
||||
}
|
||||
|
||||
public virtual void OnTick() { } // 每次 TickInterval 触发一次
|
||||
|
||||
public virtual void OnExpire() { } // 持续时间归零时触发
|
||||
|
||||
public virtual bool IsExpired => Duration <= 0f;
|
||||
|
||||
public void Update(float delta)
|
||||
{
|
||||
Duration -= delta;
|
||||
_tickTimer += delta;
|
||||
if (_tickTimer >= TickInterval)
|
||||
{
|
||||
_tickTimer -= TickInterval;
|
||||
OnTick();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract float GetBaseDuration();
|
||||
public abstract string GetDisplayName();
|
||||
}
|
||||
|
||||
public enum StatusEffectType { Fire, Poison, Freeze, Stun }
|
||||
}
|
||||
```
|
||||
|
||||
### 12.2 FireEffect — 燃烧
|
||||
|
||||
```csharp
|
||||
public class FireEffect : StatusEffect
|
||||
{
|
||||
public override StatusEffectType EffectType => StatusEffectType.Fire;
|
||||
public override int MaxStacks => 1; // 不可叠加,触发时刷新持续时间
|
||||
|
||||
protected override float GetBaseDuration() => 3.0f; // 燃烧 3 秒
|
||||
// TickInterval = 0.5f → 每秒 2 次 Tick,每次 1 点伤害 = 2 DPS
|
||||
|
||||
public FireEffect() => TickInterval = 0.5f;
|
||||
|
||||
public override void OnApply(StatusEffectManager owner)
|
||||
{
|
||||
base.OnApply(owner);
|
||||
owner.SetShaderParam("_FireGlow", 1f); // 橙色光晕 Shader 参数
|
||||
}
|
||||
|
||||
public override void OnTick()
|
||||
{
|
||||
// 构造 True 类型伤害(跳过防御,不触发 IFrame)
|
||||
var info = new DamageInfo(
|
||||
attacker: null,
|
||||
damage: 1,
|
||||
type: DamageType.True,
|
||||
flags: DamageFlags.IgnoreIFrame | DamageFlags.IsDoT
|
||||
);
|
||||
Owner.ApplyDirectDamage(info);
|
||||
}
|
||||
|
||||
public override void OnExpire()
|
||||
=> Owner.SetShaderParam("_FireGlow", 0f); // 关闭光晕
|
||||
|
||||
public override string GetDisplayName() => "燃烧";
|
||||
}
|
||||
```
|
||||
|
||||
### 12.3 PoisonEffect — 毒素
|
||||
|
||||
```csharp
|
||||
public class PoisonEffect : StatusEffect
|
||||
{
|
||||
public override StatusEffectType EffectType => StatusEffectType.Poison;
|
||||
public override int MaxStacks => 3; // 最多叠加 3 层
|
||||
|
||||
protected override float GetBaseDuration() => 5.0f; // 每层独立计时 5 秒
|
||||
// TickInterval = 1.0f → 1 DPS,3 层叠加时 3 DPS
|
||||
|
||||
public PoisonEffect() => TickInterval = 1.0f;
|
||||
|
||||
public override void OnApply(StatusEffectManager owner)
|
||||
{
|
||||
base.OnApply(owner);
|
||||
UpdateShader();
|
||||
}
|
||||
|
||||
public override void OnStack()
|
||||
{
|
||||
base.OnStack();
|
||||
UpdateShader();
|
||||
}
|
||||
|
||||
public override void OnTick()
|
||||
{
|
||||
var info = new DamageInfo(
|
||||
attacker: null,
|
||||
damage: StackCount, // 伤害随叠层数增加
|
||||
type: DamageType.True,
|
||||
flags: DamageFlags.IgnoreIFrame | DamageFlags.IsDoT
|
||||
);
|
||||
Owner.ApplyDirectDamage(info);
|
||||
}
|
||||
|
||||
public override void OnExpire()
|
||||
{
|
||||
StackCount = 0;
|
||||
Owner.SetShaderParam("_PoisonGlow", 0f);
|
||||
}
|
||||
|
||||
void UpdateShader()
|
||||
=> Owner.SetShaderParam("_PoisonGlow", StackCount / (float)MaxStacks);
|
||||
|
||||
public override string GetDisplayName() => $"中毒 x{StackCount}";
|
||||
}
|
||||
```
|
||||
|
||||
### 12.4 StatusEffectManager — 宿主组件
|
||||
|
||||
挂载在 **PlayerController** 和 **EnemyBase** 上,管理所有状态效果。
|
||||
|
||||
**性能注意**:状态效果列表通常不超过 4–5 个并发 entry,`List<T>` 遍历的绝对开销极小(< 1μs/帧)。但为避免每帧 `FirstOrDefault` LINQ 分配,`ApplyEffect` / `CleanseEffect` 改用 `Dictionary<StatusEffectType, StatusEffect>` 作为 O(1) 查找结构,同时保留 `List` 作为有序更新序列:
|
||||
|
||||
```csharp
|
||||
namespace BaseGames.Combat.StatusEffects
|
||||
{
|
||||
[RequireComponent(typeof(SpriteRenderer))]
|
||||
public class StatusEffectManager : MonoBehaviour
|
||||
{
|
||||
[Header("事件频道")]
|
||||
[SerializeField] StatusEffectEventChannelSO _onStatusEffectApplied;
|
||||
[SerializeField] StatusEffectEventChannelSO _onStatusEffectExpired;
|
||||
|
||||
// 双结构:List 用于有序 Update 遍历;Dictionary 用于 O(1) 查找
|
||||
readonly List<StatusEffect> _activeList = new();
|
||||
readonly Dictionary<StatusEffectType, StatusEffect> _activeIndex = new();
|
||||
|
||||
SpriteRenderer _renderer;
|
||||
MaterialPropertyBlock _propBlock;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_renderer = GetComponent<SpriteRenderer>();
|
||||
_propBlock = new MaterialPropertyBlock();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// 倒序遍历 List,避免 RemoveAt 移位问题
|
||||
for (int i = _activeList.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var effect = _activeList[i];
|
||||
effect.Update(Time.deltaTime);
|
||||
if (effect.IsExpired)
|
||||
{
|
||||
effect.OnExpire();
|
||||
_activeIndex.Remove(effect.EffectType);
|
||||
_activeList.RemoveAt(i);
|
||||
_onStatusEffectExpired.Raise(effect.EffectType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>施加状态效果。已有相同类型时叠层/刷新。O(1) 查找。</summary>
|
||||
public void ApplyEffect(StatusEffect newEffect)
|
||||
{
|
||||
if (_activeIndex.TryGetValue(newEffect.EffectType, out var existing))
|
||||
{
|
||||
existing.OnStack();
|
||||
}
|
||||
else
|
||||
{
|
||||
newEffect.OnApply(this);
|
||||
_activeList.Add(newEffect);
|
||||
_activeIndex[newEffect.EffectType] = newEffect;
|
||||
_onStatusEffectApplied.Raise(newEffect.EffectType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>净化指定类型的状态效果。O(1) 查找。</summary>
|
||||
public void CleanseEffect(StatusEffectType type)
|
||||
{
|
||||
if (!_activeIndex.TryGetValue(type, out var effect)) return;
|
||||
effect.OnExpire();
|
||||
_activeIndex.Remove(type);
|
||||
_activeList.Remove(effect); // List.Remove 是 O(n),但列表极短,可接受
|
||||
_onStatusEffectExpired.Raise(type);
|
||||
}
|
||||
|
||||
public void CleanseAll()
|
||||
{
|
||||
foreach (var e in _activeList) e.OnExpire();
|
||||
_activeList.Clear();
|
||||
_activeIndex.Clear();
|
||||
}
|
||||
|
||||
/// <summary>由状态效果调用,直接扣减 HP(True 伤害,绕过 HurtBox)。</summary>
|
||||
public void ApplyDirectDamage(DamageInfo info)
|
||||
=> GetComponent<IDamageable>()?.TakeDamage(info.FinalDamage);
|
||||
|
||||
/// <summary>由状态效果调用,设置 Shader 参数(材质属性块,不修改共享材质)。</summary>
|
||||
public void SetShaderParam(string param, float value)
|
||||
{
|
||||
_renderer.GetPropertyBlock(_propBlock);
|
||||
_propBlock.SetFloat(param, value);
|
||||
_renderer.SetPropertyBlock(_propBlock);
|
||||
}
|
||||
|
||||
/// <summary>O(1) 查找。</summary>
|
||||
public bool HasEffect(StatusEffectType type)
|
||||
=> _activeIndex.ContainsKey(type);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 12.5 状态效果触发方式
|
||||
|
||||
状态效果由以下几种方式触发:
|
||||
|
||||
| 触发方式 | 说明 |
|
||||
|---------|------|
|
||||
| `DamageInfo.DamageType == Fire/Poison` | 伤害流水线在 `HurtBox.ReceiveDamage` 中检测,触发 `StatusEffectManager.ApplyEffect` |
|
||||
| `OnHitEffect`(魅力效果)| 命中时以概率施加(见 `17_EquipmentSystem.md §4.3`)|
|
||||
| 环境接触 | `HazardZone` 配置为 DoT 类型时,每帧发布带 `DamageType.Poison` 的 `DamageInfo` |
|
||||
|
||||
**HurtBox 集成**(在现有伤害流水线末尾添加):
|
||||
|
||||
```csharp
|
||||
// HurtBox.ReceiveDamage 末尾追加:
|
||||
if (info.DamageType == DamageType.Fire)
|
||||
Owner.GetComponent<StatusEffectManager>()?.ApplyEffect(new FireEffect());
|
||||
else if (info.DamageType == DamageType.Poison)
|
||||
Owner.GetComponent<StatusEffectManager>()?.ApplyEffect(new PoisonEffect());
|
||||
```
|
||||
|
||||
### 12.6 净化条件
|
||||
|
||||
| 效果 | 净化方式 |
|
||||
|------|---------|
|
||||
| `Fire` | 进入水域(`WaterZone` 触发 `CleanseEffect(Fire)`)|
|
||||
| `Poison` | 装备「解毒魅」(`OnEquip` 时订阅 `OnStatusEffectApplied`,自动触发净化)|
|
||||
| 所有效果 | 在存档点激活时,`SavePoint.OnInteract` 调用 `CleanseAll()` |
|
||||
|
||||
### 12.7 视觉效果汇总
|
||||
|
||||
| 状态 | Shader 参数 | 效果描述 |
|
||||
|------|------------|---------|
|
||||
| `Fire` | `_FireGlow: 0→1` | 角色轮廓橙红色自发光,强度随时间衰减 |
|
||||
| `Poison x1` | `_PoisonGlow: 0.33` | 轻微绿色色调叠加 |
|
||||
| `Poison x3` | `_PoisonGlow: 1.0` | 强绿色,角色变色明显 |
|
||||
| `Stun` | `_StunFlash: 1` | 黄白色闪烁(0.1s 间隔)|
|
||||
|
||||
> Shader 参数通过 `MaterialPropertyBlock` 修改,**不影响共享材质**,场景中多个同材质角色各自独立显示。
|
||||
|
||||
---
|
||||
|
||||
## 13. 拼刀系统(Nail Clash)
|
||||
|
||||
当**玩家与敌人的近战 HitBox 同时激活并相互重叠**时触发拼刀:双方武器碰撞,均不扣血,各自弹开,播放拼刀特效与音效。
|
||||
|
||||
> 仅携带 `CanClash` 标记的 HitBox 才参与拼刀检测(近战武器)。弹射物、环境伤害、DoT 无此标记,永远不触发拼刀。
|
||||
|
||||
### 13.1 检测架构
|
||||
|
||||
```
|
||||
Physics Layer 配置:
|
||||
PlayerAttack Layer ↔ EnemyAttack Layer → 开启碰撞检测
|
||||
|
||||
HitBox.OnTriggerEnter2D(Collider2D other)
|
||||
│
|
||||
├─ other.Layer == HurtBox → 正常伤害流水线
|
||||
│
|
||||
└─ other.Layer == 对立 HitBox Layer
|
||||
└─ TryGetComponent<HitBox>(other, out rivalHitBox)
|
||||
└─ rivalHitBox.IsActive && rivalHitBox.CanClash && this.CanClash
|
||||
└─ ClashResolver.ResolveClash(this, rivalHitBox)
|
||||
→ 中止伤害,触发拼刀效果
|
||||
```
|
||||
|
||||
### 13.2 ClashResolver
|
||||
|
||||
`ClashResolver` 是场景中的单例服务(`GameManager` 持有),接收两个 HitBox 引用并处理拼刀逻辑:
|
||||
|
||||
```csharp
|
||||
public class ClashResolver : MonoBehaviour
|
||||
{
|
||||
[SerializeField] VoidEventChannelSO _onNailClash;
|
||||
[SerializeField] ClashConfigSO _config;
|
||||
|
||||
// 防止同一碰撞在同帧被双方 HitBox 各触发一次(去重)
|
||||
readonly HashSet<int> _processedThisFrame = new();
|
||||
|
||||
void LateUpdate() => _processedThisFrame.Clear();
|
||||
|
||||
public void ResolveClash(HitBox playerHitBox, HitBox enemyHitBox)
|
||||
{
|
||||
int key = playerHitBox.GetInstanceID() ^ enemyHitBox.GetInstanceID();
|
||||
if (!_processedThisFrame.Add(key)) return; // 本帧已处理
|
||||
|
||||
// 1. 拼刀 HitStop(1 帧,比普通命中的 2 帧更短)
|
||||
HitStopManager.FreezeFrames(_config.ClashFreezeFrames);
|
||||
|
||||
// 2. 双方弹开
|
||||
ApplyClashKnockback(playerHitBox.OwnerRigidbody, enemyHitBox.transform.position);
|
||||
ApplyClashKnockback(enemyHitBox.OwnerRigidbody, playerHitBox.transform.position);
|
||||
|
||||
// 3. 广播事件
|
||||
_onNailClash.Raise();
|
||||
}
|
||||
|
||||
void ApplyClashKnockback(Rigidbody2D rb, Vector2 oppositePos)
|
||||
{
|
||||
var dir = ((Vector2)rb.transform.position - oppositePos).normalized;
|
||||
rb.AddForce(dir * _config.ClashKnockbackForce, ForceMode2D.Impulse);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 13.3 ClashConfigSO
|
||||
|
||||
存放于 `Assets/ScriptableObjects/Config/Combat/ClashConfigSO.asset`:
|
||||
|
||||
| 参数 | 类型 | 推荐值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `ClashFreezeFrames` | `int` | 1 | 拼刀冻帧(比命中的 2 帧更短,避免卡顿感)|
|
||||
| `ClashKnockbackForce` | `float` | 6.0 | 拼刀弹开力度 |
|
||||
| `ClashImpulseStrength` | `float` | 0.3 | Cinemachine Impulse 强度(轻微)|
|
||||
|
||||
### 13.4 拼刀判定规则
|
||||
|
||||
| 情况 | 结果 |
|
||||
|------|------|
|
||||
| 双方 HitBox 均激活 + 均 `CanClash = true` | 触发拼刀,双方弹开,不扣血 |
|
||||
| 仅玩家 HitBox 激活,敌人 HitBox 未激活 | 正常命中敌人 HurtBox |
|
||||
| 敌人攻击标记为 `CanClash = false`(如 Boss 重击)| 不触发拼刀,正常伤害玩家 |
|
||||
| 同帧多次碰撞 | `HashSet` 去重,每对 HitBox 每帧只触发一次 |
|
||||
|
||||
> **设计意图**:普通近战攻击默认 `CanClash = true`,让拼刀自然发生。Boss 特殊重击设 `CanClash = false`,迫使玩家闪避而非硬拼。
|
||||
Reference in New Issue
Block a user