using System.Collections.Generic; using UnityEngine; using Unheimlich.Core; using Unheimlich.Gameplay.Portals; namespace Unheimlich.Managers { /// /// Manages dynamic loading and unloading of rooms connected by portals. /// Monitors player distance to portals and loads/unloads room instances accordingly. /// Supports room pooling (keeping inactive rooms in memory) and distance-based activation. /// public class PortalRoomManager : Singleton { [Header("Settings")] [SerializeField] private float _loadDistance = 15f; [Tooltip("Distance at which a loaded room gets deactivated (must be > load distance to avoid flip-flop)")] [SerializeField] private float _deactivateDistance = 50f; [Tooltip("Distance at which a deactivated room gets reactivated (should be < deactivate distance)")] [SerializeField] private float _reactivateDistance = 30f; [SerializeField] private bool _keepRoomsInMemory = true; [Header("Distance Calculation")] [Tooltip("If true, calculates distance from player to actual room position (portal + loadRoomDistance)")] [SerializeField] private bool _useRealRoomDistance = true; private Transform _player; private Dictionary _loadedRooms = new Dictionary(); private List _registeredPortals = new List(); private GameObject _currentPlayerRoom = null; /// /// Represents a room instance that has been loaded through a portal. /// Caches room bounds for efficient distance calculations. /// private class LoadedRoom { public GameObject RoomInstance; public string RoomId; public Vector3 LoadPosition; public Portal SourcePortal; public Bounds CachedBounds; public bool BoundsCalculated = false; } private void Start() { _player = GameObject.FindGameObjectWithTag("Player")?.transform; if (_player == null) Debug.LogWarning("Player not found! Make sure your player has the 'Player' tag."); } private void Update() { if (_player == null) return; CheckPortalsForLoading(); CheckRoomsForUnloading(); } public void RegisterPortal(Portal portal) { if (!_registeredPortals.Contains(portal)) _registeredPortals.Add(portal); } public void UnregisterPortal(Portal portal) { if (_registeredPortals.Contains(portal)) _registeredPortals.Remove(portal); } public GameObject GetLoadedRoom(string roomId) { if (_loadedRooms.TryGetValue(roomId, out LoadedRoom loadedRoom)) return loadedRoom.RoomInstance; return null; } /// /// Checks all registered portals and loads rooms for portals within range. /// Optionally accounts for the portal's configured room spawn distance. /// private void CheckPortalsForLoading() { foreach (Portal portal in _registeredPortals) { float distanceToPortal = Vector3.Distance(_player.position, portal.transform.position); float effectiveLoadDistance = _loadDistance; if (_useRealRoomDistance) effectiveLoadDistance += portal.GetRoomLoadDistance(); if (distanceToPortal <= effectiveLoadDistance) LoadRoom(portal); } } /// /// Checks all loaded rooms and determines if they should be deactivated, reactivated, or unloaded /// based on distance thresholds. Prevents unloading the room the player is currently inside. /// private void CheckRoomsForUnloading() { List roomsToUnload = new List(); UpdateCurrentPlayerRoom(); foreach (var kvp in _loadedRooms) { LoadedRoom loadedRoom = kvp.Value; if (loadedRoom.RoomInstance == null) { roomsToUnload.Add(kvp.Key); continue; } if (loadedRoom.RoomInstance == _currentPlayerRoom) continue; float distanceToPortal = GetDistanceToRoom(loadedRoom); float effectiveDeactivateDistance = _deactivateDistance; float effectiveReactivateDistance = _reactivateDistance; if (_useRealRoomDistance) { float roomOffset = loadedRoom.SourcePortal.GetRoomLoadDistance(); effectiveDeactivateDistance += roomOffset; effectiveReactivateDistance += roomOffset; } if (distanceToPortal > effectiveDeactivateDistance) { if (loadedRoom.RoomInstance.activeSelf) roomsToUnload.Add(kvp.Key); } else if (distanceToPortal <= effectiveReactivateDistance) { if (!loadedRoom.RoomInstance.activeSelf) loadedRoom.RoomInstance.SetActive(true); } } foreach (string roomId in roomsToUnload) { UnloadRoom(roomId); } } /// /// Calculates the distance from the player to a loaded room. /// Lazily calculates and caches room bounds on first call for performance. /// private float GetDistanceToRoom(LoadedRoom loadedRoom) { if (!loadedRoom.BoundsCalculated) { loadedRoom.CachedBounds = CalculateRoomBounds(loadedRoom.RoomInstance); loadedRoom.BoundsCalculated = true; } return Vector3.Distance(_player.position, loadedRoom.CachedBounds.center); } /// /// Calculates the combined bounding box of all renderers in a room. /// Used to determine room center for distance calculations. /// private Bounds CalculateRoomBounds(GameObject room) { Renderer[] renderers = room.GetComponentsInChildren(); if (renderers.Length == 0) return new Bounds(room.transform.position, Vector3.one); Bounds bounds = renderers[0].bounds; for (int i = 1; i < renderers.Length; i++) bounds.Encapsulate(renderers[i].bounds); return bounds; } /// /// Determines which room (if any) currently contains the player. /// Uses collider bounds checks to detect room occupancy. /// private void UpdateCurrentPlayerRoom() { _currentPlayerRoom = null; foreach (var kvp in _loadedRooms) { LoadedRoom loadedRoom = kvp.Value; if (loadedRoom.RoomInstance == null) continue; Collider[] roomColliders = loadedRoom.RoomInstance.GetComponentsInChildren(); foreach (Collider col in roomColliders) { if (col.bounds.Contains(_player.position)) { _currentPlayerRoom = loadedRoom.RoomInstance; return; } } } } /// /// Instantiates a room from a portal's prefab at the configured spawn point. /// If the room is already loaded, reactivates it instead of creating a new instance. /// Notifies the portal when its room is successfully loaded. /// private void LoadRoom(Portal portal) { GameObject roomPrefab = portal.GetRoomPrefab(); if (roomPrefab == null) { Debug.LogWarning($"[LOAD] Portal {portal.name} has no prefab!"); return; } string roomId = portal.GetRoomId(); if (_loadedRooms.TryGetValue(roomId, out LoadedRoom existingRoom)) { if (existingRoom.RoomInstance != null) existingRoom.RoomInstance.SetActive(true); return; } Vector3 spawnPosition = portal.GetRoomSpawnPosition(); Quaternion spawnRotation = portal.GetRoomSpawnRotation(); GameObject roomInstance = Instantiate(roomPrefab, spawnPosition, spawnRotation); roomInstance.name = $"{roomPrefab.name}_Loaded"; foreach (var renderer in roomInstance.GetComponentsInChildren()) renderer.gameObject.SendMessage("OnBecameVisible", SendMessageOptions.DontRequireReceiver); LoadedRoom loadedRoom = new LoadedRoom { RoomInstance = roomInstance, RoomId = roomId, LoadPosition = portal.transform.position, SourcePortal = portal }; _loadedRooms.Add(roomId, loadedRoom); portal.OnRoomLoaded(roomInstance); } /// /// Deactivates or destroys a loaded room based on the _keepRoomsInMemory setting. /// If keeping in memory, the room is deactivated but not destroyed (faster to reactivate). /// If not keeping in memory, the room is fully destroyed and removed from tracking. /// Notifies the source portal when its room is unloaded. /// private void UnloadRoom(string roomId) { if (_loadedRooms.TryGetValue(roomId, out LoadedRoom loadedRoom)) { if (loadedRoom.RoomInstance != null) { if (loadedRoom.SourcePortal != null) loadedRoom.SourcePortal.OnRoomUnloaded(); if (_keepRoomsInMemory) loadedRoom.RoomInstance.SetActive(false); else { Destroy(loadedRoom.RoomInstance); _loadedRooms.Remove(roomId); } } } } } }