// ───────────────────────────────────────────────────────────────────────────── // WLSkyScope.cs — 하늘(RenderSettings)을 「들어간 맵 동안만」 빌려 쓰고 나갈 때 돌려준다 // (WL-816h · #816) // // ■ 왜 필요한가 — 🔴 실측(816f · 816h) // `RenderSettings`(앰비언트·스카이박스·안개·반사·sun)는 **씬마다 하나가 아니라 // 「활성 씬」 하나가 소유**한다. 이 게임은 `InGame` 을 활성으로 두고 섬·던전·전투 맵을 // 전부 **Additive** 로 붙인다(`SceneInfo.Load_AddScene` 실측). 그래서 // · 섬 씬(`Level01`)이 제 씬에 적어 둔 하늘(FarmingIsland Skybox)은 **절대 안 쓰이고** // · 누가 `RenderSettings` 에 쓰면 그것은 **모든 맵에 남는다**(전투 맵·로비까지 오염). // 816e/816f 의 `WLIslandLook.ApplyLighting` 은 값을 **쓰기만** 하고 되돌리지 않았다. // 지금은 쓰는 값이 마침 `InGame` 원본과 같아서 오염이 안 보였을 뿐이다(816h 실측 = 차이 0). // // ■ 그래서 이렇게 고친다 // `skyScenes`(섬·던전)에 **처음 들어갈 때 활성 씬의 하늘을 통째로 스냅샷**해 두고 // 우리 값(= 아레나/데모와 같은 값 · SO 재사용)으로 바꾼다. // 그 맵들이 **하나도 안 남았을 때** 스냅샷을 **글자 그대로** 되돌린다. // → 섬·던전은 같은 하늘 · 전투 맵·로비는 원래대로. // // ■ C8 되돌리기 — `WLIslandLookSettings.skyScopeEnabled = 0` 이면 아무것도 하지 않는다. // 씬·에셋 파일을 한 글자도 쓰지 않는다(전부 런타임 메모리). // // 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 팝업). // ───────────────────────────────────────────────────────────────────────────── using System.Collections; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.SceneManagement; namespace WL.Look.Farm { public static class WLSkyScope { public const string RunnerName = "~WLSkyScopeRunner"; // ── 진단(프로브가 읽는다) ──────────────────────────────────────── public static bool Held; public static int Acquires, Applies, Restores; public static string LastLog = ""; public static string HeldBy = ""; struct Snap { public bool valid; public string ownerScene; public AmbientMode mode; public Color light, sky, equator, ground; public float intensity; public Material skybox; public bool fog; public Color fogColor; public FogMode fogMode; public float fogDensity, fogStart, fogEnd; public DefaultReflectionMode reflMode; public float reflIntensity; public Light sun; } static Snap s_snap; static int s_snapHandle; // 스냅샷을 뜬 활성 씬 static bool s_installed; static WLSkyScopeRunner s_runner; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] public static void Install() { if (s_installed) return; var cfg = WLIslandLookSettings.Instance; if (cfg == null || cfg.enabled_ == 0 || cfg.skyScopeEnabled == 0) return; s_installed = true; SceneManager.sceneLoaded += OnSceneLoaded; SceneManager.sceneUnloaded += OnSceneUnloaded; SceneManager.activeSceneChanged += OnActiveSceneChanged; // 🔴 아직 아무 맵도 안 붙은 지금이 「활성 씬의 원래 하늘」이다 — 여기서 한 번만 뜬다. CaptureIfNeeded(); for (int i = 0; i < SceneManager.sceneCount; i++) { var sc = SceneManager.GetSceneAt(i); if (sc.isLoaded && IsScoped(cfg, sc.name)) Acquire(cfg, sc.name); } } static void OnActiveSceneChanged(Scene from, Scene to) { // 활성 씬이 바뀌면 하늘의 주인이 바뀐 것 — 새 주인의 원래 값을 다시 뜬다. s_snap.valid = false; Held = false; HeldBy = ""; CaptureIfNeeded(); } public static bool IsScoped(WLIslandLookSettings cfg, string sceneName) { if (cfg == null || cfg.skyScenes == null) return false; for (int i = 0; i < cfg.skyScenes.Length; i++) if (cfg.skyScenes[i] == sceneName) return true; return false; } static void OnSceneLoaded(Scene scene, LoadSceneMode mode) { var cfg = WLIslandLookSettings.Instance; if (cfg == null || cfg.enabled_ == 0 || cfg.skyScopeEnabled == 0) return; if (!IsScoped(cfg, scene.name)) return; Acquire(cfg, scene.name); } static void OnSceneUnloaded(Scene scene) { var cfg = WLIslandLookSettings.Instance; if (cfg == null || cfg.skyScopeEnabled == 0) return; if (!Held) return; // 🔴 맵을 바꿀 때 `SceneInfo` 는 **같은 프레임에** 내리기와 올리기를 함께 시작한다. // 그래서 바로 되돌리면 「섬 → 던전」 에서 잘못 되돌아간다. 두 프레임 기다렸다 // **그때도 범위 맵이 하나도 없을 때만** 되돌린다. EnsureRunner(); if (s_runner != null) s_runner.StartCoroutine(Co_MaybeRelease(cfg)); else ReleaseIfEmpty(cfg); } static IEnumerator Co_MaybeRelease(WLIslandLookSettings cfg) { yield return null; yield return null; // 🔴 섬 → 던전처럼 범위 안에서 맵만 바뀔 때는 새 맵이 다 올라올 때까지 기다린다. // (안 기다리면 반납했다가 다시 빌리면서 화면이 한 번 튄다 — 816h 실측) float wait = cfg != null ? cfg.skyReleaseDelaySeconds : 1.5f; if (wait > 0f) yield return new WaitForSeconds(wait); ReleaseIfEmpty(cfg); } static void ReleaseIfEmpty(WLIslandLookSettings cfg) { for (int i = 0; i < SceneManager.sceneCount; i++) { var sc = SceneManager.GetSceneAt(i); if (sc.isLoaded && IsScoped(cfg, sc.name)) return; // 아직 섬/던전에 있다 } Release(cfg); } // ───────────────────────────────────────────────────────────────── /// 섬·던전에 들어갈 때 — 활성 씬의 하늘을 스냅샷하고 우리 값으로 바꾼다. public static void Acquire(WLIslandLookSettings cfg, string sceneName) { if (cfg == null || cfg.skyScopeEnabled == 0) return; // 🔴 스냅샷은 **활성 씬마다 딱 한 번**만 뜬다. // 여기서 다시 뜨면, 이미 다른 시스템(던전 씬에 들어 있는 `WL_ReferenceLook`)이 // 앰비언트를 바꿔 놓은 뒤의 값을 「원래 값」으로 잘못 기억한다(816h 1차 실측에서 실제로 났다). CaptureIfNeeded(); if (!Held) { Held = true; HeldBy = sceneName; Acquires++; } ApplySky(cfg); } /// 섬·던전에서 나갈 때 — 스냅샷을 글자 그대로 되돌린다. public static void Release(WLIslandLookSettings cfg) { if (!Held) return; Restore(); Held = false; HeldBy = ""; Restores++; if (cfg != null) cfg.Log("하늘 반납 — 활성 씬(" + s_snap.ownerScene + ") 원래대로"); LastLog = "released → " + Describe(); } static void CaptureIfNeeded() { var act = SceneManager.GetActiveScene(); if (s_snap.valid && s_snapHandle == act.handle) return; s_snapHandle = act.handle; Capture(); } static void Capture() { s_snap.valid = true; s_snap.ownerScene = SceneManager.GetActiveScene().name; s_snap.mode = RenderSettings.ambientMode; s_snap.light = RenderSettings.ambientLight; s_snap.sky = RenderSettings.ambientSkyColor; s_snap.equator = RenderSettings.ambientEquatorColor; s_snap.ground = RenderSettings.ambientGroundColor; s_snap.intensity = RenderSettings.ambientIntensity; s_snap.skybox = RenderSettings.skybox; s_snap.fog = RenderSettings.fog; s_snap.fogColor = RenderSettings.fogColor; s_snap.fogMode = RenderSettings.fogMode; s_snap.fogDensity = RenderSettings.fogDensity; s_snap.fogStart = RenderSettings.fogStartDistance; s_snap.fogEnd = RenderSettings.fogEndDistance; s_snap.reflMode = RenderSettings.defaultReflectionMode; s_snap.reflIntensity = RenderSettings.reflectionIntensity; s_snap.sun = RenderSettings.sun; } static void Restore() { if (!s_snap.valid) return; RenderSettings.ambientMode = s_snap.mode; RenderSettings.ambientLight = s_snap.light; RenderSettings.ambientSkyColor = s_snap.sky; RenderSettings.ambientEquatorColor = s_snap.equator; RenderSettings.ambientGroundColor = s_snap.ground; RenderSettings.ambientIntensity = s_snap.intensity; RenderSettings.skybox = s_snap.skybox; RenderSettings.fog = s_snap.fog; RenderSettings.fogColor = s_snap.fogColor; RenderSettings.fogMode = s_snap.fogMode; RenderSettings.fogDensity = s_snap.fogDensity; RenderSettings.fogStartDistance = s_snap.fogStart; RenderSettings.fogEndDistance = s_snap.fogEnd; RenderSettings.defaultReflectionMode = s_snap.reflMode; RenderSettings.reflectionIntensity = s_snap.reflIntensity; RenderSettings.sun = s_snap.sun; // 내려간 씬의 라이트면 자동으로 null 이 된다 } /// 아레나/데모와 같은 하늘 — 값은 전부 SO 에서 온다(새 값 0). static void ApplySky(WLIslandLookSettings cfg) { // 하늘 그 자체(스카이박스·안개·반사)는 언제나 우리 값으로. if (cfg.fogOff != 0) RenderSettings.fog = false; if (cfg.skybox != null) RenderSettings.skybox = cfg.skybox; RenderSettings.defaultReflectionMode = DefaultReflectionMode.Skybox; // 🔴 앰비언트(무드)는 `WLReferenceLook`(814t 아레나 룩)의 것이다. // 던전 씬에는 `WL_ReferenceLook` 이 **씬에 들어 있어** 우리보다 먼저 켜진다. // 그때 여기서 앰비언트를 덮어쓰면 던전의 아레나 무드가 죽는다 → 이미 걸렸으면 손대지 않는다. // (섬에서는 우리가 먼저 → 이 값을 깔고, 그 위에 아레나 무드가 얹힌다 = 던전과 같은 결과) if (!WL.Look.Arena.WLReferenceLook.IsApplied) { RenderSettings.ambientMode = AmbientMode.Skybox; RenderSettings.ambientSkyColor = cfg.ambientSky; RenderSettings.ambientEquatorColor = cfg.ambientEquator; RenderSettings.ambientGroundColor = cfg.ambientGround; RenderSettings.ambientLight = cfg.ambientSky; RenderSettings.ambientIntensity = cfg.ambientIntensity; } Applies++; LastLog = "held(" + HeldBy + ") → " + Describe(); } public static string Describe() { return "amb=" + RenderSettings.ambientMode + " light=(" + RenderSettings.ambientLight.r.ToString("F3") + "," + RenderSettings.ambientLight.g.ToString("F3") + "," + RenderSettings.ambientLight.b.ToString("F3") + ")" + " skybox=" + (RenderSettings.skybox ? RenderSettings.skybox.name : "없음") + " fog=" + RenderSettings.fog + " refl=" + RenderSettings.defaultReflectionMode + " sun=" + (RenderSettings.sun ? RenderSettings.sun.name : "없음"); } static void EnsureRunner() { if (s_runner != null) return; var go = new GameObject(RunnerName); go.hideFlags = HideFlags.DontSave; Object.DontDestroyOnLoad(go); s_runner = go.AddComponent(); } } /// 코루틴 한 개를 태우기 위한 빈 그릇. public sealed class WLSkyScopeRunner : MonoBehaviour { } }