using UnityEngine; namespace WL.Player { /// 카메라 종류 public enum CameraKind { /// 3인칭 고정각 — 맵 기준 요·피치 고정(쿼터뷰). 캐릭터 회전과 무관 ThirdPerson = 0, /// 1인칭 — 현재 모드 구성에서는 미사용. PD 재선택 대비 보존 FirstPerson = 1, /// 3인칭 숄더뷰 — 요가 이동 방향을 연속 감쇠 추종. 어깨 오프셋 적용 ThirdPersonFollow = 2 } /// 가상패드 입력을 월드 방향으로 바꿀 때 쓰는 기준 방향 (PD #713 ②) public enum PadReference { /// 카메라의 현재 요 기준(화면 기준). 요 추종 모드에서는 기준을 래치해 순환을 끊는다 (PD #719 ⑤) CameraRelative = 0, /// 모드의 고정 요(yaw) 기준. 카메라가 돌아도 패드 방향의 월드 의미가 바뀌지 않는다 ModeFixedYaw = 1 } /// 포커스 오프셋의 기준 방향 public enum FocusDirection { /// 카메라 화면 위쪽 (고정 요 카메라 기준 · 기본) CameraScreenUp = 0, /// 캐릭터가 바라보는 방향 CharacterFacing = 1 } /// 시야 모드 1종의 카메라 값 [System.Serializable] public class CameraModeEntry { [Tooltip("UI 표시용 이름")] public string label = "Mode"; [Tooltip("3인칭 고정각 / 1인칭 / 3인칭 숄더뷰")] public CameraKind kind = CameraKind.ThirdPerson; [Header("3인칭 고정 각도")] [Tooltip("내려다보는 각도(도). 90=탑뷰 / 45=쿼터뷰")] [Range(0f, 90f)] public float pitch = 57f; [Tooltip("이동 중에 쓸 내려다보는 각도(도). FollowCameraSettings.usePitchMoving 이 켜져 있을 때만 쓴다 · PD #719 ④ 기본 off")] [Range(0f, 90f)] public float pitchMoving = 0f; [Tooltip("맵 기준 고정 방향(도). 캐릭터 회전과 무관")] public float yaw = 315f; [Tooltip("주시점에서 카메라까지 거리(m)")] public float distance = 11f; [Tooltip("주시점 = 캐릭터 위치 + 이 오프셋(m)")] public Vector3 lookAtOffset = new Vector3(0f, 1.1f, 0f); [Header("공통")] [Tooltip("카메라 화각(도). 0 이하면 카메라 원래 값 유지")] public float fieldOfView = 45f; [Tooltip("캐릭터가 화면 위쪽(카메라 전방)을 볼 때의 화면 세로 위치. 낮출수록 전방이 더 보인다")] [Range(0.05f, 0.95f)] public float screenAnchorY = 0.3f; [Tooltip("캐릭터가 화면 아래쪽(카메라 쪽)을 볼 때의 화면 세로 위치. 0.5 = 정중앙")] [Range(0.05f, 0.95f)] public float screenAnchorYTowardCamera = 0.5f; [Tooltip("위치 추종 감쇠 시간(초). 작을수록 빠르게 따라붙는다 · PD #719 ② 두 모드 동일 0.15")] public float positionSmoothTime = 0.15f; [Tooltip("카메라 근접 클리핑(m)")] public float nearClipPlane = 0.3f; [Header("3인칭 숄더뷰 (kind = ThirdPersonFollow 일 때만)")] [Tooltip("어깨 오프셋(m). + = 우측 어깨")] public float followShoulderOffsetX = 0.3f; [Tooltip("주시점 높이(m). 캐릭터 발밑 기준")] public float followPivotHeight = 1.6f; [Tooltip("각도차가 0° 일 때의 요 추종 감쇠 시간(초). 작은 방향 변화는 여유 있게 · PD #719 ① 임시 0.55")] public float followYawSmoothTimeNear = 0.55f; [Tooltip("각도차가 180° 일 때의 요 추종 감쇠 시간(초). 큰 각도는 짧은 시간상수로 자연스럽게 빨리 돈다 — 스냅 상태기가 필요 없어진다. PD 긴급 피드백으로 0.20 → 0.30 완화(너무 빨리 돌아 캐릭터가 화면 좌우로 밀렸다)")] public float followYawSmoothTimeFar = 0.30f; [Tooltip("요 추종 각속도 상한(도/초). 0 = 무제한. PD #724 — 반지름 8m 궤도를 무제한 감쇠로 돌리면 최대 440°/s(61m/s)까지 휘둘러 캐릭터가 화면을 벗어났다. 위치 상한 대신 회전 자체를 눌러, 캐릭터가 보이는 상태로 방향이 바뀌게 한다 · 임시 180")] public float followYawMaxSpeed = 0f; [Tooltip("이 각도(도) 이내의 방향 변화는 무시한다. PD #719 ① = 0(게이트 제거). 미세 떨림이 보이면 2 정도만")] [Range(0f, 10f)] public float followYawDeadzone = 0f; [Tooltip("이 속도(m/s) 미만이면 정지로 보고 요를 유지한다. PD #719 ① 에서 **유일하게 남기는 게이트**")] public float followMoveSpeedThreshold = 0.15f; [Header("룩어헤드 — 이동 방향으로 주시점 밀기 (PD #712 ① · #720)")] [Tooltip("이동 속도가 최대일 때 주시점을 진행 방향으로 미는 최대 거리(m). 0 이면 끔. lookAheadScaleByDistance 가 켜져 있으면 기준 모드 값 × 거리비로 대체된다")] public float lookAheadMaxDistance = 1.5f; [Header("가상패드 → 월드 방향 기준 (PD #713 ②)")] [Tooltip("패드 입력을 월드 방향으로 바꿀 기준")] public PadReference padReference = PadReference.CameraRelative; [Tooltip("요 추종 목표를 캐릭터 facing 이 아니라 가상패드 입력 방향(월드)으로 삼는다 · PD #713 ②")] public bool followYawFromMoveInput = false; [Header("1인칭")] [Tooltip("캐릭터 위치 기준 눈높이 오프셋(m)")] public Vector3 firstPersonEyeOffset = new Vector3(0f, 1.62f, 0.12f); [Tooltip("1인칭에서 내려다보는 각도(도)")] [Range(-40f, 60f)] public float firstPersonPitch = 8f; [Tooltip("1인칭에서 캐릭터 모델을 그림자만 남기고 숨긴다")] public bool hideCharacterInFirstPerson = true; [Header("지형 충돌 회피")] public bool avoidGeometry = true; [Tooltip("충돌 지점에서 카메라를 띄우는 여유 거리(m)")] public float collisionPadding = 0.3f; } /// /// 추적 카메라 + 시야 모드 2종(1 숄더뷰 / 2 근거리) 수치의 단일 출처(SOT). /// 모바일 기준 — 회전·줌 등 사용자 시야 조절 입력은 없다. 모드 전환만 가능. /// 에셋 경로: Assets/WL/Settings/FollowCameraSettings.asset /// /// PD #719 재설계 — 모드 1 의 "멈췄다 따라잡기·툭 튐"을 만들던 게이트를 전부 없애고 연속 감쇠 하나로 바꿨다. /// 제거: 요 각속도 상한 · 주시점 데드존 · 카메라 속도 상한(캐릭터 속도 배수) · 급반전 유예 · 반전 스냅 상태기 /// 남긴 게이트: 정지 시 요 유지(followMoveSpeedThreshold) 하나뿐 /// PD #721 — 모드 2 에서 캐릭터가 시야에서 사라지지 않도록 가림 회피·안전 사각형·스파이크 상한을 넣었다. /// [CreateAssetMenu(fileName = "FollowCameraSettings", menuName = "WL/Follow Camera Settings")] public class FollowCameraSettings : ScriptableObject { [Header("포커스 오프셋 기준 (PD 선택)")] [Tooltip("전방 포커스를 어느 방향으로 밀지. 기본 = 카메라 화면 위쪽")] public FocusDirection focusDirection = FocusDirection.CameraScreenUp; [Header("앵커 보간 (캐릭터가 카메라 쪽을 볼 때 정중앙으로)")] [Tooltip("캐릭터 진행 방향에 따라 앵커를 보간한다. 1인칭 모드는 항상 제외")] public bool anchorFollowsFacing = true; [Tooltip("앵커 전환 감쇠 시간(초). 두 모드 공통 · 임시 0.4")] public float anchorSmoothTime = 0.4f; [Header("지형 충돌 회피 (PD #721 ①)")] [Tooltip("충돌 회피로 카메라를 당길 때의 최소 거리(m)")] public float minCollisionDistance = 2.5f; [Tooltip("주시점이 콜라이더 안에 있어 거리 0 으로 잡히는 히트는 무시한다")] public bool ignoreInitialOverlap = true; [Tooltip("가림 회피로 거리를 바꿀 때의 감쇠 시간(초). 0 이면 구 동작(매 프레임 새로 계산 → 히트/미스가 갈릴 때 15m↔3m 순간이동) · PD #721 ①-a 임시 0.25")] public float occlusionDistanceSmoothTime = 0.25f; [Tooltip("당김만으로 가림이 안 풀릴 때 피치를 위로 올리는 최대 각(도). 위에서 내려다보면 나무·바위를 넘어 캐릭터가 보인다 · PD #721 ①-b 임시 15(30→45)")] public float occlusionPitchBoostMax = 15f; [Tooltip("가림 피치 상향/복귀 감쇠 시간(초) · 임시 0.25")] public float occlusionPitchSmoothTime = 0.25f; [Tooltip("가림 오브젝트 렌더러를 반투명하게 만든다 (PD #721 ①-c 선택 · 기본 off)")] public bool occlusionFadeEnabled = false; [Header("캐릭터 화면 이탈 방지 (PD #721 ②)")] [Tooltip("룩어헤드·전방 포커스·어깨 오프셋이 합쳐져 캐릭터가 프레임을 벗어나지 않도록 오프셋을 줄인다. PD 긴급 피드백으로 **모드 1 에도** 적용(요 추종 중에도 매 프레임)")] public bool viewportClampEnabled = true; [Tooltip("캐릭터가 있어야 하는 뷰포트 안전 사각형의 좌하단 (0~1) · 임시 (0.15, 0.12)")] public Vector2 safeViewportMin = new Vector2(0.15f, 0.12f); [Tooltip("캐릭터가 있어야 하는 뷰포트 안전 사각형의 우상단 (0~1) · 임시 (0.85, 0.80)")] public Vector2 safeViewportMax = new Vector2(0.85f, 0.80f); [Header("카메라 순간이동 방지 (PD #721 ③)")] [Tooltip("프레임당 카메라 이동 상한 = 캐릭터 속도 × 이 배수. 모드 전환 보간에는 적용하지 않는다 · 임시 3")] public float cameraSpeedSpikeFactor = 3f; [Tooltip("캐릭터가 멈춰 있어도 허용하는 카메라 속도 하한(m/s). 상한이 0 이 되어 카메라가 얼어붙는 것을 막는다 · 임시 8")] public float cameraSpeedSpikeFloor = 8f; [Header("이동 중 피치 하향 (PD #717 ② · #719 ④ 로 기본 off)")] [Tooltip("이동 중 pitchMoving 로 내려간다. 세로 출렁임을 만들어 PD #719 ④ 로 기본 off · PD 가 원하면 on")] public bool usePitchMoving = false; [Tooltip("피치 하향/복귀 감쇠 시간(초) · 임시 0.4")] public float pitchBlendSmoothTime = 0.4f; [Tooltip("피치 하향이 최대가 되는 기준 속도(m/s) · 임시 3.85")] public float pitchSpeedReference = 3.85f; [Header("룩어헤드 공통 (PD #712 ① · #719 거리비 · #720)")] [Tooltip("이동 방향으로 주시점을 미는 기능을 켠다. 두 모드 공통")] public bool lookAheadEnabled = true; [Tooltip("룩어헤드 전환 감쇠 시간(초). PD 긴급 피드백으로 0.5 → 0.7 완화(카메라가 조작 방향으로 너무 빨리 밀려 캐릭터가 안 보였다)")] public float lookAheadSmoothTime = 0.7f; [Tooltip("룩어헤드가 향하는 **방향** 자체의 감쇠 시간(초). 급전환 때 오프셋이 화면을 가로질러 휘두르는 것을 막는다 · 임시 0.35")] public float lookAheadDirSmoothTime = 0.35f; [Tooltip("최대 오프셋에 도달하는 기준 속도(m/s) · 임시 3.85")] public float lookAheadSpeedReference = 3.85f; [Tooltip("PD #719 보충 — 기준 모드의 룩어헤드를 **거리비로 스케일**해 다른 모드에 적용한다. 모드 1 이 '모드 2 를 가까이 당긴 카메라'처럼 느껴지게 한다")] public bool lookAheadScaleByDistance = true; [Tooltip("거리비 스케일의 기준이 되는 모드 번호(0 부터). 기본 1 = 모드 2 근거리")] public int lookAheadReferenceModeIndex = 1; [Header("모드 전환 보간 (PD #709 ③-d)")] [Tooltip("모드 1 <-> 2 전환 시 카메라 위치·각도·화각을 이 시간(초)에 걸쳐 보간한다. 보간 중에는 룩어헤드·요 추종 갱신을 멈춘다(합성 방지 · PD #719 ③)")] public float modeBlendSeconds = 0.6f; [Header("가상패드 기준 요 래치 (PD #719 ⑤)")] [Tooltip("패드를 누르는 동안 카메라 요를 래치해 '패드 → 월드 → 카메라 요 → 다시 패드 기준' 순환을 끊는다. 패드 방향이 이 각도(도) 이상 바뀌거나 손을 뗄 때만 다시 래치한다 · 임시 30")] public float inputYawRelatchAngle = 30f; [Header("시작 모드 (0=숄더뷰 · 1=근거리)")] [Range(0, 1)] public int startModeIndex = 1; [Header("시야 모드 2종 (PD 조정값 · 현재 임시값)")] public CameraModeEntry[] modes = new CameraModeEntry[] { new CameraModeEntry { label = "1 숄더뷰", kind = CameraKind.ThirdPersonFollow, pitch = 33f, distance = 8f, fieldOfView = 55f, screenAnchorY = 0.42f, screenAnchorYTowardCamera = 0.42f, // PD #722 — 캐릭터를 중심으로 도는 궤도 회전. 주시점을 캐릭터에서 떼어 놓는 값은 전부 0 이다. positionSmoothTime = 0.12f, nearClipPlane = 0.1f, followShoulderOffsetX = 0f, followPivotHeight = 1.6f, followYawSmoothTimeNear = 0.55f, followYawSmoothTimeFar = 0.30f, followYawMaxSpeed = 180f, followYawDeadzone = 0f, followMoveSpeedThreshold = 0.15f, lookAheadMaxDistance = 0f, padReference = PadReference.CameraRelative, followYawFromMoveInput = true, avoidGeometry = true, collisionPadding = 0.25f }, new CameraModeEntry { label = "2 근거리", kind = CameraKind.ThirdPerson, pitch = 30f, yaw = 315f, distance = 15f, lookAtOffset = new Vector3(0f, 1.1f, 0f), fieldOfView = 50f, screenAnchorY = 0.3f, screenAnchorYTowardCamera = 0.5f, positionSmoothTime = 0.15f, nearClipPlane = 0.3f, lookAheadMaxDistance = 1.5f, avoidGeometry = true, collisionPadding = 0.3f } }; public int ModeCount { get { return modes != null ? modes.Length : 0; } } public CameraModeEntry GetMode(int index) { if (modes == null || modes.Length == 0) return null; return modes[Mathf.Clamp(index, 0, modes.Length - 1)]; } /// /// 이 모드에 실제로 적용되는 룩어헤드 최대 거리(m). /// lookAheadScaleByDistance 가 켜져 있으면 **기준 모드 값 × (이 모드 거리 / 기준 모드 거리)** 로 계산한다 /// (PD #719 보충 — 모드 1 이 "모드 2 를 가까이 당긴 카메라"처럼 느껴지게). /// 기준 모드 자신이거나 거리가 0 이면 모드에 적힌 값을 그대로 쓴다. /// public float EffectiveLookAhead(int index) { var m = GetMode(index); if (m == null) return 0f; // 모드가 0 으로 껐으면 거리비 스케일도 적용하지 않는다. // PD #722 — 모드 1 은 캐릭터를 중심으로 궤도 회전해야 해서 룩어헤드를 0 으로 끈다(주시점을 캐릭터에서 떼면 안 된다). if (m.lookAheadMaxDistance <= 0.0001f) return 0f; if (!lookAheadScaleByDistance) return m.lookAheadMaxDistance; int refIdx = Mathf.Clamp(lookAheadReferenceModeIndex, 0, Mathf.Max(ModeCount - 1, 0)); if (refIdx == Mathf.Clamp(index, 0, Mathf.Max(ModeCount - 1, 0))) return m.lookAheadMaxDistance; var rm = GetMode(refIdx); if (rm == null || rm.distance <= 0.0001f) return m.lookAheadMaxDistance; return rm.lookAheadMaxDistance * (m.distance / rm.distance); } } }