26 KiB
04 · 战斗系统
命名空间
BaseGames.Combat
所属文档集 ← 返回索引 · 总览
依赖BaseGames.Player·BaseGames.Enemies·BaseGames.Feedback
参见 54_PoiseSystem — 霸体 / 打断等级系统
目录
- 系统总览
- DamageInfo — 伤害信息结构
- HitBox — 攻击判定
- HurtBox — 受击响应
- 伤害流水线
- 击退计算
- 连击链与攻击属性
- 伤害层级与 Layer 矩阵
- 事件频道集成
- CombatConfigSO
- 编辑器友好设计
- 状态效果系统(StatusEffect / DoT)
- 拼刀系统(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) |
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 工作流程
AttackState初始化时,将攻击属性注入HitBox(SetDamageSource(DamageSourceSO))- AnimationEvent
EnableHitBox触发 →HitBox.Activate()(碰撞体 enabled = true) OnTriggerEnter2D检测碰撞层:- HurtBox Layer → 构建
DamageInfo,传递给HurtBox.ReceiveDamage(DamageInfo) - 对立 HitBox Layer(PlayerAttack ↔ EnemyAttack)且对方
_canClash == true→ 路由到ClashResolver.ResolveClash(this, other),中止伤害处理
- HurtBox Layer → 构建
- 记录命中目标,在冷却时间内不重复命中
- 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) |
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 基类
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 — 燃烧
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 — 毒素
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 作为有序更新序列:
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 集成(在现有伤害流水线末尾添加):
// 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 引用并处理拼刀逻辑:
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,迫使玩家闪避而非硬拼。