Snapshot before project reorganization

This commit is contained in:
2026-02-12 16:28:36 +01:00
parent 8758d6eaeb
commit 9b7bb2aa62
27 changed files with 1864 additions and 119 deletions
@@ -9,6 +9,9 @@ namespace Unheimlich.Gameplay.CameraControl
/// <summary>
/// Handles camera panning and player/camera alignment to targets.
/// Uses CameraPanSettings ScriptableObject for configuration.
/// Supports two modes:
/// - Pan: Smooth panning with edge detection (for object inspection)
/// - FreeLook: Direct mouse/stick control with angle clamping (for detective board)
/// </summary>
public class CameraPanController : MonoBehaviour
{
@@ -16,8 +19,10 @@ namespace Unheimlich.Gameplay.CameraControl
[SerializeField] private UnityEngine.Camera _camera;
[SerializeField] private Transform _playerTransform;
[Header("Settings")]
[SerializeField] private CameraPanSettings _settings;
[Header("Default Settings")]
[SerializeField] private CameraPanSettings _defaultSettings;
private CameraPanSettings _settings;
private Vector2 _originalCameraRotation;
private Vector3 _originalCameraLocalPosition;
@@ -28,7 +33,17 @@ namespace Unheimlich.Gameplay.CameraControl
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
{
@@ -52,10 +67,39 @@ namespace Unheimlich.Gameplay.CameraControl
_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.
/// Starts camera pan mode. The mode (Pan or FreeLook) is determined by the current settings.
/// </summary>
public void StartPan()
{
@@ -66,13 +110,26 @@ namespace Unheimlich.Gameplay.CameraControl
}
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;
_originalCameraRotation = new Vector2(_playerTransform.eulerAngles.y, currentPitch);
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;
}
/// <summary>
@@ -88,6 +145,52 @@ namespace Unheimlich.Gameplay.CameraControl
/// 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 = PlayerInput.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;
@@ -111,31 +214,38 @@ namespace Unheimlich.Gameplay.CameraControl
if (!hasInput && Mouse.current != null)
{
Vector2 mousePos = Mouse.current.position.ReadValue();
float screenWidth = Screen.width;
float screenHeight = Screen.height;
float borderPixels = _settings.PanBorderSize * Mathf.Min(screenWidth, screenHeight);
bool mouseHasMoved = Vector2.Distance(mousePos, _lastMousePosition) > 0.1f;
_lastMousePosition = mousePos;
if (mousePos.x < borderPixels)
if (mouseHasMoved)
{
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;
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;
}
}
}
@@ -148,48 +258,39 @@ namespace Unheimlich.Gameplay.CameraControl
_timeSinceLastInput += Time.deltaTime;
}
float currentYaw = _playerTransform.eulerAngles.y;
float currentPitch = _camera.transform.localEulerAngles.x;
if (currentPitch > 180f)
currentPitch -= 360f;
float targetYaw;
float targetPitch;
float lerpSpeed;
bool shouldRecenter = _settings.EnableRecentering && _timeSinceLastInput >= _settings.RecenterDelay;
if (hasInput)
{
targetYaw = _originalCameraRotation.x + panX * _settings.MaxPanAngle;
targetPitch = _originalCameraRotation.y - panY * _settings.MaxPanAngle;
targetYaw = Mathf.Clamp(targetYaw, _originalCameraRotation.x - _settings.MaxPanAngle, _originalCameraRotation.x + _settings.MaxPanAngle);
targetPitch = Mathf.Clamp(targetPitch, _originalCameraRotation.y - _settings.MaxPanAngle, _originalCameraRotation.y + _settings.MaxPanAngle);
lerpSpeed = _settings.PanSpeed;
_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;
_targetYaw = _originalCameraRotation.x;
_targetPitch = _originalCameraRotation.y;
_lerpSpeed = _settings.RecenterSpeed;
_needsUpdate = true;
}
else
else if (_needsUpdate)
{
targetYaw = currentYaw;
targetPitch = currentPitch;
lerpSpeed = 0f;
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;
}
}
float newYaw = Mathf.LerpAngle(currentYaw, targetYaw, Time.deltaTime * lerpSpeed);
float newPitch = Mathf.LerpAngle(currentPitch, targetPitch, Time.deltaTime * lerpSpeed);
_playerTransform.eulerAngles = new Vector3(0f, newYaw, 0f);
_camera.transform.localEulerAngles = new Vector3(newPitch, 0f, 0f);
}
/// <summary>
/// Aligns the player and camera to the target object.
/// - Player moves along target's local X axis to align with target center
/// - 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>
@@ -259,6 +360,10 @@ namespace Unheimlich.Gameplay.CameraControl
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;
@@ -275,11 +380,6 @@ namespace Unheimlich.Gameplay.CameraControl
float targetYaw = targetRotation.eulerAngles.y;
float targetPitch = -Mathf.Asin(directionToTarget.y) * Mathf.Rad2Deg;
Vector3 targetRightAxis = target.right;
Vector3 playerToTargetCenter = targetCenter - _playerTransform.position;
float offsetAlongTargetRight = Vector3.Dot(playerToTargetCenter, targetRightAxis);
Vector3 targetPlayerPos = _playerTransform.position + targetRightAxis * offsetAlongTargetRight;
Vector3 currentPlayerPos = Vector3.Lerp(startPlayerPos, targetPlayerPos, t);
_playerTransform.position = currentPlayerPos;