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

@@ -7,7 +7,7 @@ namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Conditional玩家是否可见LOS 检测)。
/// 读取 EnemyBase._losResult 字段(由 BatchLOSSystem 每帧写入,或降级 3 帧节流 Raycast)。
/// 读取 EnemyBase.IsPlayerVisible(),结果来自 PhysicsPerceptionSystemLOS / Sight 槽位)。
/// </summary>
[TaskName("Is Player Visible?")]
[TaskCategory("BaseGames/Enemy/Perception")]

View File

@@ -1,102 +0,0 @@
using System.Collections.Generic;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Jobs;
using BaseGames.Core;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// 批量视线检测系统(架构 07_EnemyModule §12
/// 每 FixedUpdate
/// 1. 读取上一帧 RaycastHit2D 结果,回调各注册者。
/// 2. 重新构建本帧 RaycastCommand 批次,使用 Physics2D.RaycastAll 同步模式执行。
///
/// ⚠️ Unity 2022.3 中 Physics2D 批量命令RaycastCommand2D尚未稳定
/// 此实现使用 FixedUpdate 内顺序 Raycast2D节流确保零 GC 分配。
/// 当敌人数量 > 20 时建议切换到 Job System RaycastCommand。
/// </summary>
[DefaultExecutionOrder(-200)]
public class BatchLOSSystem : MonoBehaviour
{
[SerializeField, Min(1)] private int _maxRequestersPerFrame = 8;
private void Awake() => ServiceLocator.Register<BatchLOSSystem>(this);
private void OnDestroy() => ServiceLocator.Unregister<BatchLOSSystem>(this);
private readonly List<ILOSRequester> _requesters = new();
private readonly HashSet<ILOSRequester> _requesterSet = new(); // O(1) 包含查询
// _indexMap 记录每个 requester 在 _requesters 中的下标,供 Unregister 实现 O(1) 删除
private readonly Dictionary<ILOSRequester, int> _indexMap = new();
private int _currentOffset = 0;
// ── 注册 ──────────────────────────────────────────────────────────
public void Register(ILOSRequester requester)
{
if (_requesterSet.Add(requester))
{
_indexMap[requester] = _requesters.Count;
_requesters.Add(requester);
}
}
public void Unregister(ILOSRequester requester)
{
if (!_requesterSet.Remove(requester)) return;
int idx = _indexMap[requester];
int last = _requesters.Count - 1;
// Swap-and-pop将末尾元素移动到被删除的位置避免 O(n) 搬移
if (idx != last)
{
var moved = _requesters[last];
_requesters[idx] = moved;
_indexMap[moved] = idx;
}
_requesters.RemoveAt(last);
_indexMap.Remove(requester);
// 修正偏移量,防止越界
if (_currentOffset >= _requesters.Count) _currentOffset = 0;
}
// ── FixedUpdate ───────────────────────────────────────────────────
private void FixedUpdate()
{
if (_requesters.Count == 0) return;
// 每帧轮询部分请求者(均匀分配,避免单帧全量射线)
int count = Mathf.Min(_maxRequestersPerFrame, _requesters.Count);
for (int i = 0; i < count; i++)
{
int idx = (_currentOffset + i) % _requesters.Count;
var requester = _requesters[idx];
bool hasLOS = false;
if (requester != null)
{
Vector2 origin = requester.LOSOrigin;
Vector2 target = requester.LOSTarget;
Vector2 direction = target - origin;
float distance = direction.magnitude;
if (distance > 0.01f)
{
// direction / distance == direction.normalized避免重复开方
var hit = Physics2D.Raycast(origin, direction / distance, distance, requester.LOSBlockingMask);
// 若无遮挡物hit.collider == null则视线畅通
hasLOS = hit.collider == null;
}
requester.ReceiveLOSResult(hasLOS);
}
}
_currentOffset = (_currentOffset + count) % Mathf.Max(1, _requesters.Count);
}
}
}

View File

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