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:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2cd6fa1109af6d04fafb6c1556eea81d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user