using System.Collections.Generic; using Unity.Collections; using Unity.Mathematics; using UnityEngine; namespace AutomaticDoorSystem.Utilities { public class BoxColliderPoolManager : MonoBehaviour { public static BoxColliderPoolManager Instance { get; private set; } [Header("Pool Configuration")] [Tooltip("Maximum number of BoxColliders in the pool")] [Range(2, 100)] public int maxPoolSize = 25; [Tooltip("Distance from player at which BoxColliders are activated")] [Range(5f, 100f)] public float cullingDistance = 25f; [Tooltip("Frequency of distance checks in seconds")] [Range(0.1f, 2f)] public float distanceCheckInterval = 0.5f; [Tooltip("Minimum distance between BoxColliders to prevent overlap")] [Range(0f, 5f)] public float minimumSpacing = 0.5f; [Tooltip("How much closer a new door must be to steal a BoxCollider")] [Range(0.5f, 2f)] public float reassignmentThreshold = 1.3f; [Tooltip("Keep BoxColliders assigned to out-of-range doors until needed")] public bool keepOutOfRangeAssignments = true; [Tooltip("Attach the panel's baked Steam Audio geometry (SteamAudioDynamicObject exported asset) to the " + "pooled panel proxy, so moving doors occlude and reflect sound. Panels without an exported asset " + "are skipped.")] public bool enableSteamAudioGeometry = true; private struct ColliderAssignment { public int doorId; public int panelIndex; } private Transform _cameraTransform; private Transform _poolContainer; private BoxCollider[] _colliderPool; private Rigidbody[] _rigidbodyPool; private SteamAudio.SteamAudioDynamicObject[] _audioGeometryPool; private ColliderAssignment[] _colliderAssignments; private DoorSelectionStrategy _selectionStrategy; private float _updateAccumulator; private WaitForSeconds _updateWait; private bool _duplicateIdWarningLogged; private bool _dependencyWarningLogged; // The pool re-configures colliders every update interval - without this, per-door // warnings repeat forever and drown the console. private readonly HashSet _fallbackSizeWarnedDoors = new HashSet(); private void Awake() { if (Instance != null && Instance != this) { Destroy(this); return; } Instance = this; if (DoorDataBridge.Instance == null && GetComponent() == null) { gameObject.AddComponent(); } InitializePool(); } private void OnDestroy() { if (Instance == this) { Instance = null; } if (_selectionStrategy.IsCreated) { _selectionStrategy.Dispose(); } } private void Start() { CacheCameraReference(); Invoke(nameof(ForceInitialUpdate), 0.5f); } private void ForceInitialUpdate() { if (DoorDataBridge.Instance != null) { UpdateColliderActivation(); } else { Invoke(nameof(ForceInitialUpdate), 0.5f); } } private void OnEnable() { _updateAccumulator = 0f; } private void Update() { if (DoorDataBridge.Instance == null) { return; } _updateAccumulator += Time.deltaTime; if (_updateAccumulator >= distanceCheckInterval) { _updateAccumulator = 0f; UpdateColliderActivation(); } } private void LateUpdate() { UpdateColliderPositions(); } private void InitializePool() { _poolContainer = new GameObject("BoxCollider_Pool").transform; _poolContainer.SetParent(transform); _poolContainer.localPosition = Vector3.zero; _colliderPool = new BoxCollider[maxPoolSize]; _rigidbodyPool = new Rigidbody[maxPoolSize]; _audioGeometryPool = new SteamAudio.SteamAudioDynamicObject[maxPoolSize]; _colliderAssignments = new ColliderAssignment[maxPoolSize]; _selectionStrategy = new DoorSelectionStrategy(500, maxPoolSize, Allocator.Persistent); _updateWait = new WaitForSeconds(distanceCheckInterval); for (int i = 0; i < maxPoolSize; i++) { _colliderAssignments[i] = new ColliderAssignment { doorId = -1, panelIndex = -1 }; } for (int i = 0; i < maxPoolSize; i++) { GameObject colliderObj = new GameObject($"PooledBoxCollider_{i:D2}"); colliderObj.transform.SetParent(_poolContainer); colliderObj.transform.localPosition = Vector3.zero; Rigidbody rb = colliderObj.AddComponent(); rb.isKinematic = true; rb.useGravity = false; rb.interpolation = RigidbodyInterpolation.None; rb.collisionDetectionMode = CollisionDetectionMode.Discrete; BoxCollider boxCollider = colliderObj.AddComponent(); boxCollider.enabled = false; _colliderPool[i] = boxCollider; _rigidbodyPool[i] = rb; } } private void CacheCameraReference() { if (Camera.main != null) { _cameraTransform = Camera.main.transform; } else { Invoke(nameof(RetryCameraReference), 1f); } } private void RetryCameraReference() { if (Camera.main != null) { _cameraTransform = Camera.main.transform; } } private void UpdateColliderActivation() { if (_cameraTransform == null || DoorDataBridge.Instance == null) { if (!_dependencyWarningLogged) { _dependencyWarningLogged = true; Debug.LogWarning("[BoxColliderPoolManager] Camera or DoorDataBridge not available"); } return; } _dependencyWarningLogged = false; float3 playerPosition = _cameraTransform.position; _selectionStrategy.BeginSelection(); var allDoors = DoorDataBridge.Instance.GetAllDoorInfo(); if (allDoors == null || allDoors.Count == 0) { return; } for (int i = 0; i < allDoors.Count; i++) { var doorInfo = allDoors[i]; _selectionStrategy.AddCandidate(doorInfo.doorId, doorInfo.position, playerPosition); } _selectionStrategy.FilterByDistance(cullingDistance); _selectionStrategy.SortByDistance(); int duplicateIds = _selectionStrategy.RemoveDuplicateIds(); if (duplicateIds > 0 && !_duplicateIdWarningLogged) { _duplicateIdWarningLogged = true; Debug.LogError( $"[BoxColliderPoolManager] {duplicateIds} door(s) in range share a Door Id with another door. " + "Pool slots are keyed by Door Id, so the duplicates are ignored and those doors get no collider. " + "Run Tools > AutomaticDoorSystem > Setup Validator to find and renumber them.", this); } _selectionStrategy.RemoveSpatialDuplicates(minimumSpacing); int assignedCount = _selectionStrategy.AssignPoolSlots( maxPoolSize, keepOutOfRangeAssignments, reassignmentThreshold ); int colliderIndex = 0; for (int i = 0; i < assignedCount && colliderIndex < maxPoolSize; i++) { var candidate = _selectionStrategy.GetCandidate(i); int doorId = candidate.doorId; if (DoorDataBridge.Instance.TryGetDoorInfo(doorId, out var doorInfo)) { if (DoorDataBridge.Instance.TryGetDoorPanels(doorId, out var panels, out int panelCount)) { for (int panelIdx = 0; panelIdx < panelCount && colliderIndex < maxPoolSize; panelIdx++) { ConfigureColliderForPanel(colliderIndex, doorInfo, panels[panelIdx], panelIdx); _colliderAssignments[colliderIndex] = new ColliderAssignment { doorId = doorId, panelIndex = panelIdx }; colliderIndex++; } } } } for (int i = colliderIndex; i < maxPoolSize; i++) { _colliderPool[i].enabled = false; // OnDisable removes the instanced mesh from the Steam Audio scene; the component // and its loaded asset stay cached for when this slot serves the same door again. if (_audioGeometryPool[i] != null) _audioGeometryPool[i].enabled = false; _colliderAssignments[i] = new ColliderAssignment { doorId = -1, panelIndex = -1 }; } } private void UpdateColliderPositions() { if (DoorDataBridge.Instance == null) { return; } for (int i = 0; i < maxPoolSize; i++) { var assignment = _colliderAssignments[i]; if (assignment.doorId == -1 || !_colliderPool[i].enabled) { continue; } if (DoorDataBridge.Instance.IsDoorLocked(assignment.doorId)) { continue; } if (DoorDataBridge.Instance.TryGetDoorPanels(assignment.doorId, out var panels, out int panelCount)) { if (assignment.panelIndex >= 0 && assignment.panelIndex < panelCount) { var panelInfo = panels[assignment.panelIndex]; _rigidbodyPool[i].MovePosition(panelInfo.position); _rigidbodyPool[i].MoveRotation(panelInfo.rotation); } } } } private void ConfigureColliderForPanel(int poolIndex, DoorDataBridge.DoorInfo doorInfo, DoorDataBridge.DoorPanelInfo panelInfo, int panelIndex) { BoxCollider collider = _colliderPool[poolIndex]; Rigidbody rb = _rigidbodyPool[poolIndex]; if (panelInfo.hasColliderData) { collider.size = panelInfo.colliderSize; collider.center = panelInfo.colliderCenter; } else { // Fallback - generic door size (add BoxCollider to door panel in subscene prefab) collider.size = new Vector3(1f, 2.5f, 0.1f); collider.center = new Vector3(0.5f, 1.25f, 0f); if (_fallbackSizeWarnedDoors.Add(doorInfo.doorId)) { Debug.LogWarning( $"[BoxColliderPoolManager] Door {doorInfo.doorId} panel has no BoxCollider data - using " + "fallback size. Add a BoxCollider to the door panels in the subscene prefab. " + "(logged once per door)"); } } rb.MoveRotation(panelInfo.rotation); rb.MovePosition(panelInfo.position); collider.enabled = true; ConfigureAudioGeometry(poolIndex, panelInfo.audioGeometry); } /// /// Attaches the panel's baked Steam Audio geometry to the pooled proxy. The proxy already /// follows the panel transform for its collider, and SteamAudioDynamicObject tracks /// transform.hasChanged itself, so the occlusion geometry moves with the door for free. /// The component caches its instanced mesh from the FIRST asset it loads, so a slot /// reassigned to a different door needs a fresh component — changing the field would /// silently keep simulating the old door's geometry. /// private void ConfigureAudioGeometry(int poolIndex, SteamAudio.SerializedData asset) { var current = _audioGeometryPool[poolIndex]; if (!enableSteamAudioGeometry || asset == null) { if (current != null) current.enabled = false; return; } if (current != null && current.asset == asset) { current.enabled = true; return; } if (current != null) Destroy(current); current = _colliderPool[poolIndex].gameObject.AddComponent(); current.asset = asset; _audioGeometryPool[poolIndex] = current; } private int LayerMaskToLayer(LayerMask layerMask) { int layerNumber = 0; int layer = layerMask.value; while (layer > 1) { layer = layer >> 1; layerNumber++; } return layerNumber; } private void OnDrawGizmosSelected() { if (_cameraTransform == null) return; Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(_cameraTransform.position, cullingDistance); } public int GetActiveColliderCount() { int count = 0; for (int i = 0; i < maxPoolSize; i++) { if (_colliderPool[i].enabled) { count++; } } return count; } /// /// Read-only view of one pool slot for diagnostics (Door Doctor's Colliders tab). /// doorId is -1 for a free slot. Returns false once poolIndex runs past the pool. /// public bool TryGetSlotInfo(int poolIndex, out int doorId, out int panelIndex, out BoxCollider collider) { doorId = -1; panelIndex = -1; collider = null; if (_colliderAssignments == null || poolIndex < 0 || poolIndex >= _colliderAssignments.Length) { return false; } doorId = _colliderAssignments[poolIndex].doorId; panelIndex = _colliderAssignments[poolIndex].panelIndex; collider = poolIndex < _colliderPool.Length ? _colliderPool[poolIndex] : null; return true; } } }