feat: Add HurtBoxOwnerGuard to prevent multiple damage registrations from the same HitBox activation

- Implemented HurtBoxOwnerGuard to ensure that multiple HurtBoxes on the same character do not register damage multiple times during a single HitBox activation.
- Added custom editor for HitBox to facilitate the creation of shape colliders with HitBoxColliderProxy.
- Introduced PhysicsPerceptionSystem for enemy perception, supporting multiple detection modes including RangeCircle, BatchLOS, FanCast, and BoxCast.
- Created EnemyPatrolZone to define patrol and chase areas for enemies, allowing for shared zones among multiple enemies.
- Added BD_IsOutsideZone conditional task for Behavior Designer to check if an enemy or player is outside a defined patrol zone.
This commit is contained in:
2026-06-02 16:10:44 +08:00
parent bcd8b0e90b
commit 06048c966a
47 changed files with 1912 additions and 1195 deletions

View File

@@ -0,0 +1,227 @@
using System.Text;
using UnityEditor;
using UnityEngine;
using BaseGames.Enemies.Perception;
namespace BaseGames.Editor
{
[CustomEditor(typeof(PhysicsPerceptionSystem))]
public sealed class PhysicsPerceptionSystemEditor : UnityEditor.Editor
{
// ── Scene 视图 Gizmo ──────────────────────────────────────────────────
[DrawGizmo(GizmoType.NonSelected | GizmoType.Selected | GizmoType.InSelectionHierarchy)]
static void DrawGizmos(PhysicsPerceptionSystem system, GizmoType gizmoType)
{
var slots = system.EditorSlots;
if (slots == null) return;
bool isSelected = (gizmoType & (GizmoType.Selected | GizmoType.InSelectionHierarchy)) != 0;
Vector3 rootPos = system.transform.position;
float facingSign = system.transform.localScale.x < 0f ? -1f : 1f;
foreach (var slot in slots)
{
if (string.IsNullOrEmpty(slot.slotName)) continue;
Color baseColor = ResolveGizmoColor(slot.gizmoColor);
Color fill = baseColor; fill.a = isSelected ? 0.12f : 0.04f;
Color outline = baseColor; outline.a = isSelected ? 0.90f : 0.40f;
// 每个 Slot 独立检测原点X 随朝向翻转)
Vector3 slotCenter = rootPos + new Vector3(slot.offset.x * facingSign, slot.offset.y, 0f);
switch (slot.type)
{
case PhysicsPerceptionSystem.SlotType.RangeCircle:
if (slot.radius <= 0f) break;
Handles.color = fill;
Handles.DrawSolidDisc(slotCenter, Vector3.forward, slot.radius);
Handles.color = outline;
Handles.DrawWireDisc(slotCenter, Vector3.forward, slot.radius);
break;
case PhysicsPerceptionSystem.SlotType.FanCast:
if (slot.radius > 0f && slot.fanAngle > 0f)
{
DrawFanGizmo(slotCenter, facingSign, slot, fill, outline);
if (isSelected) DrawOriginDot(slotCenter, outline);
}
break;
case PhysicsPerceptionSystem.SlotType.BoxCast:
if (slot.boxSize.x > 0f && slot.boxSize.y > 0f)
{
DrawBoxGizmo(slotCenter, facingSign, slot, fill, outline);
if (isSelected) DrawOriginDot(slotCenter, outline);
}
break;
// BatchLOS: eye-position marker + optional range disc + runtime ray
case PhysicsPerceptionSystem.SlotType.BatchLOS:
DrawBatchLOSGizmo(system, slotCenter, slot, outline, isSelected);
break;
}
}
Handles.color = Color.white;
}
/// <summary>在 Slot 原点处画一个小十字点,帮助可视化 offset 配置。</summary>
static void DrawOriginDot(Vector3 pos, Color color)
{
Handles.color = color;
Handles.DrawSolidDisc(pos, Vector3.forward, 0.04f);
}
/// <summary>
/// 仅当颜色字段为 Unity 默认值 Color(0,0,0,0)(全零 = 未配置)时,
/// 返回易辨识的紫色回退alpha=1用户明确设置的任何颜色包括近黑色均原样保留。
/// </summary>
static Color ResolveGizmoColor(Color c)
{
bool isDefault = c.r + c.g + c.b + c.a < 0.01f;
return isDefault ? new Color(0.85f, 0.3f, 1.0f, 1.0f) : c;
}
static void DrawBatchLOSGizmo(PhysicsPerceptionSystem system, Vector3 slotCenter,
PhysicsPerceptionSystem.PerceptionSlot slot, Color slotColor, bool isSelected)
{
// slotColor 已经过 ResolveGizmoColor 处理alpha 已由调用方设为 outline alpha。
// 所有 gizmo 元素统一画在 slotCenter由 slot.offset 控制),
// 与 LOSOrigin实际射线起点解耦——gizmo 跟 offset 走。
float facingSign = system.transform.localScale.x < 0f ? -1f : 1f;
// ── 最大检测范围圆slot.radius > 0 时)──
if (slot.radius > 0f)
{
Color fill = slotColor; fill.a = isSelected ? 0.08f : 0.03f;
Handles.color = fill;
Handles.DrawSolidDisc(slotCenter, Vector3.forward, slot.radius);
Color rim = slotColor; rim.a = isSelected ? 0.70f : 0.30f;
Handles.color = rim;
Handles.DrawWireDisc(slotCenter, Vector3.forward, slot.radius);
}
// ── 眼睛中心圆点 ──
Handles.color = slotColor;
Handles.DrawSolidDisc(slotCenter, Vector3.forward, 0.08f);
// ── 朝向指示箭头(沿 facingSign 方向,明确"方向性"视线感知)──
float arrowLen = slot.radius > 0f ? Mathf.Min(slot.radius * 0.6f, 0.6f) : 0.4f;
Vector3 fwdArrow = new Vector3(facingSign * arrowLen, 0f, 0f);
Handles.DrawLine(slotCenter, slotCenter + fwdArrow);
Vector3 arrowTip = slotCenter + fwdArrow;
float headLen = 0.08f;
Handles.DrawLine(arrowTip, arrowTip + new Vector3(-facingSign * headLen, headLen, 0f));
Handles.DrawLine(arrowTip, arrowTip + new Vector3(-facingSign * headLen, -headLen, 0f));
// ── 放射线(仅选中时显示,避免杂乱)──
if (isSelected)
{
float innerR = 0.12f;
float outerR = slot.radius > 0f ? Mathf.Min(slot.radius, 0.30f) : 0.26f;
for (int i = 0; i < 8; i++)
{
float deg = i * 45f;
float rad = deg * Mathf.Deg2Rad;
Vector3 dir = new Vector3(Mathf.Cos(rad), Mathf.Sin(rad), 0f);
Handles.DrawLine(slotCenter + dir * innerR, slotCenter + dir * outerR);
}
}
// ── 运行时:视线连线;绿 = 可见 / 红 = 遮挡 ──
if (!Application.isPlaying) return;
var owner = system.EditorOwner ?? system.GetComponentInParent<BaseGames.Enemies.EnemyBase>();
if (owner == null || owner.PlayerTransform == null) return;
bool visible = owner.IsPlayerVisible();
Color rayCol = visible
? new Color(0.2f, 1.0f, 0.3f, 0.9f)
: new Color(1.0f, 0.3f, 0.3f, 0.45f);
Handles.color = rayCol;
Handles.DrawDottedLine(slotCenter, owner.PlayerTransform.position, 3f);
if (visible)
Handles.DrawSolidDisc(owner.PlayerTransform.position, Vector3.forward, 0.09f);
}
static void DrawFanGizmo(Vector3 center, float facingSign,
PhysicsPerceptionSystem.PerceptionSlot slot, Color fill, Color outline)
{
float halfAngle = slot.fanAngle * 0.5f;
Vector3 fromDir = RotateVec3(new Vector3(facingSign, 0f, 0f), -halfAngle);
Handles.color = fill;
Handles.DrawSolidArc(center, Vector3.forward, fromDir, slot.fanAngle, slot.radius);
Handles.color = outline;
Handles.DrawWireArc(center, Vector3.forward, fromDir, slot.fanAngle, slot.radius);
// Edge rays for clarity
Vector3 edgeL = RotateVec3(new Vector3(facingSign, 0f, 0f), -halfAngle) * slot.radius;
Vector3 edgeR = RotateVec3(new Vector3(facingSign, 0f, 0f), halfAngle) * slot.radius;
Handles.DrawLine(center, center + edgeL);
Handles.DrawLine(center, center + edgeR);
}
static void DrawBoxGizmo(Vector3 center, float facingSign,
PhysicsPerceptionSystem.PerceptionSlot slot, Color fill, Color outline)
{
Vector3 boxCenter = center + new Vector3(slot.boxOffset.x * facingSign, slot.boxOffset.y, 0f);
float hw = slot.boxSize.x * 0.5f;
float hh = slot.boxSize.y * 0.5f;
Vector3[] corners =
{
boxCenter + new Vector3(-hw, -hh, 0f),
boxCenter + new Vector3( hw, -hh, 0f),
boxCenter + new Vector3( hw, hh, 0f),
boxCenter + new Vector3(-hw, hh, 0f),
};
Handles.DrawSolidRectangleWithOutline(corners, fill, outline);
}
static Vector3 RotateVec3(Vector3 v, float angleDeg)
{
float rad = angleDeg * Mathf.Deg2Rad;
float cos = Mathf.Cos(rad);
float sin = Mathf.Sin(rad);
return new Vector3(cos * v.x - sin * v.y, sin * v.x + cos * v.y, 0f);
}
// ── Inspector ─────────────────────────────────────────────────────────
public override void OnInspectorGUI()
{
DrawDefaultInspector();
if (!Application.isPlaying) return;
var system = (PhysicsPerceptionSystem)target;
var detected = system.EditorDetected;
if (detected == null || detected.Count == 0) return;
EditorGUILayout.Space();
EditorGUILayout.LabelField("── 实时检测结果 ──", EditorStyles.boldLabel);
var sb = new StringBuilder();
foreach (var kvp in detected)
{
sb.Clear();
if (kvp.Value.Count > 0)
{
sb.Append("✓");
foreach (var go in kvp.Value)
sb.Append(' ').Append(go != null ? go.name : "null");
}
EditorGUILayout.LabelField(kvp.Key, kvp.Value.Count > 0 ? sb.ToString() : "—");
}
Repaint();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2cd6fa1109af6d04fafb6c1556eea81d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: