248 lines
16 KiB
C#
248 lines
16 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|