diff --git a/Assets/WL/Island/Resources/WL/WLIslandSettings.asset b/Assets/WL/Island/Resources/WL/WLIslandSettings.asset index 9edb9e85c..f77790e6c 100644 --- a/Assets/WL/Island/Resources/WL/WLIslandSettings.asset +++ b/Assets/WL/Island/Resources/WL/WLIslandSettings.asset @@ -78,6 +78,16 @@ MonoBehaviour: applyStartCoin: 1 startCoin: 10 saveResetToken: 816g-1 + dungeonCameraMatchIsland: 1 + camPitch: 45 + camYaw: 0 + camDistance: 22 + camFov: 60 + camLookHeight: 0 + camLockYaw: 1 + camLeadMultiplier: 1 + fixDegenerateCameras: 1 + cameraGuardInterval: 0.5 dungeons: - enabled_: 1 id: 1 diff --git a/Assets/WL/Island/WLIslandBridge.cs b/Assets/WL/Island/WLIslandBridge.cs index 777015746..efd871b9d 100644 --- a/Assets/WL/Island/WLIslandBridge.cs +++ b/Assets/WL/Island/WLIslandBridge.cs @@ -380,11 +380,20 @@ namespace WL.Island /// 코루틴·입력을 도맡는 숨김 러너(새 Manager 가 아니다 · 씬에 남지 않는다). public sealed class WLIslandRunner : MonoBehaviour { + float _guardTimer; + void Update() { var cfg = WLIslandSettings.Instance; if (cfg == null || cfg.enabled_ == 0) return; WLDungeonGate.TickAll(cfg); + + // WL-816i 에러 ② — 투영이 깨진 카메라 감시(자세한 근거는 WLIslandCameraRig.cs) + if (cfg.fixDegenerateCameras == 0) return; + _guardTimer -= Time.unscaledDeltaTime; + if (_guardTimer > 0f) return; + _guardTimer = cfg.cameraGuardInterval > 0f ? cfg.cameraGuardInterval : 0.5f; + WLDungeonCameraRig.RepairDegenerateCameras(cfg); } } } diff --git a/Assets/WL/Island/WLIslandCameraRig.cs b/Assets/WL/Island/WLIslandCameraRig.cs index b3a61c8bb..a815d9fbc 100644 --- a/Assets/WL/Island/WLIslandCameraRig.cs +++ b/Assets/WL/Island/WLIslandCameraRig.cs @@ -47,6 +47,7 @@ namespace WL.Island bool _oOrtho; Vector3 _oLookAt; float[] _oCamMin, _oCamMax, _oCamHeight, _oCamUp; + bool _leadTouched; float _oLead = 1f; public static WLDungeonCameraRig Create(WLIslandSettings cfg, UnityEngine.SceneManagement.Scene scene) { @@ -138,6 +139,14 @@ namespace WL.Island if (!Mathf.Approximately(_cam.fieldOfView, _cfg.camFov)) _cam.fieldOfView = _cfg.camFov; } + // 이동 방향 리드(#755) 배율 — 기본 1 = 아무것도 만지지 않는다(다른 팀 기능 그대로). + // 0 으로 두면 던전에서 화면 중심이 흔들리지 않아 섬과 완전히 같은 고정 구도가 된다. + if (_cfg.camLeadMultiplier >= 0f && !Mathf.Approximately(_cfg.camLeadMultiplier, 1f)) + { + if (!_leadTouched) { _leadTouched = true; _oLead = CameraMoveLead.LeadMultiplier; } + CameraMoveLead.LeadMultiplier = _cfg.camLeadMultiplier; + } + Applies++; LastApplied = "pitch " + camHeight.ToString("F2") + "° · 거리 " + _cfg.camDistance.ToString("F2") + " m · FOV " + _cfg.camFov.ToString("F1") + " · (RealCamera away " + away.ToString("F4") + @@ -170,6 +179,7 @@ namespace WL.Island void OnDestroy() { + if (_leadTouched) { CameraMoveLead.LeadMultiplier = _oLead; _leadTouched = false; } if (!_saved || _rc == null) return; _rc.minDistance = _oMin; _rc.maxDistance = _oMax; _rc.cameraHeight = _oHeight; _rc.DistanceUp = _oUp; _rc.rotateAround = _oRot; _rc.cameraPan = _oPan; _rc.m_LookAt = _oLookAt; @@ -184,6 +194,45 @@ namespace WL.Island } } + // ───────────────────────────────────────────────────────────────── + // WL-816i 에러 ② — "Screen position out of view frustum" 감시·수리 + // + // 🔴 실측(이 프로젝트 에디터 · 같은 문구 그대로 재현): + // 이 에러는 **좌표가 화면 밖이라서** 나는 것이 아니다. 화면 밖·클립 밖 좌표(뷰포트 1,0 · z 1000)를 + // 넣어도 정상 카메라에서는 에러가 **나지 않는다**. 에러가 나는 유일한 조건은 **카메라의 투영이 깨진 것**이다: + // · near == far → 에러 + // · 직교인데 orthographicSize ≤ 0 → 에러 + // · aspect ≤ 0 → 에러 + // · 카메라를 꺼 두거나 오브젝트를 꺼 둔 것 → 에러 아님(정상 동작) + // PD 콘솔의 「screen pos 1080, 0, 1000」 = (pixelRect.xMax, pixelRect.yMin, 대상 깊이) — + // NGUI `UIAnchor.Update`(UIAnchor.cs:206-207)가 화면 우하단 앵커를 계산할 때 넣는 값 모양 그대로다. + // → 그래서 값을 넣는 쪽이 아니라 **깨진 카메라**를 찾아 되살린다. 깨진 카메라는 어차피 아무것도 못 그린다. + // ───────────────────────────────────────────────────────────────── + public static int Repairs; + public static string LastRepair = ""; + + /// 투영이 깨진 카메라를 찾아 되살린다. 되살린 수를 돌려준다(정상이면 0 · 로그도 없다). + public static int RepairDegenerateCameras(WLIslandSettings cfg) + { + if (cfg == null || cfg.fixDegenerateCameras == 0) return 0; + var cams = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); + int n = 0; + for (int i = 0; i < cams.Length; i++) + { + var c = cams[i]; + if (c == null) continue; + string why = null; + if (c.nearClipPlane >= c.farClipPlane) { why = "near " + c.nearClipPlane + " ≥ far " + c.farClipPlane; c.nearClipPlane = 0.3f; c.farClipPlane = 500f; } + if (c.orthographic && c.orthographicSize <= 0f) { why = (why == null ? "" : why + " · ") + "직교 size " + c.orthographicSize; c.orthographicSize = 5f; } + if (c.aspect <= 0f || float.IsNaN(c.aspect)) { why = (why == null ? "" : why + " · ") + "aspect " + c.aspect; c.ResetAspect(); } + if (why == null) continue; + n++; Repairs++; + LastRepair = c.name + "(" + (c.gameObject.scene.IsValid() ? c.gameObject.scene.name : "-") + ") — " + why; + WLIslandBridge.Log(cfg, "🔴 투영이 깨진 카메라를 되살렸다 — " + LastRepair); + } + return n; + } + // ───────────────────────────────────────────────────────────────── // 실측 보고용 — 지금 화면의 구도를 잰다(섬·던전 공통 · 캡처와 함께 표로 남긴다) // ───────────────────────────────────────────────────────────────── diff --git a/Assets/WL/Island/WLIslandSettings.cs b/Assets/WL/Island/WLIslandSettings.cs index 4d0820816..c7c9c1f47 100644 --- a/Assets/WL/Island/WLIslandSettings.cs +++ b/Assets/WL/Island/WLIslandSettings.cs @@ -340,6 +340,20 @@ namespace WL.Island [Tooltip("1 이면 던전에서 카메라를 드래그로 돌릴 수 없다(섬과 같은 고정 구도). 0 이면 회전 허용.")] public int camLockYaw = 1; + [Tooltip("던전에서 「이동 방향 리드」(#755 · CameraLeadSettings)를 얼마나 쓸지. " + + "1 = 지금 그대로(리드 살아 있음 · 움직이면 화면 중심이 최대 1.5 m 흔들린다) · " + + "0 = 리드 끔(섬과 완전히 같은 고정 구도). 음수면 만지지 않는다. " + + "🔴 후진 피치(backwardPitchAdd 6°)는 이 값으로 꺼지지 않는다 — 그건 CameraLeadSettings 소관.")] + public float camLeadMultiplier = 1f; + + [Tooltip("1 이면 투영이 깨진 카메라(near==far · 직교 size ≤ 0 · aspect ≤ 0)를 찾아 되살린다. " + + "이 상태의 카메라에 ScreenToWorldPoint 를 쓰면 유니티가 " + + "\"Screen position out of view frustum\" 에러를 낸다(WL-816i 실측). 0 이면 감시도 수리도 안 한다.")] + public int fixDegenerateCameras = 1; + + [Tooltip("위 감시 주기(초).")] + public float cameraGuardInterval = 0.5f; + // ───────────────────────────────── 던전 표 [Header("던전 표 — 한 줄 = 던전 하나")] public WLDungeonDef[] dungeons = new WLDungeonDef[0];