using UnityEngine; using Unity.Cinemachine; /// /// 드론 카메라 자유비행(6DOF) 모드. /// GamepadInputHandler의 입력을 받아 Cinemachine 카메라를 직접 이동/회전합니다. /// 관성 기반 물리 모델로 자연스러운 드론 느낌을 구현합니다. /// public class DroneCameraMode : MonoBehaviour { [Header("Movement Physics")] [SerializeField] private float acceleration = 5.0f; [SerializeField] private float drag = 3.0f; [SerializeField] private float maxSpeed = 10f; [Header("Rotation")] [SerializeField] private float yawSpeed = 90f; [SerializeField] private float pitchSpeed = 60f; [SerializeField] private float rotationSmoothing = 0.1f; [Header("FOV Control")] [SerializeField] private float fovSpeed = 30f; [SerializeField] private float minFOV = 10f; [SerializeField] private float maxFOV = 90f; [Header("Targeting")] [SerializeField] private float raycastDistance = 100f; [SerializeField] private float sphereCastRadius = 0.5f; // SphereCast 반경 (넓게 잡음) // 상태 private bool isActive; private Vector3 velocity; private float currentYaw; private float currentPitch; private Quaternion targetRotation; // 타겟팅 (Head 본이 아닌 캐릭터 단위) private int currentTargetIndex = -1; private Transform currentTarget; public Transform CurrentTarget => currentTarget; // 참조 private GamepadInputHandler gamepadInput; private CinemachineCamera targetCamera; // 드론 모드 진입 전 상태 저장 private Transform savedFollow; private Transform savedLookAt; private Vector3 savedPosition; private Quaternion savedRotation; private float savedFOV; public bool IsActive => isActive; private void Awake() { gamepadInput = GetComponent(); if (gamepadInput == null) gamepadInput = gameObject.AddComponent(); } /// /// 드론 모드 활성화. 대상 Cinemachine 카메라의 Follow/LookAt을 해제하고 직접 제어를 시작합니다. /// public void Activate(CinemachineCamera camera) { if (camera == null || isActive) return; targetCamera = camera; // 현재 상태 저장 savedFollow = camera.Follow; savedLookAt = camera.LookAt; savedPosition = camera.transform.position; savedRotation = camera.transform.rotation; savedFOV = camera.Lens.FieldOfView; // Cinemachine 추적 해제 → 직접 제어 camera.Follow = null; camera.LookAt = null; // 현재 회전을 오일러로 분해 Vector3 euler = camera.transform.eulerAngles; currentYaw = euler.y; currentPitch = euler.x; if (currentPitch > 180f) currentPitch -= 360f; // -180~180 범위로 targetRotation = camera.transform.rotation; velocity = Vector3.zero; isActive = true; Debug.Log($"[DroneCameraMode] 활성화 - 카메라: {camera.name}"); } /// /// 드론 모드 비활성화. Follow/LookAt을 복원합니다. /// public void Deactivate() { if (!isActive || targetCamera == null) return; // Follow/LookAt 복원 targetCamera.Follow = savedFollow; targetCamera.LookAt = savedLookAt; velocity = Vector3.zero; isActive = false; Debug.Log("[DroneCameraMode] 비활성화"); } /// /// 드론 모드 비활성화 + 원래 위치로 복귀 /// public void DeactivateAndRestore() { if (!isActive || targetCamera == null) return; targetCamera.transform.position = savedPosition; targetCamera.transform.rotation = savedRotation; var lens = targetCamera.Lens; lens.FieldOfView = savedFOV; targetCamera.Lens = lens; Deactivate(); } /// /// 현재 위치를 새 홈 포지션으로 저장 /// public void SaveCurrentAsHome() { if (!isActive || targetCamera == null) return; savedPosition = targetCamera.transform.position; savedRotation = targetCamera.transform.rotation; savedFOV = targetCamera.Lens.FieldOfView; Debug.Log("[DroneCameraMode] 현재 위치를 홈으로 저장"); } /// /// 홈 포지션으로 복귀 (드론 모드 유지) /// public void ReturnToHome() { if (!isActive || targetCamera == null) return; targetCamera.transform.position = savedPosition; targetCamera.transform.rotation = savedRotation; Vector3 euler = savedRotation.eulerAngles; currentYaw = euler.y; currentPitch = euler.x; if (currentPitch > 180f) currentPitch -= 360f; targetRotation = savedRotation; velocity = Vector3.zero; var lens = targetCamera.Lens; lens.FieldOfView = savedFOV; targetCamera.Lens = lens; Debug.Log("[DroneCameraMode] 홈 위치로 복귀"); } /// /// 드론 상태를 직렬화 가능한 데이터로 반환 /// public DroneStateData GetState() { return new DroneStateData { isActive = isActive, position = isActive && targetCamera != null ? targetCamera.transform.position : savedPosition, rotation = isActive && targetCamera != null ? targetCamera.transform.eulerAngles : savedRotation.eulerAngles, fov = isActive && targetCamera != null ? targetCamera.Lens.FieldOfView : savedFOV, speed = velocity.magnitude, targetName = currentTarget != null ? currentTarget.name : null }; } private void LateUpdate() { if (!isActive || targetCamera == null || gamepadInput == null) return; float dt = Time.deltaTime; // === 회전 === bool hasStickInput = gamepadInput.RightStick.sqrMagnitude > 0.01f; if (currentTarget != null && !hasStickInput) { // 타겟 자동 추적: 카메라가 타겟을 부드럽게 바라봄 Vector3 dirToTarget = currentTarget.position - targetCamera.transform.position; if (dirToTarget.sqrMagnitude > 0.001f) { Quaternion lookRot = Quaternion.LookRotation(dirToTarget); targetCamera.transform.rotation = Quaternion.Slerp( targetCamera.transform.rotation, lookRot, 1f - Mathf.Exp(-dt / Mathf.Max(rotationSmoothing, 0.001f)) ); // yaw/pitch 동기화 (스틱 조작 시 이어서 진행되도록) Vector3 euler = targetCamera.transform.eulerAngles; currentYaw = euler.y; currentPitch = euler.x; if (currentPitch > 180f) currentPitch -= 360f; } } else { // 수동 회전: 우스틱으로 조작 currentYaw += gamepadInput.RightStick.x * yawSpeed * dt; currentPitch -= gamepadInput.RightStick.y * pitchSpeed * dt; currentPitch = Mathf.Clamp(currentPitch, -85f, 85f); targetRotation = Quaternion.Euler(currentPitch, currentYaw, 0f); targetCamera.transform.rotation = Quaternion.Slerp( targetCamera.transform.rotation, targetRotation, 1f - Mathf.Exp(-dt / Mathf.Max(rotationSmoothing, 0.001f)) ); } // === 이동 === Vector3 inputDir = Vector3.zero; // 좌스틱 Y → 전진/후진 inputDir += targetCamera.transform.forward * gamepadInput.LeftStick.y; // 좌스틱 X → 좌우 이동 inputDir += targetCamera.transform.right * gamepadInput.LeftStick.x; // RT → 상승, LT → 하강 float verticalInput = gamepadInput.RightTrigger - gamepadInput.LeftTrigger; inputDir += Vector3.up * verticalInput; // 물리: 가속 + 감쇠 velocity += inputDir * (acceleration * dt); velocity -= velocity * (drag * dt); // 최대 속도 제한 float currentMaxSpeed = maxSpeed; if (velocity.magnitude > currentMaxSpeed) velocity = velocity.normalized * currentMaxSpeed; targetCamera.transform.position += velocity * dt; // === FOV === if (gamepadInput.LeftShoulderHeld || gamepadInput.RightShoulderHeld) { float fovDelta = 0f; if (gamepadInput.LeftShoulderHeld) fovDelta -= fovSpeed * dt; // LB: 줌 인 (FOV 감소) if (gamepadInput.RightShoulderHeld) fovDelta += fovSpeed * dt; // RB: 줌 아웃 (FOV 증가) var lens = targetCamera.Lens; lens.FieldOfView = Mathf.Clamp(lens.FieldOfView + fovDelta, minFOV, maxFOV); targetCamera.Lens = lens; } // 타겟팅은 CameraController.HandleGamepadButtons()에서 처리 } /// /// 외부에서 호출: 카메라 중앙 SphereCast로 타겟팅 /// public void TryTargetFromCamera() { TryRaycastTarget(); } /// /// 외부에서 호출: 캐릭터 목록 순환 /// public void CycleTargetPublic(int direction) { CycleTarget(direction); } /// /// 카메라 중앙에서 SphereCast를 발사해 가장 가까운 캐릭터를 타겟팅합니다. /// private void TryRaycastTarget() { Camera mainCam = Camera.main; if (mainCam == null) return; var headTargets = GetHeadTargets(); if (headTargets == null || headTargets.Count == 0) { // 타겟이 없으면 SystemController에서 새로고침 시도 var sc = SystemController.Instance; if (sc != null) { sc.RefreshAvatarHeadColliders(); headTargets = sc.GetAvatarHeadTargets(); if (headTargets == null || headTargets.Count == 0) return; } else return; } Ray ray = mainCam.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0f)); RaycastHit[] hits = Physics.SphereCastAll(ray, sphereCastRadius, raycastDistance); Transform closestHead = null; float closestDist = float.MaxValue; int closestIndex = -1; foreach (var hit in hits) { // Head 타겟 목록에 있는지 확인 (콜라이더 종류 무관) int idx = headTargets.IndexOf(hit.transform); if (idx >= 0 && hit.distance < closestDist) { closestHead = hit.transform; closestDist = hit.distance; closestIndex = idx; } } if (closestHead != null) { currentTarget = closestHead; currentTargetIndex = closestIndex; Debug.Log($"[DroneCameraMode] 타겟 설정: {closestHead.name} (거리: {closestDist:F2}m)"); } else { Debug.Log("[DroneCameraMode] 타겟을 찾지 못했습니다 - 가장 가까운 캐릭터로 설정합니다"); // SphereCast가 안 맞으면 거리 기반으로 가장 가까운 캐릭터 선택 SetClosestTarget(); } } /// /// 카메라에서 가장 가까운 캐릭터를 타겟으로 설정합니다. /// private void SetClosestTarget() { var headTargets = GetHeadTargets(); if (headTargets == null || headTargets.Count == 0 || targetCamera == null) return; Vector3 camPos = targetCamera.transform.position; float closestDist = float.MaxValue; int closestIndex = -1; for (int i = 0; i < headTargets.Count; i++) { if (headTargets[i] == null) continue; float dist = Vector3.Distance(camPos, headTargets[i].position); if (dist < closestDist) { closestDist = dist; closestIndex = i; } } if (closestIndex >= 0) { currentTargetIndex = closestIndex; currentTarget = headTargets[closestIndex]; Debug.Log($"[DroneCameraMode] 가장 가까운 타겟: {currentTarget.name} (거리: {closestDist:F2}m)"); } } /// /// 등록된 캐릭터 목록에서 다음/이전 캐릭터로 타겟을 전환합니다. /// private void CycleTarget(int direction) { var headTargets = GetHeadTargets(); if (headTargets == null || headTargets.Count == 0) return; if (headTargets.Count == 1) { currentTargetIndex = 0; currentTarget = headTargets[0]; Debug.Log($"[DroneCameraMode] 타겟 (유일): {currentTarget.name}"); return; } currentTargetIndex += direction; // 순환 if (currentTargetIndex >= headTargets.Count) currentTargetIndex = 0; else if (currentTargetIndex < 0) currentTargetIndex = headTargets.Count - 1; currentTarget = headTargets[currentTargetIndex]; Debug.Log($"[DroneCameraMode] 타겟 전환 [{currentTargetIndex + 1}/{headTargets.Count}]: {currentTarget.name}"); } /// /// 외부에서 타겟을 직접 설정합니다. /// public void SetTarget(Transform target) { currentTarget = target; var headTargets = GetHeadTargets(); currentTargetIndex = headTargets != null ? headTargets.IndexOf(target) : -1; } /// /// 현재 타겟을 해제합니다. /// public void ClearTarget() { if (currentTarget != null) { Debug.Log($"[DroneCameraMode] 타겟 해제: {currentTarget.name}"); } currentTarget = null; currentTargetIndex = -1; } private System.Collections.Generic.List GetHeadTargets() { var systemController = SystemController.Instance; return systemController != null ? systemController.GetAvatarHeadTargets() : null; } [System.Serializable] public class DroneStateData { public bool isActive; public Vector3 position; public Vector3 rotation; public float fov; public float speed; public string targetName; } }