Files
Ipseity-Demo/Assets/Unheimlich_POC/Scripts/Managers/PortalRoomManager.cs
T

295 lines
11 KiB
C#

using System.Collections.Generic;
using UnityEngine;
using Unheimlich.Gameplay.Portals;
namespace Unheimlich.Managers
{
/// <summary>
/// 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.
/// </summary>
public class PortalRoomManager : MonoBehaviour
{
public static PortalRoomManager Instance { get; private set; }
[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<string, LoadedRoom> _loadedRooms = new Dictionary<string, LoadedRoom>();
private List<Portal> _registeredPortals = new List<Portal>();
private GameObject _currentPlayerRoom = null;
/// <summary>
/// Represents a room instance that has been loaded through a portal.
/// Caches room bounds for efficient distance calculations.
/// </summary>
private class LoadedRoom
{
public GameObject RoomInstance;
public string RoomId;
public Vector3 LoadPosition;
public Portal SourcePortal;
public Bounds CachedBounds;
public bool BoundsCalculated = false;
}
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(this);
return;
}
Instance = this;
}
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;
}
/// <summary>
/// Checks all registered portals and loads rooms for portals within range.
/// Optionally accounts for the portal's configured room spawn distance.
/// </summary>
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);
}
}
/// <summary>
/// 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.
/// </summary>
private void CheckRoomsForUnloading()
{
List<string> roomsToUnload = new List<string>();
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);
}
}
/// <summary>
/// Calculates the distance from the player to a loaded room.
/// Lazily calculates and caches room bounds on first call for performance.
/// </summary>
private float GetDistanceToRoom(LoadedRoom loadedRoom)
{
if (!loadedRoom.BoundsCalculated)
{
loadedRoom.CachedBounds = CalculateRoomBounds(loadedRoom.RoomInstance);
loadedRoom.BoundsCalculated = true;
}
return Vector3.Distance(_player.position, loadedRoom.CachedBounds.center);
}
/// <summary>
/// Calculates the combined bounding box of all renderers in a room.
/// Used to determine room center for distance calculations.
/// </summary>
private Bounds CalculateRoomBounds(GameObject room)
{
Renderer[] renderers = room.GetComponentsInChildren<Renderer>();
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;
}
/// <summary>
/// Determines which room (if any) currently contains the player.
/// Uses collider bounds checks to detect room occupancy.
/// </summary>
private void UpdateCurrentPlayerRoom()
{
_currentPlayerRoom = null;
foreach (var kvp in _loadedRooms)
{
LoadedRoom loadedRoom = kvp.Value;
if (loadedRoom.RoomInstance == null)
continue;
Collider[] roomColliders = loadedRoom.RoomInstance.GetComponentsInChildren<Collider>();
foreach (Collider col in roomColliders)
{
if (col.bounds.Contains(_player.position))
{
_currentPlayerRoom = loadedRoom.RoomInstance;
return;
}
}
}
}
/// <summary>
/// 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.
/// </summary>
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<MeshRenderer>())
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);
}
/// <summary>
/// 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.
/// </summary>
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);
}
}
}
}
}
}