Project_WL/AgentScripts/WL_StartMapBuilder.cs

705 lines
36 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
// 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 '["<srcScene>","<dstScene>","<dstPrefab>","<rootName>","<sceneName>","<navAssetName>", x,y,z, camYaw, bakeNav, groupLights]'
//
// CLAUDE.md §1 준수: 네임스페이스 풀어 쓰기
//
// 🔴 실행 전 필독
// 1) PD 가 Play 중이면 실행 금지 (씬을 열고 저장한다).
// 2) 대상 프리팹/씬을 덮어쓰므로 백업 후 실행한다.
//
// 산출물
// <dstPrefab> 맵 프리팹 (MapData 포함)
// <dstScene> Lobby.unity 복제 + LoadMapMgr 필드 변경
// <navMeshDir>/<navAssetName>.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<UnityEngine.GameObject>(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<UnityEngine.GameObject>();
var drop = new System.Collections.Generic.List<string>();
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>();
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<MapZoneData>();
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<LoadMapMgr>();
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<UnityEngine.MonoBehaviour>(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<UnityEngine.GameObject>();
var all = root.GetComponentsInChildren<UnityEngine.Transform>(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<UnityEngine.Camera>(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<UnityEngine.AudioListener>(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<UnityEngine.Camera>(true);
if (left != null && left.Length > 0) W("프리팹에 카메라가 " + left.Length + "대 남아 있다");
}
// 벤더 데모 스크립트 제거 (오브젝트는 남기고 컴포넌트만)
static void StripBehaviours(UnityEngine.GameObject root)
{
int n = 0;
var mbs = root.GetComponentsInChildren<UnityEngine.MonoBehaviour>(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<UnityEngine.Light>(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<UnityEngine.Object>(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<UnityEditor.EditorBuildSettingsScene>(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 + " 를 수동 등록할 것.");
}
}