fix(combat): HitBox 命中节奏统一 + 投射物穿透可配
HitBox 新增 HitMode Single/Interval:Interval 用 Enter/Exit 跟踪占用 + Update 轮询,对停留目标按间隔重判,不再依赖 OnTriggerEnter 单次语义。BodyContactDamage 改用 Interval 模式,修复停留在接触判定内、无敌结束后不再受伤的 bug;FlyingEnemy 接触伤害加按目标间隔节流。ProjectileConfigSO 新增 MaxHits 默认 1 即命中即消失,Projectile 按命中预算回收,修掉默认无限穿透;弹反守卫避免反射后立即回收。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,11 +12,28 @@ namespace BaseGames.Combat
|
||||
/// 多碰撞体模式:在子节点上挂载 HitBoxColliderProxy + Collider2D 即可组合任意形状。
|
||||
/// HitBox 本身可不带 Collider2D(仅代理子节点)或同时拥有直属 Collider2D。
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// 命中节奏模式。
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="Single"/>:每次激活对每个目标只判一次(近战挥击、普通投射物)。</item>
|
||||
/// <item><see cref="Interval"/>:对停留在判定盒内的同一目标,每 <c>_hitInterval</c> 秒重判一次
|
||||
/// (接触伤害、持续 AOE、危险区)。靠 Enter/Exit 跟踪占用 + Update 轮询,
|
||||
/// 不依赖 OnTriggerEnter 的单次语义。</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public enum HitMode { Single, Interval }
|
||||
|
||||
public class HitBox : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private DamageSourceSO _defaultSource;
|
||||
[SerializeField] private float _hitCooldown = 0.1f;
|
||||
|
||||
[Header("命中节奏")]
|
||||
[Tooltip("Single=每次激活每个目标判一次;Interval=对停留目标按间隔持续重判(接触伤害/持续区域)。")]
|
||||
[SerializeField] private HitMode _hitMode = HitMode.Single;
|
||||
[Tooltip("Interval 模式下,对同一目标重复造成伤害的间隔(秒)。")]
|
||||
[SerializeField] private float _hitInterval = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// HitBox 标识符,供 PlayerAnimationEvents / EnemyAnimationEvents 按名称精确激活特定判定盒。
|
||||
/// 留空表示"无 Id";事件 payload 为空时将操作所有 HitBox。
|
||||
@@ -79,6 +96,7 @@ namespace BaseGames.Combat
|
||||
// 每次激活清空当前激活期已命中目标集合(防止连击连段导致同一阶段多次命中目标)
|
||||
_hitThisActivation.Clear();
|
||||
_hitCooldownTimers.Clear();
|
||||
_intervalTargets.Clear();
|
||||
}
|
||||
|
||||
public void Deactivate()
|
||||
@@ -88,6 +106,7 @@ namespace BaseGames.Combat
|
||||
foreach (var proxy in _proxies) proxy.SetEnabled(false);
|
||||
_hitThisActivation.Clear();
|
||||
_hitCooldownTimers.Clear();
|
||||
_intervalTargets.Clear();
|
||||
}
|
||||
|
||||
/// <summary>仅替换当前 DamageSource(不改变激活状态,供 PlayerCombat 连击段切换)。</summary>
|
||||
@@ -123,6 +142,31 @@ namespace BaseGames.Combat
|
||||
foreach (var proxy in _proxies) proxy.SetEnabled(false);
|
||||
_hitThisActivation.Clear();
|
||||
_hitCooldownTimers.Clear();
|
||||
_intervalTargets.Clear();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 仅 Interval 模式需要轮询:对仍停留在判定盒内的目标按间隔重判
|
||||
if (!_isActive || _hitMode != HitMode.Interval || _intervalTargets.Count == 0) return;
|
||||
|
||||
float now = Time.time;
|
||||
_intervalTickBuffer.Clear();
|
||||
foreach (var kv in _intervalTargets)
|
||||
if (now - kv.Value >= _hitInterval) _intervalTickBuffer.Add(kv.Key);
|
||||
|
||||
for (int i = 0; i < _intervalTickBuffer.Count; i++)
|
||||
{
|
||||
Collider2D col = _intervalTickBuffer[i];
|
||||
// 目标被销毁/禁用:移除占用记录(Exit 可能因对象失活而未触发)
|
||||
if (col == null || !col.isActiveAndEnabled)
|
||||
{
|
||||
_intervalTargets.Remove(col);
|
||||
continue;
|
||||
}
|
||||
_intervalTargets[col] = now;
|
||||
DealDamage(col, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other) => HandleTriggerEnter(other, null);
|
||||
@@ -137,9 +181,26 @@ namespace BaseGames.Combat
|
||||
Debug.LogWarning($"[HitBox] {name}: 无 DamageSourceSO,跳过命中。", this);
|
||||
return;
|
||||
}
|
||||
// 同一激活期防止对同一 Collider 重复命中(一次攻击每个目标至多命中一次)
|
||||
if (_hitMode == HitMode.Interval)
|
||||
{
|
||||
// 周期接触模式:记录占用并立即结算一次;后续由 Update 按间隔对仍停留的目标重判。
|
||||
_intervalTargets[other] = Time.time;
|
||||
DealDamage(other, sourceCollider);
|
||||
return;
|
||||
}
|
||||
|
||||
// Single 模式:同一激活期同一 Collider 只命中一次(一次攻击每个目标至多命中一次)+ 抖动冷却
|
||||
if (!_hitThisActivation.Add(other)) return;
|
||||
if (!CheckCooldown(other)) return;
|
||||
DealDamage(other, sourceCollider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实际伤害结算:自身排除 → 拼刀 → HurtBox → IBreakable。
|
||||
/// 由 Single 模式 Enter、Interval 模式 Enter 与 Update 共同调用。
|
||||
/// </summary>
|
||||
private void DealDamage(Collider2D other, Collider2D sourceCollider)
|
||||
{
|
||||
// 排除自身:攻击方与受击方属于同一根 GameObject 时跳过(防止近战 EnemyHitBox 命中同一敌人的 EnemyHurtBox)
|
||||
if (other.transform.root == _attackerTransform.root) return;
|
||||
|
||||
@@ -191,14 +252,30 @@ namespace BaseGames.Combat
|
||||
private readonly HashSet<Collider2D> _hitThisActivation = new(8);
|
||||
// ── 同目标多帧命中冷却(防止 Trigger 处于重叠状态时重入等殊情导致的连击)──
|
||||
private readonly Dictionary<Collider2D, float> _hitCooldownTimers = new(8);
|
||||
// ── Interval 模式:当前停留在判定盒内的目标 → 上次命中时间(Enter 加入 / Exit 移除)──
|
||||
private readonly Dictionary<Collider2D, float> _intervalTargets = new(8);
|
||||
// Update 轮询时的临时缓冲(避免遍历字典时修改 + 降低 GC)
|
||||
private readonly List<Collider2D> _intervalTickBuffer = new(8);
|
||||
|
||||
/// <summary>
|
||||
/// 配置为周期接触伤害模式(BodyContactDamage 等持续接触源在 OnEnable 时调用)。
|
||||
/// 使持续接触的判定盒按 <paramref name="interval"/> 对停留目标重复造成伤害,
|
||||
/// 而不依赖 OnTriggerEnter 的单次语义。
|
||||
/// </summary>
|
||||
public void SetIntervalMode(float interval)
|
||||
{
|
||||
_hitMode = HitMode.Interval;
|
||||
if (interval > 0f) _hitInterval = interval;
|
||||
}
|
||||
|
||||
/// <summary>代理出口:由 HitBoxColliderProxy 或本节点 OnTriggerExit2D 转发。</summary>
|
||||
internal void HandleTriggerExit(Collider2D other)
|
||||
{
|
||||
// 目标离开判定区域时清除其冷却记录,防止持续激活的 HitBox(环境危险等)
|
||||
// 目标离开判定区域时清除其冷却记录与占用记录,防止持续激活的 HitBox(环境危险等)
|
||||
// 因有效目标持续流动而无限积累已离场对象。
|
||||
// 注意:_hitThisActivation 刻意保留,确保同一激活期内不重复命中。
|
||||
_hitCooldownTimers.Remove(other);
|
||||
_intervalTargets.Remove(other);
|
||||
}
|
||||
|
||||
private bool CheckCooldown(Collider2D other)
|
||||
|
||||
Reference in New Issue
Block a user