Advanced POC - portals, dialogue system + floating words, room loading
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user