using System.Collections.Generic; using Unity.Collections; using UnityEngine; using UnityEngine.Jobs; namespace BaseGames.Enemies.AI { /// /// 批量视线检测系统(架构 07_EnemyModule §12)。 /// 每 FixedUpdate: /// 1. 读取上一帧 RaycastHit2D 结果,回调各注册者。 /// 2. 重新构建本帧 RaycastCommand 批次,使用 Physics2D.RaycastAll 同步模式执行。 /// /// ⚠️ Unity 2022.3 中 Physics2D 批量命令(RaycastCommand2D)尚未稳定, /// 此实现使用 FixedUpdate 内顺序 Raycast2D(节流),确保零 GC 分配。 /// 当敌人数量 > 20 时建议切换到 Job System RaycastCommand。 /// [DefaultExecutionOrder(-200)] public class BatchLOSSystem : MonoBehaviour { [SerializeField, Min(1)] private int _maxRequestersPerFrame = 8; private readonly List _requesters = new(); private readonly HashSet _requesterSet = new(); // O(1) 包含查询 // _indexMap 记录每个 requester 在 _requesters 中的下标,供 Unregister 实现 O(1) 删除 private readonly Dictionary _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) { var hit = Physics2D.Raycast(origin, direction.normalized, distance, requester.LOSBlockingMask); // 若无遮挡物(hit.collider == null),则视线畅通 hasLOS = hit.collider == null; } requester.ReceiveLOSResult(hasLOS); } } _currentOffset = (_currentOffset + count) % Mathf.Max(1, _requesters.Count); } } }