Advanced POC - portals, dialogue system + floating words, room loading

This commit is contained in:
2026-01-26 20:31:27 +01:00
parent b5ca4763b3
commit 203b00578a
426 changed files with 123482 additions and 29 deletions
@@ -0,0 +1,214 @@
using UnityEngine;
using System;
using Unheimlich.Gameplay.Player;
using Unheimlich.Interactions.Dialogues;
using Unheimlich.Interactions.FloatingWords;
using Unheimlich.Data.Dialogues;
namespace Unheimlich.Managers
{
/// <summary>
/// Manages gaze-based focus detection for dialogue choices using raycasts.
/// Handles progressive focus timers, aim assist integration, visual feedback,
/// and automatic choice selection when player maintains focus for a duration.
/// </summary>
public class DialogueFocusSystem
{
private readonly DialogueManager _manager;
private readonly Camera _mainCamera;
private readonly PlayerLook _playerLook;
private ChoiceFocusHandler _currentFocusedHandler;
private float _focusTimer = 0f;
private float _retentionTimer = 0f;
private float _raycastTimer = 0f;
private bool _isFocusLocked = false;
private readonly LayerMask _choiceLayer;
private readonly float _focusRange;
private readonly float _focusTime;
private readonly float _raycastInterval;
private readonly float _recoveryDuration;
private readonly float _focusRetentionTime;
private readonly float _shakeIntensity;
private static readonly Vector3 ViewportCenter = new Vector3(0.5f, 0.5f, 0f);
public event Action<DialogueChoiceData> OnChoiceMade;
public DialogueFocusSystem(
DialogueManager manager,
Camera mainCamera,
PlayerLook playerLook,
LayerMask choiceLayer,
float focusRange,
float focusTime,
float raycastInterval,
float recoveryDuration,
float focusRetentionTime,
float shakeIntensity)
{
_manager = manager;
_mainCamera = mainCamera;
_playerLook = playerLook;
_choiceLayer = choiceLayer;
_focusRange = focusRange;
_focusTime = focusTime;
_raycastInterval = raycastInterval;
_recoveryDuration = recoveryDuration;
_focusRetentionTime = focusRetentionTime;
_shakeIntensity = shakeIntensity;
}
/// <summary>
/// Updates the focus detection system. Should be called every frame during active dialogue.
/// Performs raycasts at configured intervals to detect choice focus.
/// </summary>
public void Update()
{
if (!_manager.IsInDialogue || !_manager.CurrentDialogueUI.AreChoicesVisible())
return;
_raycastTimer += Time.deltaTime;
if (_raycastTimer >= _raycastInterval)
{
CheckForChoiceFocus();
_raycastTimer = 0f;
}
}
/// <summary>
/// Performs raycast from camera center to detect focused dialogue choices.
/// Manages focus state transitions, retention timers, visual feedback, and choice selection.
/// Implements stabilization to prevent rapid focus switching between choices.
/// </summary>
private void CheckForChoiceFocus()
{
if (_isFocusLocked)
{
if (_retentionTimer > 0f)
_retentionTimer -= Time.deltaTime;
return;
}
if (_mainCamera == null)
return;
Ray ray = _mainCamera.ViewportPointToRay(ViewportCenter);
ChoiceFocusHandler hitHandler = null;
bool raycastHitChoice = Physics.Raycast(ray, out RaycastHit hit, _focusRange, _choiceLayer);
if (raycastHitChoice)
{
hitHandler = hit.collider.GetComponent<ChoiceFocusHandler>();
if (_playerLook != null)
_playerLook.ApplyAimAssist(hit.collider.bounds.center);
}
if (hitHandler != _currentFocusedHandler)
{
_retentionTimer -= Time.deltaTime;
if (hitHandler != null && _retentionTimer <= 0f)
{
if (_currentFocusedHandler != null)
{
_isFocusLocked = true;
_currentFocusedHandler.ResetFeedback(_recoveryDuration);
}
_currentFocusedHandler = hitHandler;
_focusTimer = 0f;
_retentionTimer = _focusRetentionTime;
if (_playerLook != null)
_playerLook.LockAim(true);
DialogueAnimationSettings settings = _manager.CurrentAnimationSettings;
bool shouldRotate = settings == null || settings.EnableRotation;
if (shouldRotate && _manager.CurrentDialogueUI != null)
{
float focusSpeed = settings != null ? settings.FocusRotationSpeed : 60f;
_manager.CurrentDialogueUI.RotationAnimation(focusSpeed);
}
}
}
else if (hitHandler != null)
{
_retentionTimer = _focusRetentionTime;
}
bool isFocusActive = _currentFocusedHandler != null && (_retentionTimer > 0f);
if (isFocusActive)
{
_focusTimer += Time.deltaTime;
float progress = _focusTimer / _focusTime;
_currentFocusedHandler.ApplyFocusFeedback(progress);
if (_playerLook != null)
_playerLook.Shake(progress / _shakeIntensity);
if (_focusTimer >= _focusTime)
{
_isFocusLocked = true;
_currentFocusedHandler.ResetFeedback(_recoveryDuration);
if (_currentFocusedHandler.DialogueData != null)
{
OnChoiceMade?.Invoke(_currentFocusedHandler.DialogueData);
DialogueNode nextNode = _currentFocusedHandler.GetNextNode();
_manager.GoToNextNode(nextNode);
}
DialogueAnimationSettings settings = _manager.CurrentAnimationSettings;
bool shouldRotate = settings == null || settings.EnableRotation;
if (shouldRotate && _manager.CurrentDialogueUI != null)
{
float normalSpeed = settings != null ? settings.NormalRotationSpeed : 36f;
_manager.CurrentDialogueUI.RotationAnimation(normalSpeed);
}
ResetAnimationTimers();
if (_playerLook != null)
_playerLook.LockAim(false);
}
}
else if (_currentFocusedHandler != null && _retentionTimer <= 0f)
{
_isFocusLocked = true;
_currentFocusedHandler.ResetFeedback(_recoveryDuration);
DialogueAnimationSettings settings = _manager.CurrentAnimationSettings;
bool shouldRotate = settings == null || settings.EnableRotation;
if (shouldRotate && _manager.CurrentDialogueUI != null)
{
float normalSpeed = settings != null ? settings.NormalRotationSpeed : 36f;
_manager.CurrentDialogueUI.RotationAnimation(normalSpeed);
}
ResetAnimationTimers();
if (_playerLook != null)
_playerLook.LockAim(false);
}
}
private void ResetAnimationTimers()
{
_currentFocusedHandler = null;
_focusTimer = 0f;
_retentionTimer = 0f;
}
public void UnlockFocus()
{
_isFocusLocked = false;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 22514e9ce0f2e6c4b9f5f718ae139b09
@@ -0,0 +1,240 @@
using UnityEngine;
using System;
using Unheimlich.Data.Dialogues;
using Unheimlich.Gameplay.Player;
using Unheimlich.UI;
using Unheimlich.Interactions.Dialogues;
using System.Collections.Generic;
namespace Unheimlich.Managers
{
public class DialogueManager : MonoBehaviour
{
public static DialogueManager Instance { get; private set; }
[Header("State")]
public bool IsInDialogue = false;
[Header("Focus Settings")]
[SerializeField] private LayerMask _choiceLayer;
[SerializeField] private float _focusRange = 5f;
[SerializeField] private float _focusTime = 1.5f;
[SerializeField] private float _raycastInterval = 0.05f;
[Header("Focus Stabilization")]
[SerializeField] private float _recoveryDuration = 0.5f;
[SerializeField] private float _focusRetentionTime = 0.1f;
[SerializeField] private float _shakeIntensity = 30f;
public DialogueAnimationSettings CurrentAnimationSettings => _currentAnimationSettings;
public bool IsDialogueActive => IsInDialogue;
public Transform CurrentChoiceAnchor => _currentChoiceAnchor;
public DialogueUI CurrentDialogueUI => _activeDialogueUI;
public object CurrentOwner { get; private set; }
public DialogueConversation CurrentConversation => _currentConversation;
public DialogueNode CurrentNode => _currentNode;
public event Action<DialogueChoiceData> OnChoiceMade;
public event Action<object> OnDialogueCompleted;
private DialogueFocusSystem _focusSystem;
private Camera _mainCamera;
private PlayerLook _playerLook;
private DialogueAnimationSettings _currentAnimationSettings;
private DialogueUI _activeDialogueUI;
private Transform _currentChoiceAnchor;
private DialogueConversation _currentConversation;
private DialogueNode _currentNode;
private float _autoCloseTimer = 0f;
private bool _isWaitingForAutoClose = false;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(this);
return;
}
Instance = this;
}
private void Start()
{
_playerLook = PlayerLook.Instance;
_mainCamera = Camera.main;
_focusSystem = new DialogueFocusSystem(
this,
_mainCamera,
_playerLook,
_choiceLayer,
_focusRange,
_focusTime,
_raycastInterval,
_recoveryDuration,
_focusRetentionTime,
_shakeIntensity
);
_focusSystem.OnChoiceMade += (choice) => OnChoiceMade?.Invoke(choice);
}
private void Update()
{
if (!IsInDialogue)
return;
if (!_activeDialogueUI.AreChoicesVisible())
{
if (_isWaitingForAutoClose && _currentNode != null && _currentNode.AutoCloseDelay > 0f)
{
_autoCloseTimer += Time.deltaTime;
if (_autoCloseTimer >= _currentNode.AutoCloseDelay)
{
_activeDialogueUI.FadeOutAndHide(() =>
{
OnDialogueCompleted?.Invoke(CurrentOwner);
CleanupDialogue();
});
IsInDialogue = false;
_isWaitingForAutoClose = false;
if (DialogueBlurEffectURP.Instance != null)
{
DialogueBlurEffectURP.Instance.HideBlur();
}
return;
}
}
if (PlayerInputManager.Instance != null &&
PlayerInputManager.Instance.InteractAction.action.WasPressedThisFrame())
{
EndDialogue();
}
}
else
{
_focusSystem?.Update();
}
}
public void StartConversation(DialogueConversation conversation, Transform choiceAnchor = null, DialogueAnimationSettings animationSettings = null, DialogueUI customDialogueUI = null, object owner = null)
{
if (conversation == null)
{
Debug.LogError("Cannot start conversation: conversation is null");
return;
}
DialogueNode firstNode = conversation.GetFirstNode();
if (firstNode == null)
{
Debug.LogError($"Cannot start conversation {conversation.name}: no dialogue nodes found");
return;
}
_currentConversation = conversation;
_currentNode = firstNode;
_currentChoiceAnchor = choiceAnchor;
_currentAnimationSettings = animationSettings;
_activeDialogueUI = customDialogueUI;
CurrentOwner = owner;
ShowCurrentNode();
}
private void ShowCurrentNode()
{
if (_currentNode == null || _currentConversation == null)
return;
UnlockFocus();
IsInDialogue = true;
if(_currentChoiceAnchor == null)
{
Debug.LogWarning("No choice anchor found!");
return;
}
Transform anchor = _currentChoiceAnchor;
string speakerName = _currentConversation.SpeakerName;
string dialogueText = _currentNode.DialogueText;
_activeDialogueUI.ShowDialogueBox(speakerName, dialogueText);
_autoCloseTimer = 0f;
_isWaitingForAutoClose = false;
if (_currentNode.Choices != null && _currentNode.Choices.Count > 0)
{
List<DialogueChoiceData> validChoices = new List<DialogueChoiceData>();
foreach (var choiceData in _currentNode.Choices)
{
if (choiceData != null && choiceData.IsValid())
validChoices.Add(choiceData);
}
if (validChoices.Count > 0)
_activeDialogueUI.ShowChoices(validChoices, anchor, _currentAnimationSettings);
}
else
_isWaitingForAutoClose = true;
}
public void GoToNextNode(DialogueNode nextNode)
{
if (nextNode == null)
{
OnDialogueCompleted?.Invoke(CurrentOwner);
EndDialogue();
return;
}
UnlockFocus();
if (_activeDialogueUI != null)
_activeDialogueUI.HideChoices();
_currentNode = nextNode;
ShowCurrentNode();
}
public void EndDialogue()
{
IsInDialogue = false;
if (_activeDialogueUI != null)
_activeDialogueUI.HideDialogueBox();
if (DialogueBlurEffectURP.Instance != null)
DialogueBlurEffectURP.Instance.HideBlur();
CleanupDialogue();
}
private void CleanupDialogue()
{
_currentChoiceAnchor = null;
_currentAnimationSettings = null;
_activeDialogueUI = null;
CurrentOwner = null;
_currentConversation = null;
_currentNode = null;
_autoCloseTimer = 0f;
_isWaitingForAutoClose = false;
}
public void UnlockFocus()
{
_focusSystem?.UnlockFocus();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bd83d1e8e266cff41b8a5c2928f1f80f
@@ -0,0 +1,102 @@
using UnityEngine;
using Unheimlich.Gameplay.Portals;
namespace Unheimlich.Managers
{
/// <summary>
/// Dynamically adjusts Unity's shadow rendering distance based on proximity to active portals.
/// When the player is near a portal with a loaded room, shadow distance is extended to ensure
/// shadows from the remote room are visible through the portal. This improves visual quality
/// while maintaining performance when far from portals.
/// </summary>
public class DynamicShadowDistance : MonoBehaviour
{
private const float PORTAL_CHECK_DISTANCE = 100f;
[Header("Shadow Distance Settings")]
[SerializeField] private float _baseShadowDistance = 50f;
[SerializeField] private float _extendedShadowDistance = 250f;
[SerializeField] private float _transitionSpeed = 2f;
[Header("Detection")]
[SerializeField] private float _checkInterval = 0.5f;
private float _targetShadowDistance;
private float _currentShadowDistance;
private float _nextCheckTime;
private Transform _playerTransform;
private void Start()
{
_playerTransform = Camera.main.transform;
_currentShadowDistance = _baseShadowDistance;
_targetShadowDistance = _baseShadowDistance;
QualitySettings.shadowDistance = _baseShadowDistance;
}
private void Update()
{
if (Time.time >= _nextCheckTime)
{
_nextCheckTime = Time.time + _checkInterval;
UpdateTargetShadowDistance();
}
if (!Mathf.Approximately(_currentShadowDistance, _targetShadowDistance))
{
_currentShadowDistance = Mathf.Lerp(_currentShadowDistance, _targetShadowDistance, Time.deltaTime * _transitionSpeed);
QualitySettings.shadowDistance = _currentShadowDistance;
}
}
/// <summary>
/// Checks if any active portal with a loaded room is within range.
/// If found, extends shadow distance to cover the remote room.
/// </summary>
private void UpdateTargetShadowDistance()
{
bool needsExtendedDistance = false;
foreach (var portal in Portal.AllPortals)
{
if (portal == null || !portal.isActiveAndEnabled)
continue;
if (portal.LoadedRoomInstance != null)
{
float distanceToPortal = Vector3.Distance(_playerTransform.position, portal.transform.position);
if (distanceToPortal < PORTAL_CHECK_DISTANCE)
{
needsExtendedDistance = true;
break;
}
}
}
_targetShadowDistance = needsExtendedDistance ? _extendedShadowDistance : _baseShadowDistance;
}
private void OnValidate()
{
if (_baseShadowDistance < 0)
_baseShadowDistance = 0;
if (_extendedShadowDistance < _baseShadowDistance)
_extendedShadowDistance = _baseShadowDistance;
}
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
if (_playerTransform == null)
return;
Gizmos.color = new Color(1f, 1f, 0f, 0.2f);
Gizmos.DrawWireSphere(_playerTransform.position, QualitySettings.shadowDistance);
UnityEditor.Handles.Label(_playerTransform.position + Vector3.up * 2,
$"Shadow Distance: {QualitySettings.shadowDistance:F0}");
}
#endif
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e8f216e6440b9d445ac547aa24daf2b7
@@ -0,0 +1,94 @@
using UnityEngine;
using UnityEngine.InputSystem;
namespace Unheimlich.Managers
{
/// <summary>
/// Centralizes Input System handling for the player.
/// Manages input state, action references, and global input enable/disable functionality.
/// </summary>
[RequireComponent(typeof(PlayerInput))]
public class PlayerInputManager : MonoBehaviour
{
public static PlayerInputManager Instance { get; private set; }
[Header("Input Values (Read only)")]
public Vector2 MoveInput;
public Vector2 LookInput;
public InputActionReference JumpAction;
public InputActionReference InteractAction;
public static bool IsInputEnabled { get; private set; } = true;
public static bool IsLookingEnabled { get; private set; } = true;
private InputActionMap _playerActionMap;
private InputAction _lookAction;
private InputAction _interactAction;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(this);
return;
}
Instance = this;
IsInputEnabled = true;
PlayerInput playerInput = GetComponent<PlayerInput>();
playerInput.actions.Enable();
_playerActionMap = playerInput.actions.FindActionMap("Player");
_lookAction = _playerActionMap.FindAction("Look");
_interactAction = _playerActionMap.FindAction("Interact");
JumpAction.action.Enable();
InteractAction.action.Enable();
}
/// <summary>
/// Globally toggles player input on/off. When disabled, only look and interact actions remain active.
/// Used during dialogues, inspections, or UI interactions.
/// </summary>
public static void ToggleInput(bool enable)
{
if (Instance == null) return;
IsInputEnabled = enable;
if (enable)
{
IsLookingEnabled = true;
Instance._playerActionMap.Enable();
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
else
{
Instance._playerActionMap.Disable();
Instance._lookAction.Enable();
Instance._interactAction.Enable();
IsLookingEnabled = false;
}
}
public void OnMove(InputAction.CallbackContext context)
{
MoveInput = context.ReadValue<Vector2>();
}
public void Look(InputAction.CallbackContext context)
{
LookInput = context.ReadValue<Vector2>();
}
public void OnJump(InputAction.CallbackContext context)
{
}
public void OnInteract(InputAction.CallbackContext context)
{
}
}
}
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 860eff61af1c75243b697035c7b7fc83
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- JumpAction: {fileID: -2099379676528639254, guid: 052faaac586de48259a63d0c4782560b, type: 3}
- InteractAction: {fileID: 1781555164194001046, guid: 052faaac586de48259a63d0c4782560b, type: 3}
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -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);
}
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 25abbf68f203db94381b5c22eb1573bc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 100
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: