- 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.
308 lines
14 KiB
C#
308 lines
14 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using BaseGames.Core;
|
||
|
||
namespace BaseGames.Combat
|
||
{
|
||
/// <summary>
|
||
/// 攻击判定盒。挂载在武器 Prefab 或技能 HitBox Prefab 的子节点上。
|
||
/// 直接挂在 Player Prefab 子节点 [HitBoxGround/Up/Down/Air]。
|
||
/// Collider2D 需设 IsTrigger = true,Layer = 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;
|
||
// 缓存 IClashService:OnTriggerEnter2D 为物理热路径,避免每次调用 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;
|
||
|
||
// ⚡ 零 GC:struct 工厂,运行时字段内联传入
|
||
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
|
||
}
|
||
}
|