using UnityEngine; using Cinemachine; using System.Collections; using Unity.BossRoom.CameraUtils; public class EdgePanningController : MonoBehaviour { [Header("Camera References")] public CinemachineFreeLook virtualCamera; private CinemachineCameraOffset cameraOffset; 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 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) { cameraOffset = virtualCamera.gameObject.AddComponent(); } // 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; lastValidPosition = targetCharacter.position; } private void LateUpdate() { // Toggle free roam mode if (Input.GetKeyDown(KeyCode.Backspace)) { ToggleFreeRoamMode(); } if (isFreeRoamMode) { HandleFreeRoam(); return; } if (targetCharacter == null) return; HandleDotaStylePanning(); } private void HandleDotaStylePanning() { Vector3 mousePos = Input.mousePosition; Vector3 panDirection = Vector3.zero; // Check screen edges bool nearEdge = false; // Left edge if (mousePos.x <= edgeThreshold) { float strength = 1f - (mousePos.x / edgeThreshold); panDirection.x = -strength; nearEdge = true; } // Right edge else if (mousePos.x >= Screen.width - edgeThreshold) { float strength = 1f - ((Screen.width - mousePos.x) / edgeThreshold); panDirection.x = strength; nearEdge = true; } // Bottom edge if (mousePos.y <= edgeThreshold) { float strength = 1f - (mousePos.y / edgeThreshold); panDirection.z = -strength; nearEdge = true; } // Top edge else if (mousePos.y >= Screen.height - edgeThreshold) { 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; 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); } } } // 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 { CenterOnHero(); } // Space bar to instantly center (another Dota feature) if (Input.GetKeyDown(KeyCode.Space)) { CenterOnHero(); } } private void CenterOnHero() { 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) { virtualCamera.Follow = null; virtualCamera.LookAt = null; } else { if (targetCharacter != null) { virtualCamera.Follow = targetCharacter; virtualCamera.LookAt = targetCharacter; } CenterOnHero(); } } 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) { StopCoroutine(shakeCoroutine); } shakeCoroutine = StartCoroutine(ShakeRoutine(duration, magnitude)); } private IEnumerator ShakeRoutine(float duration, float magnitude) { float elapsed = 0f; while (elapsed < duration) { float x = Random.Range(-1f, 1f) * magnitude; float y = Random.Range(-1f, 1f) * magnitude; float z = Random.Range(-1f, 1f) * magnitude; // 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; } // 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); } } }