369 lines
18 KiB
C#
369 lines
18 KiB
C#
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
// WLIslandLook.cs — **실제 게임이 로드하는 섬 씬**(FarmingIsland/Scenes/Level01)에
|
||
|
|
// 데모 룩과 풀밭을 런타임으로 얹는다 (WL-816e · #816)
|
||
|
|
//
|
||
|
|
// ■ 왜 필요한가(발주서 §0)
|
||
|
|
// 816a 는 복사본 씬 `Assets/WL/Look/Farm/Scenes/WL_FarmLook.unity` 에만 룩을 저장했다.
|
||
|
|
// 게임은 `Level01` 을 로드하므로 PD 캡처의 섬은 여전히 **원본 색**(쨍한 파란 하늘·원색 초록)이었다.
|
||
|
|
// → 원본 씬을 고치지 않고(`Assets/FarmingIsland/**` 0줄) **로드 직후** 얹는다.
|
||
|
|
//
|
||
|
|
// ■ 걸리는 지점
|
||
|
|
// `[RuntimeInitializeOnLoadMethod]` + `SceneManager.sceneLoaded` — 기존 파일 **0줄 수정**.
|
||
|
|
// (816d 의 `WLIslandBridge` 를 고치지 않는다 = 다른 worktree 와 충돌 0)
|
||
|
|
//
|
||
|
|
// ■ C8 되돌리기 — `WLIslandLookSettings.enabled_ = 0` → 아무것도 얹지 않는다.
|
||
|
|
// 씬·머티리얼·프리팹 **파일을 쓰지 않으므로** 복원 작업 자체가 없다.
|
||
|
|
//
|
||
|
|
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 팝업).
|
||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
using System.Collections;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using UnityEngine;
|
||
|
|
using UnityEngine.Rendering;
|
||
|
|
using UnityEngine.SceneManagement;
|
||
|
|
using CryingSnow.FarmingIsland;
|
||
|
|
using FIIsland = CryingSnow.FarmingIsland.Island;
|
||
|
|
using FISoil = CryingSnow.FarmingIsland.Soil;
|
||
|
|
|
||
|
|
namespace WL.Look.Farm
|
||
|
|
{
|
||
|
|
/// <summary>섬 씬에 룩·풀밭을 얹는 정적 진입점.</summary>
|
||
|
|
public static class WLIslandLook
|
||
|
|
{
|
||
|
|
public const string RunnerName = "~WLIslandLookRunner";
|
||
|
|
public const string ReferenceLookName = "WL_ReferenceLook";
|
||
|
|
|
||
|
|
// ── 진단 ────────────────────────────────────────────────────────
|
||
|
|
public static int SwappedRenderers, SwappedSlots, Rescans, LightingApplied, GrassRebuilds;
|
||
|
|
public static bool LookApplied, ReferenceLookApplied, GrassSpawned;
|
||
|
|
public static string LastLog = "";
|
||
|
|
|
||
|
|
static bool s_installed;
|
||
|
|
static WLIslandLookRunner s_runner;
|
||
|
|
static readonly HashSet<Renderer> s_done = new HashSet<Renderer>();
|
||
|
|
static readonly HashSet<FIIsland> s_hooked = new HashSet<FIIsland>();
|
||
|
|
|
||
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||
|
|
public static void Install()
|
||
|
|
{
|
||
|
|
if (s_installed) return;
|
||
|
|
var cfg = WLIslandLookSettings.Instance;
|
||
|
|
if (cfg == null || cfg.enabled_ == 0) return;
|
||
|
|
s_installed = true;
|
||
|
|
|
||
|
|
SceneManager.sceneLoaded += OnSceneLoaded;
|
||
|
|
SceneManager.sceneUnloaded += OnSceneUnloaded;
|
||
|
|
|
||
|
|
for (int i = 0; i < SceneManager.sceneCount; i++)
|
||
|
|
{
|
||
|
|
var sc = SceneManager.GetSceneAt(i);
|
||
|
|
if (sc.isLoaded) OnSceneLoaded(sc, LoadSceneMode.Additive);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>에디터 프로브가 Play 중에 다시 걸 때 쓴다.</summary>
|
||
|
|
public static void ForceApply(Scene scene)
|
||
|
|
{
|
||
|
|
var cfg = WLIslandLookSettings.Instance;
|
||
|
|
if (cfg == null) return;
|
||
|
|
s_done.Clear();
|
||
|
|
EnsureRunner();
|
||
|
|
s_runner.StartCoroutine(Co_Apply(cfg, scene));
|
||
|
|
}
|
||
|
|
|
||
|
|
static void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||
|
|
{
|
||
|
|
var cfg = WLIslandLookSettings.Instance;
|
||
|
|
if (cfg == null || cfg.enabled_ == 0) return;
|
||
|
|
if (!IsIslandScene(cfg, scene)) return;
|
||
|
|
EnsureRunner();
|
||
|
|
s_runner.StartCoroutine(Co_Apply(cfg, scene));
|
||
|
|
}
|
||
|
|
|
||
|
|
static void OnSceneUnloaded(Scene scene)
|
||
|
|
{
|
||
|
|
s_done.Clear();
|
||
|
|
s_hooked.Clear();
|
||
|
|
LookApplied = false; GrassSpawned = false; ReferenceLookApplied = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
static bool IsIslandScene(WLIslandLookSettings cfg, Scene scene)
|
||
|
|
{
|
||
|
|
if (!scene.IsValid() || !scene.isLoaded) return false;
|
||
|
|
if (cfg.islandSceneNames != null)
|
||
|
|
for (int i = 0; i < cfg.islandSceneNames.Length; i++)
|
||
|
|
if (scene.name == cfg.islandSceneNames[i]) return true;
|
||
|
|
|
||
|
|
// 이름을 몰라도 IslandManager 가 있으면 섬이다.
|
||
|
|
var roots = scene.GetRootGameObjects();
|
||
|
|
for (int i = 0; i < roots.Length; i++)
|
||
|
|
if (roots[i].GetComponentInChildren<IslandManager>(true) != null) return true;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
static void EnsureRunner()
|
||
|
|
{
|
||
|
|
if (s_runner != null) return;
|
||
|
|
var go = new GameObject(RunnerName);
|
||
|
|
go.hideFlags = HideFlags.DontSave;
|
||
|
|
Object.DontDestroyOnLoad(go);
|
||
|
|
s_runner = go.AddComponent<WLIslandLookRunner>();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────────
|
||
|
|
// 본체
|
||
|
|
// ─────────────────────────────────────────────────────────────────
|
||
|
|
static IEnumerator Co_Apply(WLIslandLookSettings cfg, Scene scene)
|
||
|
|
{
|
||
|
|
yield return null; // FI 매니저들의 Awake 를 기다린다
|
||
|
|
|
||
|
|
if (cfg.disableFiGraphicsManager != 0) DisableGraphicsManager(scene);
|
||
|
|
|
||
|
|
if (cfg.applyLook != 0)
|
||
|
|
{
|
||
|
|
SwapMaterials(cfg, scene);
|
||
|
|
LookApplied = true;
|
||
|
|
}
|
||
|
|
if (cfg.applyLighting != 0) ApplyLighting(cfg, scene);
|
||
|
|
if (cfg.applyLook != 0 && cfg.applyReferenceLook != 0) ApplyReferenceLook(cfg, scene);
|
||
|
|
|
||
|
|
if (cfg.grassEnabled != 0) SpawnGrass(cfg, scene);
|
||
|
|
|
||
|
|
HookIslands(cfg, scene);
|
||
|
|
|
||
|
|
cfg.Log("섬 룩 적용 — 렌더러 " + SwappedRenderers + " · 슬롯 " + SwappedSlots
|
||
|
|
+ " · 조명 " + (cfg.applyLighting != 0 ? "데모" : "원본")
|
||
|
|
+ " · ReferenceLook " + (ReferenceLookApplied ? "on" : "off")
|
||
|
|
+ " · 풀 " + WLIslandGrass.LastLog);
|
||
|
|
|
||
|
|
// 늦게 생기는 오브젝트(상인·작물·아이템)까지 다시 훑는다
|
||
|
|
if (cfg.rescanAtSeconds != null)
|
||
|
|
{
|
||
|
|
float prev = 0f;
|
||
|
|
for (int i = 0; i < cfg.rescanAtSeconds.Length; i++)
|
||
|
|
{
|
||
|
|
float w = cfg.rescanAtSeconds[i] - prev;
|
||
|
|
prev = cfg.rescanAtSeconds[i];
|
||
|
|
if (w > 0f) yield return new WaitForSeconds(w);
|
||
|
|
if (!scene.isLoaded) yield break;
|
||
|
|
if (cfg.applyLook != 0) { SwapMaterials(cfg, scene); Rescans++; }
|
||
|
|
HookIslands(cfg, scene);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 섬 확장 감시 — 새로 열린 타일에도 자동으로 풀이 깔린다
|
||
|
|
if (cfg.rebuildPollSeconds > 0f)
|
||
|
|
{
|
||
|
|
int last = CountUnlocked(scene);
|
||
|
|
while (scene.isLoaded)
|
||
|
|
{
|
||
|
|
yield return new WaitForSeconds(cfg.rebuildPollSeconds);
|
||
|
|
if (!scene.isLoaded) yield break;
|
||
|
|
HookIslands(cfg, scene);
|
||
|
|
int now = CountUnlocked(scene);
|
||
|
|
if (now != last)
|
||
|
|
{
|
||
|
|
last = now;
|
||
|
|
if (cfg.applyLook != 0) SwapMaterials(cfg, scene);
|
||
|
|
RequestGrassRebuild(cfg);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static int CountUnlocked(Scene scene)
|
||
|
|
{
|
||
|
|
var islands = Object.FindObjectsByType<FIIsland>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||
|
|
int n = 0;
|
||
|
|
for (int i = 0; i < islands.Length; i++)
|
||
|
|
if (islands[i] != null && islands[i].IsUnlocked && islands[i].gameObject.activeInHierarchy) n++;
|
||
|
|
return n;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>FI `FIIsland.OnActivated`(public event)에 붙는다 — FI 코드 0줄.</summary>
|
||
|
|
static void HookIslands(WLIslandLookSettings cfg, Scene scene)
|
||
|
|
{
|
||
|
|
var islands = Object.FindObjectsByType<FIIsland>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||
|
|
for (int i = 0; i < islands.Length; i++)
|
||
|
|
{
|
||
|
|
var isl = islands[i];
|
||
|
|
if (isl == null || isl.gameObject.scene != scene) continue;
|
||
|
|
if (s_hooked.Contains(isl)) continue;
|
||
|
|
s_hooked.Add(isl);
|
||
|
|
isl.OnActivated += () => OnIslandActivated(cfg, isl);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static void OnIslandActivated(WLIslandLookSettings cfg, FIIsland isl)
|
||
|
|
{
|
||
|
|
if (cfg == null || cfg.enabled_ == 0) return;
|
||
|
|
EnsureRunner();
|
||
|
|
s_runner.StartCoroutine(Co_AfterActivate(cfg, isl));
|
||
|
|
}
|
||
|
|
|
||
|
|
static IEnumerator Co_AfterActivate(WLIslandLookSettings cfg, FIIsland isl)
|
||
|
|
{
|
||
|
|
// 확장 애니메이션(DOTween DOScale 1초) + 소품 배치가 끝나기를 기다린다
|
||
|
|
yield return new WaitForSeconds(cfg.rebuildDelaySeconds);
|
||
|
|
if (isl == null) yield break;
|
||
|
|
if (cfg.applyLook != 0) SwapMaterials(cfg, isl.gameObject.scene);
|
||
|
|
RequestGrassRebuild(cfg);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void RequestGrassRebuild(WLIslandLookSettings cfg)
|
||
|
|
{
|
||
|
|
if (cfg.grassEnabled == 0) return;
|
||
|
|
var g = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
|
||
|
|
if (g == null) return;
|
||
|
|
g.Rebuild();
|
||
|
|
GrassRebuilds++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────────
|
||
|
|
// ① 머티리얼 — 원본을 **읽기만** 하고 렌더러의 sharedMaterials 만 바꾼다
|
||
|
|
// ─────────────────────────────────────────────────────────────────
|
||
|
|
public static void SwapMaterials(WLIslandLookSettings cfg, Scene scene)
|
||
|
|
{
|
||
|
|
if (cfg.materialRemap == null || cfg.materialRemap.Length == 0) return;
|
||
|
|
|
||
|
|
var rends = Object.FindObjectsByType<Renderer>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||
|
|
for (int i = 0; i < rends.Length; i++)
|
||
|
|
{
|
||
|
|
var r = rends[i];
|
||
|
|
if (r == null || r.gameObject.scene != scene) continue;
|
||
|
|
if (s_done.Contains(r)) continue;
|
||
|
|
if (r is ParticleSystemRenderer) continue;
|
||
|
|
|
||
|
|
// 🔴 농사 타일은 코드가 `material.color` 로 직접 색을 칠한다(Soil.Initialize).
|
||
|
|
// Toon 셰이더에는 그 프로퍼티가 없어 갈아끼우면 밭 색이 죽는다 → 원본 유지.
|
||
|
|
if (cfg.skipSoilRenderers != 0 && r.GetComponent<FISoil>() != null) { s_done.Add(r); continue; }
|
||
|
|
|
||
|
|
bool isIslandBody = r.GetComponent<FIIsland>() != null;
|
||
|
|
|
||
|
|
var mats = r.sharedMaterials;
|
||
|
|
bool changed = false;
|
||
|
|
for (int m = 0; m < mats.Length; m++)
|
||
|
|
{
|
||
|
|
var src = mats[m];
|
||
|
|
if (src == null) continue;
|
||
|
|
Material dst = null;
|
||
|
|
|
||
|
|
if (isIslandBody && cfg.islandTopMaterial != null) dst = cfg.islandTopMaterial;
|
||
|
|
else dst = Lookup(cfg, src);
|
||
|
|
|
||
|
|
if (dst == null || dst == src) continue;
|
||
|
|
mats[m] = dst; changed = true; SwappedSlots++;
|
||
|
|
}
|
||
|
|
if (changed) { r.sharedMaterials = mats; SwappedRenderers++; }
|
||
|
|
s_done.Add(r);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static Material Lookup(WLIslandLookSettings cfg, Material src)
|
||
|
|
{
|
||
|
|
for (int i = 0; i < cfg.materialRemap.Length; i++)
|
||
|
|
{
|
||
|
|
var e = cfg.materialRemap[i];
|
||
|
|
if (e == null || e.enabled_ == 0 || e.from == null || e.to == null) continue;
|
||
|
|
if (e.from == src) return e.to;
|
||
|
|
// 런타임 인스턴스("Palette (Instance)")도 잡는다
|
||
|
|
if (src.name.StartsWith(e.from.name) && src.shader == e.from.shader) return e.to;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────────
|
||
|
|
// ② 조명 · 앰비언트 · 스카이박스 (816a 실측 = 데모/아레나 값)
|
||
|
|
// ─────────────────────────────────────────────────────────────────
|
||
|
|
public static void ApplyLighting(WLIslandLookSettings cfg, Scene scene)
|
||
|
|
{
|
||
|
|
RenderSettings.ambientMode = AmbientMode.Skybox;
|
||
|
|
RenderSettings.ambientSkyColor = cfg.ambientSky;
|
||
|
|
RenderSettings.ambientEquatorColor = cfg.ambientEquator;
|
||
|
|
RenderSettings.ambientGroundColor = cfg.ambientGround;
|
||
|
|
RenderSettings.ambientLight = cfg.ambientSky;
|
||
|
|
RenderSettings.ambientIntensity = cfg.ambientIntensity;
|
||
|
|
if (cfg.fogOff != 0) RenderSettings.fog = false;
|
||
|
|
if (cfg.skybox != null) RenderSettings.skybox = cfg.skybox;
|
||
|
|
RenderSettings.defaultReflectionMode = DefaultReflectionMode.Skybox;
|
||
|
|
|
||
|
|
var lights = Object.FindObjectsByType<Light>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||
|
|
for (int i = 0; i < lights.Length; i++)
|
||
|
|
{
|
||
|
|
var l = lights[i];
|
||
|
|
if (l == null || l.gameObject.scene != scene) continue;
|
||
|
|
if (l.type != LightType.Directional) continue;
|
||
|
|
l.color = cfg.dirLightColor;
|
||
|
|
l.intensity = cfg.dirLightIntensity;
|
||
|
|
if (cfg.dirLightSoftShadows != 0 && l.shadows != LightShadows.None) l.shadows = LightShadows.Soft;
|
||
|
|
RenderSettings.sun = l;
|
||
|
|
LightingApplied++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────────
|
||
|
|
// ③ 아레나와 같은 외곽선·대비·무드 (씬 비의존 — 816a 실측)
|
||
|
|
// ─────────────────────────────────────────────────────────────────
|
||
|
|
public static void ApplyReferenceLook(WLIslandLookSettings cfg, Scene scene)
|
||
|
|
{
|
||
|
|
var existing = Object.FindFirstObjectByType<WL.Look.Arena.WLReferenceLook>(FindObjectsInactive.Include);
|
||
|
|
if (existing != null)
|
||
|
|
{
|
||
|
|
if (!existing.gameObject.activeSelf) existing.gameObject.SetActive(true);
|
||
|
|
ReferenceLookApplied = true;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
var go = new GameObject(ReferenceLookName);
|
||
|
|
SceneManager.MoveGameObjectToScene(go, scene);
|
||
|
|
go.AddComponent<WL.Look.Arena.WLReferenceLook>(); // OnEnable 이 적용한다
|
||
|
|
ReferenceLookApplied = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────────
|
||
|
|
// ④ 풀밭
|
||
|
|
// ─────────────────────────────────────────────────────────────────
|
||
|
|
public static void SpawnGrass(WLIslandLookSettings cfg, Scene scene)
|
||
|
|
{
|
||
|
|
var g = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
|
||
|
|
if (g == null)
|
||
|
|
{
|
||
|
|
var go = new GameObject(WLIslandGrass.ObjectName);
|
||
|
|
SceneManager.MoveGameObjectToScene(go, scene);
|
||
|
|
go.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
|
||
|
|
g = go.AddComponent<WLIslandGrass>(); // OnEnable 이 만든다
|
||
|
|
g.cfg = cfg;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
g.cfg = cfg;
|
||
|
|
g.Rebuild();
|
||
|
|
}
|
||
|
|
GrassSpawned = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────────
|
||
|
|
// ⑤ FI GraphicsManager — Play 중 FI 의 URP 에셋 **파일**을 고친다(816a 실측)
|
||
|
|
// 우리 활성 URP 가 아니라 화면 영향 0. `Assets/FarmingIsland/**` 0줄을 지키려면 꺼야 한다.
|
||
|
|
// ─────────────────────────────────────────────────────────────────
|
||
|
|
static void DisableGraphicsManager(Scene scene)
|
||
|
|
{
|
||
|
|
var roots = scene.GetRootGameObjects();
|
||
|
|
for (int i = 0; i < roots.Length; i++)
|
||
|
|
{
|
||
|
|
var comps = roots[i].GetComponentsInChildren<MonoBehaviour>(true);
|
||
|
|
for (int c = 0; c < comps.Length; c++)
|
||
|
|
{
|
||
|
|
if (comps[c] == null) continue;
|
||
|
|
if (comps[c].GetType().Name != "GraphicsManager") continue;
|
||
|
|
comps[c].enabled = false;
|
||
|
|
comps[c].gameObject.SetActive(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>코루틴 숙주. 씬을 넘어 살아남는다.</summary>
|
||
|
|
public sealed class WLIslandLookRunner : MonoBehaviour { }
|
||
|
|
}
|