Files
zeling_v2/Assets/_Game/Scripts/Combat/HitBox.cs
Joywayer 47bdc67cdf feat: Implement DownDash ability and related systems
- Added DownDash ability with cooldown and speed configuration.
- Introduced DownDashState to handle down dashing mechanics, including gravity manipulation and animation playback.
- Updated PlayerMovement to support DownDash functionality.
- Enhanced PlayerStats to manage spring charge consumption and healing.
- Modified PlayerCombat and WeaponHitBoxInstance to support new hit confirmation events.
- Updated AbilityType to include new form types for character abilities.
- Improved Gizmos for better visualization of enemy detection and attack ranges.
- Added feedback systems for form switching in PlayerFeedback and IFeedbackPlayer.
- Refactored combat and movement states to accommodate new abilities and ensure smooth transitions.
2026-05-22 00:09:50 +08:00

308 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using UnityEngine;
using BaseGames.Core;
namespace BaseGames.Combat
{
/// <summary>
/// 攻击判定盒。挂载在武器 Prefab 或技能 HitBox Prefab 的子节点上。
/// 直接挂在 Player Prefab 子节点 [HitBoxGround/Up/Down/Air]。
/// Collider2D 需设 IsTrigger = trueLayer = PlayerHitBox 或 EnemyHitBox。
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class HitBox : MonoBehaviour
{
[SerializeField] private DamageSourceSO _defaultSource;
[SerializeField] private float _hitCooldown = 0.1f;
/// <summary>
/// HitBox 标识符,供 PlayerAnimationEvents / EnemyAnimationEvents 按名称精确激活特定判定盒。
/// 留空表示"无 Id";事件 payload 为空时将操作所有 HitBox。
/// </summary>
[SerializeField] private string _id = "";
public string Id => _id;
/// <summary>
/// 对立阵营 HitBox 所在的 Layer 掩码(用于拼刀检测)。
/// Inspector 中将 PlayerHitBox 与 EnemyHitBox 两个 Layer 均勾选。
/// </summary>
[SerializeField] private LayerMask _rivalHitBoxMask;
private DamageSourceSO _currentSource;
private Transform _attackerTransform;
private Rigidbody2D _ownerRigidbody;
private bool _isActive;
private IClashService _clashService;
private Collider2D _collider;
/// <summary>HitBox 当前是否激活(供 ClashResolver 查询)。</summary>
public bool IsActive => _isActive;
/// <summary>当前 Source 是否携带 CanClash 标记(供 ClashResolver 查询)。</summary>
public bool CanClash => _currentSource != null && _currentSource.Flags.HasFlag(DamageFlags.CanClash);
/// <summary>宿主角色的 Rigidbody2D用于拼刀弹开力计算。</summary>
public Rigidbody2D OwnerRigidbody => _ownerRigidbody;
// 拼刀检测所需的对立层掩码Inspector 配置)
/// <summary>命中确认委托PlayerCombat / EnemyCombat 订阅)。</summary>
public event System.Action<DamageInfo> OnHitConfirmed;
// 宿主投射物缓存Activate 时填入DamageInfo.SourceProjectile 写入用)
private Projectile _ownerProjectile;
/// <summary>
/// 激活 HitBox。source/attacker 均可选,未传则使用 Inspector 默认值。
/// ⚠️ 不存在 Activate(float duration) 重载。
/// </summary>
public void Activate(DamageSourceSO source = null, Transform attacker = null)
{
_currentSource = source ?? _defaultSource;
_attackerTransform = attacker ?? transform;
_isActive = true;
_collider.enabled = true;
// 缓存宿主 Rigidbody2D沿父层级向上查找
_ownerRigidbody = _attackerTransform.GetComponentInParent<Rigidbody2D>();
// 每次激活清空当前激活期已命中目标集合(防止连击连段导致同一阶段多次命中目标)
_hitThisActivation.Clear();
_hitCooldownTimers.Clear();
}
public void Deactivate()
{
_isActive = false;
_collider.enabled = false;
_hitThisActivation.Clear();
_hitCooldownTimers.Clear();
}
/// <summary>仅替换当前 DamageSource不改变激活状态供 PlayerCombat 连击段切换)。</summary>
public void SetDamageSource(DamageSourceSO source)
{
if (source != null) _currentSource = source;
}
private void Awake()
{
// 确保 Collider2D 是 Trigger
_collider = GetComponent<Collider2D>();
if (!_collider.isTrigger)
Debug.LogWarning($"[HitBox] {name}: Collider2D.isTrigger 应为 true。", this);
// 初始状态关闭碰撞体,防止未激活时产生物理检测
_collider.enabled = false;
// 缓存 IClashServiceOnTriggerEnter2D 为物理热路径,避免每次调用 Dictionary 查找
_clashService = ServiceLocator.GetOrDefault<IClashService>();
// 缓存宿主投射物(仅 Projectile GameObject 上挂载的 HitBox 非 null
_ownerProjectile = GetComponent<Projectile>();
}
private void OnDisable()
{
_isActive = false;
if (_collider != null) _collider.enabled = false;
_hitThisActivation.Clear();
_hitCooldownTimers.Clear();
}
private void OnTriggerExit2D(Collider2D other)
{
// 目标离开判定区域时清除其冷却记录,防止持续激活的 HitBox环境危险等
// 因有效目标持续流动而无限积累已离场对象。
// 注意_hitThisActivation 刻意保留,确保同一激活期内不重复命中。
_hitCooldownTimers.Remove(other);
}
private void OnTriggerEnter2D(Collider2D other) {
if (!_isActive) return;
if (_currentSource == null)
{
Debug.LogWarning($"[HitBox] {name}: 无 DamageSourceSO跳过命中。", this);
return;
}
// 同一激活期防止对同一 Collider 重复命中(一次攻击每个目标至多命中一次)
if (!_hitThisActivation.Add(other)) return;
if (!CheckCooldown(other)) return;
// 排除自身:攻击方与受击方属于同一根 GameObject 时跳过(防止近战 EnemyHitBox 命中同一敌人的 EnemyHurtBox
if (other.transform.root == _attackerTransform.root) return;
Vector2 knockDir = ((Vector2)other.bounds.center
- (Vector2)_attackerTransform.position).normalized;
// ⚡ 零 GCstruct 工厂,运行时字段内联传入
var info = DamageInfo.From(
_currentSource,
knockDir,
_attackerTransform.position,
_attackerTransform.gameObject.layer,
_ownerProjectile);
// ① 拼刀检测:当前 HitBox 携带 CanClash 标记,且碰到对立阵营的 HitBox 层
int otherLayer = other.gameObject.layer;
bool isRivalHitBoxLayer = (_rivalHitBoxMask.value & (1 << otherLayer)) != 0;
if (isRivalHitBoxLayer && CanClash)
{
var rivalHitBox = other.GetComponent<HitBox>();
if (rivalHitBox != null && rivalHitBox.IsActive && rivalHitBox.CanClash)
{
_clashService?.ResolveClash(this, rivalHitBox);
return; // 拼刀,中止伤害流水线
}
}
// ② 命中 HurtBox
var hurtBox = other.GetComponent<HurtBox>();
if (hurtBox != null)
{
// 用 HitBox 自身碰撞盒中心在 HurtBox 表面上的最近点作为受击位置。
// 对大体积/长条形受击体(如地刺),此点远比 HurtBox 节点中心更准确。
Vector3 hitPoint = other.ClosestPoint(_collider.bounds.center);
hurtBox.ReceiveDamage(info, hitPoint);
OnHitConfirmed?.Invoke(info);
return;
}
// ③ 命中 IBreakable机关/障碍物)
other.GetComponent<IBreakable>()?.TryInteract(info);
}
// ── 当前激活期已命中目标集合(防止复合子 Collider 导致同帧多次命中)────────────
private readonly HashSet<Collider2D> _hitThisActivation = new(8);
// ── 同目标多帧命中冷却(防止 Trigger 处于重叠状态时重入等殊情导致的连击)──
private readonly Dictionary<Collider2D, float> _hitCooldownTimers = new(8);
private bool CheckCooldown(Collider2D other)
{
float now = Time.time;
if (_hitCooldownTimers.TryGetValue(other, out float last) && now - last < _hitCooldown)
return false;
_hitCooldownTimers[other] = now;
return true;
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
var col = GetComponent<Collider2D>();
if (col == null) return;
Color fill = _isActive ? new Color(1f, 0.15f, 0.15f, 0.25f) : new Color(1f, 0.15f, 0.15f, 0.05f);
Color outline = _isActive ? new Color(1f, 0.15f, 0.15f, 0.90f) : new Color(1f, 0.15f, 0.15f, 0.25f);
DrawCollider2DFilled(col, fill, outline);
}
// ── Gizmo 辅助(填充 + 轮廓,不依赖外部工具类)──────────────────────────
/// <summary>根据 Collider2D 类型绘制带填充色和轮廓的 2D Gizmo供 HurtBox 等复用)。</summary>
public static void DrawCollider2DFilled(Collider2D col, Color fill, Color outline)
{
var prevMatrix = UnityEditor.Handles.matrix;
UnityEditor.Handles.matrix = col.transform.localToWorldMatrix;
switch (col)
{
case BoxCollider2D box:
DrawFilledRect2D(box.offset, box.size, fill, outline);
break;
case CircleCollider2D circle:
DrawFilledCircle2D(circle.offset, circle.radius, fill, outline);
break;
case CapsuleCollider2D caps:
DrawFilledCapsule2D(caps.offset, caps.size, caps.direction, fill, outline);
break;
case PolygonCollider2D poly:
for (int p = 0; p < poly.pathCount; p++)
DrawFilledPolygonPath2D(poly.GetPath(p), fill, outline);
break;
default:
UnityEditor.Handles.matrix = Matrix4x4.identity;
DrawFilledRect2D(col.bounds.center, col.bounds.size, fill, outline);
break;
}
UnityEditor.Handles.matrix = prevMatrix;
}
/// <summary>向后兼容的线框接口(内部改调 DrawCollider2DFilled。</summary>
public static void DrawCollider2DWire(Collider2D col)
=> DrawCollider2DFilled(col, new Color(1f, 1f, 1f, 0.05f), new Color(1f, 1f, 1f, 0.8f));
private static void DrawFilledRect2D(Vector2 center, Vector2 size, Color fill, Color outline)
{
float hx = size.x * 0.5f, hy = size.y * 0.5f;
var verts = new Vector3[]
{
new Vector3(center.x - hx, center.y + hy, 0f),
new Vector3(center.x + hx, center.y + hy, 0f),
new Vector3(center.x + hx, center.y - hy, 0f),
new Vector3(center.x - hx, center.y - hy, 0f),
};
UnityEditor.Handles.DrawSolidRectangleWithOutline(verts, fill, outline);
}
private static void DrawFilledCircle2D(Vector2 center, float radius, Color fill, Color outline)
{
Vector3 c = new Vector3(center.x, center.y, 0f);
UnityEditor.Handles.color = fill;
UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, radius);
UnityEditor.Handles.color = outline;
UnityEditor.Handles.DrawWireDisc(c, Vector3.back, radius);
}
private static readonly System.Collections.Generic.List<Vector3> _capsuleVertsBuf
= new System.Collections.Generic.List<Vector3>(64);
private static void DrawFilledCapsule2D(Vector2 offset, Vector2 size,
CapsuleDirection2D dir, Color fill, Color outline)
{
bool vert = dir == CapsuleDirection2D.Vertical;
float radius = vert ? size.x * 0.5f : size.y * 0.5f;
float half = Mathf.Max(0f, (vert ? size.y : size.x) * 0.5f - radius);
Vector2 axis = vert ? Vector2.up : Vector2.right;
Vector2 capA = offset + axis * half; // 顶部(竖向)或右端(横向)圆心
Vector2 capB = offset - axis * half; // 底部(竖向)或左端(横向)圆心
const int segs = 12;
_capsuleVertsBuf.Clear();
float baseA = vert ? 0f : -Mathf.PI * 0.5f;
for (int i = 0; i <= segs; i++)
{
float a = baseA + (float)i / segs * Mathf.PI;
_capsuleVertsBuf.Add(new Vector3(capA.x + Mathf.Cos(a) * radius,
capA.y + Mathf.Sin(a) * radius, 0f));
}
float baseB = vert ? Mathf.PI : Mathf.PI * 0.5f;
for (int i = 0; i <= segs; i++)
{
float a = baseB + (float)i / segs * Mathf.PI;
_capsuleVertsBuf.Add(new Vector3(capB.x + Mathf.Cos(a) * radius,
capB.y + Mathf.Sin(a) * radius, 0f));
}
var arr = _capsuleVertsBuf.ToArray();
UnityEditor.Handles.color = fill;
UnityEditor.Handles.DrawAAConvexPolygon(arr);
UnityEditor.Handles.color = outline;
for (int i = 0; i < arr.Length; i++)
UnityEditor.Handles.DrawLine(arr[i], arr[(i + 1) % arr.Length]);
}
private static void DrawFilledPolygonPath2D(Vector2[] path, Color fill, Color outline)
{
if (path == null || path.Length < 3) return;
var verts = System.Array.ConvertAll(path, p => new Vector3(p.x, p.y, 0f));
UnityEditor.Handles.color = fill;
UnityEditor.Handles.DrawAAConvexPolygon(verts); // 凹多边形仅轮廓准确,填充近似
UnityEditor.Handles.color = outline;
for (int i = 0; i < verts.Length; i++)
UnityEditor.Handles.DrawLine(verts[i], verts[(i + 1) % verts.Length]);
}
#endif
}
}