using UnityEngine; using Cinemachine; using System.Collections; using Unity.BossRoom.CameraUtils; public class EdgePanningController : MonoBehaviour { [Header("Camera References")] [Tooltip("Assign your Cinemachine FreeLook here.")] public CinemachineFreeLook virtualCamera; // This is the hero (or unit) we want to eventually recenter on. private Transform targetCharacter; // We create (or assign) a separate GameObject that the FreeLook will Follow/LookAt. private Transform cameraTarget; [Header("Edge Panning Settings")] [Tooltip("Distance in pixels from screen edge to start panning.")] public float edgeThreshold = 10f; [Tooltip("Pan speed in world units per second.")] public float panSpeed = 20f; [Tooltip("Maximum distance (in world units) the camera can pan away from the hero.")] public float maxPanDistance = 30f; [Header("Centering / Return Settings")] [Tooltip("Delay (seconds) after panning stops before auto-centering on hero begins.")] public float returnDelay = 0.5f; [Tooltip("Speed at which cameraTarget moves back to hero when auto-returning.")] public float returnSpeed = 15f; // Smoothing for the automatic “return to hero” motion [Header("Smoothing")] [Tooltip("Time for SmoothDamp when auto-centering back to hero.")] public float smoothTime = 0.1f; // State-tracking private bool isPanning = false; private bool isReturning = false; private float timeSincePanStopped = 0f; private Vector3 currentVelocity = Vector3.zero; // Free camera (debug) mode private bool isFreeRoamMode = false; [Tooltip("Speed for free-roam mode.")] public float freeRoamSpeed = 20f; private void Awake() { CameraController.OnCameraAttached += SetTargetCharacter; } private void OnDestroy() { CameraController.OnCameraAttached -= SetTargetCharacter; } private void Start() { if (virtualCamera == null) { Debug.LogError("EdgePanningController: You must assign a CinemachineFreeLook to virtualCamera."); enabled = false; return; } // Initially, we’ll leave cameraTarget null until we know who the hero is. } /// /// Called (by BossRoom’s CameraController) when the hero/character is attached. /// private void SetTargetCharacter(Transform characterTransform) { // characterTransform is actually the child transform of the player prefab. // We'll treat its parent as the "hero root". targetCharacter = characterTransform.parent; // Create a new, empty GameObject to serve as the camera’s focus point. if (cameraTarget == null) { GameObject go = new GameObject("CameraTarget"); cameraTarget = go.transform; } // Place the cameraTarget initially on the hero’s position. cameraTarget.position = targetCharacter.position; // Tell the FreeLook to Follow/LookAt this cameraTarget virtualCamera.Follow = cameraTarget; virtualCamera.LookAt = cameraTarget; } private void LateUpdate() { // Toggle free-roam for debugging if (Input.GetKeyDown(KeyCode.Backspace)) { ToggleFreeRoamMode(); } if (isFreeRoamMode) { HandleFreeRoam(); return; } if (targetCharacter == null || cameraTarget == null) return; HandleDotaStylePanning(); } private void HandleDotaStylePanning() { Vector3 mousePos = Input.mousePosition; Vector3 panDirection = Vector3.zero; 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; } if (nearEdge) { // We are actively panning isPanning = true; isReturning = false; timeSincePanStopped = 0f; // Compute a world-space direction on the XZ plane relative to camera’s facing Vector3 forward = virtualCamera.transform.forward; forward.y = 0; forward.Normalize(); Vector3 right = virtualCamera.transform.right; right.y = 0; right.Normalize(); Vector3 worldPanDir = (right * panDirection.x + forward * panDirection.z).normalized; // Move cameraTarget along that direction (scaled by panSpeed and deltaTime) Vector3 newPos = cameraTarget.position + worldPanDir * panSpeed * Time.deltaTime; // Clamp so cameraTarget never goes further than maxPanDistance from the hero Vector3 offsetFromHero = newPos - targetCharacter.position; if (offsetFromHero.magnitude > maxPanDistance) { offsetFromHero = offsetFromHero.normalized * maxPanDistance; newPos = targetCharacter.position + offsetFromHero; } cameraTarget.position = newPos; } else { // No edge-pan input. Start the return-to-hero timer if we were panning. if (isPanning) { isPanning = false; timeSincePanStopped = 0f; } timeSincePanStopped += Time.deltaTime; // After returnDelay, smoothly move cameraTarget back toward hero if (timeSincePanStopped >= returnDelay && !isReturning) { isReturning = true; } if (isReturning) { // Smoothly interpolate cameraTarget back to the hero’s position Vector3 targetPos = targetCharacter.position; cameraTarget.position = Vector3.SmoothDamp( cameraTarget.position, targetPos, ref currentVelocity, smoothTime, returnSpeed ); // If close enough, stop returning if (Vector3.Distance(cameraTarget.position, targetPos) < 0.1f) { cameraTarget.position = targetPos; isReturning = false; currentVelocity = Vector3.zero; } } } // Double-click to instantly center on hero if (Input.GetMouseButtonDown(0) && Input.GetMouseButtonDown(0)) { CenterOnHero(); } // Space bar to instantly center on hero if (Input.GetKeyDown(KeyCode.Space)) { CenterOnHero(); } } private void CenterOnHero() { if (targetCharacter == null || cameraTarget == null) return; cameraTarget.position = targetCharacter.position; isPanning = false; isReturning = false; timeSincePanStopped = returnDelay; // So it won’t start returning immediately currentVelocity = Vector3.zero; } private void ToggleFreeRoamMode() { isFreeRoamMode = !isFreeRoamMode; if (isFreeRoamMode) { // Detach camera from any Follow target virtualCamera.Follow = null; virtualCamera.LookAt = null; } else { // Reattach to our cameraTarget if (cameraTarget != null) { virtualCamera.Follow = cameraTarget; virtualCamera.LookAt = cameraTarget; } CenterOnHero(); } } private void HandleFreeRoam() { float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); float upDown = 0f; if (Input.GetKey(KeyCode.E)) upDown = 1f; else if (Input.GetKey(KeyCode.Q)) upDown = -1f; Vector3 move = new Vector3(h, upDown, v); virtualCamera.transform.position += virtualCamera.transform.rotation * move * freeRoamSpeed * Time.deltaTime; } // Optional: If you have camera shake functionality, you'd need to re-implement it differently, // because we no longer use CinemachineCameraOffset for panning. Left out here for brevity. }