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:
2026-06-02 23:18:20 +08:00
parent 150440495d
commit d27ae9407d
17 changed files with 1946 additions and 335 deletions

View File

@@ -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.positionX 随朝向自动翻转"));
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 视图,不影响检测精度(建议 39"));
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("最低可见度 (01)", "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 时生效,建议 39"));
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();
}
}
}

View File

@@ -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;