using UnityEngine; using UnityEngine.EventSystems; namespace TheIsland.Core { /// /// RTS-style camera controller for free-roaming over the island. /// Supports WASD movement, mouse scroll zoom, and optional edge scrolling. /// public class CameraController : MonoBehaviour { #region Movement Settings [Header("Movement")] [Tooltip("Camera movement speed (units per second)")] [SerializeField] private float moveSpeed = 15f; [Tooltip("Movement speed multiplier when holding Shift")] [SerializeField] private float fastMoveMultiplier = 2f; [Tooltip("Smooth movement interpolation (0 = instant, 1 = very smooth)")] [Range(0f, 0.99f)] [SerializeField] private float moveSmoothness = 0.1f; #endregion #region Zoom Settings [Header("Zoom")] [Tooltip("Zoom speed (scroll sensitivity)")] [SerializeField] private float zoomSpeed = 10f; [Tooltip("Minimum camera height (closest zoom)")] [SerializeField] private float minZoom = 5f; [Tooltip("Maximum camera height (farthest zoom)")] [SerializeField] private float maxZoom = 50f; [Tooltip("Smooth zoom interpolation")] [Range(0f, 0.99f)] [SerializeField] private float zoomSmoothness = 0.1f; #endregion #region Rotation Settings [Header("Rotation (Optional)")] [Tooltip("Enable middle mouse button rotation")] [SerializeField] private bool enableRotation = true; [Tooltip("Rotation speed")] [SerializeField] private float rotationSpeed = 100f; #endregion #region Edge Scrolling [Header("Edge Scrolling (Optional)")] [Tooltip("Enable screen edge scrolling")] [SerializeField] private bool enableEdgeScrolling = false; [Tooltip("Edge threshold in pixels")] [SerializeField] private float edgeThreshold = 20f; #endregion #region Bounds [Header("Movement Bounds")] [Tooltip("Limit camera movement to a specific area")] [SerializeField] private bool useBounds = false; [SerializeField] private Vector2 boundsMin = new Vector2(-50f, -50f); [SerializeField] private Vector2 boundsMax = new Vector2(50f, 50f); #endregion #region Private Fields private Vector3 _targetPosition; private float _targetZoom; private float _currentYRotation; private Camera _camera; #endregion #region Unity Lifecycle private void Start() { _camera = GetComponent(); if (_camera == null) { _camera = Camera.main; } _targetPosition = transform.position; _targetZoom = transform.position.y; _currentYRotation = transform.eulerAngles.y; } private void Update() { // Skip keyboard input when UI input field is focused if (!IsUIInputFocused()) { HandleMovementInput(); HandleRotationInput(); } // Zoom always works (mouse scroll doesn't conflict with typing) HandleZoomInput(); ApplyMovement(); } /// /// Check if a UI input field is currently focused. /// private bool IsUIInputFocused() { if (EventSystem.current == null) return false; GameObject selected = EventSystem.current.currentSelectedGameObject; if (selected == null) return false; // Check if the selected object has an input field component return selected.GetComponent() != null || selected.GetComponent() != null; } #endregion #region Input Handling private void HandleMovementInput() { Vector3 moveDirection = Vector3.zero; // WASD / Arrow keys input if (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow)) moveDirection += GetForward(); if (Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow)) moveDirection -= GetForward(); if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow)) moveDirection -= GetRight(); if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow)) moveDirection += GetRight(); // Edge scrolling if (enableEdgeScrolling) { Vector3 edgeMove = GetEdgeScrollDirection(); moveDirection += edgeMove; } // Apply movement if (moveDirection != Vector3.zero) { float speed = moveSpeed; if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift)) { speed *= fastMoveMultiplier; } moveDirection.Normalize(); _targetPosition += moveDirection * speed * Time.deltaTime; } // Clamp to bounds if (useBounds) { _targetPosition.x = Mathf.Clamp(_targetPosition.x, boundsMin.x, boundsMax.x); _targetPosition.z = Mathf.Clamp(_targetPosition.z, boundsMin.y, boundsMax.y); } } private void HandleZoomInput() { float scrollInput = Input.GetAxis("Mouse ScrollWheel"); if (Mathf.Abs(scrollInput) > 0.01f) { _targetZoom -= scrollInput * zoomSpeed; _targetZoom = Mathf.Clamp(_targetZoom, minZoom, maxZoom); } } private void HandleRotationInput() { if (!enableRotation) return; // Middle mouse button rotation if (Input.GetMouseButton(2)) { float rotateInput = Input.GetAxis("Mouse X"); _currentYRotation += rotateInput * rotationSpeed * Time.deltaTime; } // Q/E rotation (alternative) if (Input.GetKey(KeyCode.Q)) { _currentYRotation -= rotationSpeed * Time.deltaTime; } if (Input.GetKey(KeyCode.E)) { _currentYRotation += rotationSpeed * Time.deltaTime; } } private Vector3 GetEdgeScrollDirection() { Vector3 direction = Vector3.zero; Vector3 mousePos = Input.mousePosition; if (mousePos.x < edgeThreshold) direction -= GetRight(); else if (mousePos.x > Screen.width - edgeThreshold) direction += GetRight(); if (mousePos.y < edgeThreshold) direction -= GetForward(); else if (mousePos.y > Screen.height - edgeThreshold) direction += GetForward(); return direction; } #endregion #region Movement Application private void ApplyMovement() { // Smooth position Vector3 currentPos = transform.position; Vector3 newPos = new Vector3( Mathf.Lerp(currentPos.x, _targetPosition.x, 1f - moveSmoothness), Mathf.Lerp(currentPos.y, _targetZoom, 1f - zoomSmoothness), Mathf.Lerp(currentPos.z, _targetPosition.z, 1f - moveSmoothness) ); transform.position = newPos; // Update target Y to match current (for initialization) _targetPosition.y = _targetZoom; // Apply rotation if (enableRotation) { Quaternion targetRotation = Quaternion.Euler( transform.eulerAngles.x, // Keep current X (pitch) _currentYRotation, 0f ); transform.rotation = Quaternion.Slerp( transform.rotation, targetRotation, 1f - moveSmoothness ); } } private Vector3 GetForward() { // Get forward direction on XZ plane (ignoring pitch) Vector3 forward = transform.forward; forward.y = 0; return forward.normalized; } private Vector3 GetRight() { // Get right direction on XZ plane Vector3 right = transform.right; right.y = 0; return right.normalized; } #endregion #region Public API /// /// Move camera to focus on a specific world position. /// public void FocusOn(Vector3 worldPosition, bool instant = false) { _targetPosition = new Vector3(worldPosition.x, _targetZoom, worldPosition.z); if (instant) { transform.position = new Vector3(worldPosition.x, _targetZoom, worldPosition.z); } } /// /// Set zoom level directly. /// public void SetZoom(float zoomLevel, bool instant = false) { _targetZoom = Mathf.Clamp(zoomLevel, minZoom, maxZoom); if (instant) { Vector3 pos = transform.position; pos.y = _targetZoom; transform.position = pos; } } /// /// Set movement bounds at runtime. /// public void SetBounds(Vector2 min, Vector2 max) { useBounds = true; boundsMin = min; boundsMax = max; } /// /// Disable movement bounds. /// public void DisableBounds() { useBounds = false; } #endregion #region Editor Gizmos #if UNITY_EDITOR private void OnDrawGizmosSelected() { if (useBounds) { Gizmos.color = Color.yellow; Vector3 center = new Vector3( (boundsMin.x + boundsMax.x) / 2f, transform.position.y, (boundsMin.y + boundsMax.y) / 2f ); Vector3 size = new Vector3( boundsMax.x - boundsMin.x, 0.1f, boundsMax.y - boundsMin.y ); Gizmos.DrawWireCube(center, size); } } #endif #endregion } }