705 lines
36 KiB
C#
705 lines
36 KiB
C#
|
|
// 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 + " 를 수동 등록할 것.");
|
||
|
|
}
|
||
|
|
}
|