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