Project_WL/AgentScripts/NatureArea.cs

412 lines
23 KiB
C#
Raw Normal View History

WL 리뉴얼 세션 2026-09-06~07 (#760~#800) — 전투·배경·UI·타격감 (조직 PD 로그 #746~#800 · 대화로그 §14~§24) - 전투: Knight@Attack1~3_S 3콤보(4 컨트롤러·클래스별 서브클립) · 타겟팅 4규칙(정면 우선·재타겟·즉시 공격·어그로 금지 · WLTargetingSettings) · 대쉬 후 공격(Stander@Chase_Start · DashDriver · 5 m · 사정거리+0.8 · 근거리 적 우선) · 공격 이동 FrameTable(발 접지 실측 표 · AttackRootMotion) · 충돌 반경/투사체 0.3 배(WLCollisionTuning) · 무적(임시)·펫 금지(WLGameplaySettings) - 검기: NamuFX Slash_B 배리언트 Effect_WLSwingArc 원 피팅 정합 배치 + 캘리브레이션(SlashArcMeasure · SlashTrailSettings) · 찌르기 Effect_WLStab 대기 · 램프 리본(BladeTrail · WL_BladeRibbon.shader · T_WL_BladeRibbonRamp) 보존(drawRibbon 0) - 타격감: Assets/WL/Feel(WLHitFeel · 히트스톱 0.03 · 셰이크 0.10 m · 몹 펀치 1.12 · Actor.Get_Damage 훅 1줄 · 원본 RealCamera 셰이크 결함 대체) - 배경/맵: LMHPOLY Demo_01~10 → WL_Nature01~10(프리팹·씬·NavMesh·스포너·BattleMapConfig) · 물(ToonWaterU) · 포스트 블룸 0.9/0.3 · 잔디(BruteForce·드레싱) 제거 · 마젠타 머티리얼 URP 변환 - UI: 세로 HUD(WL_HUD · 하단 5메뉴 폭 전체 · 채팅/전투 패드 숨김 · WLIngameUiOverride) · Title/TitleInfo 1080×1920 Expand + 배경 높이 fit(WLBackgroundFit) · 로딩 SortOrder_5(WLRawImageAspectSync · 초점표 8장) · Loading1~8 ASTC 6×6 - 도구: AgentScripts/*(LightProbe · WL_MapSwitch · WL760~WL800 프로브/집행/검증 · 상단 사용법 주석) · 에디터 락 프로토콜 파일(staging) - 제외(별도 커밋 예정): Assets/LMHPOLY(703 MB) · Assets/Feel(422 MB) · Assets/Shinabro(300 MB) — 에셋 스토어/구 WL 팩 원본 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:24:25 +00:00
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<string, int>();
int camCount = 0, lightCount = 0;
foreach (var r in roots)
{
var mrs = r.GetComponentsInChildren<UnityEngine.Renderer>(true).Length;
var cams = r.GetComponentsInChildren<UnityEngine.Camera>(true);
var lts = r.GetComponentsInChildren<UnityEngine.Light>(true);
camCount += cams.Length; lightCount += lts.Length;
var comps = new List<string>();
foreach (var c in r.GetComponents<UnityEngine.Component>())
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<UnityEngine.MonoBehaviour>(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<UnityEngine.Light>(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<UnityEngine.Bounds>();
L("== 수역 렌더러 ==");
foreach (var r in roots)
foreach (var rd in r.GetComponentsInChildren<UnityEngine.Renderer>(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<UnityEngine.MeshRenderer>(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<Cand>();
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<UnityEngine.Bounds>();
foreach (var r in roots)
{
bool isTerrain = r.name == "_Terrain" || r.name == "_Water";
foreach (var mr in r.GetComponentsInChildren<UnityEngine.MeshRenderer>(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<UnityEngine.MeshRenderer>(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<Spotc>();
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<KeyValuePair<UnityEngine.Vector3, string>> s_props;
static void CollectProps(UnityEngine.GameObject[] roots)
{
s_props = new List<KeyValuePair<UnityEngine.Vector3, string>>();
foreach (var r in roots)
foreach (var t in r.GetComponentsInChildren<UnityEngine.Transform>(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<UnityEngine.Vector3, string>(t.position, k));
}
L("프리팹 인스턴스 " + s_props.Count + "개 수집");
}
static Cand Evaluate(float cx, float cz, UnityEngine.Bounds b, List<UnityEngine.Bounds> 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<UnityEngine.Bounds> 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) });
}
}