231 lines
12 KiB
C#
231 lines
12 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLIslandLookSettings.cs — 실제 게임 섬(FarmingIsland Level01)에 **런타임으로** 얹는
|
||
// ① 데모 룩(머티리얼·조명) ② 데모 풀밭의 단일 출처 (WL-816e · #816)
|
||
//
|
||
// PD 지시(2026-09-13) 「섬 배경이 단순 타일로만 되어있어. 기본 타일의 디자인을
|
||
// 데모씬에서 사용한 풀밭처럼 꾸미고, 전체적인 비주얼을 맞춰줘」
|
||
//
|
||
// ■ 왜 런타임인가 — 816a 는 **복사본 씬**(WL_FarmLook)에만 룩을 저장했다.
|
||
// 게임이 실제로 로드하는 것은 `Assets/FarmingIsland/Scenes/Level01.unity` 라
|
||
// 그 씬은 아직 원본 색이다. 원본 씬을 고치지 않고(FI 0줄) 로드 직후 얹는다.
|
||
//
|
||
// ■ C8 되돌리기 — `enabled_ = 0` 이면 아무것도 얹지 않는다 = 지금 상태 100 %.
|
||
// 씬 파일·FI 에셋을 한 글자도 쓰지 않으므로 되돌리기에 복원 작업이 필요 없다.
|
||
//
|
||
// ■ C45 — 밀도·크기·제외 여백 같은 값은 전부 이 에셋에 있다(코드 상수 0).
|
||
//
|
||
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 팝업).
|
||
// 🔴 어셈블리 주의: Assets/WL/Look/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
using System;
|
||
using UnityEngine;
|
||
|
||
namespace WL.Look.Farm
|
||
{
|
||
/// <summary>원본 머티리얼 → 데모 톤 복사본. 한 줄 = 머티리얼 하나.</summary>
|
||
[Serializable]
|
||
public sealed class WLMatRemap
|
||
{
|
||
[Tooltip("0 이면 이 줄은 없는 것으로 친다.")]
|
||
public int enabled_ = 1;
|
||
|
||
[Tooltip("FarmingIsland 원본 머티리얼(한 글자도 안 고친다 · 참조만).")]
|
||
public Material from;
|
||
|
||
[Tooltip("816e/816a 가 만든 데모 톤 복사본.")]
|
||
public Material to;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 메시 하나의 **윗면(걸어다니는 면)** 을 XZ 격자 비트맵으로 구운 것.
|
||
/// FI 메시는 `isReadable = 0` 이라 런타임에 정점을 못 읽는다 → 에디터에서 구워 둔다.
|
||
/// (FI 에셋의 임포트 설정을 고치지 않기 위한 선택 — `Assets/FarmingIsland/**` 0줄)
|
||
/// </summary>
|
||
[Serializable]
|
||
public sealed class WLTileMask
|
||
{
|
||
public string meshName = "";
|
||
[Tooltip("격자 해상도(res × res).")] public int res = 64;
|
||
[Tooltip("마스크가 덮는 반폭(m). 섬 타일 8 m → 4.")] public float half = 4f;
|
||
[Tooltip("res*res 비트를 담은 패킹 배열.")] public byte[] bits;
|
||
|
||
public bool At(float lx, float lz)
|
||
{
|
||
if (bits == null || res <= 0) return false;
|
||
int cx = (int)((lx + half) / (half * 2f) * res);
|
||
int cz = (int)((lz + half) / (half * 2f) * res);
|
||
if (cx < 0 || cz < 0 || cx >= res || cz >= res) return false;
|
||
int i = cz * res + cx;
|
||
int b = i >> 3;
|
||
if (b >= bits.Length) return false;
|
||
return (bits[b] & (1 << (i & 7))) != 0;
|
||
}
|
||
|
||
public void Set(int cx, int cz, bool v)
|
||
{
|
||
if (bits == null) bits = new byte[(res * res + 7) / 8];
|
||
int i = cz * res + cx;
|
||
int b = i >> 3;
|
||
if (b >= bits.Length) return;
|
||
if (v) bits[b] |= (byte)(1 << (i & 7));
|
||
else bits[b] &= (byte)~(1 << (i & 7));
|
||
}
|
||
}
|
||
|
||
/// <summary>흩뿌릴 것 하나(풀 · 꽃 · 자갈). 데모 `InstancingSettings` 와 같은 값 구성.</summary>
|
||
[Serializable]
|
||
public sealed class WLScatterDef
|
||
{
|
||
public int enabled_ = 1;
|
||
[Tooltip("이름표(로그용).")] public string label = "풀";
|
||
public Mesh mesh;
|
||
public Material material;
|
||
[Tooltip("같은 층 안에서의 가중치. 데모 = 풀 100 : 꽃 1.")] public float probability = 100f;
|
||
[Tooltip("메시 배율. 데모 = 0.35.")] public float scale = 0.35f;
|
||
[Tooltip("법선 방향으로 띄우는 거리(m). 데모 = 0.04.")] public float normalOffset = 0.04f;
|
||
[Tooltip("이 층의 밀도(개/m 한 변). 실제 밀도 = 값². 데모 1층 = 2.5(6.25개/㎡).")]
|
||
public float density = 2.5f;
|
||
}
|
||
|
||
[CreateAssetMenu(fileName = "WLIslandLookSettings", menuName = "WL/Island Look Settings", order = 39)]
|
||
public sealed class WLIslandLookSettings : ScriptableObject
|
||
{
|
||
public const string ResourcesPath = "WL/WLIslandLookSettings";
|
||
|
||
static WLIslandLookSettings s_inst;
|
||
static bool s_tried;
|
||
|
||
public static WLIslandLookSettings Instance
|
||
{
|
||
get
|
||
{
|
||
if (s_inst == null && !s_tried) { s_tried = true; s_inst = Resources.Load<WLIslandLookSettings>(ResourcesPath); }
|
||
return s_inst;
|
||
}
|
||
}
|
||
|
||
public static void Invalidate() { s_inst = null; s_tried = false; }
|
||
|
||
// ───────────────────────────────── 마스터
|
||
[Header("되돌리기 (C8)")]
|
||
[Tooltip("0 이면 섬에 아무것도 얹지 않는다 = 지금(원본) 상태 100 %.")]
|
||
public int enabled_ = 1;
|
||
|
||
[Tooltip("1 이면 적용 결과를 Console 에 남긴다.")]
|
||
public int verboseLog = 1;
|
||
|
||
// ───────────────────────────────── §0 룩
|
||
[Header("§0 — 실게임 섬(Level01)에 데모 룩 적용")]
|
||
[Tooltip("1 이면 섬 씬 로드 직후 머티리얼을 데모 톤 복사본으로 바꾼다(씬 파일 무변경 · 런타임만).")]
|
||
public int applyLook = 1;
|
||
|
||
[Tooltip("1 이면 조명·앰비언트·스카이박스도 데모/아레나 값으로 맞춘다.")]
|
||
public int applyLighting = 1;
|
||
|
||
[Tooltip("1 이면 아레나와 같은 `WL_ReferenceLook`(외곽선·대비·무드)을 섬에도 얹는다.")]
|
||
public int applyReferenceLook = 1;
|
||
|
||
[Tooltip("머티리얼 교체 표. 한 줄 = 원본 → 복사본.")]
|
||
public WLMatRemap[] materialRemap = new WLMatRemap[0];
|
||
|
||
[Tooltip("섬 타일 윗면(걸어다니는 면)에 쓸 머티리얼. 비우면 위 표의 Island01 줄을 쓴다. " +
|
||
"체크무늬 두 초록을 데모 잔디색 한 가지로 합친 것.")]
|
||
public Material islandTopMaterial;
|
||
|
||
[Tooltip("1 이면 농사 타일(Soil)은 원본 머티리얼 그대로 둔다(색을 코드가 직접 칠하는 곳이라 건드리면 안 된다).")]
|
||
public int skipSoilRenderers = 1;
|
||
|
||
[Header("§0 — 조명 값 (816a 실측 = 데모/아레나와 동일)")]
|
||
public Color ambientSky = new Color(0.212f, 0.227f, 0.259f, 1f);
|
||
public Color ambientEquator = new Color(0.114f, 0.125f, 0.133f, 1f);
|
||
public Color ambientGround = new Color(0.047f, 0.043f, 0.035f, 1f);
|
||
public float ambientIntensity = 1f;
|
||
[Tooltip("빌트인 Default-Skybox. 비우면 스카이박스는 안 건드린다.")]
|
||
public Material skybox;
|
||
public Color dirLightColor = Color.white;
|
||
public float dirLightIntensity = 1f;
|
||
[Tooltip("1 = Soft(데모) · 0 = 원본(Hard) 유지.")]
|
||
public int dirLightSoftShadows = 1;
|
||
[Tooltip("1 이면 안개를 끈다(데모 = off).")]
|
||
public int fogOff = 1;
|
||
|
||
[Tooltip("1 이면 FI `GraphicsManager` 를 끈다. 이 컴포넌트는 Play 중 FI 의 URP 에셋 파일에 " +
|
||
"renderScale 을 **직접 써 넣어** `Assets/FarmingIsland/**` 를 더럽힌다(816a 실측). " +
|
||
"우리 프로젝트의 활성 URP 에셋이 아니라 화면에는 영향 0.")]
|
||
public int disableFiGraphicsManager = 1;
|
||
|
||
[Tooltip("런타임에 뒤늦게 생기는 오브젝트(상인·작물·아이템)까지 잡기 위해 다시 훑는 시각(초).")]
|
||
public float[] rescanAtSeconds = new float[] { 0.5f, 2f, 5f };
|
||
|
||
// ───────────────────────────────── §1 풀밭
|
||
[Header("§1 — 섬 타일 위 풀밭 (데모와 같은 인스턴싱)")]
|
||
[Tooltip("0 이면 풀을 깔지 않는다.")]
|
||
public int grassEnabled = 1;
|
||
|
||
[Tooltip("흩뿌릴 것들. 같은 density 를 가진 줄끼리 한 층으로 묶여 확률 분배된다(데모와 같은 방식).")]
|
||
public WLScatterDef[] scatter = new WLScatterDef[0];
|
||
|
||
[Tooltip("격자 흔들기(격자 칸 단위). 데모 = 0.5.")]
|
||
[Range(0f, 0.5f)] public float positionVariance = 0.5f;
|
||
|
||
[Tooltip("크기 흔들기. 데모 = 0.2 → ×0.8~1.2.")]
|
||
[Range(0f, 0.9f)] public float scaleVariance = 0.2f;
|
||
|
||
[Tooltip("1 이면 풀 하나하나를 Y 축으로 랜덤 회전한다(데모는 회전 0 = 빌보드라 불필요). " +
|
||
"빌보드 셰이더에는 영향이 없다.")]
|
||
public int randomYaw = 0;
|
||
|
||
[Header("§1 — 풀을 깔지 않는 곳 (제외 규칙)")]
|
||
[Tooltip("섬 타일 가장자리에서 이만큼(m) 안쪽까지만 깐다. 물 위로 삐져나오지 않게.")]
|
||
public float edgeInset = 0.15f;
|
||
|
||
[Tooltip("길(Road 메시)에서 이만큼(m) 떨어뜨린다.")]
|
||
public float roadMargin = 0.15f;
|
||
|
||
[Tooltip("농사 타일(Farm 트리거 · Soil 1×1)에서 이만큼(m) 떨어뜨린다.")]
|
||
public float farmMargin = 0.35f;
|
||
|
||
[Tooltip("건물·소품 콜라이더에서 이만큼(m) 떨어뜨린다.")]
|
||
public float propMargin = 0.05f;
|
||
|
||
[Tooltip("이 크기(m) 이상인 콜라이더는 섬 본체로 보고 제외 대상에서 뺀다.")]
|
||
public float propMaxSize = 7.5f;
|
||
|
||
[Header("§1 — 모바일 예산")]
|
||
[Tooltip("인스턴스 총개수 상한. 넘으면 밀도를 자동으로 낮춘다(0 = 무제한).")]
|
||
public int maxInstances = 40000;
|
||
|
||
[Tooltip("섬이 확장되면 다시 깐다 — 이 주기(초)로 잠긴 섬이 열렸는지 본다. 0 이면 감시 안 함.")]
|
||
public float rebuildPollSeconds = 1f;
|
||
|
||
[Tooltip("확장 애니메이션(DOTween 1초)이 끝나기를 기다리는 초.")]
|
||
public float rebuildDelaySeconds = 1.4f;
|
||
|
||
// ───────────────────────────────── 구운 마스크
|
||
[Header("§1 — 구운 윗면 마스크 (에디터에서 생성 · 손대지 말 것)")]
|
||
public WLTileMask[] tileMasks = new WLTileMask[0];
|
||
public WLTileMask[] roadMasks = new WLTileMask[0];
|
||
|
||
// ───────────────────────────────── 섬 씬 판별
|
||
[Header("어느 씬에 적용하는가")]
|
||
[Tooltip("이 이름의 씬에 적용한다(비워도 IslandManager 가 있으면 섬으로 본다).")]
|
||
public string[] islandSceneNames = new string[] { "Level01", "WL_FarmLook" };
|
||
|
||
public WLTileMask FindTileMask(string meshName) { return Find(tileMasks, meshName); }
|
||
public WLTileMask FindRoadMask(string meshName) { return Find(roadMasks, meshName); }
|
||
|
||
static WLTileMask Find(WLTileMask[] a, string n)
|
||
{
|
||
if (a == null || string.IsNullOrEmpty(n)) return null;
|
||
// `MeshFilter.mesh` 로 읽으면 이름 뒤에 " Instance" 가 붙는다 — 떼고 비교한다.
|
||
int k = n.IndexOf(" Instance");
|
||
if (k > 0) n = n.Substring(0, k);
|
||
for (int i = 0; i < a.Length; i++) if (a[i] != null && a[i].meshName == n) return a[i];
|
||
return null;
|
||
}
|
||
|
||
public void Log(string msg) { if (verboseLog != 0) Debug.Log("[WL-816e] " + msg); }
|
||
}
|
||
}
|