285 lines
11 KiB
C#
285 lines
11 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using Unheimlich.Core;
|
|
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 : Singleton<PortalRoomManager>
|
|
{
|
|
|
|
[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 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|