feat: Enhance Physics Perception System with new detection modes and performance optimizations
- Updated PhysicsPerceptionSystem to support seven detection modes: RangeCircle, BatchLOS, FanCast, BoxCast, Sight, RayCast, and TriggerZone. - Improved documentation for each detection mode, including performance optimization strategies. - Introduced PerceptionTriggerProxy for event-driven detection in TriggerZone slots. - Added SightBatchSystem to manage Sight slots efficiently, reducing CPU spikes during high enemy counts. - Updated SensorSlotNames to reflect new detection modes and their purposes. - Enhanced internal logic for detecting targets and managing detection events.
This commit is contained in:
@@ -1,13 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using BaseGames.Enemies.Perception;
|
||||
using SlotType = BaseGames.Enemies.Perception.PhysicsPerceptionSystem.SlotType;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
[CustomEditor(typeof(PhysicsPerceptionSystem))]
|
||||
public sealed class PhysicsPerceptionSystemEditor : UnityEditor.Editor
|
||||
{
|
||||
// ── Inspector state ───────────────────────────────────────────────────
|
||||
|
||||
private SerializedProperty _slotsProp;
|
||||
private readonly List<bool> _foldouts = new List<bool>();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_slotsProp = serializedObject.FindProperty("_slots");
|
||||
SyncFoldouts();
|
||||
}
|
||||
|
||||
private void SyncFoldouts()
|
||||
{
|
||||
if (_slotsProp == null) return;
|
||||
while (_foldouts.Count < _slotsProp.arraySize) _foldouts.Add(true);
|
||||
while (_foldouts.Count > _slotsProp.arraySize) _foldouts.RemoveAt(_foldouts.Count - 1);
|
||||
}
|
||||
|
||||
// ── Scene 视图 Gizmo ──────────────────────────────────────────────────
|
||||
|
||||
[DrawGizmo(GizmoType.NonSelected | GizmoType.Selected | GizmoType.InSelectionHierarchy)]
|
||||
@@ -25,15 +45,14 @@ namespace BaseGames.Editor
|
||||
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;
|
||||
Color fill = new Color(baseColor.r, baseColor.g, baseColor.b, isSelected ? 0.12f : 0.04f);
|
||||
Color outline = new Color(baseColor.r, baseColor.g, baseColor.b, 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:
|
||||
case SlotType.RangeCircle:
|
||||
if (slot.radius <= 0f) break;
|
||||
Handles.color = fill;
|
||||
Handles.DrawSolidDisc(slotCenter, Vector3.forward, slot.radius);
|
||||
@@ -41,7 +60,7 @@ namespace BaseGames.Editor
|
||||
Handles.DrawWireDisc(slotCenter, Vector3.forward, slot.radius);
|
||||
break;
|
||||
|
||||
case PhysicsPerceptionSystem.SlotType.FanCast:
|
||||
case SlotType.FanCast:
|
||||
if (slot.radius > 0f && slot.fanAngle > 0f)
|
||||
{
|
||||
DrawFanGizmo(slotCenter, facingSign, slot, fill, outline);
|
||||
@@ -49,7 +68,7 @@ namespace BaseGames.Editor
|
||||
}
|
||||
break;
|
||||
|
||||
case PhysicsPerceptionSystem.SlotType.BoxCast:
|
||||
case SlotType.BoxCast:
|
||||
if (slot.boxSize.x > 0f && slot.boxSize.y > 0f)
|
||||
{
|
||||
DrawBoxGizmo(slotCenter, facingSign, slot, fill, outline);
|
||||
@@ -57,17 +76,27 @@ namespace BaseGames.Editor
|
||||
}
|
||||
break;
|
||||
|
||||
// BatchLOS: eye-position marker + optional range disc + runtime ray
|
||||
case PhysicsPerceptionSystem.SlotType.BatchLOS:
|
||||
case SlotType.BatchLOS:
|
||||
DrawBatchLOSGizmo(system, slotCenter, slot, outline, isSelected);
|
||||
break;
|
||||
|
||||
case SlotType.Sight:
|
||||
DrawSightGizmo(system, slotCenter, facingSign, slot, fill, outline, isSelected);
|
||||
break;
|
||||
|
||||
case SlotType.RayCast:
|
||||
DrawRayCastGizmo(slotCenter, facingSign, slot, outline, isSelected);
|
||||
break;
|
||||
|
||||
case SlotType.TriggerZone:
|
||||
DrawTriggerZoneGizmo(slotCenter, slot, outline, isSelected);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Handles.color = Color.white;
|
||||
}
|
||||
|
||||
/// <summary>在 Slot 原点处画一个小十字点,帮助可视化 offset 配置。</summary>
|
||||
static void DrawOriginDot(Vector3 pos, Color color)
|
||||
{
|
||||
Handles.color = color;
|
||||
@@ -75,80 +104,217 @@ namespace BaseGames.Editor
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅当颜色字段为 Unity 默认值 Color(0,0,0,0)(全零 = 未配置)时,
|
||||
/// 返回易辨识的紫色回退(alpha=1);用户明确设置的任何颜色(包括近黑色)均原样保留。
|
||||
/// Color(0,0,0,0) 是未配置的默认值,返回紫色回退;用户设置的颜色原样保留。
|
||||
/// </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 Color ResolveGizmoColor(Color c) =>
|
||||
c.r + c.g + c.b + c.a < 0.01f ? 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;
|
||||
Color fill = new Color(slotColor.r, slotColor.g, slotColor.b, 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;
|
||||
Color rim = new Color(slotColor.r, slotColor.g, slotColor.b, 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);
|
||||
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 tip = slotCenter + fwdArrow;
|
||||
float headLen = 0.08f;
|
||||
Handles.DrawLine(tip, tip + new Vector3(-facingSign * headLen, headLen, 0f));
|
||||
Handles.DrawLine(tip, tip + new Vector3(-facingSign * headLen, -headLen, 0f));
|
||||
|
||||
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);
|
||||
float r = i * 45f * Mathf.Deg2Rad;
|
||||
var d = new Vector3(Mathf.Cos(r), Mathf.Sin(r), 0f);
|
||||
Handles.DrawLine(slotCenter + d * innerR, slotCenter + d * 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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sight 槽位 Gizmo:视野锥形(或全向圆形)+ 眼点 + LOS 能力提示射线 + 运行时检测连线。
|
||||
/// </summary>
|
||||
static void DrawSightGizmo(PhysicsPerceptionSystem system, Vector3 slotCenter, float facingSign,
|
||||
PhysicsPerceptionSystem.PerceptionSlot slot, Color fill, Color outline, bool isSelected)
|
||||
{
|
||||
if (slot.radius <= 0f) return;
|
||||
|
||||
bool hasCone = slot.fanAngle > 0f && slot.fanAngle < 360f;
|
||||
|
||||
if (hasCone)
|
||||
{
|
||||
DrawFanGizmo(slotCenter, facingSign, slot, fill, outline);
|
||||
}
|
||||
else
|
||||
{
|
||||
Handles.color = fill;
|
||||
Handles.DrawSolidDisc(slotCenter, Vector3.forward, slot.radius);
|
||||
Handles.color = outline;
|
||||
Handles.DrawWireDisc(slotCenter, Vector3.forward, slot.radius);
|
||||
}
|
||||
|
||||
// 眼点(视线传感器标志)
|
||||
Handles.color = outline;
|
||||
Handles.DrawSolidDisc(slotCenter, Vector3.forward, 0.07f);
|
||||
|
||||
// 选中时:视线射线提示(表明有遮挡检测能力)
|
||||
if (isSelected)
|
||||
{
|
||||
float innerR = 0.10f;
|
||||
float outerR = Mathf.Min(slot.radius * 0.35f, 0.40f);
|
||||
if (hasCone)
|
||||
{
|
||||
float halfAngle = slot.fanAngle * 0.5f;
|
||||
int lines = Mathf.Max(3, slot.fanRayCount > 0 ? slot.fanRayCount : 5);
|
||||
for (int i = 0; i <= lines; i++)
|
||||
{
|
||||
float t = (float)i / lines;
|
||||
float ang = Mathf.Lerp(-halfAngle, halfAngle, t);
|
||||
var dir = RotateVec3(new Vector3(facingSign, 0f, 0f), ang).normalized;
|
||||
Handles.DrawDottedLine(slotCenter + dir * innerR, slotCenter + dir * (outerR * 2.5f), 3f);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
float r = i * 45f * Mathf.Deg2Rad;
|
||||
var d = new Vector3(Mathf.Cos(r), Mathf.Sin(r), 0f);
|
||||
Handles.DrawDottedLine(slotCenter + d * innerR, slotCenter + d * (outerR * 2.5f), 3f);
|
||||
}
|
||||
}
|
||||
|
||||
// LOS 图标文本(Scene 视图)
|
||||
GUIStyle style = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
fontSize = 9,
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
normal = { textColor = new Color(outline.r, outline.g, outline.b, 0.85f) }
|
||||
};
|
||||
Handles.Label(slotCenter + new Vector3(0f, slot.radius + 0.2f, 0f), "LOS", style);
|
||||
}
|
||||
|
||||
// 运行时:绿线连到检测到的目标
|
||||
if (!Application.isPlaying) return;
|
||||
var detected = system.EditorDetected;
|
||||
if (detected == null || !detected.TryGetValue(slot.slotName, out var hits) || hits.Count == 0) return;
|
||||
|
||||
Color green = new Color(0.2f, 1.0f, 0.3f, 0.9f);
|
||||
Handles.color = green;
|
||||
foreach (var go in hits)
|
||||
{
|
||||
if (go == null) continue;
|
||||
Handles.DrawLine(slotCenter, go.transform.position);
|
||||
Handles.DrawSolidDisc(go.transform.position, Vector3.forward, 0.09f);
|
||||
}
|
||||
}
|
||||
|
||||
static void DrawRayCastGizmo(Vector3 slotCenter, float facingSign,
|
||||
PhysicsPerceptionSystem.PerceptionSlot slot, Color outline, bool isSelected)
|
||||
{
|
||||
if (slot.rayLength <= 0f) return;
|
||||
|
||||
Color lineColor = new Color(outline.r, outline.g, outline.b, isSelected ? 0.90f : 0.50f);
|
||||
Handles.color = lineColor;
|
||||
|
||||
// Base direction in local space with X flipped for facing
|
||||
Vector2 baseDir2D = slot.rayDirection.sqrMagnitude < 0.001f
|
||||
? Vector2.right
|
||||
: slot.rayDirection.normalized;
|
||||
var baseDir = new Vector3(baseDir2D.x * facingSign, baseDir2D.y, 0f).normalized;
|
||||
|
||||
float spread = Mathf.Clamp(slot.raySpread, 0f, 180f);
|
||||
int count = (spread > 0f && slot.rayCount > 1) ? Mathf.Clamp(slot.rayCount, 2, 9) : 1;
|
||||
|
||||
if (count == 1 || spread <= 0f)
|
||||
{
|
||||
Vector3 end = slotCenter + baseDir * slot.rayLength;
|
||||
Handles.DrawLine(slotCenter, end);
|
||||
DrawArrowHead(slotCenter, end, lineColor, 0.08f);
|
||||
}
|
||||
else
|
||||
{
|
||||
float halfSpread = spread * 0.5f;
|
||||
for (int r = 0; r < count; r++)
|
||||
{
|
||||
float t = (float)r / (count - 1);
|
||||
float ang = Mathf.Lerp(-halfSpread, halfSpread, t);
|
||||
var dir = RotateVec3(baseDir, ang).normalized;
|
||||
Vector3 end = slotCenter + dir * slot.rayLength;
|
||||
Handles.DrawLine(slotCenter, end);
|
||||
}
|
||||
// Highlight center ray
|
||||
Vector3 centerEnd = slotCenter + baseDir * slot.rayLength;
|
||||
Color bright = new Color(lineColor.r, lineColor.g, lineColor.b, 1f);
|
||||
DrawArrowHead(slotCenter, centerEnd, bright, 0.08f);
|
||||
}
|
||||
|
||||
// Origin dot
|
||||
Handles.color = lineColor;
|
||||
Handles.DrawSolidDisc(slotCenter, Vector3.forward, 0.05f);
|
||||
}
|
||||
|
||||
static void DrawArrowHead(Vector3 from, Vector3 tip, Color color, float size)
|
||||
{
|
||||
Handles.color = color;
|
||||
Vector3 dir = (tip - from).normalized;
|
||||
Handles.DrawLine(tip, tip - dir * size + new Vector3(-dir.y, dir.x, 0f) * size * 0.5f);
|
||||
Handles.DrawLine(tip, tip - dir * size + new Vector3( dir.y, -dir.x, 0f) * size * 0.5f);
|
||||
}
|
||||
|
||||
static void DrawTriggerZoneGizmo(Vector3 slotCenter,
|
||||
PhysicsPerceptionSystem.PerceptionSlot slot, Color outline, bool isSelected)
|
||||
{
|
||||
Color dotColor = new Color(outline.r, outline.g, outline.b, isSelected ? 0.90f : 0.55f);
|
||||
Handles.color = dotColor;
|
||||
Handles.DrawSolidDisc(slotCenter, Vector3.forward, 0.10f);
|
||||
|
||||
float ring = 0.22f;
|
||||
Handles.color = new Color(outline.r, outline.g, outline.b, isSelected ? 0.60f : 0.25f);
|
||||
Handles.DrawWireDisc(slotCenter, Vector3.forward, ring);
|
||||
Handles.DrawWireDisc(slotCenter, Vector3.forward, ring * 1.55f);
|
||||
|
||||
if (isSelected)
|
||||
{
|
||||
GUIStyle style = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
fontSize = 9,
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
normal = { textColor = new Color(outline.r, outline.g, outline.b, 0.85f) }
|
||||
};
|
||||
Handles.Label(slotCenter + new Vector3(0f, ring * 1.55f + 0.15f, 0f),
|
||||
string.IsNullOrEmpty(slot.slotName) ? "TZ" : $"TZ:{slot.slotName}", style);
|
||||
}
|
||||
}
|
||||
|
||||
static void DrawFanGizmo(Vector3 center, float facingSign,
|
||||
PhysicsPerceptionSystem.PerceptionSlot slot, Color fill, Color outline)
|
||||
{
|
||||
@@ -160,7 +326,6 @@ namespace BaseGames.Editor
|
||||
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);
|
||||
@@ -171,9 +336,8 @@ namespace BaseGames.Editor
|
||||
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;
|
||||
|
||||
float hw = slot.boxSize.x * 0.5f;
|
||||
float hh = slot.boxSize.y * 0.5f;
|
||||
Vector3[] corners =
|
||||
{
|
||||
boxCenter + new Vector3(-hw, -hh, 0f),
|
||||
@@ -181,33 +345,126 @@ namespace BaseGames.Editor
|
||||
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);
|
||||
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 ─────────────────────────────────────────────────────────
|
||||
// ── Custom Inspector ──────────────────────────────────────────────────
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
DrawDefaultInspector();
|
||||
serializedObject.Update();
|
||||
SyncFoldouts();
|
||||
|
||||
if (_slotsProp == null)
|
||||
{
|
||||
DrawDefaultInspector();
|
||||
return;
|
||||
}
|
||||
|
||||
EditorGUILayout.LabelField("感知槽位", EditorStyles.boldLabel);
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
SyncFoldouts();
|
||||
|
||||
int pendingMoveUp = -1;
|
||||
int pendingMoveDown = -1;
|
||||
int pendingDuplicate = -1;
|
||||
int pendingDelete = -1;
|
||||
|
||||
for (int i = 0; i < _slotsProp.arraySize; i++)
|
||||
{
|
||||
var elem = _slotsProp.GetArrayElementAtIndex(i);
|
||||
var nameProp = elem.FindPropertyRelative("slotName");
|
||||
var typeProp = elem.FindPropertyRelative("type");
|
||||
|
||||
var isDisabledProp = elem.FindPropertyRelative("isDisabled");
|
||||
var tickIntervalProp = elem.FindPropertyRelative("tickInterval");
|
||||
bool isDisabled = isDisabledProp != null && isDisabledProp.boolValue
|
||||
&& tickIntervalProp != null && tickIntervalProp.intValue > 0;
|
||||
string disabledTag = isDisabled ? " ⊘" : "";
|
||||
string label = string.IsNullOrEmpty(nameProp.stringValue)
|
||||
? $"Slot {i}{disabledTag}"
|
||||
: $"[{i}] {nameProp.stringValue} ({(SlotType)typeProp.enumValueIndex}){disabledTag}";
|
||||
|
||||
// ── 槽位标题行(折叠 + 操作按钮)──────────────────────────
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
_foldouts[i] = EditorGUILayout.Foldout(_foldouts[i], label, true);
|
||||
GUILayout.FlexibleSpace();
|
||||
using (new EditorGUI.DisabledScope(i == 0))
|
||||
if (GUILayout.Button("▲", EditorStyles.miniButton, GUILayout.Width(22))) pendingMoveUp = i;
|
||||
using (new EditorGUI.DisabledScope(i == _slotsProp.arraySize - 1))
|
||||
if (GUILayout.Button("▼", EditorStyles.miniButton, GUILayout.Width(22))) pendingMoveDown = i;
|
||||
if (GUILayout.Button("⊕", EditorStyles.miniButton, GUILayout.Width(22))) pendingDuplicate = i;
|
||||
Color _prevSlotBtnColor = GUI.color;
|
||||
GUI.color = new Color(1f, 0.5f, 0.5f);
|
||||
if (GUILayout.Button("✕", EditorStyles.miniButton, GUILayout.Width(22))) pendingDelete = i;
|
||||
GUI.color = _prevSlotBtnColor;
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
if (_foldouts[i])
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
DrawSlotFields(elem, (SlotType)typeProp.enumValueIndex, nameProp.stringValue);
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
EditorGUILayout.Space(1);
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
|
||||
// ── 底部工具栏:添加新槽位 ──────────────────────────────────────
|
||||
EditorGUILayout.Space(2);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
if (GUILayout.Button("⊕ 添加槽位", GUILayout.Width(120)))
|
||||
{
|
||||
_slotsProp.InsertArrayElementAtIndex(_slotsProp.arraySize);
|
||||
_foldouts.Add(true);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
// ── 延迟执行槽位操作(避免在 GUI 循环中修改数组)──────────────
|
||||
if (pendingDelete >= 0)
|
||||
{
|
||||
_slotsProp.DeleteArrayElementAtIndex(pendingDelete);
|
||||
SyncFoldouts();
|
||||
}
|
||||
else if (pendingDuplicate >= 0)
|
||||
{
|
||||
_slotsProp.InsertArrayElementAtIndex(pendingDuplicate);
|
||||
_foldouts.Insert(pendingDuplicate + 1, true);
|
||||
}
|
||||
else if (pendingMoveUp >= 0)
|
||||
{
|
||||
_slotsProp.MoveArrayElement(pendingMoveUp, pendingMoveUp - 1);
|
||||
(_foldouts[pendingMoveUp], _foldouts[pendingMoveUp - 1]) =
|
||||
(_foldouts[pendingMoveUp - 1], _foldouts[pendingMoveUp]);
|
||||
}
|
||||
else if (pendingMoveDown >= 0)
|
||||
{
|
||||
_slotsProp.MoveArrayElement(pendingMoveDown, pendingMoveDown + 1);
|
||||
(_foldouts[pendingMoveDown], _foldouts[pendingMoveDown + 1]) =
|
||||
(_foldouts[pendingMoveDown + 1], _foldouts[pendingMoveDown]);
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
// ── 运行时检测结果 ──
|
||||
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)
|
||||
{
|
||||
@@ -220,8 +477,195 @@ namespace BaseGames.Editor
|
||||
}
|
||||
EditorGUILayout.LabelField(kvp.Key, kvp.Value.Count > 0 ? sb.ToString() : "—");
|
||||
}
|
||||
|
||||
Repaint();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按槽位类型条件渲染字段——每个 SlotType 只显示与其相关的参数。
|
||||
/// </summary>
|
||||
private void DrawSlotFields(SerializedProperty elem, SlotType slotType, string slotName)
|
||||
{
|
||||
// ── 通用字段(所有类型)────────────────────────────────────────
|
||||
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("slotName"),
|
||||
new GUIContent("槽位名称", "与 SensorSlotNames 常量保持一致"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("type"),
|
||||
new GUIContent("检测类型"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("offset"),
|
||||
new GUIContent("原点偏移", "相对于 transform.position,X 随朝向自动翻转"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("gizmoColor"),
|
||||
new GUIContent("Gizmo 颜色", "全透明黑色 = 自动使用紫色回退"));
|
||||
|
||||
EditorGUILayout.Space(2);
|
||||
EditorGUILayout.LabelField("─ 运行时控制 ─", EditorStyles.miniLabel);
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("isDisabled"),
|
||||
new GUIContent("禁用", "勾选后停用本槽位(tickInterval > 0 时生效)"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("tickInterval"),
|
||||
new GUIContent("刷新间隔 (帧)", "0 = 历史兼容(每帧,isDisabled 无效)\n1 = 每帧\n3 = 每 3 帧(Sight 推荐)\n同间隔槽位自动错帧执行"));
|
||||
|
||||
EditorGUILayout.Space(2);
|
||||
|
||||
// ── 按类型显示相关字段 ─────────────────────────────────────────
|
||||
|
||||
switch (slotType)
|
||||
{
|
||||
case SlotType.RangeCircle:
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("radius"),
|
||||
new GUIContent("半径 (m)", "OverlapCircle 检测半径"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("detectLayer"),
|
||||
new GUIContent("检测层"));
|
||||
break;
|
||||
|
||||
case SlotType.BatchLOS:
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("radius"),
|
||||
new GUIContent("检测半径 (m)", "OverlapCircle 半径,必须 > 0"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("detectLayer"),
|
||||
new GUIContent("检测层", "目标所在层(通常为 Player 层)"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("losBlockMask"),
|
||||
new GUIContent("视线遮挡层", "遮挡射线的层(Platform / Wall);留 0 则不做遮挡检测"));
|
||||
break;
|
||||
|
||||
case SlotType.FanCast:
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("radius"),
|
||||
new GUIContent("半径 (m)", "扇形检测半径"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("detectLayer"),
|
||||
new GUIContent("检测层"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("fanAngle"),
|
||||
new GUIContent("扇形角度 (°)", "以朝向为中轴,左右对称展开"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("fanRayCount"),
|
||||
new GUIContent("Gizmo 分隔线数", "仅影响 Scene 视图,不影响检测精度(建议 3–9)"));
|
||||
break;
|
||||
|
||||
case SlotType.BoxCast:
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("detectLayer"),
|
||||
new GUIContent("检测层"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("boxSize"),
|
||||
new GUIContent("矩形尺寸 (m)", "宽 × 高"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("boxOffset"),
|
||||
new GUIContent("矩形偏移", "相对于原点偏移,X 随朝向翻转"));
|
||||
break;
|
||||
|
||||
case SlotType.Sight:
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("radius"),
|
||||
new GUIContent("视野半径 (m)", "Sight 传感器检测半径"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("detectLayer"),
|
||||
new GUIContent("检测层"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("fanAngle"),
|
||||
new GUIContent("视锥角度 (°)", "0 或 ≥ 360 = 全向 360°;否则以朝向为中轴左右展开"));
|
||||
EditorGUILayout.Space(2);
|
||||
EditorGUILayout.LabelField("─ LOS 遮挡设置(Sight 专用)─", EditorStyles.miniLabel);
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("losBlockMask"),
|
||||
new GUIContent("遮挡层", "遮挡物所在层(Platform / Wall / Ground);设为 0 则视线始终通过"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("losRayCount"),
|
||||
new GUIContent("LOS 采样点数", "1=中心 3=中心+上+下(推荐) 5=中心+上下左右"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("losMinVisibility"),
|
||||
new GUIContent("最低可见度 (0–1)", "0=任一采样点通过即可 1=全部通过(严格)"));
|
||||
break;
|
||||
|
||||
case SlotType.RayCast:
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("rayDirection"),
|
||||
new GUIContent("射线方向 (本地空间)", "X 分量随朝向自动翻转;(1,0)=正前方,(0,-1)=正下方"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("rayLength"),
|
||||
new GUIContent("射线长度 (m)"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("detectLayer"),
|
||||
new GUIContent("检测层"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("obstructLayer"),
|
||||
new GUIContent("遮挡层", "射线碰到此层后立即阻断(设为 0 = 射线穿透所有物体)"));
|
||||
EditorGUILayout.Space(2);
|
||||
EditorGUILayout.LabelField("─ 扩散(多射线)─", EditorStyles.miniLabel);
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("raySpread"),
|
||||
new GUIContent("扩散角 (°)", "0 = 单根射线;>0 时在此角度范围内均匀分布 N 根射线"));
|
||||
EditorGUILayout.PropertyField(elem.FindPropertyRelative("rayCount"),
|
||||
new GUIContent("射线根数", "raySpread > 0 时生效,建议 3–9"));
|
||||
break;
|
||||
|
||||
case SlotType.TriggerZone:
|
||||
EditorGUILayout.HelpBox(
|
||||
"TriggerZone 槽位为事件驱动,零轮询开销。\n" +
|
||||
"需在此 GameObject 的子节点上挂载 PerceptionTriggerProxy,\n" +
|
||||
"设置相同 slotName,且 Collider2D.isTrigger = true。",
|
||||
MessageType.Info);
|
||||
EditorGUILayout.Space(4);
|
||||
DrawTriggerZoneManagement(slotName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTriggerZoneManagement(string slotName)
|
||||
{
|
||||
var system = (PhysicsPerceptionSystem)target;
|
||||
|
||||
EditorGUILayout.LabelField("── TriggerZone 子节点 ──", EditorStyles.miniLabel);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(slotName))
|
||||
{
|
||||
EditorGUILayout.HelpBox("请先填写槽位名称,再管理代理节点。", MessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找所有匹配的代理节点
|
||||
var allProxies = system.GetComponentsInChildren<PerceptionTriggerProxy>(true);
|
||||
var matchList = new List<PerceptionTriggerProxy>();
|
||||
foreach (var p in allProxies)
|
||||
if (p.slotName == slotName) matchList.Add(p);
|
||||
|
||||
if (matchList.Count == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox($"未找到 slotName = \"{slotName}\" 的代理节点。", MessageType.Warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var proxy in matchList)
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
EditorGUILayout.ObjectField(proxy, typeof(PerceptionTriggerProxy), true);
|
||||
|
||||
if (GUILayout.Button("选中", EditorStyles.miniButton, GUILayout.Width(44)))
|
||||
{
|
||||
Selection.activeGameObject = proxy.gameObject;
|
||||
EditorGUIUtility.PingObject(proxy.gameObject);
|
||||
}
|
||||
|
||||
var col = proxy.GetComponent<Collider2D>();
|
||||
bool badTrigger = col == null || !col.isTrigger;
|
||||
Color prevC = GUI.color;
|
||||
GUI.color = badTrigger ? Color.yellow : new Color(0.5f, 1f, 0.5f);
|
||||
EditorGUILayout.LabelField(
|
||||
badTrigger ? "⚠ isTrigger" : "✓ Trigger",
|
||||
GUILayout.Width(76));
|
||||
GUI.color = prevC;
|
||||
|
||||
GUI.color = new Color(1f, 0.5f, 0.5f);
|
||||
if (GUILayout.Button("删除", EditorStyles.miniButton, GUILayout.Width(44)))
|
||||
Undo.DestroyObjectImmediate(proxy.gameObject);
|
||||
GUI.color = prevC;
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(3);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
if (GUILayout.Button($"⊕ 创建代理节点 ({slotName})", GUILayout.Width(220)))
|
||||
{
|
||||
var go = new GameObject($"TriggerZone_{slotName}");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Create TriggerZone Proxy");
|
||||
go.transform.SetParent(system.transform, false);
|
||||
go.transform.localPosition = Vector3.zero;
|
||||
|
||||
// CircleCollider2D must be added BEFORE PerceptionTriggerProxy ([RequireComponent])
|
||||
var circle = go.AddComponent<CircleCollider2D>();
|
||||
circle.isTrigger = true;
|
||||
circle.radius = 1.5f;
|
||||
|
||||
var proxy = go.AddComponent<PerceptionTriggerProxy>();
|
||||
proxy.slotName = slotName;
|
||||
|
||||
Selection.activeGameObject = go;
|
||||
EditorGUIUtility.PingObject(go);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1240,12 +1240,22 @@ namespace BaseGames.Editor
|
||||
case "los": enumIdx = 1; radius = 0f; layer = 0; break;
|
||||
case "attack_melee":enumIdx = 0; radius = 1.5f; layer = playerLayer; break;
|
||||
case "attack_range":enumIdx = 0; radius = 8f; layer = playerLayer; break;
|
||||
case "patrol": enumIdx = 0; radius = 5f; layer = 0; break;
|
||||
case "alert": enumIdx = 0; radius = 3f; layer = playerLayer; break;
|
||||
case "sight": enumIdx = 4; radius = 6f; layer = playerLayer; break;
|
||||
}
|
||||
|
||||
elem.FindPropertyRelative("type").enumValueIndex = enumIdx;
|
||||
elem.FindPropertyRelative("radius").floatValue = radius;
|
||||
elem.FindPropertyRelative("detectLayer").intValue = layer;
|
||||
|
||||
// sight 槽位默认设置推荐的 LOS 采样点数(3:中心+上+下)
|
||||
if (name == "sight")
|
||||
{
|
||||
var losRayCountProp = elem.FindPropertyRelative("losRayCount");
|
||||
if (losRayCountProp != null) losRayCountProp.intValue = 3;
|
||||
}
|
||||
|
||||
// 各 slot 分配语义化默认颜色,可在 Inspector 中按需覆盖
|
||||
Color defaultColor = name switch
|
||||
{
|
||||
@@ -1253,6 +1263,9 @@ namespace BaseGames.Editor
|
||||
"los" => new Color(0.00f, 0.80f, 1.00f, 1f), // 青
|
||||
"attack_melee" => new Color(1.00f, 0.20f, 0.20f, 1f), // 红
|
||||
"attack_range" => new Color(1.00f, 0.40f, 0.60f, 1f), // 粉红
|
||||
"patrol" => new Color(0.20f, 0.90f, 0.20f, 1f), // 绿
|
||||
"alert" => new Color(1.00f, 0.90f, 0.10f, 1f), // 黄
|
||||
"sight" => new Color(0.30f, 0.85f, 1.00f, 1f), // 浅蓝(LOS 传感器)
|
||||
_ => Color.clear, // 未知 slot 回退为紫色
|
||||
};
|
||||
elem.FindPropertyRelative("gizmoColor").colorValue = defaultColor;
|
||||
|
||||
Reference in New Issue
Block a user