Project_WL/Assets/WL/Camera/CameraMoveLead.cs

255 lines
13 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;
/// <summary>
/// 플레이어 이동 방향에 따른 카메라 리드(전진/후진)·피치 가산(후진) 계산기.
///
/// ■ 무엇을 하나
/// 전진(카메라에서 멀어지는 성분) → 궤도 중심과 주시점을 진행 방향으로 leadDistance 만큼 함께 민다.
/// 둘을 같은 값으로 밀기 때문에 카메라는 회전 없이 통째로 평행 이동하고,
/// 캐릭터는 화면에서 아래로 내려가 진행 방향이 더 보인다.
/// 후진(카메라 쪽으로 오는 성분) → ⓐ 궤도 파라미터 cameraHeight 를 올려 더 위에서 내려다보게 하고
/// ⓑ 주시점을 camFwd 축으로 backwardLeadDistance(음수) 만큼 밀어
/// 캐릭터를 화면 위로 올린다(구 WL 앵커 전환과 같은 방향 · PM 결정 ①).
/// 정지·측면 → 전부 0 으로 복귀(SmoothDamp).
///
/// ■ 어디에 붙나 — MonoBehaviour 가 아니다.
/// RealCamera 가 private 필드로 1개 들고 Update_Cam() 안에서 Evaluate() 를 부른다.
/// · Update_Cam() 은 CamStatus == 1(평소)에서만 호출된다 → 인트로(0)·스킬 팔로우(2)·탑뷰(3)·
/// 보스 연출은 **호출 자체가 안 되므로** 별도의 상태 검사 없이 자동 배제된다.
/// · 프리팹(Main Camera.prefab) 을 건드리지 않는다 — 컴포넌트 추가·직렬화 필드 추가가 없다.
/// · 값은 전부 Resources/WL/CameraLeadSettings.asset(ScriptableObject · C45)에서 읽는다.
/// 에셋이 없거나 enabled = false 면 모든 출력이 0 → RealCamera 는 100% 기존 동작.
///
/// ■ 왜 "RealCamera 가 쓴 transform.position 에 오프셋을 더하는 후처리 컴포넌트" 가 아닌가 (기각 근거)
/// RealCamera 는 transform.position = Vector3.Lerp(transform.position, camPosition, dt*smooth) 로
/// **자기 출력을 다시 입력으로 읽는** 저역통과 필터다. 여기에 매 프레임 오프셋 e 를 사후 가산하면
/// 평형점이 p = camPosition + ((2a1)/a)·e (a = dt*smooth) 가 된다.
/// 실측 a = (1/60)×4 = 0.0667 → 계수 13.0. 즉 의도한 오프셋의 13배가 **반대 방향**으로 나오고
/// 프레임률에 따라 계수가 또 변한다. 사후 가산 방식은 성립하지 않는다.
///
/// ■ 이동 방향 = 입력이 아니라 위치 차분
/// 조이스틱(PCActor.Run_byJoystick → NavMeshAgent.Move)과 자동 이동(NavMeshAgent 경로)을 모두 포함한다.
/// Update_Cam 은 LateUpdate 에서 호출되므로 그 프레임의 이동이 끝난 뒤의 위치다.
/// </summary>
// [System.Serializable] 을 일부러 붙이지 않는다 — 붙이면 RealCamera 의 필드가 직렬화되어
// Main Camera.prefab 이 더러워진다. 값의 SOT 는 ScriptableObject 한 곳이다.
public class CameraMoveLead
{
const string SettingsPath = "WL/CameraLeadSettings"; // Assets/WL/Settings/Resources/WL/CameraLeadSettings.asset
static CameraLeadSettings s_Settings;
static bool s_Loaded;
/// <summary>설정 에셋. 없으면 null — 그 경우 이 기능은 통째로 비활성이다.</summary>
public static CameraLeadSettings Settings
{
get
{
if (!s_Loaded)
{
s_Loaded = true;
s_Settings = Resources.Load<CameraLeadSettings>(SettingsPath);
}
return s_Settings;
}
}
/// <summary>에디터/진단에서 에셋을 갈아끼울 때.</summary>
public static void ReloadSettings() { s_Loaded = false; s_Settings = null; }
/// <summary>
/// WL #811d — 런타임 리드 배율(전진·후진·측면 리드 전부에 곱한다 · 기본 1). 3타 ComboStage(isLast) 에
/// CombatCameraDirector 가 잠시 1.3 으로 올렸다가 되돌린다(값은 WLCombatCameraSettings). 1 이면 기존 동작과 동일.
/// </summary>
public static float LeadMultiplier = 1f;
// ── 상태
Transform _bound; // 현재 추적 중인 타겟(바뀌면 리셋)
Vector3 _prevPos;
float _bindDistance; // 바인드 시점의 DistanceAway — 리드 거리비의 기준
bool _hasPrev;
Vector3 _lead, _leadVel;
Vector3 _leadDir = Vector3.forward, _leadDirVel;
float _pitch, _pitchVel;
// ── 진단 (settings.recordSample 이 켜져 있을 때만 갱신)
public struct Sample
{
public float time, speed, fwd01, back01, speed01, leadLen, pitchAdd, yawAdd, camYaw, camPitch;
public Vector3 leadDir, camPos, lookPoint;
}
public Sample LastSample;
public Vector3 LeadOffset { get { return _lead; } }
public float PitchAdd { get { return _pitch; } }
/// <summary>리드·피치를 즉시 0 으로 되돌린다(순간이동·타겟 교체·비활성).</summary>
public void Snap()
{
_lead = Vector3.zero; _leadVel = Vector3.zero;
_leadDirVel = Vector3.zero;
_pitch = 0f; _pitchVel = 0f;
}
/// <summary>
/// RealCamera.Update_Cam() 에서 매 프레임 1회 호출한다.
/// </summary>
/// <param name="target">추적 대상(플레이어) Transform — RealCamera.target</param>
/// <param name="actor">추적 대상 Actor — RealCamera 의 m_Target. 기준 속도(Get_MoveSpeed)를 읽는다. null 허용</param>
/// <param name="camTf">카메라 Transform — 수평 전방으로 전/후진을 판정한다</param>
/// <param name="distanceAway">RealCamera.DistanceAway — 리드 거리비 스케일용</param>
/// <param name="cameraHeight">RealCamera.cameraHeight — 피치 상한·요 상쇄 계산의 기준</param>
/// <param name="lead">궤도 중심과 주시점에 **함께** 더할 수평 오프셋(월드)</param>
/// <param name="pitchAdd">cameraHeight 에 더할 각(도)</param>
/// <param name="yawAdd">rotateAround 에 더할 각(도) — 피치 가산의 수평 방위 커플링 상쇄분</param>
public void Evaluate(Transform target, Actor actor, Transform camTf,
float distanceAway, float cameraHeight,
out Vector3 lead, out float pitchAdd, out float yawAdd)
{
lead = Vector3.zero; pitchAdd = 0f; yawAdd = 0f;
var st = Settings;
if (st == null || !st.enabled || target == null || camTf == null)
{
if (_lead != Vector3.zero || _pitch != 0f) Snap();
_hasPrev = false;
return;
}
float dt = Time.deltaTime;
// 타겟 교체(맵 이동·부활·PC 교체) → 차분을 버리고 리셋
if (!ReferenceEquals(_bound, target))
{
_bound = target;
_prevPos = target.position;
_bindDistance = distanceAway;
_hasPrev = true;
Snap();
return;
}
if (dt <= 0f) { lead = _lead; pitchAdd = _pitch; yawAdd = YawComp(st, cameraHeight, _pitch); return; }
Vector3 delta = target.position - _prevPos;
_prevPos = target.position;
if (!_hasPrev) { _hasPrev = true; return; }
delta.y = 0f;
float dist = delta.magnitude;
// 순간이동(포탈·워프·리스폰)
if (dist > st.teleportDistance) { Snap(); return; }
float speed = dist / dt;
// 카메라 수평 전방 — +방향으로 이동 = 카메라에서 멀어짐 = 전진
Vector3 camFwd = camTf.forward; camFwd.y = 0f;
if (camFwd.sqrMagnitude < 1e-6f) { lead = _lead; pitchAdd = _pitch; yawAdd = YawComp(st, cameraHeight, _pitch); return; }
camFwd.Normalize();
// 기준 속도 — 0 이면 캐릭터의 실제 이동속도(테이블·버프 반영)를 쓴다 (C45)
float refSpeed = st.speedReference;
if (refSpeed <= 0.0001f && actor != null) refSpeed = actor.Get_MoveSpeed();
float lo = st.moveSpeedThreshold;
float speed01 = (refSpeed > lo + 0.0001f) ? Mathf.Clamp01((speed - lo) / (refSpeed - lo)) : (speed > lo ? 1f : 0f);
float fwd01 = 0f, back01 = 0f;
float side01 = 0f, sideSign = 0f; // #759 좌/우 성분 (camRight 축)
Vector3 camRight = Vector3.Cross(Vector3.up, camFwd); camRight.Normalize();
if (speed > lo && dist > 1e-6f)
{
Vector3 dir = delta / dist;
float dotR = Vector3.Dot(dir, camRight);
side01 = Mathf.Clamp01(Mathf.Abs(dotR));
sideSign = dotR >= 0f ? 1f : -1f;
// 측면 무반응 반각: |각| 이 (90° sideDeadAngle) 를 넘으면 0
float dot = Vector3.Dot(dir, camFwd);
float gate = Mathf.Cos((90f - Mathf.Clamp(st.sideDeadAngle, 0f, 45f)) * Mathf.Deg2Rad);
float denom = Mathf.Max(1f - gate, 1e-4f);
fwd01 = Mathf.Clamp01((dot - gate) / denom);
back01 = Mathf.Clamp01((-dot - gate) / denom);
// 리드가 향하는 방향 자체도 감쇠 — 급반전 때 오프셋이 화면을 가로지르지 않게 (구 WL #720)
if (st.leadDirSmoothTime > 0.001f)
_leadDir = Vector3.SmoothDamp(_leadDir, dir, ref _leadDirVel, st.leadDirSmoothTime, Mathf.Infinity, dt);
else _leadDir = dir;
if (_leadDir.sqrMagnitude > 1e-6f) _leadDir.Normalize(); else _leadDir = dir;
}
// ── 리드 거리비 스케일 (실내·보스에서 카메라가 가까우면 리드도 함께 줄어든다)
float leadScale = LeadMultiplier; // #811d 3타 리드 배율(기본 1)
if (st.scaleLeadByDistance)
{
float refDist = st.leadReferenceDistance > 0.0001f ? st.leadReferenceDistance : _bindDistance;
if (refDist > 0.0001f) leadScale *= distanceAway / refDist;
}
// ── 전진 리드(진행 방향) + 후진 리드(camFwd 축 · backwardLeadDistance 음수면 카메라 쪽으로 당김)
Vector3 wantedLead = _leadDir * (st.leadDistance * leadScale * fwd01 * speed01)
+ camFwd * (st.backwardLeadDistance * leadScale * back01 * speed01)
+ camRight * (st.lateralLeadDistance * leadScale * side01 * sideSign * speed01); // #759 측면 리드
// 시선 방향 성분 제거(선택) — 수평 리드는 34.93° 카메라에서 82 % 가 줌인으로 소모된다.
// blend = 1 이면 시선에 수직인 성분만 남아 깊이 변화 0(구 WL screenAnchorY/FocusShift 와 같은 방향).
if (st.leadPerpendicularBlend > 0.0001f && wantedLead.sqrMagnitude > 1e-8f)
wantedLead -= Vector3.Project(wantedLead, camTf.forward) * Mathf.Clamp01(st.leadPerpendicularBlend);
if (st.leadSmoothTime > 0.001f)
_lead = Vector3.SmoothDamp(_lead, wantedLead, ref _leadVel, st.leadSmoothTime, Mathf.Infinity, dt);
else { _lead = wantedLead; _leadVel = Vector3.zero; }
// ── 후진 피치 (총합이 pitchTotalMax 를 넘지 않게 입력 단계에서 한 번만 제한)
float wantedPitch = Mathf.Max(0f, Mathf.Min(st.backwardPitchAdd * back01 * speed01,
st.pitchTotalMax - cameraHeight));
float maxSpd = st.pitchMaxSpeed > 0.0001f ? st.pitchMaxSpeed : Mathf.Infinity;
if (st.pitchSmoothTime > 0.001f)
_pitch = Mathf.SmoothDamp(_pitch, wantedPitch, ref _pitchVel, st.pitchSmoothTime, maxSpd, dt);
else { _pitch = wantedPitch; _pitchVel = 0f; }
lead = _lead;
pitchAdd = _pitch;
yawAdd = YawComp(st, cameraHeight, _pitch);
if (st.recordSample)
{
LastSample.time = Time.time;
LastSample.speed = speed; LastSample.fwd01 = fwd01; LastSample.back01 = back01; LastSample.speed01 = speed01;
LastSample.leadLen = _lead.magnitude; LastSample.leadDir = _leadDir;
LastSample.pitchAdd = _pitch; LastSample.yawAdd = yawAdd;
LastSample.camPos = camTf.position;
LastSample.camYaw = camTf.eulerAngles.y; LastSample.camPitch = camTf.eulerAngles.x;
LastSample.lookPoint = target.position + _lead;
}
}
/// <summary>
/// 피치 가산이 만드는 수평 방위 회전을 정확히 상쇄한다.
///
/// RealCamera 의 궤도 벡터는 Quaternion.Euler(h, r, 0) * Vector3.one 이다.
/// 이 벡터의 수평 방위각은 r + atan2(1, sin h + cos h) 이므로, h 를 올리면 r 이 그대로여도
/// 카메라가 옆으로 돈다(실측: h = 78° 에서 파라미터 +1° 당 +0.32°, +6° 면 +2.2° · 반지름 4.65 m 에서 약 18 cm).
/// 따라서 r 에 그 차이만큼을 빼 주면 방위가 고정된다.
/// </summary>
static float YawComp(CameraLeadSettings st, float baseHeight, float add)
{
if (st == null || !st.compensatePitchYawCoupling || Mathf.Abs(add) < 1e-4f) return 0f;
return -(YawOffsetDeg(baseHeight + add) - YawOffsetDeg(baseHeight));
}
static float YawOffsetDeg(float h)
{
float r = h * Mathf.Deg2Rad;
return Mathf.Atan2(1f, Mathf.Sin(r) + Mathf.Cos(r)) * Mathf.Rad2Deg;
}
/// <summary>진단용 — 파라미터 cameraHeight 에 대응하는 실제 궤도 고도각(도).</summary>
public static float RealElevationDeg(float h)
{
float r = h * Mathf.Deg2Rad;
return Mathf.Asin(Mathf.Clamp((Mathf.Sin(r) - Mathf.Cos(r)) / Mathf.Sqrt(3f), -1f, 1f)) * Mathf.Rad2Deg;
}
}