diff --git a/Assets/Scripts/Gameplay/GameplayObjects/Character/EdgePanningController.cs b/Assets/Scripts/Gameplay/GameplayObjects/Character/EdgePanningController.cs index 375411e9..ef5e1561 100644 --- a/Assets/Scripts/Gameplay/GameplayObjects/Character/EdgePanningController.cs +++ b/Assets/Scripts/Gameplay/GameplayObjects/Character/EdgePanningController.cs @@ -5,198 +5,245 @@ using Unity.BossRoom.CameraUtils; public class EdgePanningController : MonoBehaviour { + [Header("Camera References")] public CinemachineFreeLook virtualCamera; private CinemachineCameraOffset cameraOffset; - - private Coroutine shakeCoroutine; - - public float edgeThreshold = 50f; // Edge detection distance from screen edges - public float resetRadiusPercentage = 3f; // Center reset radius in percentage of screen - public float panSpeed = 0.1f; // How much to offset per frame - public float resetSpeed = 2f; // Speed of resetting panning - public Vector2 panLimit = new Vector2(0.5f, 0.5f); // Max panning limit (x, y) - - // --- Added fields for free roam mode --- - public float freeRoamSpeed = 10f; // Speed of free roam movement - private bool isFreeRoamMode = false; // Toggle for free roam mode - private Vector3 savedCameraPosition; // Stores camera position before free roam - private Quaternion savedCameraRotation; // Stores camera rotation before free roam - private Transform savedFollowTarget; // Stores the original follow target - - private Vector3 defaultOffset; - private Vector3 panningOffset; + private Transform cameraTransform; + + [Header("Edge Panning Settings")] + public float edgeThreshold = 10f; // Distance from screen edge to trigger panning + public float panSpeed = 20f; // World units per second + public float maxPanDistance = 30f; // Maximum distance from player + public float returnSpeed = 15f; // Speed when returning to player + public float returnDelay = 0.5f; // Delay before auto-return starts + + [Header("Smoothing")] + public float smoothTime = 0.1f; // Camera movement smoothing + + // Camera state + private Transform targetCharacter; + private Vector3 panOffset = Vector3.zero; + private Vector3 currentVelocity = Vector3.zero; + private float timeSincePanStopped = 0f; private bool isPanning = false; - private bool targetFound = false; - private bool shouldReset = false; // Flag to trigger reset immediately - - private Transform targetCharacter; // The character the camera follows - + private bool isReturning = false; + private Vector3 lastValidPosition; + + // Free camera mode (for debugging) + private bool isFreeRoamMode = false; + public float freeRoamSpeed = 20f; + + // Camera shake + private Coroutine shakeCoroutine; + private void Start() { + // Get camera offset component cameraOffset = virtualCamera.GetComponent(); if (cameraOffset == null) { - Debug.LogError("[EdgePanningController] No CinemachineCameraOffset component found! Add it to the FreeLook Camera."); - return; + cameraOffset = virtualCamera.gameObject.AddComponent(); } - - defaultOffset = cameraOffset.m_Offset; - panningOffset = defaultOffset; + + // Cache camera transform + cameraTransform = virtualCamera.transform; + + // Initialize offset + cameraOffset.m_Offset = Vector3.zero; } - + private void Awake() { CameraController.OnCameraAttached += SetTargetCharacter; } - + private void OnDestroy() { CameraController.OnCameraAttached -= SetTargetCharacter; } - + private void SetTargetCharacter(Transform characterTransform) { targetCharacter = characterTransform.parent; - targetFound = true; + lastValidPosition = targetCharacter.position; } - - private void Update() + + private void LateUpdate() { - // Check for backspace key to toggle free roam mode + // Toggle free roam mode if (Input.GetKeyDown(KeyCode.Backspace)) { ToggleFreeRoamMode(); } - // When in free roam mode, handle free movement and bypass normal edge panning if (isFreeRoamMode) { HandleFreeRoam(); - return; // Skip the rest of Update so the panning logic is not executed + return; } - - // Normal camera behavior (edge panning) if a target is set - if (targetFound) - HandleEdgePanning(); + + if (targetCharacter == null) return; + + HandleDotaStylePanning(); } - - /// - /// Toggles the free roam mode on/off. - /// - private void ToggleFreeRoamMode() + + private void HandleDotaStylePanning() { - isFreeRoamMode = !isFreeRoamMode; - if (isFreeRoamMode) + Vector3 mousePos = Input.mousePosition; + Vector3 panDirection = Vector3.zero; + + // Check screen edges + bool nearEdge = false; + + // Left edge + if (mousePos.x <= edgeThreshold) { - // Activate free roam: save current camera state and disable following - savedCameraPosition = virtualCamera.transform.position; - savedCameraRotation = virtualCamera.transform.rotation; - savedFollowTarget = virtualCamera.Follow; // Save the current follow target if any - virtualCamera.Follow = null; // Detach so the camera can be controlled directly + float strength = 1f - (mousePos.x / edgeThreshold); + panDirection.x = -strength; + nearEdge = true; } - else + // Right edge + else if (mousePos.x >= Screen.width - edgeThreshold) { - // Deactivate free roam: restore the previous camera state and resume following target - virtualCamera.transform.position = savedCameraPosition; - virtualCamera.transform.rotation = savedCameraRotation; - virtualCamera.Follow = savedFollowTarget; // Reattach the follow target - - // Reset panning variables so the camera aligns with its default offset - cameraOffset.m_Offset = defaultOffset; - isPanning = false; - panningOffset = defaultOffset; + float strength = 1f - ((Screen.width - mousePos.x) / edgeThreshold); + panDirection.x = strength; + nearEdge = true; } - } - - /// - /// Handles free roam camera movement when free roam mode is active. - /// Uses the Horizontal/Vertical axes and Q/E keys to move up and down. - /// - private void HandleFreeRoam() - { - float horizontal = Input.GetAxis("Horizontal"); // A/D or Left/Right - float vertical = Input.GetAxis("Vertical"); // W/S or Up/Down - - // Use E to ascend and Q to descend (you can change these keys if needed) - float ascent = 0f; - if (Input.GetKey(KeyCode.E)) - ascent = 1f; - else if (Input.GetKey(KeyCode.Q)) - ascent = -1f; - - // Compute movement relative to current camera orientation - Vector3 movement = new Vector3(horizontal, ascent, vertical); - virtualCamera.transform.position += virtualCamera.transform.rotation * movement * freeRoamSpeed * Time.deltaTime; - } - - private void HandleEdgePanning() - { - if (targetCharacter == null) return; - - Vector3 newOffset = panningOffset; - Vector3 mousePos = Input.mousePosition; - - float screenWidth = Screen.width; - float screenHeight = Screen.height; - - // Center Reset Radius (3% of screen size) - float resetRadiusX = screenWidth * (resetRadiusPercentage / 100f); - float resetRadiusY = screenHeight * (resetRadiusPercentage / 100f); - Vector2 screenCenter = new Vector2(screenWidth / 2, screenHeight / 2); - - bool isNearCenter = Mathf.Abs(mousePos.x - screenCenter.x) <= resetRadiusX && - Mathf.Abs(mousePos.y - screenCenter.y) <= resetRadiusY; - - bool isHoveringTarget = IsCursorOverTargetCharacter(); - - // If the cursor touches the center or target even for a moment, trigger reset - if (isNearCenter || isHoveringTarget) + + // Bottom edge + if (mousePos.y <= edgeThreshold) { - shouldReset = true; + float strength = 1f - (mousePos.y / edgeThreshold); + panDirection.z = -strength; + nearEdge = true; } - - // When resetting, smoothly return to the default position - if (shouldReset && isPanning) + // Top edge + else if (mousePos.y >= Screen.height - edgeThreshold) { - cameraOffset.m_Offset = Vector3.Lerp(cameraOffset.m_Offset, defaultOffset, Time.deltaTime * resetSpeed); - - // Once the camera gets close enough to default, stop panning and reset the flag - if (Vector3.Distance(cameraOffset.m_Offset, defaultOffset) < 0.01f) + float strength = 1f - ((Screen.height - mousePos.y) / edgeThreshold); + panDirection.z = strength; + nearEdge = true; + } + + // Handle panning state + if (nearEdge) + { + isPanning = true; + isReturning = false; + timeSincePanStopped = 0f; + + // Convert pan direction to world space (relative to camera orientation) + Vector3 forward = cameraTransform.forward; + forward.y = 0; + forward.Normalize(); + + Vector3 right = cameraTransform.right; + right.y = 0; + right.Normalize(); + + Vector3 worldPanDirection = (right * panDirection.x + forward * panDirection.z).normalized * panDirection.magnitude; + + // Apply panning + Vector3 targetOffset = panOffset + worldPanDirection * panSpeed * Time.deltaTime; + + // Clamp to max distance + if (targetOffset.magnitude > maxPanDistance) + { + targetOffset = targetOffset.normalized * maxPanDistance; + } + + panOffset = targetOffset; + } + else + { + // Not near edge - handle return to player + if (isPanning) { isPanning = false; - panningOffset = defaultOffset; - shouldReset = false; + timeSincePanStopped = 0f; + } + + // Check if we should return to player + timeSincePanStopped += Time.deltaTime; + + if (timeSincePanStopped >= returnDelay && panOffset.magnitude > 0.1f) + { + isReturning = true; + + // Smoothly return to player + float returnAmount = returnSpeed * Time.deltaTime; + if (panOffset.magnitude <= returnAmount) + { + panOffset = Vector3.zero; + isReturning = false; + } + else + { + panOffset = Vector3.Lerp(panOffset, Vector3.zero, returnAmount / panOffset.magnitude); + } } - return; } - - // If the cursor is near the edges, adjust the offset (within limits) - if (mousePos.x <= edgeThreshold) newOffset.x = Mathf.Max(defaultOffset.x - panLimit.x, newOffset.x - panSpeed); - if (mousePos.x >= screenWidth - edgeThreshold) newOffset.x = Mathf.Min(defaultOffset.x + panLimit.x, newOffset.x + panSpeed); - if (mousePos.y <= edgeThreshold) newOffset.y = Mathf.Max(defaultOffset.y - panLimit.y, newOffset.y - panSpeed); - if (mousePos.y >= screenHeight - edgeThreshold) newOffset.y = Mathf.Min(defaultOffset.y + panLimit.y, newOffset.y + panSpeed); - - if (newOffset != panningOffset) + + // Apply the offset smoothly + Vector3 targetPosition = targetCharacter.position + panOffset; + cameraOffset.m_Offset = Vector3.SmoothDamp(cameraOffset.m_Offset, panOffset, ref currentVelocity, smoothTime); + + // Double-click to center on hero (Dota feature) + if (Input.GetMouseButtonDown(0) && Input.GetMouseButtonDown(0)) // Simplified double-click detection { - isPanning = true; + CenterOnHero(); + } + + // Space bar to instantly center (another Dota feature) + if (Input.GetKeyDown(KeyCode.Space)) + { + CenterOnHero(); } - - cameraOffset.m_Offset = Vector3.Lerp(cameraOffset.m_Offset, newOffset, Time.deltaTime * 3f); - panningOffset = cameraOffset.m_Offset; } - - private bool IsCursorOverTargetCharacter() + + private void CenterOnHero() { - if (targetCharacter == null) return false; - - Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); - if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity)) + panOffset = Vector3.zero; + cameraOffset.m_Offset = Vector3.zero; + currentVelocity = Vector3.zero; + isPanning = false; + isReturning = false; + timeSincePanStopped = returnDelay; // Skip the delay + } + + private void ToggleFreeRoamMode() + { + isFreeRoamMode = !isFreeRoamMode; + + if (isFreeRoamMode) { - return hit.transform == targetCharacter; + virtualCamera.Follow = null; + virtualCamera.LookAt = null; + } + else + { + if (targetCharacter != null) + { + virtualCamera.Follow = targetCharacter; + virtualCamera.LookAt = targetCharacter; + } + CenterOnHero(); } - return false; } - + + private void HandleFreeRoam() + { + float horizontal = Input.GetAxis("Horizontal"); + float vertical = Input.GetAxis("Vertical"); + float ascent = 0f; + + if (Input.GetKey(KeyCode.E)) ascent = 1f; + else if (Input.GetKey(KeyCode.Q)) ascent = -1f; + + Vector3 movement = new Vector3(horizontal, ascent, vertical); + cameraTransform.position += cameraTransform.rotation * movement * freeRoamSpeed * Time.deltaTime; + } + public void ShakeCamera(float duration = 0.3f, float magnitude = 0.2f) { if (shakeCoroutine != null) @@ -205,23 +252,51 @@ public class EdgePanningController : MonoBehaviour } shakeCoroutine = StartCoroutine(ShakeRoutine(duration, magnitude)); } - + private IEnumerator ShakeRoutine(float duration, float magnitude) { float elapsed = 0f; - Vector3 originalOffset = defaultOffset; - + while (elapsed < duration) { float x = Random.Range(-1f, 1f) * magnitude; float y = Random.Range(-1f, 1f) * magnitude; float z = Random.Range(-1f, 1f) * magnitude; - - cameraOffset.m_Offset = originalOffset + new Vector3(x, y, z); + + // Apply shake on top of existing offset + Vector3 shakeOffset = new Vector3(x, y, z); + cameraOffset.m_Offset = panOffset + shakeOffset; + elapsed += Time.deltaTime; yield return null; } - - cameraOffset.m_Offset = originalOffset; // Reset to default offset + + // Reset to current pan offset + cameraOffset.m_Offset = panOffset; + } + + // Helper method to check if cursor is over UI + private bool IsCursorOverUI() + { + return UnityEngine.EventSystems.EventSystem.current != null && + UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject(); + } + + // Debug visualization + private void OnDrawGizmos() + { + if (!Application.isPlaying || targetCharacter == null) return; + + // Draw max pan distance + Gizmos.color = Color.yellow; + Gizmos.DrawWireSphere(targetCharacter.position, maxPanDistance); + + // Draw current offset + if (panOffset.magnitude > 0.1f) + { + Gizmos.color = Color.red; + Gizmos.DrawLine(targetCharacter.position, targetCharacter.position + panOffset); + Gizmos.DrawWireSphere(targetCharacter.position + panOffset, 1f); + } } -} +} \ No newline at end of file