Project_WL/Assets/WL/Scripts/Player/FollowCameraSettings.cs

248 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using UnityEngine;
namespace WL.Player
{
/// <summary>카메라 종류</summary>
public enum CameraKind
{
/// <summary>3인칭 고정각 — 맵 기준 요·피치 고정(쿼터뷰). 캐릭터 회전과 무관</summary>
ThirdPerson = 0,
/// <summary>1인칭 — 현재 모드 구성에서는 미사용. PD 재선택 대비 보존</summary>
FirstPerson = 1,
/// <summary>3인칭 숄더뷰 — 요가 이동 방향을 연속 감쇠 추종. 어깨 오프셋 적용</summary>
ThirdPersonFollow = 2
}
/// <summary>가상패드 입력을 월드 방향으로 바꿀 때 쓰는 기준 방향 (PD #713 ②)</summary>
public enum PadReference
{
/// <summary>카메라의 현재 요 기준(화면 기준). 요 추종 모드에서는 기준을 래치해 순환을 끊는다 (PD #719 ⑤)</summary>
CameraRelative = 0,
/// <summary>모드의 고정 요(yaw) 기준. 카메라가 돌아도 패드 방향의 월드 의미가 바뀌지 않는다</summary>
ModeFixedYaw = 1
}
/// <summary>포커스 오프셋의 기준 방향</summary>
public enum FocusDirection
{
/// <summary>카메라 화면 위쪽 (고정 요 카메라 기준 · 기본)</summary>
CameraScreenUp = 0,
/// <summary>캐릭터가 바라보는 방향</summary>
CharacterFacing = 1
}
/// <summary>시야 모드 1종의 카메라 값</summary>
[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;
}
/// <summary>
/// 추적 카메라 + 시야 모드 2종(1 숄더뷰 / 2 근거리) 수치의 단일 출처(SOT).
/// 모바일 기준 — 회전·줌 등 사용자 시야 조절 입력은 없다. 모드 전환만 가능.
/// 에셋 경로: Assets/WL/Settings/FollowCameraSettings.asset
///
/// PD #719 재설계 — 모드 1 의 "멈췄다 따라잡기·툭 튐"을 만들던 게이트를 전부 없애고 연속 감쇠 하나로 바꿨다.
/// 제거: 요 각속도 상한 · 주시점 데드존 · 카메라 속도 상한(캐릭터 속도 배수) · 급반전 유예 · 반전 스냅 상태기
/// 남긴 게이트: 정지 시 요 유지(followMoveSpeedThreshold) 하나뿐
/// PD #721 — 모드 2 에서 캐릭터가 시야에서 사라지지 않도록 가림 회피·안전 사각형·스파이크 상한을 넣었다.
/// </summary>
[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)];
}
/// <summary>
/// 이 모드에 실제로 적용되는 룩어헤드 최대 거리(m).
/// lookAheadScaleByDistance 가 켜져 있으면 **기준 모드 값 × (이 모드 거리 / 기준 모드 거리)** 로 계산한다
/// (PD #719 보충 — 모드 1 이 "모드 2 를 가까이 당긴 카메라"처럼 느껴지게).
/// 기준 모드 자신이거나 거리가 0 이면 모드에 적힌 값을 그대로 쓴다.
/// </summary>
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);
}
}
}