using System.Collections.Generic; using System.Linq; // PD 지시 #771 2단계 A — Demo_01 구조 실측 + 플레이 영역(200x200 m) 후보 선정 // 실행: unity command run_script --file AgentScripts/staging/WL_Nature2/NatureArea.cs --entry NatureArea.Run --timeout 300 // 씬은 열기만 하고 절대 저장하지 않는다 (ClearDirty 로 dirty 해제). public static class NatureArea { const string SCENE = "Assets/LMHPOLY/Low Poly Nature Bundle/_Demo Scenes/Demo_01.unity"; const string OUT = @"E:\NerdNavis\nn_himminji\AgentScripts\staging\WL_Nature2\out"; const float HALF = 100f; // 플레이 영역 반폭 (200 x 200 m) const float SLOPE_LIMIT = 30f; // 소품 배치 경사 상한 (발주) const float WALK_LIMIT = 45f; // 보행 판정 경사 상한 (NavMesh agent slope) static System.Text.StringBuilder sb; static void L(string s) { sb.AppendLine(s); } public static object Run() { System.IO.Directory.CreateDirectory(OUT); sb = new System.Text.StringBuilder(); if (UnityEngine.Application.isPlaying) return "PLAYING - 중단"; var setup = UnityEditor.SceneManagement.EditorSceneManager.GetSceneManagerSetup(); try { var sc = UnityEditor.SceneManagement.EditorSceneManager.OpenScene(SCENE, UnityEditor.SceneManagement.OpenSceneMode.Single); L("== SCENE " + SCENE + " =="); // ── 1. 루트 구조 + 벤더 스크립트 실측 ────────────────── var roots = sc.GetRootGameObjects(); L("루트 오브젝트 " + roots.Length + "개"); var vendorScripts = new Dictionary(); int camCount = 0, lightCount = 0; foreach (var r in roots) { var mrs = r.GetComponentsInChildren(true).Length; var cams = r.GetComponentsInChildren(true); var lts = r.GetComponentsInChildren(true); camCount += cams.Length; lightCount += lts.Length; var comps = new List(); foreach (var c in r.GetComponents()) if (c != null && !(c is UnityEngine.Transform)) comps.Add(c.GetType().Name); L(string.Format(" [{0,-34}] children={1,-4} renderers={2,-4} cam={3} light={4} comps=[{5}]", r.name, CountDeep(r.transform), mrs, cams.Length, lts.Length, string.Join(",", comps))); foreach (var mb in r.GetComponentsInChildren(true)) { if (mb == null) continue; var t = mb.GetType(); var asm = t.Assembly.GetName().Name; if (asm.StartsWith("Unity") || asm.StartsWith("com.unity")) continue; string key = t.FullName + " (" + Path(mb.transform) + ")"; if (!vendorScripts.ContainsKey(t.FullName)) vendorScripts[t.FullName] = 0; vendorScripts[t.FullName]++; } } L("카메라 " + camCount + "대 · 라이트 " + lightCount + "개"); L("== 씬에 붙은 게임 스크립트 (제거 후보) =="); if (vendorScripts.Count == 0) L(" (없음)"); foreach (var kv in vendorScripts.OrderByDescending(k => k.Value)) L(" [" + kv.Value + "] " + kv.Key); // 라이트 상세 L("== 라이트 상세 =="); foreach (var r in roots) foreach (var lt in r.GetComponentsInChildren(true)) L(" " + Path(lt.transform) + " type=" + lt.type + " color=" + lt.color + " intensity=" + lt.intensity + " shadows=" + lt.shadows + " euler=" + lt.transform.rotation.eulerAngles.ToString("F2") + " active=" + lt.gameObject.activeInHierarchy); // ── 2. 수면 실측 ────────────────────────────────────── var water = new List(); L("== 수역 렌더러 =="); foreach (var r in roots) foreach (var rd in r.GetComponentsInChildren(true)) { if (!IsWater(rd)) continue; water.Add(rd.bounds); L(" " + rd.name + " center=" + rd.bounds.center.ToString("F1") + " size=" + rd.bounds.size.ToString("F1") + " topY=" + rd.bounds.max.y.ToString("F2")); } L("수역 " + water.Count + "개"); // ── 3. 전체 바운즈 ──────────────────────────────────── bool has = false; var b = new UnityEngine.Bounds(); foreach (var r in roots) foreach (var mr in r.GetComponentsInChildren(false)) { if (!has) { b = mr.bounds; has = true; } else b.Encapsulate(mr.bounds); } L("bounds center=" + b.center.ToString("F1") + " size=" + b.size.ToString("F1")); CollectProps(roots); // ── 4. 후보 중심 스캔 (섬 전역 40 m 격자) ───────────── L(""); L("== 후보 중심 스캔 (40 m 격자 · 각 후보 200x200 m 를 20 m 간격 11x11 로 평가) =="); var cands = new List(); for (float cx = b.min.x + HALF; cx <= b.max.x - HALF; cx += 40f) for (float cz = b.min.z + HALF; cz <= b.max.z - HALF; cz += 40f) { var c = Evaluate(cx, cz, b, water, 20f); if (c != null) cands.Add(c); } var ranked = cands.OrderByDescending(c => c.Score()).Take(8).ToList(); L(string.Format("{0,-22} {1,6} {2,7} {3,7} {4,7} {5,8} {6,8} {7,7}", "center", "walk%", "flat%", "waterV", "yRange", "centerY", "score", "nearWtr")); foreach (var c in ranked) L(c.Line()); // PM 후보 (1단계 PickPlayerSpot 결과) L(""); L("== PM 지정 후보 (209, -148) 정밀 평가 (5 m 간격 41x41) =="); var pm = Evaluate(209f, -148f, b, water, 5f); if (pm != null) { L(pm.Line()); L(pm.Detail()); } // 상위 후보 정밀 평가 for (int i = 0; i < System.Math.Min(3, ranked.Count); i++) { var fine = Evaluate(ranked[i].cx, ranked[i].cz, b, water, 5f); L(""); L("== 상위 후보 #" + (i + 1) + " 정밀 평가 (5 m 간격) =="); if (fine != null) { L(fine.Line()); L(fine.Detail()); } } ClearDirty(); } catch (System.Exception e) { L("EXCEPTION " + e); } finally { try { if (setup != null && setup.Length > 0) UnityEditor.SceneManagement.EditorSceneManager.RestoreSceneManagerSetup(setup); } catch { } } System.IO.File.WriteAllText(System.IO.Path.Combine(OUT, "A1_area.txt"), sb.ToString(), new System.Text.UTF8Encoding(true)); return "wrote A1_area.txt (" + sb.Length + " chars)"; } // ═══════════════════════════════════════════════════════════ // 시작 지점 + 플레이 영역을 함께 고른다. // 시작 지점 조건: 경사<=15° · 수중 아님 · 머리 위 12 m 개방 · 반경 12 m 개활지가 평탄 // · 수면까지 6~35 m (강가) · 지형 가장자리에서 half+여유 안쪽 // 실행: --entry NatureArea.Spot --args '[100.0]' // ═══════════════════════════════════════════════════════════ public static object Spot(float half) { System.IO.Directory.CreateDirectory(OUT); sb = new System.Text.StringBuilder(); if (UnityEngine.Application.isPlaying) return "PLAYING - 중단"; var setup = UnityEditor.SceneManagement.EditorSceneManager.GetSceneManagerSetup(); string res = "?"; try { var sc = UnityEditor.SceneManagement.EditorSceneManager.OpenScene(SCENE, UnityEditor.SceneManagement.OpenSceneMode.Single); var roots = sc.GetRootGameObjects(); CollectProps(roots); // 지형+수역만의 바운즈 (원경 산·구름 제외) bool has = false; var tb = new UnityEngine.Bounds(); var water = new List(); foreach (var r in roots) { bool isTerrain = r.name == "_Terrain" || r.name == "_Water"; foreach (var mr in r.GetComponentsInChildren(true)) { if (IsWater(mr)) water.Add(mr.bounds); if (!isTerrain) continue; if (!has) { tb = mr.bounds; has = true; } else tb.Encapsulate(mr.bounds); } } L("지형(_Terrain+_Water) 바운즈 center=" + tb.center.ToString("F1") + " size=" + tb.size.ToString("F1")); L("수역 " + water.Count + "개"); // 전체 바운즈 (레이 시작 높이용) var full = new UnityEngine.Bounds(); bool hf = false; foreach (var r in roots) foreach (var mr in r.GetComponentsInChildren(true)) { if (!hf) { full = mr.bounds; hf = true; } else full.Encapsulate(mr.bounds); } float cosStart = UnityEngine.Mathf.Cos(15f * UnityEngine.Mathf.Deg2Rad); var cands = new List(); int nTried = 0, rejGround = 0, rejSlope = 0, rejWater = 0, rejHead = 0, rejClear = 0, rejBand = 0, rejEdge = 0; for (float x = tb.min.x + half * 0.5f; x <= tb.max.x - half * 0.5f; x += 5f) for (float z = tb.min.z + half * 0.5f; z <= tb.max.z - half * 0.5f; z += 5f) { nTried++; UnityEngine.RaycastHit h; if (!UnityEngine.Physics.Raycast(new UnityEngine.Vector3(x, full.max.y + 100f, z), UnityEngine.Vector3.down, out h, full.size.y + 500f)) { rejGround++; continue; } if (h.normal.y < cosStart) { rejSlope++; continue; } if (InWater(h.point, water)) { rejWater++; continue; } if (UnityEngine.Physics.Raycast(h.point + UnityEngine.Vector3.up * 0.5f, UnityEngine.Vector3.up, 12f)) { rejHead++; continue; } // 반경 12 m 개활지 평탄도 float ymin = h.point.y, ymax = h.point.y; int ok = 0; for (int k = 0; k < 8; k++) { float a = k * 45f * UnityEngine.Mathf.Deg2Rad; var q = h.point + new UnityEngine.Vector3(UnityEngine.Mathf.Cos(a) * 12f, 0f, UnityEngine.Mathf.Sin(a) * 12f); UnityEngine.RaycastHit h2; if (!UnityEngine.Physics.Raycast(new UnityEngine.Vector3(q.x, full.max.y + 100f, q.z), UnityEngine.Vector3.down, out h2, full.size.y + 500f)) continue; if (InWater(h2.point, water)) continue; ok++; if (h2.point.y < ymin) ymin = h2.point.y; if (h2.point.y > ymax) ymax = h2.point.y; } if (ok < 8 || (ymax - ymin) > 2.5f) { rejClear++; continue; } float dW = 9999f; for (int i = 0; i < water.Count; i++) dW = UnityEngine.Mathf.Min(dW, UnityEngine.Mathf.Sqrt(SqDistXZ(h.point, water[i]))); if (dW < 6f || dW > 35f) { rejBand++; continue; } var area = Evaluate(x, z, full, water, 20f); if (area == null || area.Score() < 0f) { rejEdge++; continue; } var s = new Spotc(); s.p = h.point; s.dWater = dW; s.drop = ymax - ymin; s.area = area; cands.Add(s); } L("시작 지점 격자 " + nTried + "점(5 m) → 후보 " + cands.Count + " (제외: 지형없음 " + rejGround + " · 경사 " + rejSlope + " · 수중 " + rejWater + " · 머리막힘 " + rejHead + " · 개활지불량 " + rejClear + " · 수면거리밖 " + rejBand + " · 영역부적합 " + rejEdge + ")"); cands.Sort((a, b2) => b2.Total().CompareTo(a.Total())); L(""); L(string.Format("{0,-26} {1,7} {2,6} {3,6} {4,6} {5,6} {6,5} {7,5} {8,7}", "start(x,y,z)", "수면m", "단차", "walk%", "flat%", "water%", "나무", "바위", "총점")); for (int i = 0; i < System.Math.Min(10, cands.Count); i++) L(cands[i].Line()); if (cands.Count > 0) { var b0 = cands[0]; L(""); L("== 채택 후보 상세 =="); L(" 시작 지점 = " + b0.p.ToString("F3")); L(" 플레이 영역 = 중심 (" + b0.p.x.ToString("F1") + ", " + b0.p.z.ToString("F1") + ") ± " + half + " m (" + (half * 2) + " x " + (half * 2) + " m)"); L(" 최근접 수면 " + b0.dWater.ToString("F1") + " m · 개활지 12 m 단차 " + b0.drop.ToString("F2") + " m"); L(" 영역 보행가능 " + (b0.area.walkPct * 100f).ToString("F1") + "% · 완경사(<=30°) " + (b0.area.flatPct * 100f).ToString("F1") + "% · 수면셀 " + b0.area.waterCells + "/" + b0.area.samples + " · 기존 나무 " + b0.area.treeNear + " 바위 " + b0.area.rockNear); L(" 영역 고도 " + b0.area.yMin.ToString("F1") + " ~ " + b0.area.yMax.ToString("F1") + " m"); res = string.Format("start=({0:F2},{1:F2},{2:F2}) center=({3:F1},{4:F1}) dWater={5:F1} walk={6:F1}% water={7} trees={8} rocks={9}", b0.p.x, b0.p.y, b0.p.z, b0.p.x, b0.p.z, b0.dWater, b0.area.walkPct * 100f, b0.area.waterCells, b0.area.treeNear, b0.area.rockNear); } else res = "후보 없음"; ClearDirty(); } catch (System.Exception e) { L("EXCEPTION " + e); res = "EXCEPTION " + e.Message; } finally { try { if (setup != null && setup.Length > 0) UnityEditor.SceneManagement.EditorSceneManager.RestoreSceneManagerSetup(setup); } catch { } } System.IO.File.WriteAllText(System.IO.Path.Combine(OUT, "A2_spot.txt"), sb.ToString(), new System.Text.UTF8Encoding(true)); return res; } class Spotc { public UnityEngine.Vector3 p; public float dWater, drop; public Cand area; public float Total() { float s = area.Score(); s += (1f - UnityEngine.Mathf.Abs(dWater - 18f) / 20f) * 25f; // 수면에서 18 m 근처가 최적 s += (2.5f - drop) * 8f; // 개활지가 평탄할수록 return s; } public string Line() { return string.Format("({0,7:F1},{1,6:F2},{2,7:F1}) {3,6:F1} {4,6:F2} {5,6:F1} {6,6:F1} {7,6:F1} {8,5} {9,5} {10,7:F1}", p.x, p.y, p.z, dWater, drop, area.walkPct * 100f, area.flatPct * 100f, area.samples == 0 ? 0f : area.waterCells * 100f / area.samples, area.treeNear, area.rockNear, Total()); } } // ── 후보 평가 ──────────────────────────────────────────────── class Cand { public float cx, cz, walkPct, flatPct, yMin, yMax, centerY, nearWater; public int waterCells, samples, treeNear, rockNear; // 발주 요구 = "물·숲·개활지가 함께 보이는 곳". 넓은 맨땅 구석이 1위가 되지 않도록 // 보행 가능 비율은 하한(45%)만 요구하고, 물·기존 소품·완만함에 배점을 나눈다. public float Score() { if (walkPct < 0.45f) return -1000f + walkPct * 100f; // 플레이 영역으로 부적합 float s = walkPct * 60f + flatPct * 40f; float wr = samples == 0 ? 0f : waterCells / (float)samples; if (wr >= 0.03f && wr <= 0.32f) s += 55f; // 물이 보이되 영역을 덮지 않음 else if (wr > 0.32f) s -= (wr - 0.32f) * 200f; // 절반이 물이면 감점 else s -= 20f; // 물이 전혀 없으면 감점 s += UnityEngine.Mathf.Min(treeNear, 12) * 2.5f; // 기존 나무 (숲 씨앗) s += UnityEngine.Mathf.Min(rockNear, 15) * 1.2f; // 기존 바위 s -= UnityEngine.Mathf.Clamp((yMax - yMin) - 12f, 0f, 60f) * 1.5f; // 12 m 넘는 고저차 감점 return s; } public string Line() { return string.Format("({0,7:F0},{1,7:F0}) {2,5:F1} {3,6:F1} {4,7} {5,7:F1} {6,8:F2} {7,8:F1} {8,7:F1}", cx, cz, walkPct * 100f, flatPct * 100f, waterCells, yMax - yMin, centerY, Score(), nearWater); } public string Detail() { return " 표본=" + samples + " 중심고도=" + centerY.ToString("F2") + " y=" + yMin.ToString("F1") + "~" + yMax.ToString("F1") + " 수면셀=" + waterCells + " 최근접수면=" + nearWater.ToString("F1") + "m" + " 영역내 나무=" + treeNear + " 바위=" + rockNear; } } // 씬 안 기존 프리팹 인스턴스의 위치·분류 (후보 평가에서 재사용) static List> s_props; static void CollectProps(UnityEngine.GameObject[] roots) { s_props = new List>(); foreach (var r in roots) foreach (var t in r.GetComponentsInChildren(true)) { if (!UnityEditor.PrefabUtility.IsAnyPrefabInstanceRoot(t.gameObject)) continue; var src = UnityEditor.PrefabUtility.GetCorrespondingObjectFromOriginalSource(t.gameObject); var p = src == null ? "" : UnityEditor.AssetDatabase.GetAssetPath(src); string k = p.Contains("/Trees/") ? "tree" : p.Contains("/Rocks/") ? "rock" : p.Contains("/Vegetation/") ? "veg" : p.Contains("/Modular Terrain/") ? "terrain" : "other"; s_props.Add(new KeyValuePair(t.position, k)); } L("프리팹 인스턴스 " + s_props.Count + "개 수집"); } static Cand Evaluate(float cx, float cz, UnityEngine.Bounds b, List water, float step) { var c = new Cand(); c.cx = cx; c.cz = cz; if (s_props != null) for (int i = 0; i < s_props.Count; i++) { var p = s_props[i].Key; if (UnityEngine.Mathf.Abs(p.x - cx) > HALF || UnityEngine.Mathf.Abs(p.z - cz) > HALF) continue; if (s_props[i].Value == "tree") c.treeNear++; else if (s_props[i].Value == "rock") c.rockNear++; } int walk = 0, flat = 0, hit = 0, n = 0, wcell = 0; float ymin = float.MaxValue, ymax = float.MinValue; float cosWalk = UnityEngine.Mathf.Cos(WALK_LIMIT * UnityEngine.Mathf.Deg2Rad); float cosFlat = UnityEngine.Mathf.Cos(SLOPE_LIMIT * UnityEngine.Mathf.Deg2Rad); for (float x = cx - HALF; x <= cx + HALF + 0.01f; x += step) for (float z = cz - HALF; z <= cz + HALF + 0.01f; z += step) { n++; UnityEngine.RaycastHit h; if (!UnityEngine.Physics.Raycast(new UnityEngine.Vector3(x, b.max.y + 100f, z), UnityEngine.Vector3.down, out h, b.size.y + 500f)) continue; hit++; if (h.point.y < ymin) ymin = h.point.y; if (h.point.y > ymax) ymax = h.point.y; bool inW = InWater(h.point, water); if (inW) { wcell++; continue; } if (h.normal.y >= cosWalk) walk++; if (h.normal.y >= cosFlat) flat++; } if (hit == 0) return null; c.samples = n; c.walkPct = walk / (float)n; c.flatPct = flat / (float)n; c.yMin = ymin; c.yMax = ymax; c.waterCells = wcell; UnityEngine.RaycastHit ch; c.centerY = UnityEngine.Physics.Raycast(new UnityEngine.Vector3(cx, b.max.y + 100f, cz), UnityEngine.Vector3.down, out ch, b.size.y + 500f) ? ch.point.y : -9999f; c.nearWater = 9999f; var center = new UnityEngine.Vector3(cx, c.centerY, cz); foreach (var w in water) { float d = UnityEngine.Mathf.Sqrt(SqDistXZ(center, w)); if (d < c.nearWater) c.nearWater = d; } return c; } static float SqDistXZ(UnityEngine.Vector3 p, UnityEngine.Bounds b) { float dx = UnityEngine.Mathf.Max(0f, UnityEngine.Mathf.Max(b.min.x - p.x, p.x - b.max.x)); float dz = UnityEngine.Mathf.Max(0f, UnityEngine.Mathf.Max(b.min.z - p.z, p.z - b.max.z)); return dx * dx + dz * dz; } static bool InWater(UnityEngine.Vector3 p, List water) { for (int i = 0; i < water.Count; i++) { var b = water[i]; if (p.x < b.min.x || p.x > b.max.x || p.z < b.min.z || p.z > b.max.z) continue; if (p.y < b.max.y + 0.3f) return true; } return false; } static bool IsWater(UnityEngine.Renderer rd) { if (rd.name.ToLowerInvariant().Contains("water")) return true; var ms = rd.sharedMaterials; for (int i = 0; i < ms.Length; i++) { if (ms[i] == null) continue; if (ms[i].name.ToLowerInvariant().Contains("water")) return true; if (ms[i].shader != null && ms[i].shader.name.ToLowerInvariant().Contains("water")) return true; } return false; } static int CountDeep(UnityEngine.Transform t) { int n = 0; for (int i = 0; i < t.childCount; i++) n += 1 + CountDeep(t.GetChild(i)); return n; } static string Path(UnityEngine.Transform t) { var s = t.name; var p = t.parent; while (p != null) { s = p.name + "/" + s; p = p.parent; } return s; } static void ClearDirty() { var t = typeof(UnityEditor.SceneManagement.EditorSceneManager); var mi = t.GetMethod("ClearSceneDirtiness", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); if (mi == null) return; for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++) mi.Invoke(null, new object[] { UnityEngine.SceneManagement.SceneManager.GetSceneAt(i) }); } }