using System; using UnityEngine; namespace GraphicsCat { public struct CameraMovementInput { public Vector3 translationDelta; public Vector3 rotationDelta; public float zoomDelta; public bool isDragging; } public partial class CameraRig : MonoBehaviour, IMGUIDockable { public enum RigType { Free, TopDown, Locked, } const int k_SpaceHeight = 10; [Space(k_SpaceHeight)] public RigType rigType = RigType.Free; [Space(k_SpaceHeight)] [Range(0, 20)] public float moveSpeed = 6f; [Range(0, 0.2f)] public float rotateSpeed = 0.1f; [Range(0, 200f)] public float zoomSpeed = 40f; [Space(k_SpaceHeight)] public Transform targetTransform; [Space(k_SpaceHeight)] public bool guiEnabled = true; private Transform m_CachedTransform; private Vector3 m_CameraInitialPosition; private Quaternion m_CameraInitialRotation; private Vector3 m_TargetInitialPosition; private Quaternion m_TargetInitialRotation; private Touch m_MovementTouchInitial; private Touch m_MovementTouchCurrent; private Vector3 m_LastRotationMousePosition; private Vector3 m_LastDragMousePosition; private float m_MovementSpeedMultiplier; private Vector3 m_TopDownPanDelta; private bool m_IsInitialized; private bool m_IsRigTypeSwitcherVisible = false; private void ProcessCameraMovement(CameraMovementInput input) { bool isFreeType = (rigType == RigType.Free); bool isTopDownType = (rigType == RigType.TopDown); if (input.translationDelta != Vector3.zero) { ApplyMovementDelta(input.translationDelta); } if (input.rotationDelta != Vector3.zero && targetTransform != null) { if (isFreeType) { OrbitAroundTarget(input.rotationDelta); } } if (input.zoomDelta != 0f) { if (isFreeType && targetTransform != null) { AdjustDistanceToTarget(input.zoomDelta); } else if (isTopDownType) { Vector3 zoomDeltaVector = m_CachedTransform.forward * input.zoomDelta; m_CachedTransform.position += zoomDeltaVector; } } } private void ApplyMovementDelta(Vector3 delta) { m_CachedTransform.position += delta; if (targetTransform != null) targetTransform.position += delta; } private void OrbitAroundTarget(Vector3 delta) { if (targetTransform == null) return; m_CachedTransform.RotateAround(targetTransform.position, Vector3.up, delta.x); m_CachedTransform.RotateAround(targetTransform.position, -m_CachedTransform.right, delta.y); } private void RotateCamera(Vector3 delta) { if (targetTransform == null) return; Vector3 referenceUp = Vector3.up; if (Vector3.Dot(m_CachedTransform.forward, Vector3.up) > 0.99f || Vector3.Dot(m_CachedTransform.forward, Vector3.up) < -0.99f) referenceUp = m_CachedTransform.right; Vector3 cameraRight = Vector3.Cross(referenceUp, m_CachedTransform.forward).normalized; Vector3 cameraUp = Vector3.Cross(m_CachedTransform.forward, cameraRight).normalized; Quaternion horizontalQuat = Quaternion.AngleAxis(delta.x, cameraUp); Quaternion verticalQuat = Quaternion.AngleAxis(delta.y, cameraRight); Quaternion totalRotation = horizontalQuat * verticalQuat; Vector3 cameraToTarget = targetTransform.position - m_CachedTransform.position; Vector3 newCameraToTarget = totalRotation * cameraToTarget; targetTransform.position = m_CachedTransform.position + newCameraToTarget; m_CachedTransform.LookAt(targetTransform.position); } private void AdjustDistanceToTarget(float delta) { if (targetTransform == null) return; Vector3 cameraToTarget = m_CachedTransform.position - targetTransform.position; float newDistance = Mathf.Max(cameraToTarget.magnitude + delta, 0.1f); m_CachedTransform.position = targetTransform.position + cameraToTarget.normalized * newDistance; } private void ResetToInitialState() { m_CachedTransform.SetPositionAndRotation(m_CameraInitialPosition, m_CameraInitialRotation); if (targetTransform != null) targetTransform.SetPositionAndRotation(m_TargetInitialPosition, m_TargetInitialRotation); } public void OnDockGUI() { GUILayout.BeginHorizontal(); { if (m_IsRigTypeSwitcherVisible) { var btnText = GetRigTypeDescription(rigType); if (GUILayout.Button(btnText)) rigType = GetNextRigType(rigType); } bool defaultStatus = true; defaultStatus &= (m_CachedTransform.position == m_CameraInitialPosition); defaultStatus &= (m_CachedTransform.rotation == m_CameraInitialRotation); GUI.enabled = !defaultStatus; if (GUILayout.Button("Reset Camera")) ResetToInitialState(); GUI.enabled = true; } GUILayout.EndHorizontal(); } private void Start() { m_CachedTransform = transform; m_CameraInitialPosition = m_CachedTransform.position; m_CameraInitialRotation = m_CachedTransform.rotation; if (targetTransform != null) { m_TargetInitialPosition = targetTransform.position; m_TargetInitialRotation = targetTransform.rotation; } m_MovementTouchInitial.fingerId = int.MinValue; m_IsInitialized = true; UpdateDockGUI(); } private void OnValidate() { UpdateDockGUI(); } private void UpdateDockGUI() { if (m_IsInitialized == false) return; if (guiEnabled) IMGUIDock.topRight.DockGUI(this); else IMGUIDock.topRight.UndockGUI(this); } private void Update() { if (rigType == RigType.Locked) return; if (Application.isMobilePlatform) HandleMobileInput(); else HandleDesktopInput(); } private Vector3 CalculateWorldPanDelta(Vector2 screenDelta) { var cam = GetComponent(); if (cam == null) return Vector3.zero; float distance; if (rigType == RigType.Free && targetTransform != null) { distance = Vector3.Distance(m_CachedTransform.position, targetTransform.position); } else if (rigType == RigType.TopDown) { float camHeight = m_CachedTransform.position.y; float forwardY = m_CachedTransform.forward.y; if (Mathf.Abs(forwardY) < 1e-3 || Mathf.Abs(camHeight) < 1e-3) { distance = camHeight; } else { distance = camHeight / Mathf.Abs(forwardY); } } else { return Vector3.zero; } float halfFOV = cam.fieldOfView * 0.5f; float worldHeightAtDistance = 2f * distance * Mathf.Tan(halfFOV * Mathf.Deg2Rad); float worldWidthAtDistance = worldHeightAtDistance * cam.aspect; float percentX = screenDelta.x / Screen.width; float percentY = screenDelta.y / Screen.height; Vector3 worldDelta = Vector3.zero; worldDelta -= m_CachedTransform.right * (percentX * worldWidthAtDistance); if (rigType == RigType.Free) { worldDelta -= m_CachedTransform.up * (percentY * worldHeightAtDistance); } else if (rigType == RigType.TopDown) { worldDelta -= GetHorizontalForwardVector() * (percentY * worldHeightAtDistance); } return worldDelta; } private void HandleDesktopInput() { var input = new CameraMovementInput(); var isTopDownType = rigType == RigType.TopDown; var isFreeType = (rigType == RigType.Free); var deltaTime = Time.smoothDeltaTime; Vector3 keyboardMovement = Vector3.zero; if (isFreeType && targetTransform != null) { if (Input.GetKey(KeyCode.W)) keyboardMovement += m_CachedTransform.forward; if (Input.GetKey(KeyCode.S)) keyboardMovement -= m_CachedTransform.forward; if (Input.GetKey(KeyCode.D)) keyboardMovement += m_CachedTransform.right; if (Input.GetKey(KeyCode.A)) keyboardMovement -= m_CachedTransform.right; if (Input.GetKey(KeyCode.E)) keyboardMovement += Vector3.up; if (Input.GetKey(KeyCode.Q)) keyboardMovement -= Vector3.up; } else if (isTopDownType) { if (Input.GetKey(KeyCode.Q)) input.zoomDelta += moveSpeed * deltaTime; if (Input.GetKey(KeyCode.E)) input.zoomDelta -= moveSpeed * deltaTime; if (Input.GetKey(KeyCode.W)) keyboardMovement += GetHorizontalForwardVector(); if (Input.GetKey(KeyCode.S)) keyboardMovement -= GetHorizontalForwardVector(); if (Input.GetKey(KeyCode.D)) keyboardMovement += m_CachedTransform.right; if (Input.GetKey(KeyCode.A)) keyboardMovement -= m_CachedTransform.right; } if (keyboardMovement != Vector3.zero) { m_MovementSpeedMultiplier += deltaTime; input.translationDelta = keyboardMovement.normalized * moveSpeed * deltaTime * m_MovementSpeedMultiplier; } else { m_MovementSpeedMultiplier = 1; } if (isFreeType && targetTransform != null) { if (Input.GetMouseButtonDown(0) || Input.GetMouseButtonDown(1)) m_LastRotationMousePosition = Input.mousePosition; if (Input.GetMouseButtonDown(2)) m_LastDragMousePosition = Input.mousePosition; if (Input.GetMouseButton(0)) { var rotateMouseDelta = (Input.mousePosition - m_LastRotationMousePosition); if (rotateMouseDelta.sqrMagnitude > float.Epsilon) { input.rotationDelta = rotateMouseDelta * rotateSpeed; m_LastRotationMousePosition = Input.mousePosition; } } else if (Input.GetMouseButton(1)) { var rotateMouseDelta = (Input.mousePosition - m_LastRotationMousePosition); if (rotateMouseDelta.sqrMagnitude > float.Epsilon) { var adjustedDelta = new Vector3(rotateMouseDelta.x * rotateSpeed, -rotateMouseDelta.y * rotateSpeed, 0); RotateCamera(adjustedDelta * 0.5f); m_LastRotationMousePosition = Input.mousePosition; } } else if (Input.GetMouseButton(2)) { var mouseDelta = (Input.mousePosition - m_LastDragMousePosition); if (mouseDelta.sqrMagnitude > float.Epsilon) { input.translationDelta = CalculateWorldPanDelta(mouseDelta); input.isDragging = true; m_LastDragMousePosition = Input.mousePosition; } } } else if (isTopDownType) { if (Input.GetMouseButtonDown(0) || Input.GetMouseButtonDown(1) || Input.GetMouseButtonDown(2)) m_LastDragMousePosition = Input.mousePosition; if (Input.GetMouseButton(0) || Input.GetMouseButton(1) || Input.GetMouseButton(2)) { var mouseDelta = (Input.mousePosition - m_LastDragMousePosition); if (mouseDelta.sqrMagnitude > float.Epsilon) { input.translationDelta = CalculateWorldPanDelta(mouseDelta); input.isDragging = true; m_LastDragMousePosition = Input.mousePosition; } } } if (Input.mouseScrollDelta.y != 0 && IsMouseInViewPort()) { if (isFreeType && targetTransform != null) { input.zoomDelta = -(Input.mouseScrollDelta.y * zoomSpeed * Time.deltaTime); } else if (isTopDownType) { input.zoomDelta = Input.mouseScrollDelta.y * zoomSpeed * Time.deltaTime; } } ProcessCameraMovement(input); } private void HandleMobileInput() { if (rigType == RigType.Free) HandleFreeMobileInput(); else if (rigType == RigType.TopDown) HandleTopDownMobileInput(); } private void HandleFreeMobileInput() { if (targetTransform == null) return; var input = new CameraMovementInput(); var touches = Input.touches; if (touches.Length == 2) { Touch t0 = touches[0]; Touch t1 = touches[1]; Vector2 t0PrevPos = t0.position - t0.deltaPosition; Vector2 t1PrevPos = t1.position - t1.deltaPosition; float prevTouchDeltaMag = (t0PrevPos - t1PrevPos).magnitude; float currentTouchDeltaMag = (t0.position - t1.position).magnitude; float pinchDelta = prevTouchDeltaMag - currentTouchDeltaMag; input.zoomDelta = pinchDelta * zoomSpeed * Time.smoothDeltaTime * 0.002f; } else { foreach (var touch in touches) { if (touch.phase == TouchPhase.Began) m_MovementTouchInitial = touch; else if (touch.phase == TouchPhase.Ended) { if (m_MovementTouchInitial.fingerId == touch.fingerId) m_MovementTouchInitial.fingerId = int.MinValue; } } m_MovementTouchCurrent.fingerId = int.MinValue; foreach (var touch in touches) { if (touch.fingerId == m_MovementTouchInitial.fingerId) m_MovementTouchCurrent = touch; } if (m_MovementTouchCurrent.fingerId != int.MinValue) { var rotateStrength = (m_MovementTouchCurrent.position - m_MovementTouchInitial.position) / Screen.width; if (rotateStrength != Vector2.zero) { var deltaTime = Time.smoothDeltaTime; var rotationDelta = 1000f * 20 * deltaTime * rotateSpeed * rotateStrength; input.rotationDelta = new Vector3(rotationDelta.x, rotationDelta.y, 0); } } } ProcessCameraMovement(input); } private void HandleTopDownMobileInput() { var input = new CameraMovementInput(); var touches = Input.touches; foreach (var touch in touches) { if (touch.phase == TouchPhase.Began) m_MovementTouchInitial = touch; else if (touch.phase == TouchPhase.Ended) { if (m_MovementTouchInitial.fingerId == touch.fingerId) m_MovementTouchInitial.fingerId = int.MinValue; } } m_MovementTouchCurrent.fingerId = int.MinValue; foreach (var touch in touches) { if (touch.fingerId == m_MovementTouchInitial.fingerId) m_MovementTouchCurrent = touch; } if (m_MovementTouchCurrent.fingerId != int.MinValue) { if (m_MovementTouchCurrent.deltaPosition != Vector2.zero) { input.translationDelta = CalculateWorldPanDelta(m_MovementTouchCurrent.deltaPosition); input.isDragging = true; } } else { m_TopDownPanDelta *= Mathf.Lerp(1f, 0f, Time.smoothDeltaTime * 5); input.translationDelta = m_TopDownPanDelta; } ProcessCameraMovement(input); } private bool IsMouseInViewPort() { if (Input.mousePosition.x < 0 || Input.mousePosition.x > Screen.width || Input.mousePosition.y < 0 || Input.mousePosition.y > Screen.height) { return false; } return true; } private Vector3 GetScreenSpaceUpVector() { var forward = m_CachedTransform.forward; if (Mathf.Abs(forward.y) < 0.01) return m_CachedTransform.up; else { forward.y = 0; return forward.normalized; } } private Vector3 GetHorizontalForwardVector() { var forward = m_CachedTransform.forward; forward.y = 0; return forward.normalized; } private string GetRigTypeDescription(RigType type) { var description = "Camera: Unknown Type"; switch (type) { case RigType.Free: description = "Camera: Free"; break; case RigType.TopDown: description = "Camera: TopDown"; break; case RigType.Locked: description = "Camera: Locked"; break; } return description; } private RigType GetNextRigType(RigType currentRigType) { var rigTypeCount = Enum.GetValues(typeof(RigType)).Length; return (RigType)(((int)currentRigType + 1) % rigTypeCount); } } }