// PD 지시 #750 — 구 WL BigIsland 맵을 힘민지 시작 맵(WL_Start)으로 변환하는 빌더 // PD 지시 #771 2단계 (2026-09-06) — 경로·이름·스폰지점·NavMesh 를 **인자화**했다. // · WL_StartMapBuilder.Run = 기존 WL_Start 동작 그대로 (기본값 복원 후 실행 · 동작 무변경) // · WL_StartMapBuilder.RunFor(...) = 임의의 원본 씬 → 임의의 맵 프리팹/씬 생성 // // 실행 // WL_Start (기존): unity command run_script --file AgentScripts/WL_StartMapBuilder.cs --entry WL_StartMapBuilder.Run // 임의 맵 : ... --entry WL_StartMapBuilder.RunFor --args '["","","","","","", x,y,z, camYaw, bakeNav, groupLights]' // // CLAUDE.md §1 준수: 네임스페이스 풀어 쓰기 // // 🔴 실행 전 필독 // 1) PD 가 Play 중이면 실행 금지 (씬을 열고 저장한다). // 2) 대상 프리팹/씬을 덮어쓰므로 백업 후 실행한다. // // 산출물 // 맵 프리팹 (MapData 포함) // Lobby.unity 복제 + LoadMapMgr 필드 변경 // /.asset NavMeshSurface 베이크 결과 (bakeNav 시) public static class WL_StartMapBuilder { // ───────────────────────────────────────────────────────────── // 기본값 = WL_Start (#750). Run() 은 항상 이 값으로 되돌린 뒤 실행한다. // ───────────────────────────────────────────────────────────── const string DEF_SRC_SCENE = "Assets/RPG Tiny Fantasy Forest PBR/Scene/BigIsland.unity"; const string DEF_DST_SCENE = "Assets/Scenes/WL_Start.unity"; const string DEF_DST_PREFAB = "Assets/Res_Addr/Map/WL_Start.prefab"; const string DEF_ROOT_NAME = "WL_Start"; const string DEF_SCENE_NAME = "WL_Start"; const string DEF_NAV_ASSET = "WL_Start_NavMesh"; const string DEF_LOG_DIR = "AgentScripts/staging/WL_Start"; static bool kDryRun = false; static bool kBuildPrefab = true; static bool kBuildScene = true; static bool kRegisterInBuild = true; static bool kBakeNavMesh = true; static bool kCopySceneAmbient = false; static bool kGroupLights = false; // true = 라이트를 'SceneAssets' 아래로 모은다 (#771) static bool kSpawnOverride = false; // true = StartPoint 대신 지정 좌표 사용 (#771) static UnityEngine.Vector3 kSpawnPos = UnityEngine.Vector3.zero; static float kCamYaw = 0f; static bool kCamYawSet = false; static float kNavMinRegionArea = -1f; // <0 이면 NavMeshSurface 기본값 유지 static bool kNavVolume = false; // true = collectObjects=Volume (center/size 사용) static UnityEngine.Vector3 kNavCenter = UnityEngine.Vector3.zero; static UnityEngine.Vector3 kNavSize = new UnityEngine.Vector3(300f, 200f, 300f); static string kSrcScene = DEF_SRC_SCENE; static string kLobbyScene = "Assets/Scenes/Lobby.unity"; static string kDstScene = DEF_DST_SCENE; static string kDstPrefab = DEF_DST_PREFAB; static string kNavMeshDir = "Assets/WL/Settings"; static string kNavAssetName = DEF_NAV_ASSET; static string kRootName = DEF_ROOT_NAME; static string kSceneName = DEF_SCENE_NAME; static string kLogDir = DEF_LOG_DIR; // 구 WL 전용 — 프리팹에 넣지 않는다. 이름 완전 일치. static readonly string[] kExcludeNames = new string[] { "PlayerSwitcher", // 구 WL 플레이어 전환기 "PlayerProgress", // 구 WL 진행도 "Main Camera", // 힘민지는 RealCamera 를 쓴다 "WL_NavMesh", // NavMeshSurface — 별도 재베이크 (§NavMesh) "WL_GlobalVolume", // 구 WL 포스트프로세스 (WL_PostProcess_Bright.asset 미이식) "Monsters", // 구 WL 몬스터 — 힘민지 ActorInfo 체계와 무관 "StartPoint", // Portal_Start 로 변환 (위치만 취한다) "EventSystem", }; // 구 WL 전용 프리팹 소스 경로 접두사 — 이 프리팹의 인스턴스는 제외 static readonly string[] kExcludeSrcPrefix = new string[] { "Assets/WL/Prefabs/Player_", "Assets/WL/Prefabs/Monsters/", "Assets/WL/Prefabs/UI_", "Assets/WL/UI/", }; // 제외 판정할 스크립트 네임스페이스 접두사 static readonly string[] kExcludeNsPrefix = new string[] { "WL.Player", "WL.UI" }; // 🔴 dryRun 실측 보정 (2026-09-05) — kExcludeNames 는 "루트"에만 걸린다. static readonly string[] kPruneDeepNames = new string[] { "Main Camera", "StartPoint", "EventSystem" }; // 벤더 데모 스크립트(태양 회전·구름 이동·데모 카메라 조작 등) — 프리팹에서 제거 (#771) static readonly string[] kStripBehaviourNames = new string[] { "CameraControl", "SunControl", "CloudsControl", "FireLightControl" }; static System.Text.StringBuilder _log; static void L(string s) { _log.AppendLine(s); UnityEngine.Debug.Log("[MapBuilder] " + s); } static void W(string s) { _log.AppendLine("WARN " + s); UnityEngine.Debug.LogWarning("[MapBuilder] " + s); } static void E(string s) { _log.AppendLine("ERROR " + s); UnityEngine.Debug.LogError("[MapBuilder] " + s); } static void ResetDefaults() { kDryRun = false; kBuildPrefab = true; kBuildScene = true; kRegisterInBuild = true; kBakeNavMesh = true; kCopySceneAmbient = false; kGroupLights = false; kSpawnOverride = false; kSpawnPos = UnityEngine.Vector3.zero; kCamYaw = 0f; kCamYawSet = false; kNavMinRegionArea = -1f; kNavVolume = false; kSrcScene = DEF_SRC_SCENE; kLobbyScene = "Assets/Scenes/Lobby.unity"; kDstScene = DEF_DST_SCENE; kDstPrefab = DEF_DST_PREFAB; kNavMeshDir = "Assets/WL/Settings"; kNavAssetName = DEF_NAV_ASSET; kRootName = DEF_ROOT_NAME; kSceneName = DEF_SCENE_NAME; kLogDir = DEF_LOG_DIR; } // 기존 진입점 — 동작 무변경 public static void Run() { ResetDefaults(); Core(); } // #771 인자화 진입점 public static object RunFor(string srcScene, string dstScene, string dstPrefab, string rootName, string sceneName, string navAssetName, float spawnX, float spawnY, float spawnZ, float camYaw, bool bakeNav, bool groupLights) { ResetDefaults(); kSrcScene = srcScene; kDstScene = dstScene; kDstPrefab = dstPrefab; kRootName = rootName; kSceneName = sceneName; kNavAssetName = navAssetName; kSpawnOverride = true; kSpawnPos = new UnityEngine.Vector3(spawnX, spawnY, spawnZ); kCamYaw = camYaw; kCamYawSet = true; kBakeNavMesh = bakeNav; kGroupLights = groupLights; kLogDir = "AgentScripts/staging/WL_Nature2/out"; Core(); return _log == null ? "(no log)" : "done — log=" + kLogDir + "/BUILDER_LOG.txt"; } // NavMesh 만 다시 굽는다 (드레싱 뒤 재베이크용 · #771) public static object Rebake(string prefabPath, string navDir, string navAssetName, float cx, float cy, float cz, float sx, float sy, float sz, float minRegionArea) { _log = new System.Text.StringBuilder(); if (UnityEngine.Application.isPlaying) { E("Play 중"); return "playing"; } kNavMeshDir = navDir; kNavAssetName = navAssetName; kNavVolume = true; kNavCenter = new UnityEngine.Vector3(cx, cy, cz); kNavSize = new UnityEngine.Vector3(sx, sy, sz); kNavMinRegionArea = minRegionArea; kLogDir = "AgentScripts/staging/WL_Nature2/out"; var setup = UnityEditor.SceneManagement.EditorSceneManager.GetSceneManagerSetup(); string res = "?"; try { UnityEditor.SceneManagement.EditorSceneManager.NewScene( UnityEditor.SceneManagement.NewSceneSetup.EmptyScene, UnityEditor.SceneManagement.NewSceneMode.Single); var src = UnityEditor.AssetDatabase.LoadAssetAtPath(prefabPath); if (src == null) { E("프리팹 없음 " + prefabPath); return "no prefab"; } var root = (UnityEngine.GameObject)UnityEditor.PrefabUtility.InstantiatePrefab(src); UnityEditor.PrefabUtility.UnpackPrefabInstance(root, UnityEditor.PrefabUnpackMode.OutermostRoot, UnityEditor.InteractionMode.AutomatedAction); root.transform.position = UnityEngine.Vector3.zero; UnityEngine.Physics.SyncTransforms(); var t = NavSurfaceType(); if (t == null) { E("NavMeshSurface 타입 없음"); return "no navmeshsurface type"; } var existing = root.GetComponent(t); if (existing != null) UnityEngine.Object.DestroyImmediate(existing); var portal = root.transform.Find("Portal_Start"); var spawn = portal != null ? portal.position + UnityEngine.Vector3.forward : UnityEngine.Vector3.zero; BakeNavMeshSurface(root, spawn); UnityEditor.PrefabUtility.SaveAsPrefabAsset(root, prefabPath); UnityEditor.AssetDatabase.SaveAssets(); L("프리팹 저장: " + prefabPath); res = "rebaked"; } catch (System.Exception ex) { E("예외 " + ex); res = "EXCEPTION " + ex.Message; } finally { Finish(); ClearDirty(); try { if (setup != null && setup.Length > 0) UnityEditor.SceneManagement.EditorSceneManager.RestoreSceneManagerSetup(setup); } catch { } } return res; } 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++) { try { mi.Invoke(null, new object[] { UnityEngine.SceneManagement.SceneManager.GetSceneAt(i) }); } catch { } } } // ───────────────────────────────────────────────────────────── static void Core() { _log = new System.Text.StringBuilder(); L("===== MapBuilder dryRun=" + kDryRun + " root=" + kRootName + " ====="); L(" src=" + kSrcScene); L(" dstPrefab=" + kDstPrefab + " dstScene=" + kDstScene + " nav=" + kNavMeshDir + "/" + kNavAssetName + ".asset"); if (UnityEngine.Application.isPlaying) { E("Play 중에는 실행할 수 없다. Stop 후 재시도."); return; } // 0) 사전 조건 if (!System.IO.File.Exists(kSrcScene)) { E("원본 씬 없음: " + kSrcScene); return; } if (!System.IO.File.Exists(kLobbyScene)) { E("Lobby.unity 없음: " + kLobbyScene); return; } if (System.IO.File.Exists(kDstPrefab) && !kDryRun) W("기존 프리팹을 덮어쓴다: " + kDstPrefab); if (System.IO.File.Exists(kDstScene) && !kDryRun) W("기존 씬을 덮어쓴다: " + kDstScene); var openScenes = UnityEditor.SceneManagement.EditorSceneManager.GetSceneManagerSetup(); if (UnityEditor.SceneManagement.EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo() == false) { E("현재 씬 저장이 취소되었다. 중단."); return; } try { var src = UnityEditor.SceneManagement.EditorSceneManager.OpenScene(kSrcScene, UnityEditor.SceneManagement.OpenSceneMode.Single); L("원본 씬 오픈: " + kSrcScene); // 1) 분류 var roots = src.GetRootGameObjects(); L("루트 오브젝트 " + roots.Length + "개"); var keep = new System.Collections.Generic.List(); var drop = new System.Collections.Generic.List(); UnityEngine.Transform startPoint = null; for (int i = 0; i < roots.Length; i++) { var go = roots[i]; var sp = FindDeep(go.transform, "StartPoint"); if (sp != null && startPoint == null) startPoint = sp; string why = ExcludeReason(go); if (why != null) { drop.Add(go.name + " <- " + why); continue; } keep.Add(go); } for (int i = 0; i < drop.Count; i++) L(" [제외] " + drop[i]); for (int i = 0; i < keep.Count; i++) L(" [포함] " + keep[i].name + " (자식 " + CountDeep(keep[i].transform) + ")"); // 2) 스폰 지점 UnityEngine.Vector3 spPos; UnityEngine.Quaternion spQuat; if (kSpawnOverride) { spPos = kSpawnPos; spQuat = UnityEngine.Quaternion.identity; L("스폰 지점 = 인자 지정 " + spPos.ToString("F4")); } else { if (startPoint == null) { E("StartPoint 를 찾지 못했다. 스폰 위치를 결정할 수 없다."); return; } spPos = startPoint.position; spQuat = startPoint.rotation; // 🔴 값으로 확보 — PruneDeep 이 StartPoint 를 파괴한 뒤에도 써야 한다 L("StartPoint 실측 world position = " + spPos.ToString("F4")); L("StartPoint 실측 world euler = " + spQuat.eulerAngles.ToString("F3")); } L(" → LoadMapMgr.Get_PortalPos 는 여기에 Vector3.forward(0,0,1) 를 더해 스폰한다"); // 3) 카메라 방향 float camYaw = 0f; if (kCamYawSet) { camYaw = kCamYaw; L("CamRot_Portal = 인자 지정 " + camYaw.ToString("F3")); } else { var mainCam = FindDeepAny(roots, "Main Camera"); if (mainCam != null) { camYaw = mainCam.rotation.eulerAngles.y; L("구 WL Main Camera world euler = " + mainCam.rotation.eulerAngles.ToString("F3") + " → CamRot_Portal 후보 yaw = " + camYaw.ToString("F3")); } else W("Main Camera 를 못 찾았다 — CamRot_Portal = 0 으로 둔다"); } // 4) 씬 조명값 실측 L("씬 RenderSettings: ambientMode=" + UnityEngine.RenderSettings.ambientMode + " sky=" + UnityEngine.RenderSettings.ambientSkyColor + " eq=" + UnityEngine.RenderSettings.ambientEquatorColor + " gr=" + UnityEngine.RenderSettings.ambientGroundColor + " intensity=" + UnityEngine.RenderSettings.ambientIntensity + " reflection=" + UnityEngine.RenderSettings.reflectionIntensity); L("씬 fog: enabled=" + UnityEngine.RenderSettings.fog + " mode=" + UnityEngine.RenderSettings.fogMode + " color=" + UnityEngine.RenderSettings.fogColor + " start=" + UnityEngine.RenderSettings.fogStartDistance + " end=" + UnityEngine.RenderSettings.fogEndDistance + " density=" + UnityEngine.RenderSettings.fogDensity); var skybox = UnityEngine.RenderSettings.skybox; L("씬 skybox 머티리얼 = " + (skybox == null ? "(없음)" : UnityEditor.AssetDatabase.GetAssetPath(skybox))); // 5) NavMesh 원본 설정 실측 DumpNavMeshSurface(roots); L("힘민지 agentTypeID 0 설정 = " + DescribeAgent(0)); if (kDryRun) { L("--- dryRun: 여기서 중단 ---"); Finish(); return; } // ─── 실제 생성 ─────────────────────────────────────── UnityEngine.GameObject prefabRoot = null; if (kBuildPrefab) { prefabRoot = new UnityEngine.GameObject(kRootName); UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(prefabRoot, src); prefabRoot.transform.position = UnityEngine.Vector3.zero; for (int i = 0; i < keep.Count; i++) keep[i].transform.SetParent(prefabRoot.transform, true); L("맵 오브젝트 " + keep.Count + "개를 '" + kRootName + "' 아래로 이동"); PruneDeep(prefabRoot); StripBehaviours(prefabRoot); if (kGroupLights) GroupLights(prefabRoot); // Portal_Start / Reincarnation_Start var portal = new UnityEngine.GameObject("Portal_Start"); portal.transform.SetParent(prefabRoot.transform, false); portal.transform.position = spPos; portal.transform.rotation = spQuat; var reinc = new UnityEngine.GameObject("Reincarnation_Start"); reinc.transform.SetParent(prefabRoot.transform, false); reinc.transform.position = spPos; reinc.transform.rotation = spQuat; L("Portal_Start / Reincarnation_Start 생성 @ " + spPos.ToString("F4")); // MapData var mapData = prefabRoot.AddComponent(); mapData.material_skybox = skybox; if (kCopySceneAmbient) { mapData.ambientMode = UnityEngine.RenderSettings.ambientMode; mapData.ambientSky = UnityEngine.RenderSettings.ambientSkyColor; mapData.ambientEquator = UnityEngine.RenderSettings.ambientEquatorColor; mapData.ambientGround = UnityEngine.RenderSettings.ambientGroundColor; mapData.ambientIntensity = UnityEngine.RenderSettings.ambientIntensity; mapData.reflectionIntensity = UnityEngine.RenderSettings.reflectionIntensity; L("앰비언트: 원본 씬 값을 복사했다"); } else L("앰비언트: MapData 코드 기본값 유지"); var zone = new MapZoneData(); zone.go_portal = portal; zone.CamRot_Portal = camYaw; zone.go_reincarnation = reinc; zone.CamRot_Reincarnation = camYaw; zone.tf_Obj = new UnityEngine.Transform[0]; mapData.m_MapZoneData = new System.Collections.Generic.List(); mapData.m_MapZoneData.Add(zone); L("MapData 구성 완료 (m_MapZoneData[0], CamRot_Portal=" + camYaw.ToString("F3") + ")"); if (kBakeNavMesh) BakeNavMeshSurface(prefabRoot, spPos); EnsureDir(System.IO.Path.GetDirectoryName(kDstPrefab)); UnityEditor.PrefabUtility.SaveAsPrefabAsset(prefabRoot, kDstPrefab); L("프리팹 저장: " + kDstPrefab); } // 6) 씬 생성 — Lobby.unity 를 복제해 LoadMapMgr 필드만 교체 if (kBuildScene) { if (System.IO.File.Exists(kDstScene)) UnityEditor.AssetDatabase.DeleteAsset(kDstScene); if (!UnityEditor.AssetDatabase.CopyAsset(kLobbyScene, kDstScene)) { E("씬 복제 실패"); Finish(); return; } UnityEditor.AssetDatabase.Refresh(); var dst = UnityEditor.SceneManagement.EditorSceneManager.OpenScene(kDstScene, UnityEditor.SceneManagement.OpenSceneMode.Single); var dstRoots = dst.GetRootGameObjects(); bool done = false; for (int i = 0; i < dstRoots.Length; i++) { var mgr = dstRoots[i].GetComponent(); if (mgr == null) continue; var so = new UnityEditor.SerializedObject(mgr); so.FindProperty("LoadSceneName").stringValue = kSceneName; so.FindProperty("LoadMapPrefab").stringValue = kDstPrefab; so.ApplyModifiedPropertiesWithoutUndo(); UnityEditor.EditorUtility.SetDirty(mgr); done = true; L("LoadMapMgr 설정: LoadSceneName=" + kSceneName + " / LoadMapPrefab=" + kDstPrefab); } if (!done) E("복제한 씬에서 LoadMapMgr 를 찾지 못했다"); UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(dst); UnityEditor.SceneManagement.EditorSceneManager.SaveScene(dst, kDstScene); L("씬 저장: " + kDstScene); } if (kRegisterInBuild) RegisterScene(kDstScene); CheckAddressables(); UnityEditor.AssetDatabase.SaveAssets(); UnityEditor.AssetDatabase.Refresh(); L("===== 완료 ====="); } catch (System.Exception ex) { E("예외: " + ex); } finally { Finish(); if (openScenes != null && openScenes.Length > 0) { try { UnityEditor.SceneManagement.EditorSceneManager.RestoreSceneManagerSetup(openScenes); } catch { } } } } // ───────────────────────────────────────────────────────────── static void Finish() { try { var dir = kLogDir; if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir); System.IO.File.WriteAllText(dir + "/BUILDER_LOG.txt", _log.ToString(), new System.Text.UTF8Encoding(true)); UnityEngine.Debug.Log("[MapBuilder] 로그 저장: " + dir + "/BUILDER_LOG.txt"); } catch (System.Exception e) { UnityEngine.Debug.LogWarning("[MapBuilder] 로그 저장 실패: " + e.Message); } } static string ExcludeReason(UnityEngine.GameObject go) { for (int i = 0; i < kExcludeNames.Length; i++) if (go.name == kExcludeNames[i]) return "이름 제외목록"; if (UnityEditor.PrefabUtility.GetPrefabInstanceStatus(go) == UnityEditor.PrefabInstanceStatus.MissingAsset) return "프리팹 원본 없음 (구 WL 전용 · 미이식)"; if (go.name.Contains("Missing Prefab")) return "Missing Prefab 표기"; var src = UnityEditor.PrefabUtility.GetCorrespondingObjectFromSource(go); if (src != null) { var p = UnityEditor.AssetDatabase.GetAssetPath(src); for (int i = 0; i < kExcludeSrcPrefix.Length; i++) if (!string.IsNullOrEmpty(p) && p.StartsWith(kExcludeSrcPrefix[i])) return "구 WL 프리팹: " + p; } var mbs = go.GetComponentsInChildren(true); for (int i = 0; i < mbs.Length; i++) { if (mbs[i] == null) continue; var t = mbs[i].GetType(); var ns = t.Namespace == null ? "" : t.Namespace; for (int j = 0; j < kExcludeNsPrefix.Length; j++) if (ns.StartsWith(kExcludeNsPrefix[j])) return "구 WL 스크립트: " + t.FullName; } return null; } static void PruneDeep(UnityEngine.GameObject root) { var kill = new System.Collections.Generic.List(); var all = root.GetComponentsInChildren(true); for (int i = 0; i < all.Length; i++) { if (all[i] == null) continue; var go = all[i].gameObject; if (go == root) continue; for (int j = 0; j < kPruneDeepNames.Length; j++) if (go.name == kPruneDeepNames[j]) { if (!kill.Contains(go)) kill.Add(go); break; } } var cams = root.GetComponentsInChildren(true); for (int i = 0; i < cams.Length; i++) if (cams[i] != null && cams[i].gameObject != root && !kill.Contains(cams[i].gameObject)) kill.Add(cams[i].gameObject); var als = root.GetComponentsInChildren(true); for (int i = 0; i < als.Length; i++) if (als[i] != null && als[i].gameObject != root && !kill.Contains(als[i].gameObject)) kill.Add(als[i].gameObject); for (int i = 0; i < kill.Count; i++) { if (kill[i] == null) continue; L(" [정리] " + FullPath(kill[i].transform)); try { UnityEngine.Object.DestroyImmediate(kill[i]); } catch (System.Exception e) { W("삭제 실패(프리팹 인스턴스 구조 제한?) " + FullPath(kill[i].transform) + " : " + e.Message); kill[i].SetActive(false); } } L("깊은 정리: " + kill.Count + "개 제거"); var left = root.GetComponentsInChildren(true); if (left != null && left.Length > 0) W("프리팹에 카메라가 " + left.Length + "대 남아 있다"); } // 벤더 데모 스크립트 제거 (오브젝트는 남기고 컴포넌트만) static void StripBehaviours(UnityEngine.GameObject root) { int n = 0; var mbs = root.GetComponentsInChildren(true); for (int i = 0; i < mbs.Length; i++) { if (mbs[i] == null) continue; var tn = mbs[i].GetType().Name; for (int j = 0; j < kStripBehaviourNames.Length; j++) if (tn == kStripBehaviourNames[j]) { L(" [스크립트 제거] " + tn + " @ " + FullPath(mbs[i].transform)); try { UnityEngine.Object.DestroyImmediate(mbs[i]); n++; } catch (System.Exception e) { W("제거 실패 → 비활성화로 대체: " + tn + " : " + e.Message); mbs[i].enabled = false; } break; } } L("벤더 데모 스크립트 " + n + "개 제거"); } // 라이트를 'SceneAssets' 아래로 모은다 (WL_Start 관례) static void GroupLights(UnityEngine.GameObject root) { var lights = root.GetComponentsInChildren(true); if (lights.Length == 0) { W("라이트가 없다"); return; } var holder = root.transform.Find("SceneAssets"); if (holder == null) { var go = new UnityEngine.GameObject("SceneAssets"); go.transform.SetParent(root.transform, false); holder = go.transform; } for (int i = 0; i < lights.Length; i++) { var t = lights[i].transform; // 이미 SceneAssets 아래면 건너뛴다 if (t.parent == holder) continue; L(" [라이트 이동] " + FullPath(t) + " type=" + lights[i].type + " intensity=" + lights[i].intensity + " color=" + lights[i].color + " euler=" + t.rotation.eulerAngles.ToString("F2")); t.SetParent(holder, true); } L("라이트 " + lights.Length + "개를 SceneAssets 아래로 이동"); } static string FullPath(UnityEngine.Transform t) { var s = t.name; var p = t.parent; while (p != null) { s = p.name + "/" + s; p = p.parent; } return s; } static UnityEngine.Transform FindDeep(UnityEngine.Transform root, string name) { if (root.name == name) return root; for (int i = 0; i < root.childCount; i++) { var r = FindDeep(root.GetChild(i), name); if (r != null) return r; } return null; } static UnityEngine.Transform FindDeepAny(UnityEngine.GameObject[] roots, string name) { for (int i = 0; i < roots.Length; i++) { var r = FindDeep(roots[i].transform, name); if (r != null) return r; } return null; } 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 void EnsureDir(string dir) { dir = dir.Replace('\\', '/'); if (System.IO.Directory.Exists(dir)) return; System.IO.Directory.CreateDirectory(dir); UnityEditor.AssetDatabase.Refresh(); } // ── NavMesh ──────────────────────────────────────────────── static System.Type NavSurfaceType() { var asms = System.AppDomain.CurrentDomain.GetAssemblies(); for (int i = 0; i < asms.Length; i++) { var t = asms[i].GetType("Unity.AI.Navigation.NavMeshSurface"); if (t != null) return t; } return null; } static void DumpNavMeshSurface(UnityEngine.GameObject[] roots) { var t = NavSurfaceType(); if (t == null) { W("Unity.AI.Navigation.NavMeshSurface 타입을 찾지 못했다 (패키지 미설치?)"); return; } for (int i = 0; i < roots.Length; i++) { var c = roots[i].GetComponentInChildren(t, true); if (c == null) continue; L("원본 NavMeshSurface 발견: " + roots[i].name + " agentTypeID=" + GetProp(c, "agentTypeID") + " collectObjects=" + GetProp(c, "collectObjects") + " useGeometry=" + GetProp(c, "useGeometry") + " voxelSize=" + GetProp(c, "voxelSize") + " tileSize=" + GetProp(c, "tileSize") + " minRegionArea=" + GetProp(c, "minRegionArea")); } } static object GetProp(object o, string name) { var t = o.GetType(); var p = t.GetProperty(name); if (p != null) return p.GetValue(o, null); var f = t.GetField(name, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); if (f != null) return f.GetValue(o); return "(?)"; } static string DescribeAgent(int agentTypeID) { var s = UnityEngine.AI.NavMesh.GetSettingsByID(agentTypeID); return "radius=" + s.agentRadius + " height=" + s.agentHeight + " slope=" + s.agentSlope + " climb=" + s.agentClimb + " voxel=" + s.voxelSize + " tile=" + s.tileSize + " minRegion=" + s.minRegionArea; } static void BakeNavMeshSurface(UnityEngine.GameObject root, UnityEngine.Vector3 spawn) { var t = NavSurfaceType(); if (t == null) { E("NavMeshSurface 타입 없음 — NavMesh 베이크를 건너뛴다"); return; } var comp = root.AddComponent(t); SetProp(comp, "agentTypeID", 0); SetProp(comp, "useGeometry", UnityEngine.AI.NavMeshCollectGeometry.PhysicsColliders); SetProp(comp, "defaultArea", 0); SetProp(comp, "layerMask", (UnityEngine.LayerMask)(~0)); if (kNavVolume) { SetEnumByName(comp, "collectObjects", "Volume"); // 값이 아니라 이름으로 — 패키지 버전별 enum 값 차이 방지 SetProp(comp, "center", kNavCenter); SetProp(comp, "size", kNavSize); L("NavMesh 수집 = " + GetProp(comp, "collectObjects") + " center=" + kNavCenter.ToString("F1") + " size=" + kNavSize.ToString("F1")); } else { SetEnumByName(comp, "collectObjects", "All"); L("NavMesh 수집 = " + GetProp(comp, "collectObjects")); } if (kNavMinRegionArea >= 0f) { SetProp(comp, "minRegionArea", kNavMinRegionArea); SetProp(comp, "overrideVoxelSize", false); L("NavMesh minRegionArea = " + kNavMinRegionArea); } L("NavMeshSurface 추가 — agent0 " + DescribeAgent(0) + " 로 베이크 시작"); var m = t.GetMethod("BuildNavMesh"); if (m == null) { E("BuildNavMesh 메서드 없음"); return; } var sw = System.Diagnostics.Stopwatch.StartNew(); m.Invoke(comp, null); sw.Stop(); L("베이크 완료 (" + sw.ElapsedMilliseconds + " ms)"); var data = GetProp(comp, "navMeshData") as UnityEngine.Object; if (data == null) { E("navMeshData 가 null — 베이크 실패. 콜라이더 유무를 확인"); return; } EnsureDir(kNavMeshDir); var path = kNavMeshDir + "/" + kNavAssetName + ".asset"; if (UnityEditor.AssetDatabase.LoadAssetAtPath(path) != null) UnityEditor.AssetDatabase.DeleteAsset(path); UnityEditor.AssetDatabase.CreateAsset(data, path); L("NavMeshData 저장: " + path); UnityEngine.AI.NavMeshHit hit; var probe = spawn + UnityEngine.Vector3.forward; if (UnityEngine.AI.NavMesh.SamplePosition(probe, out hit, 3f, UnityEngine.AI.NavMesh.AllAreas)) L("스폰 검증 OK — " + probe.ToString("F3") + " → NavMesh " + hit.position.ToString("F3") + " (거리 " + UnityEngine.Vector3.Distance(probe, hit.position).ToString("F3") + ")"); else E("스폰 검증 실패 — " + probe.ToString("F3") + " 반경 3m 안에 NavMesh 가 없다. 플레이어가 움직이지 못한다."); } // enum 필드를 "이름"으로 설정 (패키지 버전에 따라 enum 정수값이 다를 수 있다) static void SetEnumByName(object o, string name, string enumName) { var t = o.GetType(); var p = t.GetProperty(name); System.Type et = p != null ? p.PropertyType : null; System.Reflection.FieldInfo f = null; if (et == null) { f = t.GetField(name, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); if (f != null) et = f.FieldType; } if (et == null || !et.IsEnum) { W("enum 필드 없음: " + name); return; } object val; try { val = System.Enum.Parse(et, enumName); } catch { W("enum 값 없음: " + et.Name + "." + enumName + " (가능: " + string.Join(",", System.Enum.GetNames(et)) + ")"); return; } if (p != null && p.CanWrite) p.SetValue(o, val, null); else if (f != null) f.SetValue(o, val); } static void SetProp(object o, string name, object v) { var t = o.GetType(); var p = t.GetProperty(name); if (p != null && p.CanWrite) { try { if (p.PropertyType.IsEnum && v is int) v = System.Enum.ToObject(p.PropertyType, (int)v); p.SetValue(o, v, null); return; } catch (System.Exception e) { W("프로퍼티 설정 실패 " + name + ": " + e.Message); } } var f = t.GetField(name, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); if (f != null) { if (f.FieldType.IsEnum && v is int) v = System.Enum.ToObject(f.FieldType, (int)v); f.SetValue(o, v); } else W("필드/프로퍼티 없음: " + name); } // ── EditorBuildSettings ──────────────────────────────────── static void RegisterScene(string path) { var cur = UnityEditor.EditorBuildSettings.scenes; for (int i = 0; i < cur.Length; i++) if (cur[i].path == path) { L("빌드 씬 목록에 이미 등록됨: " + path); return; } var list = new System.Collections.Generic.List(cur); list.Add(new UnityEditor.EditorBuildSettingsScene(path, true)); UnityEditor.EditorBuildSettings.scenes = list.ToArray(); L("빌드 씬 목록 등록: " + path + " (총 " + list.Count + "개)"); } // ── Addressables ─────────────────────────────────────────── static void CheckAddressables() { var dir = "Assets/Res_Addr/Map"; var folderGuid = UnityEditor.AssetDatabase.AssetPathToGUID(dir); var groupAsset = "Assets/AddressableAssetsData/AssetGroups/Map.asset"; if (!System.IO.File.Exists(groupAsset)) { W("Addressables 그룹 파일 없음: " + groupAsset); return; } var txt = System.IO.File.ReadAllText(groupAsset); if (txt.Contains(folderGuid)) L("Addressables: '" + dir + "' 가 폴더 엔트리로 등록돼 있다 (GUID " + folderGuid + ") → 새 프리팹 자동 포함. 추가 작업 불필요."); else W("Addressables: 폴더 엔트리를 못 찾았다. Map.asset 을 직접 확인하고 " + kDstPrefab + " 를 수동 등록할 것."); } }