Complete reorga

This commit is contained in:
2026-03-09 17:55:23 +01:00
parent d9326e48cc
commit 10faf74405
703 changed files with 13040 additions and 1533 deletions
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a8de5fd3bd003994aaaf0ed9c2f5c44d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2ee4955d2b51b5e4abe0e243cab7267e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,80 @@
using UnityEngine;
namespace Unheimlich.World.CameraSystem
{
/// <summary>
/// Camera pan mode.
/// </summary>
public enum CameraPanMode
{
Pan,
FreeLook
}
/// <summary>
/// Head bob mode for camera pan.
/// </summary>
public enum HeadBobMode
{
Normal,
Idle,
Disabled
}
/// <summary>
/// ScriptableObject configuration for camera pan and player alignment behavior.
/// Defines pan speed, borders, angles, alignment parameters, and lerp smoothness.
/// </summary>
[CreateAssetMenu(fileName = "CameraPanSettings", menuName = "Unheimlich/Camera/Camera Pan Settings")]
public class CameraPanSettings : ScriptableObject
{
[Header("Pan Mode")]
[Tooltip("Pan: Smooth edge-based panning for object inspection | FreeLook: Direct mouse/stick control for detective board")]
public CameraPanMode PanMode = CameraPanMode.Pan;
[Header("Camera Pan")]
[Tooltip("Camera pan speed when moving the cursor/look stick")]
public float PanSpeed = 2f;
[Tooltip("Size of the border area for mouse pan (0-1, screen percentage)")]
[Range(0f, 0.5f)]
public float PanBorderSize = 0.1f;
[Tooltip("Maximum horizontal angle the camera can pan from center (left/right)")]
public float MaxPanAngleX = 15f;
[Tooltip("Maximum vertical angle the camera can pan from center (up/down)")]
public float MaxPanAngleY = 15f;
[Header("Recentering")]
[Tooltip("Enable automatic return to center when idle")]
public bool EnableRecentering = true;
[Tooltip("Speed of automatic recentering (higher = faster return to center)")]
public float RecenterSpeed = 2f;
[Tooltip("Delay before recentering starts (in seconds)")]
public float RecenterDelay = 0.1f;
[Header("Player Alignment")]
[Tooltip("Speed of player/camera alignment to target")]
public float AlignmentSpeed = 3f;
[Tooltip("Threshold for alignment completion")]
public float AlignmentThreshold = 0.5f;
[Tooltip("Fixed distance from the player to the target when aligning (in meters)")]
public float FixedDistanceFromTarget = 1.5f;
[Header("Head Bob Control")]
[Tooltip("Head bob mode: Normal (default behavior), Idle (force subtle breathing), Disabled (no movement)")]
public HeadBobMode HeadBobMode = HeadBobMode.Idle;
[Header("Lerp Smoothing")]
[Tooltip("Use SmoothStep for smoother interpolation (true) or linear lerp (false)")]
public bool UseSmoothStep = true;
[Tooltip("Custom animation curve for alignment (overrides SmoothStep if set)")]
public AnimationCurve AlignmentCurve = null;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3e231c75dddecc0469543c190a9234b1
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d6292657fd6825b4b958453d22c2de26
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,468 @@
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
using Unheimlich.World.CameraSystem;
using Unheimlich.Features.Player;
using InputManager = Unheimlich.Core.Input.InputManager;
namespace Unheimlich.World.CameraSystem
{
/// <summary>
/// Handles camera panning and player/camera alignment to targets.
/// </summary>
public class CameraPanController : MonoBehaviour
{
[Header("Default Settings")]
[SerializeField] private CameraPanSettings _defaultSettings;
private CameraPanSettings _settings;
private UnityEngine.Camera _camera;
private Transform _playerTransform;
private Vector2 _originalCameraRotation;
private Vector3 _originalCameraLocalPosition;
private Vector3 _originalPlayerPosition;
private Vector3 _originalPlayerRotation;
private Vector3 _originalCameraLocalRotation;
private Coroutine _alignmentCoroutine;
private Coroutine _returnCoroutine;
private float _timeSinceLastInput;
private float _targetYaw;
private float _targetPitch;
private float _currentYaw;
private float _currentPitch;
private float _lerpSpeed;
private bool _needsUpdate;
private CameraPanMode _mode = CameraPanMode.Pan;
private Vector2 _lastMousePosition;
public bool IsPanning { get; private set; }
public CameraPanMode Mode => _mode;
public CameraPanSettings Settings
{
get => _settings;
set => _settings = value;
}
private void Awake()
{
// Defer initialization if PlayerController not ready
if (PlayerController.Instance != null)
{
_camera = PlayerController.Instance.PlayerCamera;
_playerTransform = PlayerController.Instance.transform;
}
if (_defaultSettings != null)
_settings = _defaultSettings;
}
private void Start()
{
// Retry if not initialized in Awake
if (_camera == null && PlayerController.Instance != null)
{
_camera = PlayerController.Instance.PlayerCamera;
_playerTransform = PlayerController.Instance.transform;
}
if (_camera != null)
{
_originalCameraLocalPosition = _camera.transform.localPosition;
_originalCameraLocalRotation = _camera.transform.localEulerAngles;
}
if (_playerTransform != null)
_originalPlayerPosition = _playerTransform.position;
_originalPlayerRotation = _playerTransform.eulerAngles;
if (_settings == null && _defaultSettings != null)
_settings = _defaultSettings;
}
private void LateUpdate()
{
if (IsPanning && _needsUpdate)
{
if (_mode == CameraPanMode.FreeLook)
{
float normalizedYaw = Mathf.Repeat(_currentYaw, 360f);
float normalizedPitch = _currentPitch;
_playerTransform.eulerAngles = new Vector3(0f, normalizedYaw, 0f);
_camera.transform.localEulerAngles = new Vector3(normalizedPitch, 0f, 0f);
}
else
{
_currentYaw = Mathf.LerpAngle(_currentYaw, _targetYaw, Time.deltaTime * _lerpSpeed);
_currentPitch = Mathf.LerpAngle(_currentPitch, _targetPitch, Time.deltaTime * _lerpSpeed);
float normalizedYaw = Mathf.Repeat(_currentYaw, 360f);
float normalizedPitch = _currentPitch;
_playerTransform.eulerAngles = new Vector3(0f, normalizedYaw, 0f);
_camera.transform.localEulerAngles = new Vector3(normalizedPitch, 0f, 0f);
}
}
}
/// <summary>
/// Starts camera pan mode. The mode (Pan or FreeLook) is determined by the current settings.
/// </summary>
public void StartPan()
{
if (_settings == null)
{
Debug.LogError("[CameraPanController] Settings not assigned!");
return;
}
IsPanning = true;
_mode = _settings.PanMode;
_timeSinceLastInput = 0f;
if (Mouse.current != null)
_lastMousePosition = Mouse.current.position.ReadValue();
float currentPitch = _camera.transform.localEulerAngles.x;
if (currentPitch > 180f)
currentPitch -= 360f;
float currentYaw = _playerTransform.eulerAngles.y;
if (currentYaw > 180f)
currentYaw -= 360f;
_originalCameraRotation = new Vector2(currentYaw, currentPitch);
_currentYaw = currentYaw;
_currentPitch = currentPitch;
_targetYaw = currentYaw;
_targetPitch = currentPitch;
_needsUpdate = false;
}
public void StopPan()
{
IsPanning = false;
}
/// <summary>
/// Updates camera pan based on input (gamepad or mouse).
/// Call this from Update when pan is active.
/// </summary>
public void UpdatePan()
{
if (!IsPanning || _camera == null || _settings == null)
return;
if (_mode == CameraPanMode.FreeLook)
{
UpdateFreeLook();
}
else
{
UpdatePanMode();
}
}
private void UpdateFreeLook()
{
float lookX = 0f;
float lookY = 0f;
bool hasInput = false;
var lookAction = InputManager.GetLookAction();
if (lookAction != null)
{
Vector2 lookInput = lookAction.ReadValue<Vector2>();
if (lookInput.sqrMagnitude > 0.01f)
{
lookX = lookInput.x;
lookY = lookInput.y;
hasInput = true;
}
}
if (hasInput)
{
float sensitivity = _settings.PanSpeed * 0.1f;
_currentYaw += lookX * sensitivity;
_currentPitch -= lookY * sensitivity;
_currentYaw = Mathf.Clamp(_currentYaw, _originalCameraRotation.x - _settings.MaxPanAngleX, _originalCameraRotation.x + _settings.MaxPanAngleX);
_currentPitch = Mathf.Clamp(_currentPitch, _originalCameraRotation.y - _settings.MaxPanAngleY, _originalCameraRotation.y + _settings.MaxPanAngleY);
_needsUpdate = true;
}
}
private void UpdatePanMode()
{
if (!IsPanning || _camera == null || _settings == null)
return;
float panX = 0f;
float panY = 0f;
bool hasInput = false;
var lookAction = InputManager.GetLookAction();
if (lookAction != null)
{
Vector2 lookInput = lookAction.ReadValue<Vector2>();
if (lookInput.sqrMagnitude > 0.01f)
{
panX = lookInput.x;
panY = lookInput.y;
hasInput = true;
}
}
if (!hasInput && Mouse.current != null)
{
Vector2 mousePos = Mouse.current.position.ReadValue();
bool mouseHasMoved = Vector2.Distance(mousePos, _lastMousePosition) > 0.1f;
_lastMousePosition = mousePos;
if (mouseHasMoved)
{
float screenWidth = Screen.width;
float screenHeight = Screen.height;
float borderPixels = _settings.PanBorderSize * Mathf.Min(screenWidth, screenHeight);
if (mousePos.x < borderPixels)
{
panX = -(borderPixels - mousePos.x) / borderPixels;
hasInput = true;
}
else if (mousePos.x > screenWidth - borderPixels)
{
panX = (mousePos.x - (screenWidth - borderPixels)) / borderPixels;
hasInput = true;
}
if (mousePos.y < borderPixels)
{
panY = -(borderPixels - mousePos.y) / borderPixels;
hasInput = true;
}
else if (mousePos.y > screenHeight - borderPixels)
{
panY = (mousePos.y - (screenHeight - borderPixels)) / borderPixels;
hasInput = true;
}
}
}
if (hasInput)
{
_timeSinceLastInput = 0f;
}
else
{
_timeSinceLastInput += Time.deltaTime;
}
bool shouldRecenter = _settings.EnableRecentering && _timeSinceLastInput >= _settings.RecenterDelay;
if (hasInput)
{
_targetYaw = _originalCameraRotation.x + panX * _settings.MaxPanAngleX;
_targetPitch = _originalCameraRotation.y - panY * _settings.MaxPanAngleY;
_targetYaw = Mathf.Clamp(_targetYaw, _originalCameraRotation.x - _settings.MaxPanAngleX, _originalCameraRotation.x + _settings.MaxPanAngleX);
_targetPitch = Mathf.Clamp(_targetPitch, _originalCameraRotation.y - _settings.MaxPanAngleY, _originalCameraRotation.y + _settings.MaxPanAngleY);
_lerpSpeed = _settings.PanSpeed;
_needsUpdate = true;
}
else if (shouldRecenter)
{
_targetYaw = _originalCameraRotation.x;
_targetPitch = _originalCameraRotation.y;
_lerpSpeed = _settings.RecenterSpeed;
_needsUpdate = true;
}
else if (_needsUpdate)
{
float yawDiff = Mathf.Abs(Mathf.DeltaAngle(_currentYaw, _targetYaw));
float pitchDiff = Mathf.Abs(Mathf.DeltaAngle(_currentPitch, _targetPitch));
if (yawDiff < 0.01f && pitchDiff < 0.01f)
{
_needsUpdate = false;
}
}
}
/// <summary>
/// Aligns the player and camera to the target object.
/// - Player moves to a fixed distance in front of the target
/// - Player rotates to face the target
/// - Camera pitch adjusts to look at target center
/// </summary>
public void AlignToTarget(Transform target, System.Action onComplete = null)
{
if (_alignmentCoroutine != null)
StopCoroutine(_alignmentCoroutine);
_alignmentCoroutine = StartCoroutine(AlignCameraToTargetCoroutine(target, onComplete));
}
/// <summary>
/// Returns player and camera to original position and rotation.
/// </summary>
public void ReturnToOriginalPosition(System.Action onComplete = null)
{
if (_returnCoroutine != null)
StopCoroutine(_returnCoroutine);
_returnCoroutine = StartCoroutine(ReturnToOriginalPositionCoroutine(onComplete));
}
/// <summary>
/// Saves current player and camera state as the original position.
/// Call this before starting alignment.
/// </summary>
public void SaveOriginalState()
{
if (_camera != null)
{
_originalCameraLocalPosition = _camera.transform.localPosition;
_originalCameraLocalRotation = _camera.transform.localEulerAngles;
}
_originalPlayerPosition = _playerTransform.position;
_originalPlayerRotation = _playerTransform.eulerAngles;
}
private IEnumerator AlignCameraToTargetCoroutine(Transform target, System.Action onComplete)
{
if (_camera == null || target == null || _settings == null)
{
onComplete?.Invoke();
yield break;
}
SaveOriginalState();
Vector3 targetCenter = target.position;
Renderer targetRenderer = target.GetComponent<Renderer>();
if (targetRenderer != null)
{
targetCenter = targetRenderer.bounds.center;
}
else
{
Collider targetCollider = target.GetComponent<Collider>();
if (targetCollider != null)
targetCenter = targetCollider.bounds.center;
}
Vector3 startPlayerPos = _playerTransform.position;
Vector3 startPlayerRotation = _playerTransform.eulerAngles;
Vector3 startCameraLocalRotation = _camera.transform.localEulerAngles;
float startPitch = startCameraLocalRotation.x;
if (startPitch > 180f)
startPitch -= 360f;
Vector3 targetForward = target.forward;
Vector3 targetPlayerPos = targetCenter - targetForward * _settings.FixedDistanceFromTarget;
targetPlayerPos.y = startPlayerPos.y;
float elapsed = 0f;
float duration = 1f / _settings.AlignmentSpeed;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = GetInterpolationValue(elapsed / duration);
Vector3 currentCameraPos = _camera.transform.position;
Vector3 toTarget = targetCenter - currentCameraPos;
Vector3 directionToTarget = toTarget.normalized;
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget);
float targetYaw = targetRotation.eulerAngles.y;
float targetPitch = -Mathf.Asin(directionToTarget.y) * Mathf.Rad2Deg;
Vector3 currentPlayerPos = Vector3.Lerp(startPlayerPos, targetPlayerPos, t);
_playerTransform.position = currentPlayerPos;
float currentYaw = Mathf.LerpAngle(startPlayerRotation.y, targetYaw, t);
_playerTransform.eulerAngles = new Vector3(0f, currentYaw, 0f);
float currentPitch = Mathf.LerpAngle(startPitch, targetPitch, t);
_camera.transform.localEulerAngles = new Vector3(currentPitch, 0f, 0f);
yield return null;
}
_alignmentCoroutine = null;
onComplete?.Invoke();
}
private IEnumerator ReturnToOriginalPositionCoroutine(System.Action onComplete)
{
if (_camera == null || _settings == null)
{
onComplete?.Invoke();
yield break;
}
Vector3 startPlayerPos = _playerTransform.position;
Vector3 startPlayerRotation = _playerTransform.eulerAngles;
Vector3 startCameraLocalPos = _camera.transform.localPosition;
Vector3 startCameraLocalRotation = _camera.transform.localEulerAngles;
float startPitch = startCameraLocalRotation.x;
if (startPitch > 180f)
startPitch -= 360f;
float targetPitch = _originalCameraLocalRotation.x;
if (targetPitch > 180f)
targetPitch -= 360f;
float elapsed = 0f;
float duration = 1f / _settings.AlignmentSpeed;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = GetInterpolationValue(elapsed / duration);
_playerTransform.position = Vector3.Lerp(startPlayerPos, _originalPlayerPosition, t);
float currentYaw = Mathf.LerpAngle(startPlayerRotation.y, _originalPlayerRotation.y, t);
_playerTransform.eulerAngles = new Vector3(0f, currentYaw, 0f);
float currentPitch = Mathf.LerpAngle(startPitch, targetPitch, t);
_camera.transform.localEulerAngles = new Vector3(currentPitch, 0f, 0f);
_camera.transform.localPosition = Vector3.Lerp(startCameraLocalPos, _originalCameraLocalPosition, t);
yield return null;
}
_playerTransform.position = _originalPlayerPosition;
_playerTransform.eulerAngles = _originalPlayerRotation;
_camera.transform.localPosition = _originalCameraLocalPosition;
_camera.transform.localEulerAngles = _originalCameraLocalRotation;
_returnCoroutine = null;
onComplete?.Invoke();
}
private float GetInterpolationValue(float t)
{
if (_settings.AlignmentCurve != null && _settings.AlignmentCurve.length > 0)
return _settings.AlignmentCurve.Evaluate(t);
if (_settings.UseSmoothStep)
return Mathf.SmoothStep(0f, 1f, t);
return t;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 69e8150bab500424eb366f0ac8761995
@@ -0,0 +1,434 @@
using UnityEngine;
using UnityEngine.InputSystem;
using Unheimlich.Features.Dialogue;
using Unheimlich.Core;
namespace Unheimlich.World.CameraSystem
{
/// <summary>
/// Virtual camera system for detective board inspection (Alan Wake 2 style).
/// Handles smooth transition, edge-based panning, and zoom.
/// </summary>
public class DetectiveBoardVirtualCamera : Singleton<DetectiveBoardVirtualCamera>
{
[Header("References")]
[SerializeField] private Camera _virtualCamera;
private Transform _boardTarget;
[Header("Transition Settings")]
[SerializeField] private float _transitionSpeed = 3f;
[SerializeField] private AnimationCurve _transitionCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
[SerializeField] private float _initialDistance = 1.5f;
[Header("Pan Settings")]
[SerializeField] private float _panSpeed = 2f;
[SerializeField] private float _panSmoothTime = 0.2f;
[SerializeField][Range(0f, 0.5f)] private float _edgeBorderSize = 0.1f;
[SerializeField] private Vector2 _maxPanOffset = new Vector2(1.5f, 0.5f);
[SerializeField] private bool _useDynamicPanLimits = true;
[SerializeField] private float _panMargin = 0.0f;
[SerializeField] private bool _debugLimits = false;
[Header("Zoom Settings")]
[SerializeField] private float _minDistance = 0.5f;
[SerializeField] private float _maxDistance = 3f;
[SerializeField] private float _zoomSpeed = 2f;
[SerializeField] private float _zoomSmoothTime = 0.15f;
[SerializeField] private float _scrollSensitivity = 0.3f;
private Camera _playerCamera;
private Transform _playerCameraTransform;
private Vector3 _startPosition;
private Quaternion _startRotation;
private Vector3 _targetCenterPosition;
private Quaternion _targetCenterRotation;
private bool _isActive;
private bool _isTransitioning;
private float _transitionProgress;
private Renderer _cachedBoardRenderer;
private Vector2 _currentPanOffset;
private Vector2 _targetPanOffset;
private Vector2 _panVelocity;
private Vector2 _dynamicMaxPanOffset;
private float _currentDistance;
private float _targetDistance;
private float _zoomVelocity;
private Vector2 _lastMousePosition;
public bool IsActive => _isActive;
public Camera VirtualCamera => _virtualCamera;
protected override void OnAwake()
{
if (_virtualCamera == null)
_virtualCamera = GetComponent<Camera>();
if (_virtualCamera != null)
_virtualCamera.enabled = false;
}
public void ActivateFromPlayerPosition(Camera playerCamera, Transform boardTarget, Renderer boardRenderer = null)
{
if (_isActive)
return;
Debug.Log($"[VirtualCam] ActivateFromPlayerPosition - DebugLimits: {_debugLimits}");
_playerCamera = playerCamera;
_playerCameraTransform = playerCamera.transform;
_boardTarget = boardTarget;
_cachedBoardRenderer = boardRenderer;
_startPosition = _playerCameraTransform.position;
_startRotation = _playerCameraTransform.rotation;
_virtualCamera.transform.position = _startPosition;
_virtualCamera.transform.rotation = _startRotation;
CalculateTargetPosition();
_currentDistance = _initialDistance;
_targetDistance = _initialDistance;
_currentPanOffset = Vector2.zero;
_targetPanOffset = Vector2.zero;
_isActive = true;
_isTransitioning = true;
_transitionProgress = 0f;
_virtualCamera.enabled = true;
_playerCamera.enabled = false;
Canvas boardCanvas = _boardTarget.GetComponentInChildren<Canvas>();
if (boardCanvas != null && boardCanvas.renderMode == RenderMode.WorldSpace)
{
boardCanvas.worldCamera = _virtualCamera;
}
if (Mouse.current != null)
_lastMousePosition = Mouse.current.position.ReadValue();
}
public void Activate(Camera playerCamera, Transform boardTarget)
{
if (_isActive)
return;
_playerCamera = playerCamera;
_playerCameraTransform = playerCamera.transform;
_boardTarget = boardTarget;
_startPosition = _playerCameraTransform.position;
_startRotation = _playerCameraTransform.rotation;
CalculateTargetPosition();
_currentDistance = _initialDistance;
_targetDistance = _initialDistance;
_currentPanOffset = Vector2.zero;
_targetPanOffset = Vector2.zero;
_isActive = true;
_isTransitioning = true;
_transitionProgress = 0f;
_virtualCamera.enabled = true;
_playerCamera.enabled = false;
Canvas boardCanvas = _boardTarget.GetComponentInChildren<Canvas>();
if (boardCanvas != null && boardCanvas.renderMode == RenderMode.WorldSpace)
{
boardCanvas.worldCamera = _virtualCamera;
}
if (Mouse.current != null)
_lastMousePosition = Mouse.current.position.ReadValue();
}
public void Deactivate(System.Action onComplete = null)
{
if (!_isActive)
{
onComplete?.Invoke();
return;
}
StartCoroutine(ReturnTransitionCoroutine(onComplete));
}
private void CalculateTargetPosition()
{
Vector3 boardCenter = _boardTarget.position;
Renderer renderer = _boardTarget.GetComponent<Renderer>();
if (renderer != null)
boardCenter = renderer.bounds.center;
else
{
Collider collider = _boardTarget.GetComponent<Collider>();
if (collider != null)
boardCenter = collider.bounds.center;
}
_targetCenterPosition = boardCenter - _boardTarget.forward * _initialDistance;
_targetCenterRotation = Quaternion.LookRotation(_boardTarget.forward);
}
private void Update()
{
if (_debugLimits)
Debug.Log($"[VirtualCam] Update - Active: {_isActive} | Transitioning: {_isTransitioning}");
if (!_isActive)
return;
if (_isTransitioning)
{
UpdateTransition();
}
else
{
UpdatePanning();
UpdateZoom();
ApplyCameraTransform();
}
}
private void UpdateTransition()
{
_transitionProgress += Time.deltaTime * _transitionSpeed;
float t = _transitionCurve.Evaluate(Mathf.Clamp01(_transitionProgress));
Vector3 position = Vector3.Lerp(_startPosition, _targetCenterPosition, t);
Quaternion rotation = Quaternion.Slerp(_startRotation, _targetCenterRotation, t);
_virtualCamera.transform.position = position;
_virtualCamera.transform.rotation = rotation;
if (_transitionProgress >= 1f)
{
_isTransitioning = false;
}
}
private void UpdatePanning()
{
float panX = 0f;
float panY = 0f;
bool hasInput = false;
if (Mouse.current != null)
{
Vector2 mousePos = Mouse.current.position.ReadValue();
bool mouseHasMoved = Vector2.Distance(mousePos, _lastMousePosition) > 0.1f;
_lastMousePosition = mousePos;
if (mouseHasMoved)
{
float screenWidth = Screen.width;
float screenHeight = Screen.height;
float borderPixels = _edgeBorderSize * Mathf.Min(screenWidth, screenHeight);
if (mousePos.x < borderPixels)
{
panX = -(borderPixels - mousePos.x) / borderPixels;
hasInput = true;
}
else if (mousePos.x > screenWidth - borderPixels)
{
panX = (mousePos.x - (screenWidth - borderPixels)) / borderPixels;
hasInput = true;
}
if (mousePos.y < borderPixels)
{
panY = -(borderPixels - mousePos.y) / borderPixels;
hasInput = true;
}
else if (mousePos.y > screenHeight - borderPixels)
{
panY = (mousePos.y - (screenHeight - borderPixels)) / borderPixels;
hasInput = true;
}
}
}
if (hasInput)
{
float deltaX = panX * _panSpeed * Time.deltaTime;
float deltaY = panY * _panSpeed * Time.deltaTime;
_targetPanOffset.x += deltaX;
_targetPanOffset.y += deltaY;
}
Vector2 maxOffset = _useDynamicPanLimits ? _dynamicMaxPanOffset : _maxPanOffset;
_targetPanOffset.x = Mathf.Clamp(_targetPanOffset.x, -maxOffset.x, maxOffset.x);
_targetPanOffset.y = Mathf.Clamp(_targetPanOffset.y, -maxOffset.y, maxOffset.y);
if (!hasInput && _useDynamicPanLimits)
{
if (Mathf.Abs(_targetPanOffset.x) > maxOffset.x)
{
float targetX = Mathf.Sign(_targetPanOffset.x) * maxOffset.x;
_targetPanOffset.x = Mathf.Lerp(_targetPanOffset.x, targetX, Time.deltaTime * 3f);
}
if (Mathf.Abs(_targetPanOffset.y) > maxOffset.y)
{
float targetY = Mathf.Sign(_targetPanOffset.y) * maxOffset.y;
_targetPanOffset.y = Mathf.Lerp(_targetPanOffset.y, targetY, Time.deltaTime * 3f);
}
}
_currentPanOffset = Vector2.SmoothDamp(_currentPanOffset, _targetPanOffset, ref _panVelocity, _panSmoothTime);
}
private void UpdateZoom()
{
if (Mouse.current != null)
{
float scroll = Mouse.current.scroll.ReadValue().y;
if (Mathf.Abs(scroll) > 0.1f)
{
_targetDistance -= scroll * _scrollSensitivity * Time.deltaTime;
_targetDistance = Mathf.Clamp(_targetDistance, _minDistance, _maxDistance);
}
}
_currentDistance = Mathf.SmoothDamp(_currentDistance, _targetDistance, ref _zoomVelocity, _zoomSmoothTime);
}
private void ApplyCameraTransform()
{
if (_debugLimits)
Debug.Log($"[VirtualCam] ApplyCameraTransform called - BoardTarget: {(_boardTarget != null ? _boardTarget.name : "NULL")}");
Vector3 boardCenter = _boardTarget.position;
Renderer renderer = _cachedBoardRenderer;
if (renderer == null)
renderer = _boardTarget.GetComponent<Renderer>();
if (renderer != null)
boardCenter = renderer.bounds.center;
else
{
Collider collider = _boardTarget.GetComponent<Collider>();
if (collider != null)
boardCenter = collider.bounds.center;
}
if (_debugLimits)
Debug.Log($"[VirtualCam] Renderer: {(renderer != null ? "Found" : "NULL")} | UseDynamicLimits: {_useDynamicPanLimits}");
Vector3 right = _boardTarget.right;
Vector3 up = _boardTarget.up;
Vector3 forward = _boardTarget.forward;
if (_useDynamicPanLimits)
{
CalculateDynamicPanLimits(renderer);
}
Vector3 panOffset = right * _currentPanOffset.x + up * _currentPanOffset.y;
Vector3 targetPos = boardCenter + panOffset - forward * _currentDistance;
_virtualCamera.transform.position = targetPos;
_virtualCamera.transform.rotation = Quaternion.LookRotation(forward);
}
private void CalculateDynamicPanLimits(Renderer boardRenderer)
{
if (_virtualCamera == null)
{
if (_debugLimits)
Debug.LogWarning("[VirtualCam] No VirtualCamera! Using fixed maxPanOffset.");
_dynamicMaxPanOffset = _maxPanOffset;
return;
}
float distance = _currentDistance;
float fov = _virtualCamera.fieldOfView;
float aspect = _virtualCamera.aspect;
float halfHeightAtDistance = distance * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
float halfWidthAtDistance = halfHeightAtDistance * aspect;
float boardHalfWidth;
float boardHalfHeight;
if (boardRenderer != null)
{
Bounds bounds = boardRenderer.bounds;
Vector3 localSize = _boardTarget.InverseTransformVector(bounds.size);
boardHalfWidth = Mathf.Abs(localSize.x) * 0.5f;
boardHalfHeight = Mathf.Abs(localSize.y) * 0.5f;
}
else
{
RectTransform rectTransform = _boardTarget.GetComponent<RectTransform>();
if (rectTransform != null)
{
Vector2 size = rectTransform.rect.size;
Vector3 scale = rectTransform.lossyScale;
boardHalfWidth = (size.x * scale.x) * 0.5f;
boardHalfHeight = (size.y * scale.y) * 0.5f;
}
else
{
if (_debugLimits)
Debug.LogWarning("[VirtualCam] No Renderer or RectTransform found! Using fixed maxPanOffset.");
_dynamicMaxPanOffset = _maxPanOffset;
return;
}
}
_dynamicMaxPanOffset.x = Mathf.Max(0f, boardHalfWidth - halfWidthAtDistance - _panMargin);
_dynamicMaxPanOffset.y = Mathf.Max(0f, boardHalfHeight - halfHeightAtDistance - _panMargin);
if (_debugLimits)
{
Debug.Log($"[VirtualCam] Distance: {distance:F2} | FOV: {fov:F1} | Aspect: {aspect:F2}");
Debug.Log($"[VirtualCam] Viewport: {halfWidthAtDistance*2:F2}x{halfHeightAtDistance*2:F2} | Board: {boardHalfWidth*2:F2}x{boardHalfHeight*2:F2}");
Debug.Log($"[VirtualCam] MaxPan: X={_dynamicMaxPanOffset.x:F2} Y={_dynamicMaxPanOffset.y:F2} | Current: X={_targetPanOffset.x:F2} Y={_targetPanOffset.y:F2}");
}
}
private System.Collections.IEnumerator ReturnTransitionCoroutine(System.Action onComplete)
{
Vector3 currentPos = _virtualCamera.transform.position;
Quaternion currentRot = _virtualCamera.transform.rotation;
float elapsed = 0f;
float duration = 1f / _transitionSpeed;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = _transitionCurve.Evaluate(Mathf.Clamp01(elapsed / duration));
_virtualCamera.transform.position = Vector3.Lerp(currentPos, _startPosition, t);
_virtualCamera.transform.rotation = Quaternion.Slerp(currentRot, _startRotation, t);
yield return null;
}
_virtualCamera.enabled = false;
_playerCamera.enabled = true;
_isActive = false;
Canvas boardCanvas = _boardTarget.GetComponentInChildren<Canvas>();
if (boardCanvas != null && boardCanvas.renderMode == RenderMode.WorldSpace)
{
boardCanvas.worldCamera = _playerCamera;
}
onComplete?.Invoke();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: df440cd4d3cc30d4e9f5b44ab4931a12
@@ -0,0 +1,102 @@
using UnityEngine;
using Unheimlich.Features.Portals;
namespace Unheimlich.World.CameraSystem
{
/// <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,8 @@
fileFormatVersion: 2
guid: b29704286be24904999abcd0ff0cc448
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,49 @@
using UnityEngine;
using Unheimlich.Core.Interaction;
using Unheimlich.Core.Utils;
namespace Unheimlich.World.Environment
{
public class DoorInteractable : InteractableBase
{
[Header("Door Settings")]
public bool IsOpen = false;
[SerializeField] private float _openAngle = 90f;
[SerializeField] private float _animationDuration = 0.5f;
[SerializeField] private LeanTweenType _easeType = LeanTweenType.easeInOutQuad;
private Quaternion _closedRotation;
private Quaternion _openRotation;
protected virtual void Start()
{
_closedRotation = transform.rotation;
_openRotation = _closedRotation * Quaternion.Euler(0, _openAngle, 0);
InteractionText = IsOpen ? "Close" : "Open";
}
public override void Interact()
{
ToggleDoor();
}
public void ToggleDoor()
{
gameObject.CancelAllTweens();
if (!IsOpen)
{
TweenHelper.RotateLocal(gameObject, _openRotation.eulerAngles, _animationDuration, null, _easeType);
IsOpen = true;
InteractionText = "Close";
}
else
{
TweenHelper.RotateLocal(gameObject, _closedRotation.eulerAngles, _animationDuration, null, _easeType);
IsOpen = false;
InteractionText = "Open";
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d620ab476d55f2542a31783a6684f189
@@ -0,0 +1,99 @@
using UnityEngine;
using Unheimlich.Features.Player;
namespace Unheimlich.World.Environment
{
public class SecurityCameraController : MonoBehaviour
{
[Header("Tracking Settings")]
[SerializeField] private float _trackingDistance = 10f;
[SerializeField] private float _rotationSpeed = 2f;
[SerializeField] private Transform _pivotPoint;
[Header("LED Settings")]
[SerializeField] private MeshRenderer _ledRenderer;
[SerializeField] private float _blinkSpeed = 2f;
[SerializeField] private float _minSmoothness = 0f;
[SerializeField] private float _maxSmoothness = 0.5f;
private CharacterController _playerController;
private Material _ledMaterial;
private float _blinkTimer;
private bool _isTracking;
void Start()
{
if (PlayerController.Instance != null)
_playerController = PlayerController.Instance.GetComponent<CharacterController>();
if (_ledRenderer != null)
{
_ledMaterial = _ledRenderer.material;
}
if (_pivotPoint == null)
_pivotPoint = transform;
}
void Update()
{
if (_playerController == null) return;
float distanceToPlayer = Vector3.Distance(_pivotPoint.position, _playerController.transform.position);
_isTracking = distanceToPlayer <= _trackingDistance;
if (_isTracking)
{
TrackPlayer();
}
UpdateLedBlink();
}
void TrackPlayer()
{
Vector3 directionToPlayer = _playerController.transform.position - _pivotPoint.position;
if (directionToPlayer.sqrMagnitude > 0.001f)
{
Vector3 forward = -directionToPlayer.normalized;
Vector3 right = Vector3.Cross(Vector3.up, forward).normalized;
Vector3 up = Vector3.Cross(forward, right);
Quaternion targetRotation = Quaternion.LookRotation(up, forward);
_pivotPoint.rotation = Quaternion.Slerp(_pivotPoint.rotation, targetRotation, Time.deltaTime * _rotationSpeed);
}
}
void UpdateLedBlink()
{
if (_ledMaterial == null) return;
_blinkTimer += Time.deltaTime * _blinkSpeed;
float smoothness = Mathf.Lerp(_minSmoothness, _maxSmoothness, Mathf.PingPong(_blinkTimer, 1f));
_ledMaterial.SetFloat("_Smoothness", smoothness);
}
void OnDestroy()
{
if (_ledMaterial != null)
Destroy(_ledMaterial);
}
void OnDrawGizmosSelected()
{
if (_pivotPoint == null)
_pivotPoint = transform;
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(_pivotPoint.position, _trackingDistance);
Gizmos.color = Color.red;
Gizmos.DrawLine(_pivotPoint.position, _pivotPoint.position + _pivotPoint.forward * 2f);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 17a573927733b4343acc662995710bc2