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];