#nullable enable using UnityEngine; using UnityEngine.InputSystem; namespace DTNavigation.CameraSystem { /// /// 마우스 기반 카메라 컨트롤러 (New Input System). /// /// ■ 마우스 조작 (Inspector에서 바인딩 변경 가능) /// - 휠 스크롤 : 줌 인/아웃 /// - 우클릭 드래그 : 자유 회전 (수평=Y축 / 수직=X축 피치) /// - 좌클릭 드래그 : 화면 평면 이동 (패닝) /// /// ■ UI 블로킹 규칙 /// - UI 위에서 새 드래그/줌 시작 불가 /// - 드래그 도중 UI 위로 이동해도 드래그 유지 /// /// ■ 카메라 내비게이션 /// - NavigateToTransform : 씬 오브젝트 Transform으로 부드럽게 이동 /// - NavigateToPosition : 직접 지정한 위치/회전값으로 부드럽게 이동 /// public sealed class CameraController : MonoBehaviour { // ── 속도 설정 ────────────────────────────────────────────── [Header("줌 (마우스 휠)")] [SerializeField] private float _zoomSpeed = 20f; [Header("회전 (우클릭 드래그)")] [SerializeField] private float _rotateSpeed = 120f; [Header("이동 (좌클릭 드래그)")] [SerializeField] private float _panSpeed = 0.05f; [Header("카메라 내비게이션")] [SerializeField] private float _defaultNavDuration = 1f; // ── Input Actions (Inspector에서 바인딩 변경 가능) ───────── [Header("Input Actions")] [SerializeField] private InputAction _zoomAction = new( "Zoom", InputActionType.Value, binding: "/scroll/y"); [SerializeField] private InputAction _panButtonAction = new( "Pan", InputActionType.Button, binding: "/leftButton"); [SerializeField] private InputAction _rotateButtonAction = new( "Rotate", InputActionType.Button, binding: "/rightButton"); [SerializeField] private InputAction _mouseDeltaAction = new( "MouseDelta", InputActionType.Value, binding: "/delta"); // ── 드래그 상태 ──────────────────────────────────────────── private bool _isDraggingLeft; private bool _isDraggingRight; // ── UI 블로킹 ────────────────────────────────────────────── private bool _isPointerOverUI; // ── 카메라 내비게이션 ────────────────────────────────────── private bool _isNavigating; private Vector3 _navStartPos; private Quaternion _navStartRot; private Vector3 _navTargetPos; private Quaternion _navTargetRot; private float _navDuration; private float _navElapsed; // ── Unity 생명주기 ───────────────────────────────────────── private void OnEnable() { _zoomAction.Enable(); _panButtonAction.Enable(); _rotateButtonAction.Enable(); _mouseDeltaAction.Enable(); } private void OnDisable() { _zoomAction.Disable(); _panButtonAction.Disable(); _rotateButtonAction.Disable(); _mouseDeltaAction.Disable(); } private void Update() { HandleMouseInput(); UpdateNavigation(); } // ── UI 블로킹 API ────────────────────────────────────────── /// UI 영역에 포인터가 진입했을 때 NavigationController에서 호출합니다. public void OnUIPointerEnter() => _isPointerOverUI = true; /// UI 영역에서 포인터가 벗어났을 때 NavigationController에서 호출합니다. public void OnUIPointerLeave() => _isPointerOverUI = false; // ── 카메라 내비게이션 공개 API ───────────────────────────── /// /// 씬에 배치된 Transform의 위치/회전으로 부드럽게 이동합니다. /// duration 이 0 이하면 Inspector의 기본값을 사용합니다. /// public void NavigateToTransform(Transform target, float duration = -1f) { var dur = duration < 0f ? _defaultNavDuration : duration; StartNavigation(target.position, target.rotation, dur); } /// /// 직접 지정한 위치/회전 값으로 부드럽게 이동합니다. /// duration 이 0 이하면 Inspector의 기본값을 사용합니다. /// public void NavigateToPosition(Vector3 position, Quaternion rotation, float duration = -1f) { var dur = duration < 0f ? _defaultNavDuration : duration; StartNavigation(position, rotation, dur); } // ── 마우스 입력 처리 ─────────────────────────────────────── private void HandleMouseInput() { if (_isNavigating) return; // ── 줌: UI 위가 아닐 때만 허용 ──────────────────────── var scroll = _zoomAction.ReadValue(); if (Mathf.Abs(scroll) > 0.0001f && !_isPointerOverUI) HandleZoom(scroll); // ── 좌클릭 드래그 시작/종료 ─────────────────────────── if (_panButtonAction.WasPressedThisFrame() && !_isPointerOverUI) _isDraggingLeft = true; if (_panButtonAction.WasReleasedThisFrame()) _isDraggingLeft = false; // ── 우클릭 드래그 시작/종료 ─────────────────────────── if (_rotateButtonAction.WasPressedThisFrame() && !_isPointerOverUI) _isDraggingRight = true; if (_rotateButtonAction.WasReleasedThisFrame()) _isDraggingRight = false; // ── 드래그 처리: 버튼을 떼지 않으면 UI 위에서도 유지 ── var delta = _mouseDeltaAction.ReadValue(); if (_isDraggingLeft) HandlePan(delta); if (_isDraggingRight) HandleRotate(delta); } private void HandleZoom(float scroll) => transform.Translate(0f, 0f, scroll * _zoomSpeed * Time.deltaTime * 60f, Space.Self); private void HandlePan(Vector2 delta) => transform.Translate(-delta.x * _panSpeed, -delta.y * _panSpeed, 0f, Space.Self); private void HandleRotate(Vector2 delta) { // 수평: 월드 Y축 기준 회전 transform.Rotate(Vector3.up, delta.x * _rotateSpeed * Time.deltaTime, Space.World); // 수직: 로컬 X축 기준 피치 transform.Rotate(Vector3.right, -delta.y * _rotateSpeed * Time.deltaTime, Space.Self); } // ── 카메라 내비게이션 내부 ───────────────────────────────── private void StartNavigation(Vector3 targetPos, Quaternion targetRot, float duration) { _navStartPos = transform.position; _navStartRot = transform.rotation; _navTargetPos = targetPos; _navTargetRot = targetRot; _navDuration = Mathf.Max(duration, 0.01f); _navElapsed = 0f; _isNavigating = true; _isDraggingLeft = false; _isDraggingRight = false; } private void UpdateNavigation() { if (!_isNavigating) return; _navElapsed += Time.deltaTime; var t = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(_navElapsed / _navDuration)); transform.position = Vector3.Lerp(_navStartPos, _navTargetPos, t); transform.rotation = Quaternion.Slerp(_navStartRot, _navTargetRot, t); if (_navElapsed >= _navDuration) { transform.position = _navTargetPos; transform.rotation = _navTargetRot; _isNavigating = false; } } } }