# 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 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 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` 遍历的绝对开销极小(< 1μs/帧)。但为避免每帧 `FirstOrDefault` LINQ 分配,`ApplyEffect` / `CleanseEffect` 改用 `Dictionary` 作为 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 _activeList = new(); readonly Dictionary _activeIndex = new(); SpriteRenderer _renderer; MaterialPropertyBlock _propBlock; void Awake() { _renderer = GetComponent(); _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); } } } /// 施加状态效果。已有相同类型时叠层/刷新。O(1) 查找。 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); } } /// 净化指定类型的状态效果。O(1) 查找。 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(); } /// 由状态效果调用,直接扣减 HP(True 伤害,绕过 HurtBox)。 public void ApplyDirectDamage(DamageInfo info) => GetComponent()?.TakeDamage(info.FinalDamage); /// 由状态效果调用,设置 Shader 参数(材质属性块,不修改共享材质)。 public void SetShaderParam(string param, float value) { _renderer.GetPropertyBlock(_propBlock); _propBlock.SetFloat(param, value); _renderer.SetPropertyBlock(_propBlock); } /// O(1) 查找。 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()?.ApplyEffect(new FireEffect()); else if (info.DamageType == DamageType.Poison) Owner.GetComponent()?.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(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 _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`,迫使玩家闪避而非硬拼。