583 lines
29 KiB
C#
583 lines
29 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLIslandGrass.cs — 섬 타일 **윗면**에 데모와 똑같은 풀·꽃·자갈을 깐다 (WL-816e · #816)
|
||
//
|
||
// ■ 왜 이렇게 하나 (816a 의 「터레인이 없어 구조상 불가」를 뒤집은 근거)
|
||
// 데모의 풀은 Terrain 기능이 아니라 `Environment.Instancing.InstancesBehaviour`
|
||
// (추상 클래스)가 GPU 인스턴싱으로 그리는 것이다. Terrain 은 **점을 어디에 찍을지**
|
||
// 알려 주는 역할만 한다(`TerrainInstancesBehaviour.GetInstanceData`).
|
||
// → 같은 추상 클래스를 상속해 **점을 섬 타일 윗면에서 뽑으면** 지형 없이 같은 그림이 나온다.
|
||
// 데모 에셋(`Assets/3DPixelArtEnvironment/**`)은 **한 글자도 고치지 않는다**(상속만).
|
||
//
|
||
// ■ 비용 — 그리기는 `Graphics.DrawMeshInstancedIndirect` 1회/설정.
|
||
// 풀·꽃·자갈 3종 = **드로우콜 3개**(인스턴스 수와 무관). 그림자 캐스팅 off.
|
||
//
|
||
// ■ 안전 — 콜라이더 0 · NavMeshSurface 가 굽는 대상 0 · 레이캐스트 대상 0.
|
||
// 걷기·밭 갈기·수확·건설에 물리적으로 관여할 수 없다.
|
||
//
|
||
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다.
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using Environment.Instancing;
|
||
using CryingSnow.FarmingIsland;
|
||
using FIIsland = CryingSnow.FarmingIsland.Island;
|
||
using FIFarm = CryingSnow.FarmingIsland.Farm;
|
||
using FISoil = CryingSnow.FarmingIsland.Soil;
|
||
|
||
namespace WL.Look.Farm
|
||
{
|
||
[DisallowMultipleComponent]
|
||
public sealed class WLIslandGrass : InstancesBehaviour
|
||
{
|
||
public const string ObjectName = "~WL_IslandGrass";
|
||
|
||
// ── 진단(프로브·보고가 읽는다) ─────────────────────────────────
|
||
public static int Instances, Tiles, Rebuilds, Excluded, DrawnConfigs;
|
||
public static long Triangles;
|
||
public static float UsedDensity;
|
||
public static string LastLog = "";
|
||
|
||
// 816o — 구름 경계 배치 진단
|
||
public static int Candidates, CloudRefreshes, CloudExcluded, EdgeCells, EdgeDilate, EdgeSamples;
|
||
public static float EdgeFraction, EdgeCellSize, EdgeSampleCell, BakeMs, FilterMs;
|
||
public static bool CloudEdgeOn;
|
||
public static string CloudWhy = "";
|
||
|
||
public WLIslandLookSettings cfg;
|
||
|
||
Bounds _bounds = new Bounds(Vector3.zero, Vector3.one * 16f);
|
||
readonly List<Rect> _blockers = new List<Rect>(128);
|
||
|
||
// ── 816o 캐시 — 한 번 뽑은 후보를 들고 있다가, 구름이 흐르면 「고르기」만 다시 한다 ──
|
||
struct Cand { public float x, z, y; public int def; public float sc; public float yaw; }
|
||
Cand[] _cand;
|
||
int _candCount;
|
||
InstancingSettings[] _cset;
|
||
WLScatterDef[] _cdef;
|
||
long[] _ctri;
|
||
bool _cacheValid;
|
||
float _cacheY;
|
||
bool _fastRefresh;
|
||
readonly WLCloudShadowField _field = new WLCloudShadowField();
|
||
|
||
public override Bounds CalculateInstancesBounds() { return _bounds; }
|
||
|
||
/// <summary>다시 깐다(섬이 확장됐을 때). 멱등 — 몇 번 불러도 안전.</summary>
|
||
public void Rebuild()
|
||
{
|
||
_cacheValid = false;
|
||
_fastRefresh = false;
|
||
if (!isActiveAndEnabled) { enabled = true; return; } // OnEnable 이 알아서 만든다
|
||
enabled = false; // OnDisable → 버퍼 해제
|
||
enabled = true; // OnEnable → 다시 생성
|
||
Rebuilds++;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 816o — 구름이 흐른 만큼 **띠만** 다시 고른다. 타일·제외 사각형은 다시 훑지 않는다
|
||
/// (`FindObjectsByType` 를 타지 않으므로 전체 재생성보다 훨씬 싸다).
|
||
/// </summary>
|
||
public void RefreshCloudEdges()
|
||
{
|
||
if (!_cacheValid || !isActiveAndEnabled) return;
|
||
if (cfg == null || cfg.cloudEdgeEnabled == 0) return;
|
||
_fastRefresh = true;
|
||
enabled = false;
|
||
enabled = true;
|
||
_fastRefresh = false;
|
||
CloudRefreshes++;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────
|
||
// 점 뽑기 — 데모 `TerrainInstancesBehaviour.GetInstanceData` 와 같은 절차
|
||
// ─────────────────────────────────────────────────────────────────
|
||
public override Dictionary<InstancingSettings, List<InstanceData>> GetInstanceData()
|
||
{
|
||
if (_fastRefresh && _cacheValid) return BuildFromCache(true);
|
||
|
||
_cacheValid = false;
|
||
Instances = 0; Tiles = 0; Excluded = 0; Triangles = 0; DrawnConfigs = 0; UsedDensity = 0f;
|
||
Candidates = 0; EdgeFraction = 1f; EdgeCells = 0; EdgeCellSize = 0f; EdgeDilate = 0;
|
||
BakeMs = 0f; FilterMs = 0f; CloudEdgeOn = false; CloudWhy = ""; _edgeOn = false;
|
||
|
||
if (cfg == null) cfg = WLIslandLookSettings.Instance;
|
||
if (cfg == null || cfg.enabled_ == 0 || cfg.grassEnabled == 0) { LastLog = "꺼짐"; return null; }
|
||
if (cfg.scatter == null || cfg.scatter.Length == 0) { LastLog = "scatter 표가 비었다"; return null; }
|
||
|
||
// 🔴 컴퓨트 버퍼(StructuredBuffer)를 못 쓰는 기기에서는 깔지 않는다(데모 셰이더와 같은 요구).
|
||
if (!SystemInfo.supportsComputeShaders || !SystemInfo.supportsInstancing)
|
||
{
|
||
LastLog = "기기가 인스턴싱/컴퓨트버퍼 미지원 → 풀 생략(그래도 게임은 그대로 돈다)";
|
||
cfg.Log(LastLog);
|
||
return null;
|
||
}
|
||
|
||
// 우리 오브젝트는 항상 원점·무회전·크기 1 이어야 한다(_LocalToWorld = 단위행렬).
|
||
transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
|
||
transform.localScale = Vector3.one;
|
||
transform.hasChanged = true; // 기반 클래스 Update 가 _LocalToWorld 를 밀어 넣게 한다
|
||
|
||
var tiles = CollectTiles();
|
||
Tiles = tiles.Count;
|
||
if (tiles.Count == 0) { LastLog = "깔 타일 0"; return null; }
|
||
|
||
BuildBlockers(tiles);
|
||
|
||
// 경계 박스 — 구름 격자를 굽기 전에 정해져야 한다
|
||
var bmin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
|
||
var bmax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
|
||
for (int i = 0; i < tiles.Count; i++)
|
||
{
|
||
var t = tiles[i];
|
||
bmin = Vector3.Min(bmin, new Vector3(t.center.x - 4f, t.center.y - 1f, t.center.z - 4f));
|
||
bmax = Vector3.Max(bmax, new Vector3(t.center.x + 4f, t.center.y + 3f, t.center.z + 4f));
|
||
}
|
||
_bounds = new Bounds((bmin + bmax) * 0.5f, bmax - bmin);
|
||
_cacheY = tiles[0].center.y;
|
||
|
||
// ── 816o — 바닥과 **같은 식**으로 구름 그림자 색 띠의 경계를 굽는다 ──
|
||
float coverage = 1f;
|
||
if (cfg.cloudEdgeEnabled != 0)
|
||
{
|
||
var src = cfg.cloudEdgeSourceMaterial != null ? cfg.cloudEdgeSourceMaterial : cfg.islandTopMaterial;
|
||
if (_field.Configure(src))
|
||
{
|
||
BakeField();
|
||
coverage = Mathf.Clamp(_field.EdgeFraction, 0.02f, 1f);
|
||
_edgeOn = true; CloudEdgeOn = true;
|
||
}
|
||
else CloudWhy = _field.Why;
|
||
}
|
||
|
||
// 밀도 자동 조절(모바일 예산) — 띠 면적(coverage)을 실측해서 반영한다
|
||
float edgeScale = _edgeOn ? Mathf.Max(0.05f, cfg.cloudEdgeDensityScale) : 1f;
|
||
float budgetScale = 1f;
|
||
if (cfg.maxInstances > 0)
|
||
{
|
||
float area = tiles.Count * 64f; // 타일 8×8
|
||
float topLayer = 0f;
|
||
for (int i = 0; i < cfg.scatter.Length; i++)
|
||
{
|
||
var s = cfg.scatter[i];
|
||
if (s == null || s.enabled_ == 0) continue;
|
||
if (s.density > topLayer) topLayer = s.density;
|
||
}
|
||
float d0 = topLayer * edgeScale;
|
||
float est = area * d0 * d0 * 0.93f * coverage; // 윗면은 8×8 의 약 93 %
|
||
if (est > cfg.maxInstances) budgetScale = Mathf.Sqrt(cfg.maxInstances / est);
|
||
}
|
||
float densScale = budgetScale * edgeScale;
|
||
|
||
// 같은 density 끼리 한 층으로 묶는다(데모의 FirstLayer/SecondLayer 와 같은 구조)
|
||
var layers = new List<float>(4);
|
||
for (int i = 0; i < cfg.scatter.Length; i++)
|
||
{
|
||
var s = cfg.scatter[i];
|
||
if (s == null || s.enabled_ == 0 || s.mesh == null || s.material == null || s.scale <= 0f) continue;
|
||
float d = s.density * densScale;
|
||
if (d <= 0f) continue;
|
||
bool found = false;
|
||
for (int k = 0; k < layers.Count; k++) if (Mathf.Abs(layers[k] - d) < 1e-5f) { found = true; break; }
|
||
if (!found) layers.Add(d);
|
||
}
|
||
|
||
var allDefs = new List<WLScatterDef>(8);
|
||
var allSets = new List<InstancingSettings>(8);
|
||
var cands = new List<Cand>(8192);
|
||
|
||
for (int L = 0; L < layers.Count; L++)
|
||
{
|
||
float density = layers[L];
|
||
|
||
// 이 층에 속한 정의들 + 누적 확률
|
||
var defs = new List<WLScatterDef>(4);
|
||
float wsum = 0f;
|
||
for (int i = 0; i < cfg.scatter.Length; i++)
|
||
{
|
||
var s = cfg.scatter[i];
|
||
if (s == null || s.enabled_ == 0 || s.mesh == null || s.material == null || s.scale <= 0f) continue;
|
||
if (Mathf.Abs(s.density * densScale - density) > 1e-5f) continue;
|
||
defs.Add(s); wsum += Mathf.Max(0f, s.probability);
|
||
}
|
||
if (defs.Count == 0 || wsum <= 0f) continue;
|
||
|
||
int baseIdx = allDefs.Count;
|
||
for (int i = 0; i < defs.Count; i++)
|
||
{
|
||
allDefs.Add(defs[i]);
|
||
allSets.Add(new InstancingSettings
|
||
{
|
||
Mesh = defs[i].mesh,
|
||
Material = defs[i].material,
|
||
Probability = defs[i].probability,
|
||
Scale = defs[i].scale,
|
||
NormalOffset = defs[i].normalOffset,
|
||
});
|
||
}
|
||
|
||
float step = 1f / density;
|
||
float pv = cfg.positionVariance;
|
||
float sv = cfg.scaleVariance;
|
||
|
||
for (int i = 0; i < tiles.Count; i++)
|
||
{
|
||
var t = tiles[i];
|
||
int ix0 = Mathf.CeilToInt((t.center.x - 4f) / step - 0.5f);
|
||
int ix1 = Mathf.FloorToInt((t.center.x + 4f) / step - 0.5f);
|
||
int iz0 = Mathf.CeilToInt((t.center.z - 4f) / step - 0.5f);
|
||
int iz1 = Mathf.FloorToInt((t.center.z + 4f) / step - 0.5f);
|
||
|
||
for (int ix = ix0; ix <= ix1; ix++)
|
||
{
|
||
for (int iz = iz0; iz <= iz1; iz++)
|
||
{
|
||
// 같은 칸이면 몇 번 다시 깔아도 같은 결과(확장 때 풀이 튀지 않는다)
|
||
uint h = Hash((uint)(ix * 73856093) ^ (uint)(iz * 19349663) ^ (uint)(L * 83492791));
|
||
float jx = (Frac(h, 0) - 0.5f) * 2f * pv;
|
||
float jz = (Frac(h, 1) - 0.5f) * 2f * pv;
|
||
float wx = (ix + 0.5f + jx) * step;
|
||
float wz = (iz + 0.5f + jz) * step;
|
||
|
||
if (!OnTile(t, wx, wz)) { Excluded++; continue; }
|
||
if (Blocked(wx, wz)) { Excluded++; continue; }
|
||
|
||
// 어느 정의가 걸리나(가중 추첨 · 결정적)
|
||
float r = Frac(h, 2) * wsum;
|
||
int pick = defs.Count - 1;
|
||
float acc = 0f;
|
||
for (int d = 0; d < defs.Count; d++)
|
||
{
|
||
acc += Mathf.Max(0f, defs[d].probability);
|
||
if (r <= acc) { pick = d; break; }
|
||
}
|
||
|
||
float sc = 1f + (Frac(h, 3) - 0.5f) * 2f * sv;
|
||
float yaw = cfg.randomYaw != 0 ? Frac(h, 4) * 360f : 0f;
|
||
|
||
cands.Add(new Cand { x = wx, z = wz, y = t.center.y, def = baseIdx + pick, sc = sc, yaw = yaw });
|
||
}
|
||
}
|
||
}
|
||
|
||
if (density > UsedDensity) UsedDensity = density;
|
||
}
|
||
|
||
_cdef = allDefs.ToArray();
|
||
_cset = allSets.ToArray();
|
||
_ctri = new long[_cdef.Length];
|
||
for (int i = 0; i < _cdef.Length; i++)
|
||
{
|
||
long tri = 0;
|
||
var m = _cdef[i].mesh;
|
||
for (int s = 0; s < m.subMeshCount; s++) tri += m.GetIndexCount(s) / 3;
|
||
_ctri[i] = tri;
|
||
}
|
||
_cand = cands.ToArray();
|
||
_candCount = _cand.Length;
|
||
Candidates = _candCount;
|
||
_budgetScale = budgetScale;
|
||
_cacheValid = _cdef.Length > 0 && _candCount > 0;
|
||
if (!_cacheValid) { LastLog = "후보 0"; cfg.Log(LastLog); return null; }
|
||
|
||
return BuildFromCache(false);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────
|
||
// 816o — 띠 굽기 + 캐시에서 고르기
|
||
// ─────────────────────────────────────────────────────────────────
|
||
float _budgetScale = 1f;
|
||
bool _edgeOn;
|
||
|
||
void BakeField()
|
||
{
|
||
float cell = Mathf.Max(0.05f, cfg.cloudEdgeCell);
|
||
int dil = Mathf.Max(0, cfg.cloudEdgeDilate);
|
||
// 갱신 주기 동안 구름이 흐르는 만큼 미리 덮어 둔다 — 띠가 움직여도 빈틈이 안 생긴다
|
||
if (cfg.cloudEdgeDriftCover != 0 && cfg.cloudEdgeRefreshSeconds > 0f)
|
||
dil += Mathf.CeilToInt(_field.DriftPerSecond * cfg.cloudEdgeRefreshSeconds / cell);
|
||
|
||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||
_field.Bake(_bounds, _cacheY, cell, cfg.cloudEdgeMaxCells, dil,
|
||
cfg.cloudEdgeSampleCell, cfg.cloudEdgeMaxSamples);
|
||
sw.Stop();
|
||
BakeMs = (float)sw.Elapsed.TotalMilliseconds;
|
||
EdgeCells = _field.Cells; EdgeCellSize = _field.CellSize;
|
||
EdgeSamples = _field.Samples; EdgeSampleCell = _field.SampleCellSize;
|
||
EdgeFraction = _field.EdgeFraction; EdgeDilate = dil;
|
||
}
|
||
|
||
Dictionary<InstancingSettings, List<InstanceData>> BuildFromCache(bool refresh)
|
||
{
|
||
if (refresh)
|
||
{
|
||
_field.Tick();
|
||
BakeField();
|
||
}
|
||
|
||
Instances = 0; Triangles = 0; DrawnConfigs = 0; CloudExcluded = 0;
|
||
|
||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||
var buckets = new List<InstanceData>[_cdef.Length];
|
||
for (int i = 0; i < buckets.Length; i++) buckets[i] = new List<InstanceData>(256);
|
||
|
||
Vector3 bc = _bounds.center;
|
||
bool edge = _edgeOn;
|
||
for (int i = 0; i < _candCount; i++)
|
||
{
|
||
var c = _cand[i];
|
||
if (edge && !_field.IsEdge(c.x, c.z)) { CloudExcluded++; continue; }
|
||
|
||
var rot = c.yaw != 0f ? Quaternion.Euler(0f, c.yaw, 0f) : Quaternion.identity;
|
||
var trs = Matrix4x4.TRS(new Vector3(c.x, c.y, c.z) - bc, rot, Vector3.one * c.sc);
|
||
var def = _cdef[c.def];
|
||
trs *= Matrix4x4.TRS(def.normalOffset * Vector3.up, Quaternion.identity,
|
||
new Vector3(def.scale, def.scale, def.scale));
|
||
buckets[c.def].Add(new InstanceData { TRS = trs, Normal = Vector3.up });
|
||
}
|
||
sw.Stop();
|
||
FilterMs = (float)sw.Elapsed.TotalMilliseconds;
|
||
|
||
var result = new Dictionary<InstancingSettings, List<InstanceData>>();
|
||
for (int i = 0; i < buckets.Length; i++)
|
||
{
|
||
if (buckets[i].Count == 0) continue;
|
||
result.Add(_cset[i], buckets[i]);
|
||
Instances += buckets[i].Count;
|
||
DrawnConfigs++;
|
||
Triangles += _ctri[i] * buckets[i].Count;
|
||
}
|
||
|
||
LastLog = "타일 " + Tiles + " · 후보 " + Candidates + " · 인스턴스 " + Instances
|
||
+ " · 드로우콜 " + DrawnConfigs + " · 삼각형 " + Triangles + " · 제외점 " + Excluded
|
||
+ " · 밀도 " + UsedDensity.ToString("F2") + "(=" + (UsedDensity * UsedDensity).ToString("F2") + "개/㎡)"
|
||
+ (_budgetScale < 1f ? " · 예산으로 밀도 ×" + _budgetScale.ToString("F2") : "")
|
||
+ (_edgeOn
|
||
? " · 구름띠 " + (EdgeFraction * 100f).ToString("F1") + "%(칸 " + EdgeCellSize.ToString("F2")
|
||
+ "m×" + EdgeCells + " · 넓힘 " + EdgeDilate + " · 노이즈 " + EdgeSampleCell.ToString("F2")
|
||
+ "m×" + EdgeSamples + ") · 띠밖 제외 " + CloudExcluded
|
||
+ " · 굽기 " + BakeMs.ToString("F1") + "ms · 고르기 " + FilterMs.ToString("F1") + "ms"
|
||
: " · 구름띠 off" + (CloudWhy.Length > 0 ? "(" + CloudWhy + ")" : ""));
|
||
cfg.Log(LastLog);
|
||
return result.Count == 0 ? null : result;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────
|
||
// 섬 타일 모으기
|
||
// ─────────────────────────────────────────────────────────────────
|
||
public struct TileRef
|
||
{
|
||
public Transform tf; // 섬 트랜스폼
|
||
public Vector3 center; // 윗면 중심(월드 · y = 윗면)
|
||
public WLTileMask mask; // 윗면 마스크(섬 로컬)
|
||
public Transform roadTf;
|
||
public WLTileMask roadMask;
|
||
}
|
||
|
||
readonly List<TileRef> _tiles = new List<TileRef>(96);
|
||
|
||
List<TileRef> CollectTiles()
|
||
{
|
||
_tiles.Clear();
|
||
var islands = Object.FindObjectsByType<FIIsland>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||
for (int i = 0; i < islands.Length; i++)
|
||
{
|
||
var isl = islands[i];
|
||
if (isl == null || !isl.IsUnlocked || isl.IsBridge) continue;
|
||
if (!isl.gameObject.activeInHierarchy) continue;
|
||
if (isl.transform.localScale.x < 0.99f) continue; // 확장 애니메이션 중
|
||
|
||
var mf = isl.GetComponent<MeshFilter>();
|
||
if (mf == null || mf.sharedMesh == null) continue;
|
||
var mask = cfg.FindTileMask(mf.sharedMesh.name);
|
||
if (mask == null) continue; // 모르는 메시 = 안 깐다(안전)
|
||
|
||
Transform roadTf = null; WLTileMask roadMask = null;
|
||
var rt = isl.transform.Find("Road");
|
||
if (rt != null && rt.gameObject.activeInHierarchy)
|
||
{
|
||
var rmf = rt.GetComponent<MeshFilter>();
|
||
if (rmf != null && rmf.sharedMesh != null)
|
||
{
|
||
roadMask = cfg.FindRoadMask(rmf.sharedMesh.name);
|
||
if (roadMask != null) roadTf = rt;
|
||
}
|
||
}
|
||
|
||
_tiles.Add(new TileRef
|
||
{
|
||
tf = isl.transform,
|
||
center = isl.transform.position, // 윗면 y = 섬 원점 y (실측: 메시 maxY = 0)
|
||
mask = mask,
|
||
roadTf = roadTf,
|
||
roadMask = roadMask,
|
||
});
|
||
}
|
||
return _tiles;
|
||
}
|
||
|
||
bool OnTile(TileRef t, float wx, float wz)
|
||
{
|
||
var w = new Vector3(wx, t.center.y, wz);
|
||
var l = t.tf.InverseTransformPoint(w);
|
||
|
||
// 가장자리 여백 — 점과 ±inset 네 방향이 전부 윗면이어야 한다
|
||
float e = cfg.edgeInset;
|
||
if (!t.mask.At(l.x, l.z)) return false;
|
||
if (e > 0f)
|
||
{
|
||
if (!t.mask.At(l.x + e, l.z) || !t.mask.At(l.x - e, l.z) ||
|
||
!t.mask.At(l.x, l.z + e) || !t.mask.At(l.x, l.z - e)) return false;
|
||
}
|
||
|
||
// 길 제외
|
||
if (t.roadMask != null && t.roadTf != null)
|
||
{
|
||
var lr = t.roadTf.InverseTransformPoint(w);
|
||
float m = cfg.roadMargin;
|
||
if (t.roadMask.At(lr.x, lr.z)) return false;
|
||
if (m > 0f && (t.roadMask.At(lr.x + m, lr.z) || t.roadMask.At(lr.x - m, lr.z) ||
|
||
t.roadMask.At(lr.x, lr.z + m) || t.roadMask.At(lr.x, lr.z - m))) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────
|
||
// 제외 사각형 — 농사 타일 · 건물 · 소품
|
||
// ─────────────────────────────────────────────────────────────────
|
||
void BuildBlockers(List<TileRef> tiles)
|
||
{
|
||
_blockers.Clear();
|
||
var scene = tiles.Count > 0 ? tiles[0].tf.gameObject.scene : default(UnityEngine.SceneManagement.Scene);
|
||
|
||
// ① 농사 — Farm 이 Awake 에서 만드는 BoxCollider(size = (length,2,width))가 밭의 정확한 넓이다.
|
||
var farms = Object.FindObjectsByType<FIFarm>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||
for (int i = 0; i < farms.Length; i++)
|
||
{
|
||
var f = farms[i];
|
||
if (f == null || f.gameObject.scene != scene) continue;
|
||
var bc = f.GetComponent<BoxCollider>();
|
||
if (bc != null) AddBox(f.transform, bc.center, bc.size, cfg.farmMargin);
|
||
else AddRect(f.transform.position.x, f.transform.position.z, 6f, 5f, cfg.farmMargin);
|
||
}
|
||
|
||
// ② 농사 타일 하나하나(1×1) — Farm 이 런타임에 만든다. 이중 안전.
|
||
var soils = Object.FindObjectsByType<FISoil>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||
for (int i = 0; i < soils.Length; i++)
|
||
{
|
||
var s = soils[i];
|
||
if (s == null || s.gameObject.scene != scene) continue;
|
||
var p = s.transform.position;
|
||
AddRect(p.x, p.z, 1f, 1f, cfg.farmMargin);
|
||
}
|
||
|
||
// ③ 건물·소품 — 섬 본체(Colliders/…)와 움직이는 것(플레이어·동물)은 뺀다.
|
||
var cols = Object.FindObjectsByType<Collider>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||
for (int i = 0; i < cols.Length; i++)
|
||
{
|
||
var c = cols[i];
|
||
if (c == null || c.gameObject.scene != scene) continue;
|
||
var tf = c.transform;
|
||
|
||
// 섬 본체 박스 + 네 방향 벽(Colliders 아래)
|
||
if (tf.name == "Colliders" || (tf.parent != null && tf.parent.name == "Colliders")) continue;
|
||
// 물
|
||
if (c.gameObject.layer == LayerMask.NameToLayer("Water")) continue;
|
||
// 움직이는 것
|
||
if (c.GetComponentInParent<PlayerController>() != null) continue;
|
||
if (c.attachedRigidbody != null && !c.attachedRigidbody.isKinematic) continue;
|
||
// 밭 트리거는 ①에서 이미 넣었다
|
||
if (c.GetComponent<FIFarm>() != null) continue;
|
||
if (c.GetComponent<FISoil>() != null) continue;
|
||
|
||
var b = ColliderXZ(c);
|
||
if (b.width <= 0f || b.height <= 0f) continue;
|
||
if (b.width >= cfg.propMaxSize && b.height >= cfg.propMaxSize) continue; // 섬 크기 = 본체
|
||
_blockers.Add(Inflate(b, cfg.propMargin));
|
||
}
|
||
}
|
||
|
||
bool Blocked(float x, float z)
|
||
{
|
||
for (int i = 0; i < _blockers.Count; i++)
|
||
{
|
||
var r = _blockers[i];
|
||
if (x >= r.xMin && x <= r.xMax && z >= r.yMin && z <= r.yMax) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void AddBox(Transform tf, Vector3 center, Vector3 size, float margin)
|
||
{
|
||
// 콜라이더가 disabled 여도 안전하게 — bounds 대신 직접 계산한다.
|
||
var c = tf.TransformPoint(center);
|
||
var ex = tf.TransformVector(new Vector3(size.x * 0.5f, 0f, 0f));
|
||
var ez = tf.TransformVector(new Vector3(0f, 0f, size.z * 0.5f));
|
||
float hx = Mathf.Abs(ex.x) + Mathf.Abs(ez.x);
|
||
float hz = Mathf.Abs(ex.z) + Mathf.Abs(ez.z);
|
||
AddRect(c.x, c.z, hx * 2f, hz * 2f, margin);
|
||
}
|
||
|
||
void AddRect(float cx, float cz, float w, float h, float margin)
|
||
{
|
||
_blockers.Add(new Rect(cx - w * 0.5f - margin, cz - h * 0.5f - margin, w + margin * 2f, h + margin * 2f));
|
||
}
|
||
|
||
static Rect Inflate(Rect r, float m)
|
||
{
|
||
return new Rect(r.xMin - m, r.yMin - m, r.width + m * 2f, r.height + m * 2f);
|
||
}
|
||
|
||
static Rect ColliderXZ(Collider c)
|
||
{
|
||
var bx = c as BoxCollider;
|
||
if (bx != null)
|
||
{
|
||
var tf = c.transform;
|
||
var ctr = tf.TransformPoint(bx.center);
|
||
var ex = tf.TransformVector(new Vector3(bx.size.x * 0.5f, 0f, 0f));
|
||
var ez = tf.TransformVector(new Vector3(0f, 0f, bx.size.z * 0.5f));
|
||
float hx = Mathf.Abs(ex.x) + Mathf.Abs(ez.x);
|
||
float hz = Mathf.Abs(ex.z) + Mathf.Abs(ez.z);
|
||
return new Rect(ctr.x - hx, ctr.z - hz, hx * 2f, hz * 2f);
|
||
}
|
||
var sp = c as SphereCollider;
|
||
if (sp != null)
|
||
{
|
||
var tf = c.transform;
|
||
var ctr = tf.TransformPoint(sp.center);
|
||
float s = Mathf.Max(Mathf.Abs(tf.lossyScale.x), Mathf.Abs(tf.lossyScale.z));
|
||
float r = sp.radius * s;
|
||
return new Rect(ctr.x - r, ctr.z - r, r * 2f, r * 2f);
|
||
}
|
||
var cp = c as CapsuleCollider;
|
||
if (cp != null)
|
||
{
|
||
var tf = c.transform;
|
||
var ctr = tf.TransformPoint(cp.center);
|
||
float s = Mathf.Max(Mathf.Abs(tf.lossyScale.x), Mathf.Abs(tf.lossyScale.z));
|
||
float r = cp.radius * s;
|
||
return new Rect(ctr.x - r, ctr.z - r, r * 2f, r * 2f);
|
||
}
|
||
var b = c.bounds; // MeshCollider 등
|
||
if (b.size.x <= 0f || b.size.z <= 0f) return new Rect(0f, 0f, 0f, 0f);
|
||
return new Rect(b.min.x, b.min.z, b.size.x, b.size.z);
|
||
}
|
||
|
||
// ── 결정적 난수 ─────────────────────────────────────────────────
|
||
static uint Hash(uint x)
|
||
{
|
||
x ^= x >> 16; x *= 0x7feb352dU;
|
||
x ^= x >> 15; x *= 0x846ca68bU;
|
||
x ^= x >> 16;
|
||
return x;
|
||
}
|
||
|
||
static float Frac(uint h, int k)
|
||
{
|
||
uint v = Hash(h + (uint)(k * 0x9E3779B9U));
|
||
return (v & 0xFFFFFF) / 16777215f;
|
||
}
|
||
}
|
||
}
|