diff --git a/Assets/_Game/Scripts/Editor/Enemies/PhysicsPerceptionSystemEditor.cs b/Assets/_Game/Scripts/Editor/Enemies/PhysicsPerceptionSystemEditor.cs
index c13bf005..1d074ea6 100644
--- a/Assets/_Game/Scripts/Editor/Enemies/PhysicsPerceptionSystemEditor.cs
+++ b/Assets/_Game/Scripts/Editor/Enemies/PhysicsPerceptionSystemEditor.cs
@@ -94,6 +94,11 @@ namespace BaseGames.Editor
}
break;
+ case SlotType.BoxLOS:
+ if (slot.boxSize.x > 0f && slot.boxSize.y > 0f)
+ DrawBoxLOSGizmo(system, slotCenter, facingSign, slot, fill, outline, isSelected);
+ break;
+
case SlotType.BatchLOS:
DrawBatchLOSGizmo(system, slotCenter, slot, outline, isSelected);
break;
@@ -366,6 +371,45 @@ namespace BaseGames.Editor
Handles.DrawSolidRectangleWithOutline(corners, fill, outline);
}
+ ///
+ /// BoxLOS 槽位 Gizmo:矩形区域(同 BoxCast)+ 眼点 + LOS 提示 + 运行时检测连线。
+ ///
+ static void DrawBoxLOSGizmo(PhysicsPerceptionSystem system, Vector3 slotCenter, float facingSign,
+ PhysicsPerceptionSystem.PerceptionSlot slot, Color fill, Color outline, bool isSelected)
+ {
+ DrawBoxGizmo(slotCenter, facingSign, slot, fill, outline);
+
+ // 眼点(表明有遮挡检测能力,射线原点为感知原点)
+ Handles.color = outline;
+ Handles.DrawSolidDisc(slotCenter, Vector3.forward, 0.07f);
+
+ 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) }
+ };
+ float labelY = slot.boxOffset.y + slot.boxSize.y * 0.5f + 0.2f;
+ Handles.Label(slotCenter + new Vector3(0f, labelY, 0f), "BoxLOS", 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 Vector3 RotateVec3(Vector3 v, float angleDeg)
{
float rad = angleDeg * Mathf.Deg2Rad;
@@ -563,6 +607,23 @@ namespace BaseGames.Editor
new GUIContent("矩形偏移", "相对于原点偏移,X 随朝向翻转"));
break;
+ case SlotType.BoxLOS:
+ EditorGUILayout.PropertyField(elem.FindPropertyRelative("detectLayer"),
+ new GUIContent("检测层", "目标所在层(通常为 Player 层)"));
+ EditorGUILayout.PropertyField(elem.FindPropertyRelative("boxSize"),
+ new GUIContent("矩形尺寸 (m)", "宽 × 高(横向视野设宽 > 高)"));
+ EditorGUILayout.PropertyField(elem.FindPropertyRelative("boxOffset"),
+ new GUIContent("矩形偏移", "相对于原点偏移,X 随朝向翻转;\n仅前向视野把 X 设为 boxSize.x*0.5"));
+ EditorGUILayout.Space(2);
+ EditorGUILayout.LabelField("─ LOS 遮挡设置 ─", EditorStyles.miniLabel);
+ EditorGUILayout.PropertyField(elem.FindPropertyRelative("losBlockMask"),
+ new GUIContent("遮挡层", "遮挡物所在层(Platform / Wall / Ground);设为 0 则视线始终通过(退化为纯几何 BoxCast)"));
+ 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.Sight:
EditorGUILayout.PropertyField(elem.FindPropertyRelative("radius"),
new GUIContent("视野半径 (m)", "Sight 传感器检测半径"));
diff --git a/Assets/_Game/Scripts/Enemies/Perception/PhysicsPerceptionSystem.cs b/Assets/_Game/Scripts/Enemies/Perception/PhysicsPerceptionSystem.cs
index 138bd593..a08a3f84 100644
--- a/Assets/_Game/Scripts/Enemies/Perception/PhysicsPerceptionSystem.cs
+++ b/Assets/_Game/Scripts/Enemies/Perception/PhysicsPerceptionSystem.cs
@@ -6,7 +6,7 @@ namespace BaseGames.Enemies.Perception
{
///
/// 敌人感知系统(自研纯物理实现,商业级设计)。
- /// 每个 独立配置并独立运行,支持七种检测模式:
+ /// 每个 独立配置并独立运行,支持八种检测模式:
/// • RangeCircle — OverlapCircle 纯几何范围检测(无遮挡)
/// • BatchLOS — OverlapCircle + 单次 Raycast 自研批量视线检测(低开销,有帧延迟)
/// • FanCast — OverlapCircle + 角度过滤扇形视野(纯几何,无遮挡)
@@ -14,6 +14,7 @@ namespace BaseGames.Enemies.Perception
/// • Sight — OverlapCircle + 可选扇形角度 + 强制多点 LOS 遮挡(专用视线传感器)
/// • RayCast — 单/多根方向射线传感器,支持扩散角和遮挡层
/// • TriggerZone — 物理触发器事件驱动(子节点 PerceptionTriggerProxy,零轮询)
+ /// • BoxLOS — OverlapBox 矩形(横向)区域 + 多点 LOS 遮挡(矩形视野带遮挡)
///
/// 遮挡检测设计原则:
/// RangeCircle / FanCast / BoxCast 是纯几何传感器,不做遮挡校验。
@@ -48,6 +49,8 @@ namespace BaseGames.Enemies.Perception
RayCast,
/// 物理触发器事件驱动,依赖子节点 PerceptionTriggerProxy(零轮询开销)
TriggerZone,
+ /// OverlapBox 矩形(横向)区域 + 多点 LOS 遮挡校验(矩形视野带遮挡)
+ BoxLOS,
}
// ── 槽位定义 ──────────────────────────────────────────────────────────
@@ -98,23 +101,23 @@ namespace BaseGames.Enemies.Perception
// ── BoxCast ────────────────────────────────────────────────────
- [Tooltip("BoxCast:检测框尺寸 (宽, 高),单位米")]
+ [Tooltip("BoxCast / BoxLOS:检测框尺寸 (宽, 高),单位米")]
public Vector2 boxSize;
- [Tooltip("BoxCast:相对于感知中心的偏移,X 分量随 localScale.x 自动翻转")]
+ [Tooltip("BoxCast / BoxLOS:相对于感知中心的偏移,X 分量随 localScale.x 自动翻转。\n仅前向视野:X 设为 boxSize.x * 0.5(矩形整块推到前方);前后双向:X 设为 0(对称)。")]
public Vector2 boxOffset;
- // ── Sight LOS(仅 Sight 类型使用)────────────────────────────
+ // ── LOS(Sight / BoxLOS 共用)────────────────────────────────
- [Tooltip("[Sight 专用] 视线遮挡检测层(通常为 Platform + Wall + Ground)。\n⚠ 留 0 时视线始终通过(losBlockMask = 0 = 无遮挡层)。")]
+ [Tooltip("[Sight / BoxLOS] 视线遮挡检测层(通常为 Platform + Wall + Ground)。\n⚠ 留 0 时视线始终通过(losBlockMask = 0 = 无遮挡层)。")]
public LayerMask losBlockMask;
[Range(1, 5)]
- [Tooltip("[Sight 专用] LOS 多点采样数量:\n1 = 包围盒中心(最快)\n3 = 中心 + 上 + 下(推荐)\n5 = 中心 + 上下左右(最稳健)")]
+ [Tooltip("[Sight / BoxLOS] LOS 多点采样数量:\n1 = 包围盒中心(最快)\n3 = 中心 + 上 + 下(推荐)\n5 = 中心 + 上下左右(最稳健)")]
public int losRayCount;
[Range(0f, 1f)]
- [Tooltip("[Sight 专用] 可见度阈值(0–1):\n0 = 任意 1 条通过即可(宽松)\n0.5 = 50% 通过\n1 = 全部通过(严格)")]
+ [Tooltip("[Sight / BoxLOS] 可见度阈值(0–1):\n0 = 任意 1 条通过即可(宽松)\n0.5 = 50% 通过\n1 = 全部通过(严格)")]
public float losMinVisibility;
// ── RayCast ───────────────────────────────────────────────────
@@ -274,6 +277,7 @@ namespace BaseGames.Enemies.Perception
case SlotType.BoxCast: RefreshBoxCast(ref slot, list); break;
case SlotType.Sight: RefreshSight(ref slot, list); break;
case SlotType.RayCast: RefreshRayCast(ref slot, list); break;
+ case SlotType.BoxLOS: RefreshBoxLOS(ref slot, list); break;
}
FireDiffEvents(slot.slotName);
@@ -380,6 +384,34 @@ namespace BaseGames.Enemies.Perception
}
}
+ ///
+ /// 矩形视线传感器:OverlapBox(横向矩形区域)+ 多点 LOS 遮挡校验。
+ /// 形状同 ,但对每个候选目标从感知原点做遮挡射线,
+ /// 用于"横向矩形视野带遮挡"的场景(如走廊 / 平台横向警戒)。
+ /// 矩形以 / 定义;
+ /// 遮挡复用 / losRayCount / losMinVisibility。
+ /// losBlockMask = 0 时视线始终通过(退化为纯几何 BoxCast)。
+ ///
+ private void RefreshBoxLOS(ref PerceptionSlot slot, List list)
+ {
+ if (slot.boxSize.x <= 0f || slot.boxSize.y <= 0f || slot.detectLayer == 0) return;
+
+ float facingSign = transform.localScale.x < 0f ? -1f : 1f;
+ Vector2 origin = (Vector2)transform.position + new Vector2(slot.offset.x * facingSign, slot.offset.y);
+ Vector2 center = origin + new Vector2(slot.boxOffset.x * facingSign, slot.boxOffset.y);
+
+ int count = Physics2D.OverlapBoxNonAlloc(center, slot.boxSize, 0f, _overlapBuffer, slot.detectLayer);
+ for (int i = 0; i < count; i++)
+ {
+ var col = _overlapBuffer[i];
+ if (col == null) continue;
+ // 遮挡校验:从感知原点(眼点)到目标;losBlockMask = 0 时始终通过
+ if (!HasLineOfSight(origin, col.gameObject,
+ slot.losBlockMask, Mathf.Max(1, slot.losRayCount), slot.losMinVisibility)) continue;
+ if (!list.Contains(col.gameObject)) list.Add(col.gameObject);
+ }
+ }
+
///
/// 视线传感器:OverlapCircle + 可选扇形角度过滤 + 强制多点 LOS 遮挡校验。
/// 唯一内置遮挡检测的轮询槽位类型。