330 lines
17 KiB
C#
330 lines
17 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
|
// CameraFramingOverride.cs — 맵 진입 시 카메라 구도(내려다보는 각 + 거리) 1회 오버라이드
|
|
//
|
|
// PD 지시 #813 · 발주서 WL-811o §1-1 (2026-09-09) · 근거 = qa/W2-4b_재채점_v2.md D-8 · 채점 7-3
|
|
//
|
|
// ■ 관측된 결함 (QA 실측 · 캡처 Screenshots_WL/Q4b/01_enter_hud.png)
|
|
// 존1 진입 직후 큰 덤불/바위가 PC 를 화면의 40 % 가까이 덮는다. 카메라가 거의 눕다시피 해서
|
|
// 카메라~PC 선분이 지면 가까이 깔리기 때문이다(아래 실측).
|
|
//
|
|
// ■ 원인 (코드·프리팹 실측)
|
|
// Main Camera.prefab arr_cCamData[0] = minDistance 3 · maxDistance 3 · DistanceUp 2 · **cameraHeight 65**
|
|
// (원본 커밋 d5df1b2c9 「카메라 수정」 에서 78 → 65 · 1.5/1.6/1 → 3/3/2 로 바뀌었다.)
|
|
// RealCamera 궤도 수식으로 환산하면 **내림각 25.6°** — 카메라~주시점 선분이 PC 로부터 2.5 m 지점에서
|
|
// 지면 위 2.26 m 밖에 안 된다. 맵의 바위·덤불이 그보다 높으면 그대로 PC 를 덮는다.
|
|
//
|
|
// ■ 이 파일이 하는 것 — 원본 훅 0 · RealCamera 무수정 · 폴링만(AutoCombatOnEnter 선례)
|
|
// 맵(런) 진입 경계에서 **1회**, RealCamera 의 공개 데이터 arr_cCamData[0](일반 프리셋)에
|
|
// SO 값(cameraHeight · distance · distanceUp)을 쓰고, 살아 있는 공개 필드에도 같은 값을 반영한다.
|
|
// · arr_cCamData[0] 을 쓰는 이유 = 원본이 나중에 Init_Cam()(private · Set_Target(t, rot) 경유)으로
|
|
// 프리셋을 **다시 읽어** 라이브 필드를 덮어쓰기 때문. 배열까지 바꿔야 값이 유지된다.
|
|
// (같은 이유로 811d 는 arr_cCamData[1] 보스 프리셋을 덮어쓴다 — 이 파일은 [0] 만 건드린다.)
|
|
// · 라이브 필드 반영 = minDistance/maxDistance/cameraHeight 직접 대입 + Set_DistanceUp(false)(공개 API).
|
|
// Init_Cam() 의 나머지 한 줄(private VerticalAxis)은 DistanceAway 를 즉시 스냅시키는 용도인데,
|
|
// min=max 로 두면 다음 Update_Cam 의 Clamp 가 같은 일을 하므로 필요 없다.
|
|
// · 원값은 적용 직전에 스냅샷 → SO off · 씬 전환 · 러너 파괴 때 그대로 되돌린다(C8).
|
|
// · 킬캠/보스 캠(811d)이 궤도 거리를 쓰고 있는 동안에는 적용을 미룬다(다음 폴링에서 재시도) —
|
|
// 진행 중인 램프의 base 를 오염시키지 않기 위해서다.
|
|
//
|
|
// ■ 왜 매 프레임 쓰지 않는가
|
|
// 811d CombatCameraDirector 가 킬캠·보스 캠에서 같은 필드를 램프한다. 매 프레임 쓰면 두 기능이 싸운다.
|
|
// 구도는 「진입 시 1회」로 충분하다(발주서 §1-1).
|
|
//
|
|
// ■ 값(C45)은 전부 WLCameraFramingSettings. framingEnabled = false 면 아무 것도 쓰지 않는다.
|
|
//
|
|
// 🔴 어셈블리 주의: Assets/WL/Camera/ 에 .asmdef 를 만들지 말 것(RealCamera 가 Assembly-CSharp 에 있다).
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
using UnityEngine;
|
|
using WL.CombatCamera;
|
|
|
|
namespace WL.CameraFraming
|
|
{
|
|
/// <summary>맵 진입 시 카메라 구도 1회 오버라이드. 정적 · 새 Manager/Singleton 0.</summary>
|
|
public static class CameraFramingOverride
|
|
{
|
|
static WLCameraFramingSettings Cfg { get { return WLCameraFramingSettings.Instance; } }
|
|
|
|
/// <summary>기능이 살아 있는가. 에셋이 없거나 플래그가 꺼져 있으면 false = 원본 구도 100 %.</summary>
|
|
public static bool Active
|
|
{
|
|
get { var c = Cfg; return c != null && c.enabled && c.framingEnabled && !WLCameraFramingSettings.RuntimeDisabled; }
|
|
}
|
|
|
|
// ───────────────────────────────────────── 진단 카운터 (프로브가 읽는다)
|
|
public static int Boundaries, Applied, Restored, SkippedLobby, SkippedBusy, SkippedNoCam;
|
|
public static string LastReason = "", LastLog = "";
|
|
|
|
/// <summary>지금 우리 값이 걸려 있는가.</summary>
|
|
public static bool IsApplied { get { return s_applied; } }
|
|
|
|
/// <summary>적용 중인 값(진단) — 궤도 파라미터 · 거리 · 궤도 중심 높이.</summary>
|
|
public static float AppliedHeight, AppliedDistance, AppliedUp;
|
|
|
|
/// <summary>원값 스냅샷(진단) — 복원 대상.</summary>
|
|
public static float OriginHeight, OriginMin, OriginMax, OriginUp;
|
|
|
|
// ───────────────────────────────────────── 내부 상태
|
|
static PCActor s_lastPc;
|
|
static int s_lastMapId = int.MinValue;
|
|
static float s_nextPoll;
|
|
static bool s_faulted;
|
|
|
|
static bool s_applied;
|
|
static cCamData s_data; // 우리가 값을 쓴 프리셋(원본 [Serializable] 클래스 = fake-null 아님)
|
|
static RealCamera s_cam;
|
|
static float s_wHeight, s_wMin, s_wMax, s_wUp; // 우리가 쓴 값(외부 덮어쓰기 감지)
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
// 틱
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>러너가 매 프레임 부른다(에디트 모드에서는 프로브가 ForceTick 으로 직접 부를 수 있다).</summary>
|
|
internal static void Tick()
|
|
{
|
|
if (s_faulted) return;
|
|
|
|
// C8 — 기능이 꺼지면 즉시 원값으로 되돌린다.
|
|
if (!Active) { if (s_applied) Restore("off"); return; }
|
|
|
|
var cfg = Cfg;
|
|
float now = Time.unscaledTime;
|
|
if (now < s_nextPoll) return;
|
|
s_nextPoll = now + Mathf.Max(0.02f, cfg.framingPollSeconds);
|
|
|
|
// 🔴 Debug.LogException 을 쓰지 않는다 — ErrorLogHookManager 가 그것을 플레이어 팝업으로 띄운다(813x §D-1).
|
|
try { PollBoundary(cfg); }
|
|
catch (System.Exception ex) { s_faulted = true; Log("중단(예외) — " + ex.Message); }
|
|
}
|
|
|
|
static void PollBoundary(WLCameraFramingSettings cfg)
|
|
{
|
|
var pc = MyValue.MyPC;
|
|
if (DSUtil.CheckNull(pc)) { s_lastPc = null; return; }
|
|
|
|
var info = InGameInfo.Ins;
|
|
if (DSUtil.CheckNull(info)) return; // 아직 인게임 정보가 없다 — 경계를 소비하지 않는다
|
|
|
|
int map = MyValue.MyChoiceMapData != null ? MyValue.MyChoiceMapData.n_MapID : int.MinValue;
|
|
bool pcChanged = !ReferenceEquals(pc, s_lastPc);
|
|
bool mapChanged = map != s_lastMapId;
|
|
if (!pcChanged && !mapChanged) return;
|
|
|
|
if (!cfg.framingInLobby && info.Get_GameMode() == eGameMode.Lobby)
|
|
{
|
|
s_lastPc = pc; s_lastMapId = map;
|
|
Boundaries++; SkippedLobby++;
|
|
LastReason = "lobby";
|
|
Log("로비 — 건너뜀");
|
|
return;
|
|
}
|
|
|
|
var cam = MyValue.m_RealCamera;
|
|
if (DSUtil.CheckNull(cam) || cam.arr_cCamData == null || cam.arr_cCamData.Length == 0 || cam.arr_cCamData[0] == null)
|
|
{
|
|
SkippedNoCam++; // 경계를 소비하지 않는다 — 카메라가 뜨면 다음 폴링에서 다시 본다
|
|
return;
|
|
}
|
|
|
|
// 811d 킬캠·보스 캠이 궤도 거리를 램프하는 중이면 미룬다(그 base 를 오염시키지 않는다).
|
|
if (CombatCameraDirector.KillCamActive || CombatCameraDirector.BossCamActive || CombatCameraDirector.BossPresetActive)
|
|
{
|
|
SkippedBusy++;
|
|
return;
|
|
}
|
|
|
|
s_lastPc = pc; s_lastMapId = map;
|
|
Boundaries++;
|
|
LastReason = pcChanged ? "pc" : ("map " + map);
|
|
|
|
Apply(cfg, cam);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
// 적용 · 복원
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
static void Apply(WLCameraFramingSettings cfg, RealCamera cam)
|
|
{
|
|
var d = cam.arr_cCamData[0];
|
|
|
|
// 이미 우리 값이 걸려 있고 같은 프리셋이면 원값 스냅샷을 다시 잡지 않는다(자기 값을 원값으로 착각 방지).
|
|
if (!s_applied || !ReferenceEquals(d, s_data))
|
|
{
|
|
s_data = d;
|
|
OriginHeight = d.cameraHeight; OriginMin = d.minDistance; OriginMax = d.maxDistance; OriginUp = d.DistanceUp;
|
|
}
|
|
|
|
float h = Mathf.Clamp(cfg.cameraHeight, 50f, 85f);
|
|
float dist = Mathf.Clamp(cfg.distance, 1f, 8f);
|
|
float up = cfg.distanceUp >= 0f ? cfg.distanceUp : OriginUp;
|
|
|
|
d.cameraHeight = h;
|
|
d.minDistance = dist;
|
|
d.maxDistance = dist;
|
|
d.DistanceUp = up;
|
|
|
|
// 라이브 필드 — Init_Cam() 이 하는 일과 같은 효과(공개 API 만 사용)
|
|
cam.cameraHeight = h;
|
|
cam.minDistance = dist;
|
|
cam.maxDistance = dist;
|
|
cam.Set_DistanceUp(false); // DistanceUp = arr_cCamData[islockTarget ? 1 : 0].DistanceUp
|
|
|
|
s_wHeight = h; s_wMin = dist; s_wMax = dist; s_wUp = up;
|
|
s_cam = cam;
|
|
s_applied = true;
|
|
Applied++;
|
|
AppliedHeight = h; AppliedDistance = dist; AppliedUp = up;
|
|
|
|
Log("구도 적용 #" + Applied + " (" + LastReason + ") h " + OriginHeight + "→" + h +
|
|
" · dist " + OriginMax + "→" + dist + " · up " + OriginUp + "→" + up +
|
|
" · 내림각 " + DownAngleDeg(h, dist, up).ToString("0.0") + "°");
|
|
}
|
|
|
|
/// <summary>원값으로 되돌린다(외부가 그 사이에 값을 바꿨으면 건드리지 않는다).</summary>
|
|
static void Restore(string why)
|
|
{
|
|
if (!s_applied) return;
|
|
s_applied = false;
|
|
|
|
var d = s_data;
|
|
if (d != null && d.cameraHeight == s_wHeight && d.minDistance == s_wMin && d.maxDistance == s_wMax && d.DistanceUp == s_wUp)
|
|
{
|
|
d.cameraHeight = OriginHeight; d.minDistance = OriginMin; d.maxDistance = OriginMax; d.DistanceUp = OriginUp;
|
|
|
|
var cam = s_cam;
|
|
if (!DSUtil.CheckNull(cam))
|
|
{
|
|
cam.cameraHeight = OriginHeight;
|
|
cam.minDistance = OriginMin;
|
|
cam.maxDistance = OriginMax;
|
|
cam.Set_DistanceUp(false);
|
|
}
|
|
Restored++;
|
|
Log("구도 복원(" + why + ") h→" + OriginHeight + " · dist→" + OriginMax);
|
|
}
|
|
else Log("구도 복원 포기(" + why + ") — 외부가 값을 바꿨다");
|
|
|
|
s_data = null; s_cam = null;
|
|
}
|
|
|
|
/// <summary>씬 전환·러너 파괴·앱 종료에서 부른다.</summary>
|
|
public static void ReleaseAll() { Restore("release"); s_lastPc = null; s_lastMapId = int.MinValue; }
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
// 궤도 환산 (진단·프로브 공용 · RealCamera.Update_Cam 수식 그대로)
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>궤도 파라미터 h 에서 카메라가 타겟 발밑보다 얼마나 높은지(m).</summary>
|
|
public static float CamHeightAbove(float h, float distance, float distanceUp)
|
|
{
|
|
float r = h * Mathf.Deg2Rad;
|
|
return distanceUp + distance * (Mathf.Sin(r) - Mathf.Cos(r));
|
|
}
|
|
|
|
/// <summary>궤도 파라미터 h 에서 카메라~타겟 수평 거리(m).</summary>
|
|
public static float CamHorizontal(float h, float distance)
|
|
{
|
|
float r = h * Mathf.Deg2Rad;
|
|
float s = Mathf.Sin(r) + Mathf.Cos(r);
|
|
return distance * Mathf.Sqrt(1f + s * s);
|
|
}
|
|
|
|
/// <summary>주시점(발밑 + lookAtY)까지의 내림각(도). lookAtY 기본 1.06 = RealCamera.m_LookAt.y 실측.</summary>
|
|
public static float DownAngleDeg(float h, float distance, float distanceUp, float lookAtY = 1.06f)
|
|
{
|
|
float dy = CamHeightAbove(h, distance, distanceUp) - lookAtY;
|
|
float dx = CamHorizontal(h, distance);
|
|
if (dx <= 0.0001f) return 90f;
|
|
return Mathf.Atan2(dy, dx) * Mathf.Rad2Deg;
|
|
}
|
|
|
|
/// <summary>궤도 파라미터 h 의 카메라 위치(월드) — 프로브가 가림률을 계산할 때 쓴다.</summary>
|
|
public static Vector3 CameraPosition(Vector3 targetFoot, float yawDeg, float h, float distance, float distanceUp)
|
|
{
|
|
Vector3 rotateVector = Quaternion.Euler(h, yawDeg, 0f) * Vector3.one;
|
|
return targetFoot + Vector3.up * distanceUp - rotateVector * distance;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
// 진단 · 러너
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
static void Log(string msg)
|
|
{
|
|
LastLog = msg;
|
|
var c = Cfg;
|
|
if (c != null && c.verboseLog) Debug.Log("[CameraFraming] " + msg);
|
|
}
|
|
|
|
/// <summary>프로브용: 카운터·경계 상태 초기화(적용된 값은 유지).</summary>
|
|
public static void ResetDiagnostics()
|
|
{
|
|
Boundaries = Applied = Restored = SkippedLobby = SkippedBusy = SkippedNoCam = 0;
|
|
LastReason = LastLog = "";
|
|
s_lastPc = null; s_lastMapId = int.MinValue; s_nextPoll = 0f; s_faulted = false;
|
|
}
|
|
|
|
/// <summary>프로브용: 러너 없이 1회 폴링(강제 틱).</summary>
|
|
public static void ForceTick() { s_nextPoll = 0f; Tick(); }
|
|
|
|
/// <summary>
|
|
/// 프로브용: 경계 판정(PC·InGameInfo)을 건너뛰고 이 카메라에 값을 적용한다.
|
|
/// 🔴 에디트 모드에서 InGameInfo.Ins / ServerInfo.Ins 를 건드리면 싱글턴 GameObject 가 생겨 씬이 더러워진다(813x §0-12) —
|
|
/// 그래서 프로브는 이 진입점을 쓴다. 런타임 경로(PollBoundary)와 Apply 는 같은 함수를 부른다.
|
|
/// </summary>
|
|
public static bool ProbeApply(RealCamera cam)
|
|
{
|
|
var cfg = Cfg;
|
|
if (cfg == null || !Active) return false;
|
|
if (DSUtil.CheckNull(cam) || cam.arr_cCamData == null || cam.arr_cCamData.Length == 0 || cam.arr_cCamData[0] == null) return false;
|
|
LastReason = "probe";
|
|
Boundaries++;
|
|
Apply(cfg, cam);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>프로브용: 즉시 원값 복원.</summary>
|
|
public static void ProbeRestore() { Restore("probe"); }
|
|
|
|
static CameraFramingRunner s_runner;
|
|
|
|
internal static void EnsureRunner()
|
|
{
|
|
if (!Application.isPlaying || !DSUtil.CheckNull(s_runner)) return;
|
|
var go = new GameObject("[WL811o] CameraFramingRunner");
|
|
go.hideFlags = HideFlags.HideAndDontSave;
|
|
s_runner = go.AddComponent<CameraFramingRunner>();
|
|
}
|
|
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
|
static void Boot()
|
|
{
|
|
var c = Cfg;
|
|
if (c != null && c.enabled && (c.framingEnabled || c.occluderFadeEnabled)) EnsureRunner(); // 둘 다 꺼져 있으면 오브젝트 0(C8)
|
|
}
|
|
}
|
|
|
|
/// <summary>구도 오버라이드 + 가림 소품 숨김의 시간 축. 숨김 GameObject 1개 · 코루틴 0.</summary>
|
|
internal sealed class CameraFramingRunner : MonoBehaviour
|
|
{
|
|
void Update()
|
|
{
|
|
CameraFramingOverride.Tick();
|
|
OccluderFade.Tick();
|
|
}
|
|
|
|
void OnDisable()
|
|
{
|
|
CameraFramingOverride.ReleaseAll();
|
|
OccluderFade.ReleaseAll();
|
|
}
|
|
|
|
void OnApplicationQuit()
|
|
{
|
|
CameraFramingOverride.ReleaseAll();
|
|
OccluderFade.ReleaseAll();
|
|
}
|
|
}
|
|
}
|