using System; using System.Collections; using UnityEngine; using UVC.Core; namespace UVC.Factory.Cameras { /// /// 마우스 입력에 따라 카메라를 이동, 회전, 줌하는 컨트롤러입니다. /// - 마우스 가운데 버튼 드래그: 카메라 평행 이동 (Pan) /// - 마우스 오른쪽 버튼 드래그: 카메라 회전 (Orbit) /// - 마우스 휠 스크롤: 카메라 줌 (Zoom) /// public class FactoryCameraController : SingletonScene { [Header("Panning Speed")] [Tooltip("카메라 높이가 임계값보다 낮을 때의 평행 이동 속도")] [SerializeField] private float lowAltitudePanSpeed = 0.5f; [Tooltip("카메라 높이가 임계값보다 높을 때의 평행 이동 속도")] [SerializeField] private float highAltitudePanSpeed = 10f; [Tooltip("카메라 회전 속도")] [SerializeField] private float rotationSpeed = 300f; [Tooltip("카메라 줌 속도")] [SerializeField] private float zoomSpeed = 10f; [Header("Movement Smoothing")] [Tooltip("패닝 시 마우스 이동량의 최대값을 제한하여, 프레임 드랍 시 카메라가 급격하게 튀는 현상을 방지합니다.")] [SerializeField] private float maxPanDelta = 50f; [Header("Camera")] [Tooltip("카메라 최소 높이")] [SerializeField] private float minCameraY = 2f; [Tooltip("카메라 최대 높이")] [SerializeField] private float maxCameraY = 80f; [Tooltip("카메라의 최소 수직 회전 각도 (X축)")] [SerializeField] private float minPitch = 20f; [Tooltip("카메라의 최대 수직 회전 각도 (X축)")] [SerializeField] private float maxPitch = 85f; [Tooltip("카메라의 최소 수평 회전 각도 (y축)")] [SerializeField] private float minYaw = -45f; [Tooltip("카메라의 최대 수평 회전 각도 (y축)")] [SerializeField] private float maxYaw = 45f; [Tooltip("마우스를 이용 한 회전 사용 여부")] [SerializeField] public bool EnabledRotation = true; [Tooltip("마우스를 이용 한 확대/축소 사용 여부")] [SerializeField] public bool EnabledZoom = true; /// /// 카메라의 변형이 변경될 때 발생합니다. /// /// 이 이벤트는 카메라의 변형이 업데이트될 때마다 트리거되며, /// 구독자는 위치, 회전 또는 크기 변경에 응답할 수 있습니다. 이 이벤트를 사용하여 /// UI 요소 업데이트 또는 종속 값 재계산과 같은 작업을 수행할 수 있습니다. public Action OnCameraChanged; /// /// 카메라 위치가 변경될 때 발생합니다. /// /// 이 이벤트는 카메라 위치가 업데이트될 때마다 트리거됩니다. 구독자는 /// 이 이벤트를 사용하여 UI 요소를 업데이트하거나 새 위치를 기반으로 계산을 수행하는 등 카메라 위치 변경에 대응할 수 있습니다. /// public Action OnCameraPositionChanged; /// /// 카메라 높이가 변경될 때 발생합니다. /// /// 이 이벤트는 카메라 높이가 업데이트될 때마다 트리거됩니다. 구독자는 /// 이 이벤트를 사용하여 UI 요소를 업데이트하거나 새 높이를 기반으로 계산을 수행하는 등 카메라 높이 변경에 대응할 수 있습니다. /// public Action OnCameraPositionUpChanged; /// /// 카메라의 회전이 변경될 때 발생합니다. /// /// 이 이벤트는 카메라의 회전이 업데이트될 때마다 트리거됩니다. 구독자는 /// 이 이벤트를 사용하여 카메라 방향의 변경에 응답할 수 있습니다. public Action OnCameraRotationChanged; /// /// 정의된 범위 내에서 카메라의 정규화된 수직 위치를 가져옵니다. /// /// 값은 카메라의 현재 수직 위치를 기반으로 계산되며 /// [0, 1] 범위로 고정됩니다. public float CameraYRate { get { // 카메라 높이에 따라 0~1 사이의 값을 반환합니다. return Mathf.Clamp01((transform.position.y - minCameraY) / (maxCameraY - minCameraY)); } } private Quaternion prevRotation; // 이전 카메라 각도 private Vector3 prevPosition; // 이전 카메라 위치 private Vector3 lastPanPosition; private Vector3 rotationPivot; private bool isRotating = false; private Coroutine focusCoroutine; // 현재 실행 중인 포커싱 코루틴을 저장할 변수 public bool Enable = false; // 카메라 컨트롤 활성화 여부 void Start() { // 스크립트 시작 시, 회전의 기준이 되는 중심점을 카메라 앞쪽으로 초기화합니다. rotationPivot = transform.position + transform.forward * 10f; this.prevRotation = transform.rotation; // 초기 카메라 각도 저장 this.prevPosition = transform.position; // 초기 카메라 위치 저장 } private void StopFocusCoroutine() { if (focusCoroutine != null) { StopCoroutine(focusCoroutine); focusCoroutine = null; } } private void DispatchEvnet() { if (prevPosition != transform.position || prevRotation != transform.rotation) { OnCameraChanged?.Invoke(transform); if (prevPosition != transform.position) { OnCameraPositionChanged?.Invoke(transform.position); if (prevPosition.y < transform.position.y) OnCameraPositionUpChanged?.Invoke(transform.position.y - prevPosition.y); } if (prevRotation != transform.rotation) { OnCameraRotationChanged?.Invoke(transform.rotation); } } prevRotation = transform.rotation; // 현재 카메라 각도 저장 prevPosition = transform.position; // 현재 카메라 위치 저장 } private void ValidateCameraTransform() { // 카메라의 위치가 최소/최대 높이 범위를 벗어나지 않도록 합니다. Vector3 currentPosition = transform.position; currentPosition.y = Mathf.Clamp(currentPosition.y, minCameraY, maxCameraY); transform.position = currentPosition; ValidateCameraRotation(); // 카메라의 위치가 너무 멀리 떨어지지 않도록 합니다. //float distance = Vector3.Distance(transform.position, rotationPivot); //if (distance > 100f) // 예시로 100f를 최대 거리로 설정 //{ // transform.position = rotationPivot + (transform.position - rotationPivot).normalized * 100f; //} } private void ValidateCameraRotation() { // 현재 회전값을 오일러 각으로 가져옵니다. Vector3 eulerAngles = transform.eulerAngles; // 오일러 각의 X축(Pitch) 값을 정규화하고 제한합니다. // 각도가 180도를 넘어가면 음수 값으로 변환하여 처리합니다. (예: 350도 -> -10도) float angleX = eulerAngles.x; if (angleX > 180f) angleX -= 360f; angleX = Mathf.Clamp(angleX, minPitch, maxPitch); eulerAngles.x = angleX; float angleY = eulerAngles.y; if (angleY > 180f) angleY -= 360f; angleY = Mathf.Clamp(angleY, minYaw, maxYaw); eulerAngles.y = angleY; // Z축 회전(롤)을 0으로 설정하여 카메라가 옆으로 기울어지는 것을 방지합니다. eulerAngles.z = 0f; // 수정된 오일러 각을 다시 쿼터니언으로 변환하여 적용합니다. transform.rotation = Quaternion.Euler(eulerAngles); } private Quaternion ValidateRotation(Quaternion rotation) { // 현재 회전값을 오일러 각으로 가져옵니다. Vector3 eulerAngles = rotation.eulerAngles; // 오일러 각의 X축(Pitch) 값을 정규화하고 제한합니다. // 각도가 180도를 넘어가면 음수 값으로 변환하여 처리합니다. (예: 350도 -> -10도) float angleX = eulerAngles.x; if (angleX > 180f) angleX -= 360f; angleX = Mathf.Clamp(angleX, minPitch, maxPitch); eulerAngles.x = angleX; float angleY = eulerAngles.y; if (angleY > 180f) angleY -= 360f; angleY = Mathf.Clamp(angleY, minYaw, maxYaw); eulerAngles.y = angleY; // Z축 회전(롤)을 0으로 설정하여 카메라가 옆으로 기울어지는 것을 방지합니다. eulerAngles.z = 0f; // 수정된 오일러 각을 다시 쿼터니언으로 변환하여 적용합니다. return Quaternion.Euler(eulerAngles); } // Update 대신 LateUpdate를 사용하여 카메라 움직임이 다른 모든 업데이트 이후에 처리되도록 합니다. // 이를 통해 카메라의 떨림이나 끊김 현상을 줄일 수 있습니다. void LateUpdate() { if (!Enable) return; // 카메라 컨트롤이 비활성화된 경우, 업데이트를 건너뜁니다. HandlePanning(); if (EnabledRotation) HandleRotation(); if (EnabledZoom) HandleZoom(); } /// /// 마우스 가운데 버튼으로 카메라를 평행 이동시킵니다. /// 프레임 지연으로 인한 급격한 이동을 방지하기 위해 이동량을 제한합니다. /// private void HandlePanning() { if (Input.GetMouseButtonDown(2)) { StopFocusCoroutine(); lastPanPosition = Input.mousePosition; } if (Input.GetMouseButton(2)) { Vector3 delta = Input.mousePosition - lastPanPosition; // 프레임 드랍 시 델타 값이 너무 커져서 카메라가 튀는 것을 방지하기 위해 최대값을 제한합니다. if (delta.magnitude > maxPanDelta) { delta = delta.normalized * maxPanDelta; } // 높이에 따라 동적으로 패닝 속도 결정. float currentPanSpeed = Mathf.Lerp(lowAltitudePanSpeed, highAltitudePanSpeed, CameraYRate); // 현재 카메라 높이에 따라 패닝 속도를 보간합니다. // 카메라의 로컬 좌표계를 기준으로 이동 방향을 계산합니다. Vector3 moveDirection = (transform.right * -delta.x) + (transform.forward * -delta.y); moveDirection.y = 0; // Y축 이동을 막습니다. // 계산된 방향으로 카메라를 이동시킵니다. transform.Translate(moveDirection.normalized * delta.magnitude * currentPanSpeed * Time.deltaTime, Space.World); ValidateCameraTransform(); lastPanPosition = Input.mousePosition; DispatchEvnet(); } } /// /// 마우스 오른쪽 버튼으로 카메라를 회전시킵니다. /// 회전 축이 변하는 것을 방지하여 안정적인 회전을 구현합니다. /// private void HandleRotation() { if (Input.GetMouseButtonDown(1)) { StopFocusCoroutine(); isRotating = true; // 마우스 클릭 지점으로 Ray를 쏴서 회전의 중심점(pivot)을 설정합니다. Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 1000f)) { rotationPivot = hit.point; } else { // Ray가 아무 오브젝트에도 맞지 않았다면, 카메라 앞쪽의 특정 거리를 중심점으로 사용합니다. rotationPivot = transform.position + transform.forward * 10f; } } if (Input.GetMouseButtonUp(1)) { isRotating = false; } if (isRotating && Input.GetMouseButton(1)) { float mouseX = Input.GetAxis("Mouse X") * rotationSpeed * Time.deltaTime; float mouseY = Input.GetAxis("Mouse Y") * rotationSpeed * Time.deltaTime; // 현재 X축 회전 각도를 가져와서 -180 ~ 180 범위로 정규화합니다. float currentPitch = transform.eulerAngles.x; if (currentPitch > 180f) currentPitch -= 360f; // 현재 Y축 회전 각도를 가져와서 -180 ~ 180 범위로 정규화합니다. float currentYaw = transform.eulerAngles.y; if (currentYaw > 180f) currentYaw -= 360f; // 마우스 입력으로 인해 Pitch 또는 Yaw 각도가 한계를 벗어나는지 확인합니다. if ((mouseY > 0 && currentPitch >= maxPitch) || (mouseY < 0 && currentPitch <= minPitch) || (mouseX < 0 && currentYaw >= maxYaw) || (mouseX > 0 && currentYaw <= minYaw)) { return; // 한계를 넘어서는 회전은 막습니다. } // 수평 회전으로 인해 수직 회전 축(transform.right)이 변질되는 것을 방지하기 위해 // 회전 전의 right 벡터를 미리 저장해 둡니다. Vector3 verticalRotationAxis = transform.right; // 설정된 중심점을 기준으로 카메라를 회전시킵니다. // 1. 수평 회전 (월드 Y축 기준) transform.RotateAround(rotationPivot, Vector3.up, -mouseX); // 2. 수직 회전 (미리 저장해 둔 카메라의 오른쪽 축 기준) transform.RotateAround(rotationPivot, verticalRotationAxis, mouseY); ValidateCameraTransform(); DispatchEvnet(); } } /// /// 마우스 휠로 카메라를 줌 인/아웃합니다. /// private void HandleZoom() { float scroll = Input.GetAxis("Mouse ScrollWheel"); if (scroll != 0f) { // 현재 X축 회전 각도를 확인하여 한계 범위 밖이면 줌을 막습니다. float currentPitch = transform.eulerAngles.x; if (currentPitch > 180f) currentPitch -= 360f; if (currentPitch < minPitch || currentPitch > maxPitch) return; Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); Vector3 zoomTarget; if (Physics.Raycast(ray, out RaycastHit hit)) { zoomTarget = hit.point; } else { zoomTarget = ray.GetPoint(1000); } Vector3 direction = zoomTarget - transform.position; Vector3 moveVector = direction.normalized * scroll * zoomSpeed; // 카메라가 아래로 움직이려 하고(moveVector.y < 0), 이미 최소 높이에 도달했다면 중단합니다. if (moveVector.y < 0 && transform.position.y <= minCameraY) return; // 카메라가 위로 움직이려 하고(moveVector.y > 0), 이미 최대 높이에 도달했다면 중단합니다. if (moveVector.y > 0 && transform.position.y >= maxCameraY) return; StopFocusCoroutine(); // 줌 실행 transform.position += direction.normalized * scroll * zoomSpeed; ValidateCameraTransform(); DispatchEvnet(); } } /// /// 지정된 Transform을 중심으로 카메라를 포커싱합니다. /// /// 포커스할 대상의 Transform /// 대상과의 거리 public void FocusOnTargetFast(Vector3 equipmentPosition, float distance) { if (equipmentPosition == null) return; Vector3 position = equipmentPosition; if (position.y < minCameraY) { position.y = minCameraY; // 최소 높이 제한 } else if (position.y > maxCameraY) { position.y = maxCameraY; // 최대 높이 제한 } // 카메라가 바라볼 대상의 중심점 Vector3 targetPosition = equipmentPosition; // 현재 카메라의 회전각을 유지하면서 타겟을 바라보는 방향 설정 Vector3 directionToTarget = (targetPosition - transform.position).normalized; // 타겟으로부터 지정된 거리만큼 떨어진 위치 계산 Vector3 newPosition = targetPosition - directionToTarget * distance; // 카메라 위치 설정 및 타겟을 바라보도록 함 transform.position = newPosition; transform.LookAt(targetPosition); // 회전 피봇 포인트 업데이트 rotationPivot = targetPosition; ValidateCameraTransform(); DispatchEvnet(); } /// /// 지정된 Transform을 중심으로 카메라를 포커싱합니다. /// /// 포커스할 대상의 Transform /// 대상과의 거리 /// 이동에 걸리는 시간(초), 기본값 1초 public void FocusOnTarget(Vector3 equipmentPosition, float distance, float cameraPitch = 45, float duration = 1.0f, bool keepYRotation = true) { if (equipmentPosition == null) return; StopFocusCoroutine(); Vector3 position = equipmentPosition; if (position.y < minCameraY) { position.y = minCameraY; // 최소 높이 제한 } else if (position.y > maxCameraY) { position.y = maxCameraY; // 최대 높이 제한 } // 코루틴을 사용하여 부드러운 이동 구현 focusCoroutine = StartCoroutine(SmoothFocusOnTarget(position, distance, cameraPitch, duration, keepYRotation)); } /// /// 카메라를 앞쪽 방향으로 지정된 거리만큼 바깥쪽으로 부드럽게 이동합니다. /// /// 이 메서드는 코루틴을 사용하여 카메라의 위치를 ​​앞쪽 방향으로 바깥쪽으로 부드럽게 전환합니다. /// 전환은 지정된 시간 동안 수행되므로 /// 시각적으로 부드러운 움직임을 구현할 수 있습니다. /// 카메라를 현재 위치에서 바깥쪽으로 이동할 거리(단위)입니다. /// 카메라 전환이 발생하는 시간(초)입니다. 기본값은 1.0초입니다. public void FocusOut(float distance, float cameraPitch = 45, float duration = 1.0f) { StopFocusCoroutine(); // 현재 카메라 위치와 회전값을 저장 Vector3 startPosition = transform.position; Quaternion startRotation = transform.rotation; // 카메라가 바라보는 방향으로 지정된 거리만큼 이동 Vector3 targetPosition = transform.position + transform.forward * distance; // 코루틴을 사용하여 부드러운 이동 구현 focusCoroutine = StartCoroutine(SmoothFocusOnTarget(targetPosition, distance, cameraPitch, duration)); } /// /// 부드럽게 타겟까지 이동하는 코루틴 /// private IEnumerator SmoothFocusOnTarget(Vector3 targetTransform, float distance, float cameraPitch, float duration, bool keepYRotation = true) { // 카메라가 바라볼 대상의 중심점 Vector3 targetPosition = targetTransform; // 시작 위치와 회전 저장 Vector3 startPosition = transform.position; Quaternion startRotation = transform.rotation; // 최종 회전값 계산 // keepYRotation 플래그에 따라 Y축 회전값을 결정합니다. float targetYaw = keepYRotation ? startRotation.eulerAngles.y : Quaternion.LookRotation(targetPosition - startPosition).eulerAngles.y; Quaternion endRotation = Quaternion.Euler(cameraPitch, targetYaw, 0); // 최종 위치 계산: 목표 지점에서 지정된 거리만큼, 계산된 최종 회전 방향의 반대쪽으로 이동합니다. Vector3 endPosition = targetPosition - (endRotation * Vector3.forward) * distance; // 회전값 검증 및 수정 endRotation = ValidateRotation(endRotation); // 이동 시간 계산을 위한 변수 float elapsedTime = 0f; while (elapsedTime < duration) { elapsedTime += Time.deltaTime; float t = Mathf.Clamp01(elapsedTime / duration); // 부드러운 이동을 위한 Easing 함수 적용 float smoothT = EaseInOutCubic(t); // 위치와 회전 보간 transform.position = Vector3.Lerp(startPosition, endPosition, smoothT); transform.rotation = Quaternion.Slerp(startRotation, endRotation, smoothT); ValidateCameraTransform(); yield return null; } // 정확한 최종 위치와 회전 설정 transform.position = endPosition; transform.rotation = endRotation; // 회전 피봇 포인트 업데이트 rotationPivot = targetPosition; ValidateCameraTransform(); DispatchEvnet(); focusCoroutine = null; // 코루틴 완료 후 참조를 null로 설정 } /// /// Cubic ease-in/out 함수로 부드러운 이동 효과를 줍니다. /// private float EaseInOutCubic(float t) { return t < 0.5f ? 4f * t * t * t : 1f - Mathf.Pow(-2f * t + 2f, 3f) / 2f; } } }