501 lines
30 KiB
C#
501 lines
30 KiB
C#
// 발주서 WL-813t4 — 보스 아레나 평지 후보 실측 (에디트 모드 · Play 0)
|
|
//
|
|
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기)
|
|
// unity command --project-path E:\NerdNavis\WL_wt\systems run_script --file AgentScripts/WL813t4_Measure.cs --entry WL813t4_Measure.Arena --timeout 1800
|
|
//
|
|
// 평가 항목(발주서 §1-1)
|
|
// ① 반경 20 m 평탄도 — 지면 Δy · 최대경사 · 30° 초과 점수
|
|
// ② NavMesh 커버(격자 ≥ 300점 중 몇 점이 NavMesh 위인가)
|
|
// ③ 연결성 — Portal_Start → 중심 PathComplete · 원주 12점 → 중심 ≥ 10/12 · 존1~4 스포너 → 중심
|
|
// ④ 813t2 y 컷 — 반경 20 m NavMesh 최고 y ≤ 2.96
|
|
// ⑤ 반경 20 m 안 물 렌더러 · 큰 바위 수
|
|
// ⑥ 스테이지 배치 산포(황금각) 슬롯이 전부 NavMesh 로 해결되는가
|
|
//
|
|
// 🔴 임시 씬을 만들어 프리팹을 인스턴스화한다(저장 0). NavMesh 자산은 AddNavMeshData 로 잠깐 올렸다 내린다.
|
|
|
|
public static class WL813t4_Measure
|
|
{
|
|
const string kMapPrefab = "Assets/Res_Addr/Map/WL_Nature.prefab";
|
|
const string kNavAsset = "Assets/WL/Settings/WL_Nature_NavMesh.asset";
|
|
const string kOut = "AgentScripts/WL813t4_MEASURE.txt";
|
|
|
|
const float kRadius = 20f; // 아레나 반경
|
|
const float kStep = 2f; // 격자 간격 → 반경 20 m 안 317점
|
|
const float kYCut = 2.96f; // 813t2 · 813t3 실측 walkable 최고 y
|
|
|
|
static System.Text.StringBuilder s;
|
|
static void L(string t) { s.AppendLine(t); UnityEngine.Debug.Log("[WL813t4] " + t); }
|
|
|
|
static string Abs(string rel)
|
|
{
|
|
return System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(),
|
|
rel.Replace('/', System.IO.Path.DirectorySeparatorChar));
|
|
}
|
|
|
|
static string Flush()
|
|
{
|
|
try { System.IO.File.WriteAllText(Abs(kOut), s.ToString(), new System.Text.UTF8Encoding(true)); }
|
|
catch (System.Exception e) { UnityEngine.Debug.LogWarning("[WL813t4] 로그 저장 실패: " + e.Message); }
|
|
return s.ToString();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
public static string Arena()
|
|
{
|
|
s = new System.Text.StringBuilder();
|
|
s_rows.Clear();
|
|
L("===== WL-813t4 아레나 평지 실측 · " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " =====");
|
|
|
|
UnityEditor.AssetDatabase.ImportAsset(kNavAsset, UnityEditor.ImportAssetOptions.ForceUpdate);
|
|
var data = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.AI.NavMeshData>(kNavAsset);
|
|
if (data == null) { L("[에러] NavMesh 자산 로드 실패"); return Flush(); }
|
|
long bytes = 0; try { bytes = new System.IO.FileInfo(Abs(kNavAsset)).Length; } catch { }
|
|
var bs = UnityEngine.AI.NavMesh.GetSettingsByID(0);
|
|
L("NavMesh " + bytes + " B · Agent0 slope " + bs.agentSlope + " · climb " + bs.agentClimb + " · voxel " + bs.voxelSize.ToString("F5"));
|
|
|
|
var cfg = UnityEngine.Resources.Load<WL.Combat.Boss.WLBossArenaSettings>("WL/WLBossArenaSettings");
|
|
var tbl = UnityEngine.Resources.Load<WL.Combat.Stage.WLStageTable>("WL/WLStageTable");
|
|
var tg = UnityEngine.Resources.Load<WL.Combat.Nav.WLTerrainGuardSettings>("WL/WLTerrainGuardSettings");
|
|
L("현재 SO — arenaCenter " + (cfg != null ? cfg.arenaCenter.ToString("F2") : "?")
|
|
+ " · useFieldBossPositionAsCenter " + (cfg != null ? (cfg.useFieldBossPositionAsCenter ? "1" : "0") : "?")
|
|
+ " · arenaRadius " + (cfg != null ? cfg.arenaRadius.ToString("F0") : "?")
|
|
+ " · maxWalkableY " + (tg != null ? tg.maxWalkableY.ToString("F2") : "?"));
|
|
if (tbl != null)
|
|
{
|
|
var d5 = tbl.Get(4); var d10 = tbl.Get(9);
|
|
L("현재 표 — 스테이지5 앵커 " + (d5 != null ? d5.anchor.ToString("F2") + " r" + d5.barrierRadius.ToString("F0") : "?")
|
|
+ " · 스테이지10 앵커 " + (d10 != null ? d10.anchor.ToString("F2") + " r" + d10.barrierRadius.ToString("F0") : "?"));
|
|
}
|
|
|
|
var prefab = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(kMapPrefab);
|
|
if (prefab == null) { L("[에러] 프리팹 로드 실패"); return Flush(); }
|
|
|
|
UnityEditor.SceneManagement.EditorSceneManager.NewScene(
|
|
UnityEditor.SceneManagement.NewSceneSetup.EmptyScene, UnityEditor.SceneManagement.NewSceneMode.Single);
|
|
var go = UnityEditor.PrefabUtility.InstantiatePrefab(prefab) as UnityEngine.GameObject;
|
|
if (go == null) { L("[에러] 인스턴스화 실패"); return Flush(); }
|
|
UnityEngine.Physics.SyncTransforms();
|
|
|
|
var inst = UnityEngine.AI.NavMesh.AddNavMeshData(data);
|
|
try
|
|
{
|
|
var cands = new System.Collections.Generic.List<UnityEngine.Vector3>();
|
|
if (s_explicit != null && s_explicit.Count > 0) cands.AddRange(s_explicit);
|
|
else
|
|
{
|
|
cands.Add(new UnityEngine.Vector3(179f, 0.01f, 4f)); // 813t3 실측 최적지(1순위)
|
|
cands.Add(new UnityEngine.Vector3(139f, 0.8f, -20f)); // 현재 아레나(대조)
|
|
|
|
// 차선 후보 2곳 — 813t3 WideFlatSearch 와 같은 잣대로 다시 훑어 1순위 다음 2개를 뽑는다
|
|
var wide = WideSearch();
|
|
int added = 0;
|
|
for (int i = 0; i < wide.Count && added < 2; i++)
|
|
{
|
|
bool dup = false;
|
|
for (int j = 0; j < cands.Count; j++)
|
|
if (UnityEngine.Vector3.Distance(wide[i], cands[j]) < 12f) { dup = true; break; }
|
|
if (dup) continue;
|
|
cands.Add(wide[i]); added++;
|
|
}
|
|
}
|
|
|
|
var start = ResolveStart(go);
|
|
for (int i = 0; i < cands.Count; i++) Evaluate(go, cands[i], start, i);
|
|
|
|
L("");
|
|
L("── 요약 표 (반경 " + kRadius + " m · 격자 " + kStep + " m) ──");
|
|
L("| 후보 | 지면 Δy | 최대경사 | 30°초과 | NavMesh 커버 | NavMesh y | y컷 " + kYCut + " 초과 | 시작→중심 | 원주12→중심 | 존→중심 | 물 | 큰바위 | 판정 |");
|
|
for (int i = 0; i < s_rows.Count; i++) L(s_rows[i]);
|
|
}
|
|
finally
|
|
{
|
|
UnityEngine.AI.NavMesh.RemoveNavMeshData(inst);
|
|
UnityEngine.Object.DestroyImmediate(go);
|
|
}
|
|
L("");
|
|
L("===== 끝 =====");
|
|
return Flush();
|
|
}
|
|
|
|
static System.Collections.Generic.List<string> s_rows = new System.Collections.Generic.List<string>();
|
|
static System.Collections.Generic.List<UnityEngine.Vector3> s_explicit;
|
|
|
|
/// <summary>결선 4곳 전수 평가(정밀 탐색 상위 + 813t3 최적지 + 현 아레나).</summary>
|
|
public static string Final()
|
|
{
|
|
s_explicit = new System.Collections.Generic.List<UnityEngine.Vector3>();
|
|
s_explicit.Add(new UnityEngine.Vector3(179f, 0.01f, 4f)); // 813t3 최적지 · 발주서 1순위
|
|
s_explicit.Add(new UnityEngine.Vector3(183f, -0.07f, 16f)); // 정밀 탐색 3위(커버 316/317)
|
|
s_explicit.Add(new UnityEngine.Vector3(185f, -0.18f, 14f)); // 정밀 탐색 1위(Δy 최소)
|
|
s_explicit.Add(new UnityEngine.Vector3(139f, 0.8f, -20f)); // 현 아레나(대조)
|
|
try { return Arena(); } finally { s_explicit = null; }
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
static UnityEngine.Vector3 ResolveStart(UnityEngine.GameObject root)
|
|
{
|
|
var portal = FindByName(root.transform, "Portal_Start");
|
|
var p = portal != null ? portal.position : new UnityEngine.Vector3(115f, 0f, 9.98f);
|
|
L("");
|
|
L("시작점 Portal_Start = " + p.ToString("F2") + (portal != null ? "" : " (씬에 없어 발주서 좌표 사용)"));
|
|
return p;
|
|
}
|
|
|
|
/// <summary>후보 1곳 전수 평가.</summary>
|
|
static void Evaluate(UnityEngine.GameObject root, UnityEngine.Vector3 want, UnityEngine.Vector3 start, int idx)
|
|
{
|
|
L("");
|
|
L("──────── 후보 #" + (idx + 1) + " " + want.ToString("F2") + " ────────");
|
|
|
|
UnityEngine.AI.NavMeshHit ch;
|
|
if (!UnityEngine.AI.NavMesh.SamplePosition(want, out ch, 5f, UnityEngine.AI.NavMesh.AllAreas))
|
|
{
|
|
L(" 🔴 NavMesh 밖(5 m) — 아레나 불가");
|
|
s_rows.Add("| " + want.ToString("F2") + " | — | — | — | **NavMesh 밖** | — | — | — | — | — | — | — | 🔴 불가 |");
|
|
return;
|
|
}
|
|
L(" NavMesh 스냅 " + ch.position.ToString("F2") + " · Δ" + UnityEngine.Vector3.Distance(want, ch.position).ToString("F2") + " m");
|
|
|
|
// ① 평탄도 + ② NavMesh 커버 + ④ y 컷
|
|
int n = 0, onMesh = 0, steep = 0, noGround = 0, aboveCut = 0;
|
|
float gyMin = 1e9f, gyMax = -1e9f, myMin = 1e9f, myMax = -1e9f, slopeMax = 0f;
|
|
double slopeSum = 0.0;
|
|
int half = UnityEngine.Mathf.CeilToInt(kRadius / kStep);
|
|
for (int a = -half; a <= half; a++)
|
|
for (int b = -half; b <= half; b++)
|
|
{
|
|
float dx = a * kStep, dz = b * kStep;
|
|
if (dx * dx + dz * dz > kRadius * kRadius) continue;
|
|
n++;
|
|
var p = new UnityEngine.Vector3(ch.position.x + dx, ch.position.y, ch.position.z + dz);
|
|
UnityEngine.RaycastHit rh;
|
|
if (UnityEngine.Physics.Raycast(new UnityEngine.Vector3(p.x, p.y + 80f, p.z), UnityEngine.Vector3.down, out rh, 240f))
|
|
{
|
|
if (rh.point.y < gyMin) gyMin = rh.point.y;
|
|
if (rh.point.y > gyMax) gyMax = rh.point.y;
|
|
float ang = UnityEngine.Vector3.Angle(rh.normal, UnityEngine.Vector3.up);
|
|
slopeSum += ang; if (ang > slopeMax) slopeMax = ang;
|
|
if (ang > 30f)
|
|
{
|
|
steep++;
|
|
if (steep <= 6) L(" [30° 초과] " + rh.point.ToString("F2") + " · " + ang.ToString("F1")
|
|
+ "° · 중심에서 " + UnityEngine.Mathf.Sqrt(dx * dx + dz * dz).ToString("F1")
|
|
+ " m · " + (rh.collider != null ? FullName(rh.collider.transform) : "?"));
|
|
}
|
|
}
|
|
else noGround++;
|
|
UnityEngine.AI.NavMeshHit h;
|
|
if (UnityEngine.AI.NavMesh.SamplePosition(p, out h, 2f, UnityEngine.AI.NavMesh.AllAreas))
|
|
{
|
|
onMesh++;
|
|
if (h.position.y < myMin) myMin = h.position.y;
|
|
if (h.position.y > myMax) myMax = h.position.y;
|
|
if (h.position.y > kYCut) aboveCut++;
|
|
}
|
|
}
|
|
float span = (gyMax > -1e8f) ? (gyMax - gyMin) : 99f;
|
|
L(" ① 평탄도 — 격자 " + n + "점 · 지면 y " + gyMin.ToString("F2") + " ~ " + gyMax.ToString("F2")
|
|
+ " (Δ " + span.ToString("F2") + " m) · 경사 평균 " + (n > noGround ? (slopeSum / (n - noGround)).ToString("F1") : "?")
|
|
+ "° 최대 " + slopeMax.ToString("F1") + "° · **30° 초과 " + steep + "/" + n + "** · 지면 없음 " + noGround);
|
|
L(" ② NavMesh 커버 — **" + onMesh + "/" + n + "**(≥300 필요) · y " + (myMin > 1e8f ? "없음" : myMin.ToString("F2") + " ~ " + myMax.ToString("F2")));
|
|
L(" ④ y 컷 " + kYCut + " 초과 NavMesh 점 — **" + aboveCut + "**");
|
|
|
|
// ③ 연결성
|
|
UnityEngine.AI.NavMeshHit sh;
|
|
string startPath = "시작점 NavMesh 밖";
|
|
if (UnityEngine.AI.NavMesh.SamplePosition(start, out sh, 5f, UnityEngine.AI.NavMesh.AllAreas))
|
|
{
|
|
var p1 = new UnityEngine.AI.NavMeshPath();
|
|
UnityEngine.AI.NavMesh.CalculatePath(sh.position, ch.position, UnityEngine.AI.NavMesh.AllAreas, p1);
|
|
startPath = p1.status.ToString();
|
|
}
|
|
int okr = 0, tried = 0;
|
|
for (int i = 0; i < 12; i++)
|
|
{
|
|
float ang = i * UnityEngine.Mathf.PI * 2f / 12f;
|
|
var rp = ch.position + new UnityEngine.Vector3(UnityEngine.Mathf.Cos(ang), 0f, UnityEngine.Mathf.Sin(ang)) * (kRadius * 0.8f);
|
|
UnityEngine.AI.NavMeshHit rh2;
|
|
if (!UnityEngine.AI.NavMesh.SamplePosition(rp, out rh2, 5f, UnityEngine.AI.NavMesh.AllAreas)) continue;
|
|
tried++;
|
|
var p3 = new UnityEngine.AI.NavMeshPath();
|
|
UnityEngine.AI.NavMesh.CalculatePath(rh2.position, ch.position, UnityEngine.AI.NavMesh.AllAreas, p3);
|
|
if (p3.status == UnityEngine.AI.NavMeshPathStatus.PathComplete) okr++;
|
|
}
|
|
L(" ③ 연결성 — 시작 " + start.ToString("F2") + " → 중심 **" + startPath + "** · 원주 12점(r " + (kRadius * 0.8f).ToString("F0") + ") → 중심 **" + okr + "/12**(NavMesh 위 " + tried + ")");
|
|
|
|
// 존 1~4(존 스포너 813001~813004) + 전체 스포너
|
|
var mgrs = root.GetComponentsInChildren<MobControlMgr>(true);
|
|
int zoneOk = 0, zoneTot = 0; string zoneDetail = "";
|
|
int allOk = 0, allTot = 0;
|
|
for (int i = 0; i < mgrs.Length; i++)
|
|
{
|
|
var p = mgrs[i].transform.position;
|
|
UnityEngine.AI.NavMeshHit h;
|
|
if (!UnityEngine.AI.NavMesh.SamplePosition(p, out h, 5f, UnityEngine.AI.NavMesh.AllAreas)) continue;
|
|
var path = new UnityEngine.AI.NavMeshPath();
|
|
UnityEngine.AI.NavMesh.CalculatePath(h.position, ch.position, UnityEngine.AI.NavMesh.AllAreas, path);
|
|
bool ok = path.status == UnityEngine.AI.NavMeshPathStatus.PathComplete;
|
|
allTot++; if (ok) allOk++;
|
|
int sid = mgrs[i].SpawnerID;
|
|
if (sid >= 813001 && sid <= 813004)
|
|
{
|
|
zoneTot++; if (ok) zoneOk++;
|
|
zoneDetail += " #" + sid + "=" + path.status;
|
|
}
|
|
}
|
|
L(" ③ 존 스포너(813001~813004) → 중심 : **" + zoneOk + "/" + zoneTot + "**" + zoneDetail);
|
|
L(" ③ 전체 스포너 " + allTot + "기 → 중심 PathComplete " + allOk);
|
|
|
|
// ⑤ 물 · 큰 바위 — 바운즈 XZ 최근접점 거리로 판정(AABB 중심 거리 = 강처럼 긴 메시에서 오판)
|
|
int water = 0, rocks = 0; string worst = "";
|
|
float rockMax = 0f, waterNear = 1e9f;
|
|
Overlap(root, ch.position, kRadius, ref water, ref rocks, ref worst, ref rockMax, ref waterNear, true);
|
|
L(" ⑤ 반경 " + kRadius + " m 겹침(XZ 최근접) — 물 **" + water + "**(가장 가까운 물까지 " + waterNear.ToString("F1")
|
|
+ " m) · 큰 바위(최대변 ≥3 m) **" + rocks + "**" + (rocks > 0 ? " · 최대 " + worst : ""));
|
|
|
|
// ⑥ 스테이지 배치 산포 슬롯(황금각 · 스테이지 10 = 15마리 기준) NavMesh 해결률
|
|
int slots = 15, slotOk = 0;
|
|
float rMax = kRadius * 0.7f;
|
|
for (int slot = 0; slot < slots; slot++)
|
|
{
|
|
float ang = slot * 2.399963f;
|
|
float rad = rMax * UnityEngine.Mathf.Sqrt((slot + 0.5f) / slots);
|
|
var p = ch.position + new UnityEngine.Vector3(UnityEngine.Mathf.Cos(ang) * rad, 0f, UnityEngine.Mathf.Sin(ang) * rad);
|
|
UnityEngine.AI.NavMeshHit h;
|
|
if (UnityEngine.AI.NavMesh.SamplePosition(p, out h, 4f, UnityEngine.AI.NavMesh.AllAreas) && h.position.y <= kYCut) slotOk++;
|
|
}
|
|
L(" ⑥ 배치 산포 슬롯 15개(황금각 · r " + rMax.ToString("F1") + ") NavMesh+y컷 해결 **" + slotOk + "/15**");
|
|
|
|
bool pass = steep == 0 && onMesh >= 300 && startPath == "PathComplete" && okr >= 10
|
|
&& aboveCut == 0 && water == 0 && rocks == 0 && zoneOk == zoneTot && zoneTot > 0;
|
|
L(" ▶ 판정 = " + (pass ? "PASS" : "미달") + " (평탄 30°초과 " + steep + " · 커버 " + onMesh + " · 시작 " + startPath
|
|
+ " · 원주 " + okr + "/12 · y컷초과 " + aboveCut + " · 물 " + water + " · 존 " + zoneOk + "/" + zoneTot + ")");
|
|
|
|
s_rows.Add("| " + want.ToString("F2") + " | " + span.ToString("F2") + " m | " + slopeMax.ToString("F1") + "° | "
|
|
+ steep + "/" + n + " | " + onMesh + "/" + n + " | " + (myMin > 1e8f ? "—" : myMin.ToString("F2") + "~" + myMax.ToString("F2"))
|
|
+ " | " + aboveCut + " | " + startPath + " | " + okr + "/12 | " + zoneOk + "/" + zoneTot + " | " + water + " | " + rocks
|
|
+ " | " + (pass ? "**PASS**" : "미달") + " |");
|
|
}
|
|
|
|
/// <summary>813t3 WideFlatSearch 와 같은 잣대(±60 m · 4 m 격자 · 반경 20 m 전체 평가) 상위 후보 좌표.</summary>
|
|
static System.Collections.Generic.List<UnityEngine.Vector3> WideSearch()
|
|
{
|
|
L("");
|
|
L("── 차선 후보 탐색 (813t3 WL813t3_Measure.WideFlatSearch 잣대 재사용 · ±60 m · 4 m 격자) ──");
|
|
var list = new System.Collections.Generic.List<Cand>();
|
|
for (int i = -15; i <= 15; i++)
|
|
for (int j = -15; j <= 15; j++)
|
|
{
|
|
var c = new UnityEngine.Vector3(139f + i * 4f, 0.8f, -20f + j * 4f);
|
|
UnityEngine.AI.NavMeshHit ch;
|
|
if (!UnityEngine.AI.NavMesh.SamplePosition(c, out ch, 2f, UnityEngine.AI.NavMesh.AllAreas)) continue;
|
|
int n = 0, on = 0, steep = 0; float ymin = 1e9f, ymax = -1e9f, smax = 0f;
|
|
for (int a = -5; a <= 5; a++)
|
|
for (int b = -5; b <= 5; b++)
|
|
{
|
|
float dx = a * 4f, dz = b * 4f;
|
|
if (dx * dx + dz * dz > 400f) continue;
|
|
n++;
|
|
var p = new UnityEngine.Vector3(ch.position.x + dx, ch.position.y, ch.position.z + dz);
|
|
UnityEngine.RaycastHit rh;
|
|
if (UnityEngine.Physics.Raycast(new UnityEngine.Vector3(p.x, p.y + 80f, p.z), UnityEngine.Vector3.down, out rh, 240f))
|
|
{
|
|
if (rh.point.y < ymin) ymin = rh.point.y;
|
|
if (rh.point.y > ymax) ymax = rh.point.y;
|
|
float ang = UnityEngine.Vector3.Angle(rh.normal, UnityEngine.Vector3.up);
|
|
if (ang > smax) smax = ang;
|
|
if (ang > 30f) steep++;
|
|
}
|
|
UnityEngine.AI.NavMeshHit h2;
|
|
if (UnityEngine.AI.NavMesh.SamplePosition(p, out h2, 2f, UnityEngine.AI.NavMesh.AllAreas)) on++;
|
|
}
|
|
if (n == 0 || on < n * 0.9f) continue;
|
|
float span = (ymax > -1e8f) ? (ymax - ymin) : 99f;
|
|
list.Add(new Cand { p = ch.position, span = span, smax = smax, steep = steep, n = n, on = on });
|
|
}
|
|
list.Sort(delegate (Cand a, Cand b) { return (a.span * 2f + a.steep).CompareTo(b.span * 2f + b.steep); });
|
|
L(" 후보(NavMesh 90 % 이상) " + list.Count + "개 · 상위 6");
|
|
var outp = new System.Collections.Generic.List<UnityEngine.Vector3>();
|
|
for (int i = 0; i < list.Count && i < 6; i++)
|
|
{
|
|
var c = list[i];
|
|
L(" " + (i + 1) + ") " + c.p.ToString("F2") + " · 지면 Δy " + c.span.ToString("F2") + " m · 최대경사 " + c.smax.ToString("F1")
|
|
+ "° · 30° 초과 " + c.steep + "/" + c.n + " · NavMesh " + c.on + "/" + c.n
|
|
+ " · 현 아레나에서 " + UnityEngine.Vector3.Distance(c.p, new UnityEngine.Vector3(139f, 0.8f, -20f)).ToString("F1") + " m");
|
|
outp.Add(c.p);
|
|
}
|
|
return outp;
|
|
}
|
|
|
|
struct Cand { public UnityEngine.Vector3 p; public float span, smax; public int steep, n, on; }
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// 겹침 판정 — 렌더러 바운즈의 XZ 최근접점 거리 (강처럼 긴 메시의 오판을 없앤다)
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
static void Overlap(UnityEngine.GameObject root, UnityEngine.Vector3 c, float r,
|
|
ref int water, ref int rocks, ref string worst, ref float rockMax, ref float waterNear, bool verbose)
|
|
{
|
|
var rends = root.GetComponentsInChildren<UnityEngine.Renderer>(true);
|
|
for (int i = 0; i < rends.Length; i++)
|
|
{
|
|
var rd = rends[i];
|
|
if (rd == null || !rd.enabled) continue;
|
|
var b = rd.bounds;
|
|
float cx = UnityEngine.Mathf.Clamp(c.x, b.min.x, b.max.x);
|
|
float cz = UnityEngine.Mathf.Clamp(c.z, b.min.z, b.max.z);
|
|
float d = UnityEngine.Mathf.Sqrt((cx - c.x) * (cx - c.x) + (cz - c.z) * (cz - c.z));
|
|
string nm = FullName(rd.transform);
|
|
bool isWater = nm.IndexOf("water", System.StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| nm.IndexOf("river", System.StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| nm.IndexOf("lake", System.StringComparison.OrdinalIgnoreCase) >= 0;
|
|
if (isWater) { if (d < waterNear) waterNear = d; if (d <= r) { water++; if (verbose && water <= 3) L(" [물] " + nm + " XZ 거리 " + d.ToString("F1") + " m · 바운즈 " + b.center.ToString("F1") + " 크기 " + b.size.ToString("F1")); } continue; }
|
|
if (d > r) continue;
|
|
bool isRock = nm.IndexOf("rock", System.StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| nm.IndexOf("stone", System.StringComparison.OrdinalIgnoreCase) >= 0
|
|
|| nm.IndexOf("cliff", System.StringComparison.OrdinalIgnoreCase) >= 0;
|
|
if (!isRock) continue;
|
|
float big = UnityEngine.Mathf.Max(b.size.x, UnityEngine.Mathf.Max(b.size.y, b.size.z));
|
|
if (big < 3f) continue;
|
|
rocks++;
|
|
if (verbose && rocks <= 4) L(" [큰 바위] " + nm + " XZ 거리 " + d.ToString("F1") + " m · 중심 " + b.center.ToString("F1") + " 크기 " + b.size.ToString("F1"));
|
|
if (big > rockMax) { rockMax = big; worst = nm + " " + b.center.ToString("F1") + " 크기 " + b.size.ToString("F1") + " · XZ " + d.ToString("F1") + " m"; }
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Refine — (179, 0.01, 4) 둘레 ±12 m 를 2 m 격자로 훑어 「30° 초과 0 · 큰 바위 0 · NavMesh ≥300」 을 찾는다
|
|
// unity command run_script … --entry WL813t4_Measure.Refine
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
public static string Refine()
|
|
{
|
|
s = new System.Text.StringBuilder();
|
|
L("===== WL-813t4 정밀 탐색 · " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " =====");
|
|
|
|
UnityEditor.AssetDatabase.ImportAsset(kNavAsset, UnityEditor.ImportAssetOptions.ForceUpdate);
|
|
var data = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.AI.NavMeshData>(kNavAsset);
|
|
var prefab = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(kMapPrefab);
|
|
if (data == null || prefab == null) { L("[에러] 자산 로드 실패"); return Flush2(); }
|
|
|
|
UnityEditor.SceneManagement.EditorSceneManager.NewScene(
|
|
UnityEditor.SceneManagement.NewSceneSetup.EmptyScene, UnityEditor.SceneManagement.NewSceneMode.Single);
|
|
var go = UnityEditor.PrefabUtility.InstantiatePrefab(prefab) as UnityEngine.GameObject;
|
|
UnityEngine.Physics.SyncTransforms();
|
|
var inst = UnityEngine.AI.NavMesh.AddNavMeshData(data);
|
|
try
|
|
{
|
|
var baseP = new UnityEngine.Vector3(179f, 0.01f, 4f);
|
|
L("기준 " + baseP.ToString("F2") + " 둘레 ±12 m · 2 m 격자 · 각 후보 반경 " + kRadius + " m 를 " + kStep + " m 격자로 평가");
|
|
L("| 중심 | 지면 Δy | 최대경사 | 30°초과 | NavMesh 커버 | y컷초과 | 큰바위 | 물까지 | 원주12 | 기준에서 |");
|
|
var best = new System.Collections.Generic.List<Row>();
|
|
for (int i = -6; i <= 6; i++)
|
|
for (int j = -6; j <= 6; j++)
|
|
{
|
|
var want = new UnityEngine.Vector3(baseP.x + i * 2f, baseP.y, baseP.z + j * 2f);
|
|
UnityEngine.AI.NavMeshHit ch;
|
|
if (!UnityEngine.AI.NavMesh.SamplePosition(want, out ch, 3f, UnityEngine.AI.NavMesh.AllAreas)) continue;
|
|
|
|
int n = 0, on = 0, steep = 0, above = 0;
|
|
float ymin = 1e9f, ymax = -1e9f, smax = 0f;
|
|
int half = UnityEngine.Mathf.CeilToInt(kRadius / kStep);
|
|
for (int a = -half; a <= half; a++)
|
|
for (int b = -half; b <= half; b++)
|
|
{
|
|
float dx = a * kStep, dz = b * kStep;
|
|
if (dx * dx + dz * dz > kRadius * kRadius) continue;
|
|
n++;
|
|
var p = new UnityEngine.Vector3(ch.position.x + dx, ch.position.y, ch.position.z + dz);
|
|
UnityEngine.RaycastHit rh;
|
|
if (UnityEngine.Physics.Raycast(new UnityEngine.Vector3(p.x, p.y + 80f, p.z), UnityEngine.Vector3.down, out rh, 240f))
|
|
{
|
|
if (rh.point.y < ymin) ymin = rh.point.y;
|
|
if (rh.point.y > ymax) ymax = rh.point.y;
|
|
float ang = UnityEngine.Vector3.Angle(rh.normal, UnityEngine.Vector3.up);
|
|
if (ang > smax) smax = ang;
|
|
if (ang > 30f) steep++;
|
|
}
|
|
UnityEngine.AI.NavMeshHit h;
|
|
if (UnityEngine.AI.NavMesh.SamplePosition(p, out h, 2f, UnityEngine.AI.NavMesh.AllAreas))
|
|
{ on++; if (h.position.y > kYCut) above++; }
|
|
}
|
|
if (on < 300) continue;
|
|
|
|
int water = 0, rocks = 0; string worst = ""; float rockMax = 0f, waterNear = 1e9f;
|
|
Overlap(go, ch.position, kRadius, ref water, ref rocks, ref worst, ref rockMax, ref waterNear, false);
|
|
|
|
int okr = 0;
|
|
for (int k = 0; k < 12; k++)
|
|
{
|
|
float ang2 = k * UnityEngine.Mathf.PI * 2f / 12f;
|
|
var rp = ch.position + new UnityEngine.Vector3(UnityEngine.Mathf.Cos(ang2), 0f, UnityEngine.Mathf.Sin(ang2)) * (kRadius * 0.8f);
|
|
UnityEngine.AI.NavMeshHit rh2;
|
|
if (!UnityEngine.AI.NavMesh.SamplePosition(rp, out rh2, 5f, UnityEngine.AI.NavMesh.AllAreas)) continue;
|
|
var p3 = new UnityEngine.AI.NavMeshPath();
|
|
UnityEngine.AI.NavMesh.CalculatePath(rh2.position, ch.position, UnityEngine.AI.NavMesh.AllAreas, p3);
|
|
if (p3.status == UnityEngine.AI.NavMeshPathStatus.PathComplete) okr++;
|
|
}
|
|
|
|
best.Add(new Row
|
|
{
|
|
p = ch.position, span = ymax - ymin, smax = smax, steep = steep, on = on, n = n,
|
|
above = above, rocks = rocks, water = water, waterNear = waterNear, okr = okr
|
|
});
|
|
}
|
|
// 정렬 = 30° 초과 → 큰 바위 → 원주 실패 → Δy
|
|
best.Sort(delegate (Row a, Row b)
|
|
{
|
|
if (a.steep != b.steep) return a.steep.CompareTo(b.steep);
|
|
if (a.rocks != b.rocks) return a.rocks.CompareTo(b.rocks);
|
|
if (a.okr != b.okr) return b.okr.CompareTo(a.okr);
|
|
return a.span.CompareTo(b.span);
|
|
});
|
|
L(" NavMesh 커버 ≥300 후보 " + best.Count + "개 · 상위 10");
|
|
for (int i = 0; i < best.Count && i < 10; i++)
|
|
{
|
|
var r = best[i];
|
|
L("| " + r.p.ToString("F2") + " | " + r.span.ToString("F2") + " m | " + r.smax.ToString("F1") + "° | "
|
|
+ r.steep + "/" + r.n + " | " + r.on + "/" + r.n + " | " + r.above + " | " + r.rocks + " | "
|
|
+ r.waterNear.ToString("F1") + " m | " + r.okr + "/12 | "
|
|
+ UnityEngine.Vector3.Distance(r.p, baseP).ToString("F1") + " m |");
|
|
}
|
|
for (int i = 0; i < best.Count; i++)
|
|
if (UnityEngine.Vector3.Distance(best[i].p, baseP) < 0.5f)
|
|
{ L(" (813t3 최적지 " + baseP.ToString("F2") + ") 순위 " + (i + 1) + "/" + best.Count + " · 30° 초과 " + best[i].steep + " · 큰 바위 " + best[i].rocks); break; }
|
|
}
|
|
finally
|
|
{
|
|
UnityEngine.AI.NavMesh.RemoveNavMeshData(inst);
|
|
UnityEngine.Object.DestroyImmediate(go);
|
|
}
|
|
L("===== 끝 =====");
|
|
return Flush2();
|
|
}
|
|
|
|
struct Row { public UnityEngine.Vector3 p; public float span, smax, waterNear; public int steep, on, n, above, rocks, water, okr; }
|
|
|
|
static string Flush2()
|
|
{
|
|
try { System.IO.File.WriteAllText(Abs("AgentScripts/WL813t4_REFINE.txt"), s.ToString(), new System.Text.UTF8Encoding(true)); }
|
|
catch (System.Exception e) { UnityEngine.Debug.LogWarning("[WL813t4] 저장 실패: " + e.Message); }
|
|
return s.ToString();
|
|
}
|
|
|
|
static string FullName(UnityEngine.Transform t)
|
|
{
|
|
string p = t.name;
|
|
int guard = 0;
|
|
while (t.parent != null && guard++ < 32) { t = t.parent; p = t.name + "/" + p; }
|
|
return p;
|
|
}
|
|
|
|
static UnityEngine.Transform FindByName(UnityEngine.Transform t, string name)
|
|
{
|
|
if (t.name == name) return t;
|
|
for (int i = 0; i < t.childCount; i++)
|
|
{
|
|
var r = FindByName(t.GetChild(i), name);
|
|
if (r != null) return r;
|
|
}
|
|
return null;
|
|
}
|
|
}
|