[WL-816i] 던전 진입 MissingReference 제거(맵 노드 1개로 상주) + 섬 구도 카메라 (#816)

- LoadMapMgr 파생 노드를 씬마다 새로 만들지 않고 DontDestroyOnLoad 하나로 상주시킨다.
  씬 전환에서 파괴된 노드를 정적 Ins 가 계속 가리켜 Get_PortalPos 의 StartCoroutine 이
  MissingReferenceException 을 내던 것을 없앤다(원본 LoadMapMgr.cs 0줄 · 파생 + private 필드 주입).
- 맵 프리팹이 없어 m_MapData 가 영원히 null 이라 Co_Get_PortalPos 가 끝나지 않던 것도 함께 해결 —
  꺼진 오브젝트에 런타임 MapData 를 만들어 넣는다(MapData.Awake/Start 미실행 = RenderSettings 무변경).
- 던전 씬의 아레나 룩 카메라(직교 · MainCamera 태그)가 전투 화면을 차지하던 것을 끄고
  전투 카메라(RealCamera)만 남긴다.
- WLDungeonCameraRig — 섬(FI Cinemachine) 실측 구도(각 45° · 거리 22 m · FOV 60 · 원근)를
  던전 카메라에 그대로 적용한다. 값은 WLIslandSettings SO.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-09-13 17:14:48 +09:00
parent 9aaddae676
commit f2e89653ed
6 changed files with 406 additions and 29 deletions

View File

@ -48,6 +48,11 @@ namespace WL.Island
WLIslandSceneSetup.EnsureDungeonMapNodes(cfg, scene, d);
FixDuplicates(cfg, scene);
// 🔴 WL-816i — 던전 카메라를 **섬 구도**(각 45° · 거리 22 m · FOV 60)로 맞춘다.
// 씬이 내려가면 이 오브젝트도 사라지고 OnDestroy 가 원래 값을 돌려놓는다(C8).
if (Object.FindFirstObjectByType<WLDungeonCameraRig>() == null)
WLDungeonCameraRig.Create(cfg, scene);
// ① NavMesh
if (cfg.bakeDungeonNavMesh != 0) BakeNavMesh(cfg, scene);
@ -173,15 +178,28 @@ namespace WL.Island
for (int i = 0; i < walkers.Length; i++)
if (walkers[i] != null) { walkers[i].gameObject.SetActive(false); rigs++; }
var allCams = Object.FindObjectsByType<Camera>(FindObjectsSortMode.None);
for (int i = 0; i < allCams.Length; i++)
// 🔴 WL-816i — 전에는 `c == Camera.main` 을 건너뛰었는데, 아레나 복사본 씬의 `Main Camera`(직교 · orthoSize 10)가
// 바로 그 MainCamera 태그를 달고 있어서 **룩 개발용 직교 카메라가 그대로 살아 전투 화면을 차지**했다.
// 던전 씬 안의 카메라는 전부 룩용이다 → 전투 카메라(RealCamera · Ingame 씬)만 남기고 모두 끈다.
// 단 전투 카메라를 못 찾으면 화면이 까매지므로 아무것도 끄지 않는다(안전).
var combat = Object.FindFirstObjectByType<RealCamera>();
var combatCam = combat != null ? combat.GetComponent<Camera>() : null;
if (combatCam != null)
{
var c = allCams[i];
if (c == null || c.gameObject.scene != scene) continue;
if (c == Camera.main) continue;
c.enabled = false; cams++;
var al = c.GetComponent<AudioListener>();
if (al != null && al.enabled) { al.enabled = false; listeners++; }
if (!combatCam.enabled) { combatCam.enabled = true; cams++; }
var allCams = Object.FindObjectsByType<Camera>(FindObjectsSortMode.None);
for (int i = 0; i < allCams.Length; i++)
{
var c = allCams[i];
if (c == null || c == combatCam || c.gameObject.scene != scene) continue;
if (c.enabled) { c.enabled = false; cams++; }
var al = c.GetComponent<AudioListener>();
if (al != null && al.enabled) { al.enabled = false; listeners++; }
}
}
else
{
WLIslandBridge.Log(cfg, "🔴 던전 전투 카메라(RealCamera)를 못 찾았다 — 씬 카메라를 그대로 둔다");
}
WLIslandBridge.Log(cfg, "던전 중복 정리 — 데모 리그 " + rigs + " · 카메라 " + cams + " · 리스너 " + listeners);

View File

@ -199,6 +199,16 @@ namespace WL.Island
if (InGameInfo.Ins != null)
{
// 🔴 WL-816i — 씬을 바꾸기 **전에** 맵 노드를 던전 것으로 돌려 놓는다.
// Load_Map 은 섬 씬 언로드와 던전 씬 로드를 같은 프레임에 건다 → 그 사이에
// `PCInfo.Make_Actors` 가 `LoadMapMgr.Ins.Get_PortalPos` 를 부르는데,
// 노드가 씬과 함께 파괴되면 MissingReferenceException 이 난다(PD 실플레이 에러 ①).
var nodes = WLIslandMapNodes.Ensure(cfg);
if (nodes != null)
{
nodes.LoadSceneName = d.sceneName;
nodes.SetStartPos(WLIslandSceneSetup.DungeonStartPos(d), WLDungeonCameraRig.SolvedYaw(cfg));
}
Log(cfg, "던전 입장 — " + d.displayName + " (map " + d.mapId + " · stage " + d.stageIndex + ")");
InGameInfo.Ins.Load_Map(d.mapId);
}
@ -219,6 +229,10 @@ namespace WL.Island
if (InGameInfo.Ins != null)
{
// 🔴 WL-816i — 나갈 때도 마찬가지다(던전 씬이 내려가며 노드가 죽지 않게 미리 섬 것으로).
var nodes = WLIslandMapNodes.Ensure(cfg);
if (nodes != null) nodes.LoadSceneName = cfg.islandSceneName;
Log(cfg, "섬으로 복귀(" + reason + ") — 보류 보상 " + s_pendingGold);
InGameInfo.Ins.Load_Map(cfg.islandMapId);
}

View File

@ -0,0 +1,210 @@
// ─────────────────────────────────────────────────────────────────────────────
// WLIslandCameraRig.cs — 던전(전장)의 카메라 구도를 **섬 구도 그대로** 만든다
//
// PD 지시(2026-09-13) 「카메라 구도는 섬에서 조작할 때의 각도와 뷰를 말한거야.
// 전장에 진입해도 바뀌지 않아야 하기 때문」
// → 기준은 **섬**. 던전 카메라를 섬 값에 맞춘다(반대가 아니다).
//
// ■ 섬 카메라 실측(Assets/FarmingIsland/Scenes/Level01.unity · 씬 파일 값 그대로)
// PlayerVirtualCamera(Cinemachine) — 회전 오일러 (45, 0, 0) · Follow = Player
// Body = CinemachineFramingTransposer · m_CameraDistance 22 · ScreenX/Y 0.5(대상 화면 정중앙)
// Lens — FieldOfView 60 · 원근(orthographic 0) · Near 0.1 · Far 500
// = 「대상에서 22 m · 45° 위 · 화각 60° · 플레이어를 따라 돌지 않는 고정 방향」
//
// ■ 던전 카메라는 힘민지 `RealCamera`(Assets/Script/Util/RealCamera.cs) 다. 원본 0줄이 원칙이라
// **그 컴포넌트의 값만** 매 프레임 채워 넣는다(Update → RealCamera.LateUpdate 순서).
// 그러면 흔들림(ShakeCamera)·스킬 뷰(CamStatus 2·3)·가림 처리는 전부 원본 그대로 산다.
//
// ■ RealCamera 의 위치 식(원본 Update_Cam)
// rotateVector = Quaternion.Euler(cameraHeight, rotateAround, cameraPan) * Vector3.one
// camPosition = target.position + Vector3.up * DistanceUp - rotateVector * DistanceAway
// Vector3.one(1,1,1) 을 돌리는 식이라 각도 = 구도 가 아니다 → 아래에서 **역으로 풀어** 값을 만든다.
// 결과 오프셋이 정확히 (0, R·sinθ, -R·cosθ) 를 yaw φ 로 돌린 것이 되게 한다(= 섬과 같은 구도).
//
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다.
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Island
{
/// <summary>던전에서만 살아 있는 카메라 보정기. 씬이 내려가면 값을 원래대로 돌려놓는다(C8).</summary>
public sealed class WLDungeonCameraRig : MonoBehaviour
{
public const string RigName = "~WL_DungeonCameraRig";
/// <summary>보고·진단용 — 마지막으로 적용한 값.</summary>
public static string LastApplied = "";
public static int Applies;
WLIslandSettings _cfg;
RealCamera _rc;
Camera _cam;
// 되돌리기(C8) — 우리가 만지기 전 값
bool _saved;
float _oMin, _oMax, _oHeight, _oUp, _oRot, _oPan, _oFov;
bool _oOrtho;
Vector3 _oLookAt;
float[] _oCamMin, _oCamMax, _oCamHeight, _oCamUp;
public static WLDungeonCameraRig Create(WLIslandSettings cfg, UnityEngine.SceneManagement.Scene scene)
{
if (cfg == null || cfg.dungeonCameraMatchIsland == 0) return null;
var go = new GameObject(RigName);
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(go, scene);
var rig = go.AddComponent<WLDungeonCameraRig>();
rig._cfg = cfg;
return rig;
}
// ─────────────────────────────────────────────────────────────────
// 섬 구도 → RealCamera 값 풀이
// ─────────────────────────────────────────────────────────────────
/// <summary>
/// 원하는 구도(내려보는 각 θ · 거리 R)를 RealCamera 의 (cameraHeight, DistanceAway, DistanceUp, rotateAround) 로 푼다.
/// 결과 오프셋 = Ry(φ) · (0, R sinθ, -R cosθ) — 섬의 Cinemachine 구도와 같은 식이다.
/// </summary>
public static bool Solve(float pitchDeg, float distance, float yawDeg,
out float camHeight, out float away, out float up, out float rotateAround)
{
camHeight = pitchDeg; away = 0f; up = 0f; rotateAround = yawDeg;
float p = pitchDeg * Mathf.Deg2Rad;
float c = Mathf.Cos(p), s = Mathf.Sin(p);
float k = c - s, m = s + c; // Rx(p)·(1,1,1) = (1, k, m)
if (Mathf.Abs(m) < 1e-4f) return false;
float a = -Mathf.Atan(1f / m); // cos a + m·sin a = 0 → 오프셋의 x 성분 0
float z = -Mathf.Sin(a) + m * Mathf.Cos(a);
if (Mathf.Abs(z) < 1e-4f) return false;
away = distance * c / z;
up = distance * s + away * k;
rotateAround = a * Mathf.Rad2Deg + yawDeg;
return true;
}
/// <summary>스테이지 진입 시 원본이 쓰는 초기 카메라 방향(MapZoneData.CamRot_Portal) 값.</summary>
public static float SolvedYaw(WLIslandSettings cfg)
{
if (cfg == null) return 0f;
float h, d, u, r;
if (!Solve(cfg.camPitch, cfg.camDistance, cfg.camYaw, out h, out d, out u, out r)) return cfg.camYaw;
return r;
}
// ─────────────────────────────────────────────────────────────────
// 적용 — Update 는 모든 LateUpdate 보다 먼저 돈다(= RealCamera 가 이 값을 그대로 쓴다)
// ─────────────────────────────────────────────────────────────────
void Update()
{
if (_cfg == null) _cfg = WLIslandSettings.Instance;
if (_cfg == null || _cfg.dungeonCameraMatchIsland == 0) return;
if (_rc == null)
{
_rc = MyValue.m_RealCamera;
if (_rc == null) return;
_cam = _rc.GetComponent<Camera>();
if (_cam == null) _cam = Camera.main;
Save();
}
float camHeight, away, up, rot;
if (!Solve(_cfg.camPitch, _cfg.camDistance, _cfg.camYaw, out camHeight, out away, out up, out rot)) return;
_rc.cameraHeight = camHeight;
_rc.minDistance = away;
_rc.maxDistance = away;
_rc.DistanceUp = up;
_rc.cameraPan = 0f;
_rc.m_LookAt = new Vector3(0f, _cfg.camLookHeight, 0f);
if (_cfg.camLockYaw != 0) _rc.rotateAround = rot;
// Init_Cam()/Set_DistanceUp() 이 이 배열에서 값을 되읽으므로 같은 값을 넣어 둔다(1프레임 튐 방지).
if (_rc.arr_cCamData != null)
for (int i = 0; i < _rc.arr_cCamData.Length; i++)
{
var d = _rc.arr_cCamData[i];
if (d == null) continue;
d.minDistance = away; d.maxDistance = away;
d.cameraHeight = camHeight; d.DistanceUp = up;
}
if (_cam != null)
{
if (_cam.orthographic) _cam.orthographic = false;
if (!Mathf.Approximately(_cam.fieldOfView, _cfg.camFov)) _cam.fieldOfView = _cfg.camFov;
}
Applies++;
LastApplied = "pitch " + camHeight.ToString("F2") + "° · 거리 " + _cfg.camDistance.ToString("F2") +
" m · FOV " + _cfg.camFov.ToString("F1") + " · (RealCamera away " + away.ToString("F4") +
" · up " + up.ToString("F4") + " · rotateAround " + rot.ToString("F3") + ")";
}
void Save()
{
if (_saved || _rc == null) return;
_saved = true;
_oMin = _rc.minDistance; _oMax = _rc.maxDistance; _oHeight = _rc.cameraHeight;
_oUp = _rc.DistanceUp; _oRot = _rc.rotateAround; _oPan = _rc.cameraPan; _oLookAt = _rc.m_LookAt;
if (_cam != null) { _oFov = _cam.fieldOfView; _oOrtho = _cam.orthographic; }
if (_rc.arr_cCamData != null)
{
int n = _rc.arr_cCamData.Length;
_oCamMin = new float[n]; _oCamMax = new float[n]; _oCamHeight = new float[n]; _oCamUp = new float[n];
for (int i = 0; i < n; i++)
{
var d = _rc.arr_cCamData[i];
if (d == null) continue;
_oCamMin[i] = d.minDistance; _oCamMax[i] = d.maxDistance;
_oCamHeight[i] = d.cameraHeight; _oCamUp[i] = d.DistanceUp;
}
}
WLIslandBridge.Log(_cfg, "던전 카메라 원래 값 보관 — 거리 " + _oMin.ToString("F2") + "~" + _oMax.ToString("F2") +
" · cameraHeight " + _oHeight.ToString("F1") + " · DistanceUp " + _oUp.ToString("F2") +
" · FOV " + _oFov.ToString("F1") + (_oOrtho ? " · 직교" : " · 원근"));
}
void OnDestroy()
{
if (!_saved || _rc == null) return;
_rc.minDistance = _oMin; _rc.maxDistance = _oMax; _rc.cameraHeight = _oHeight;
_rc.DistanceUp = _oUp; _rc.rotateAround = _oRot; _rc.cameraPan = _oPan; _rc.m_LookAt = _oLookAt;
if (_cam != null) { _cam.fieldOfView = _oFov; _cam.orthographic = _oOrtho; }
if (_rc.arr_cCamData != null && _oCamMin != null)
for (int i = 0; i < _rc.arr_cCamData.Length && i < _oCamMin.Length; i++)
{
var d = _rc.arr_cCamData[i];
if (d == null) continue;
d.minDistance = _oCamMin[i]; d.maxDistance = _oCamMax[i];
d.cameraHeight = _oCamHeight[i]; d.DistanceUp = _oCamUp[i];
}
}
// ─────────────────────────────────────────────────────────────────
// 실측 보고용 — 지금 화면의 구도를 잰다(섬·던전 공통 · 캡처와 함께 표로 남긴다)
// ─────────────────────────────────────────────────────────────────
/// <summary>현재 활성 카메라와 추적 대상 사이의 실제 구도(각·거리·FOV)를 문자열로 돌려준다.</summary>
public static string Measure(Transform target)
{
var cam = Camera.main;
if (cam == null) return "카메라 없음";
if (target == null) return "카메라 " + cam.name + " · 대상 없음 · FOV " + cam.fieldOfView.ToString("F2") +
(cam.orthographic ? (" · 직교 size " + cam.orthographicSize.ToString("F2")) : " · 원근") +
" · 오일러 " + cam.transform.eulerAngles.ToString("F2");
Vector3 off = cam.transform.position - target.position;
float horiz = new Vector2(off.x, off.z).magnitude;
float pitch = Mathf.Atan2(off.y, horiz) * Mathf.Rad2Deg;
float yaw = Mathf.Atan2(-off.x, -off.z) * Mathf.Rad2Deg;
return "카메라 " + cam.name + " · 거리 " + off.magnitude.ToString("F3") + " m · 내려보는 각 " +
pitch.ToString("F2") + "° · 수평각 " + yaw.ToString("F2") + "° · 높이 " + off.y.ToString("F3") +
" m · FOV " + cam.fieldOfView.ToString("F2") +
(cam.orthographic ? (" · 직교 size " + cam.orthographicSize.ToString("F2")) : " · 원근") +
" · near/far " + cam.nearClipPlane.ToString("F2") + "/" + cam.farClipPlane.ToString("F0") +
" · 카메라 오일러 " + cam.transform.eulerAngles.ToString("F2");
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4b386eb2f387c634992d62d3591735a9

View File

@ -41,32 +41,36 @@ namespace WL.Island
public static void EnsureMapNodes(WLIslandSettings cfg, Scene scene)
{
if (InGameInfo.Ins == null) return; // 섬 씬만 단독 Play = 원본 흐름이 없다 → 필요 없다
if (LoadMapMgr.isIns && LoadMapMgr.Ins != null) return;
var go = new GameObject(MapNodesName);
SceneManager.MoveGameObjectToScene(go, scene);
// 🔴 원본 LoadMapMgr 를 그대로 붙이면 Awake 가 빈 주소로 Addressables 를 때려 에러가 난다.
// 파생 클래스가 Awake 만 갈아끼운다 = 원본 0줄 · 주소 로드 0 · Ins 만 채운다.
var mgr = go.AddComponent<WLIslandMapNodes>();
var mgr = WLIslandMapNodes.Ensure(cfg);
if (mgr == null) return;
mgr.LoadSceneName = cfg.islandSceneName;
mgr.LoadMapPrefab = "";
MapNodesMade++;
WLIslandBridge.Log(cfg, "섬 맵 노드 생성(LoadMapMgr) — " + scene.name);
mgr.SetStartPos(IslandStartPos(), WLDungeonCameraRig.SolvedYaw(cfg));
WLIslandBridge.Log(cfg, "섬 맵 노드 준비(LoadMapMgr) — " + scene.name + " · 시작 " + mgr.StartPos.ToString("F1"));
}
/// <summary>섬의 시작 지점 = FI 플레이어가 서 있는 자리(없으면 원점).</summary>
static Vector3 IslandStartPos()
{
var fiPlayer = Object.FindFirstObjectByType<PlayerController>();
return fiPlayer != null ? fiPlayer.transform.position : Vector3.zero;
}
/// <summary>던전 씬에도 같은 이유로 `LoadMapMgr` 자리를 채운다(맵 프리팹 없음 = 주소 로드 0).</summary>
public static void EnsureDungeonMapNodes(WLIslandSettings cfg, Scene scene, WLDungeonDef d)
{
if (InGameInfo.Ins == null) return;
if (LoadMapMgr.isIns && LoadMapMgr.Ins != null && LoadMapMgr.Ins.gameObject.scene == scene) return;
var go = new GameObject(MapNodesName);
SceneManager.MoveGameObjectToScene(go, scene);
var mgr = go.AddComponent<WLIslandMapNodes>();
var mgr = WLIslandMapNodes.Ensure(cfg);
if (mgr == null) return;
mgr.LoadSceneName = d.sceneName;
mgr.LoadMapPrefab = "";
MapNodesMade++;
WLIslandBridge.Log(cfg, "던전 맵 노드 생성(LoadMapMgr) — " + scene.name);
mgr.SetStartPos(DungeonStartPos(d), WLDungeonCameraRig.SolvedYaw(cfg));
WLIslandBridge.Log(cfg, "던전 맵 노드 준비(LoadMapMgr) — " + scene.name + " · 시작 " + mgr.StartPos.ToString("F1"));
}
/// <summary>던전(아레나)에는 포털이 없다 — 스테이지 표의 앵커가 시작 지점이다.</summary>
public static Vector3 DungeonStartPos(WLDungeonDef d)
{
var def = WLDungeonSceneSetup.StageDefOf(d);
return def != null ? def.anchor : Vector3.zero;
}
// ─────────────────────────────────────────────────────────────────
@ -263,13 +267,115 @@ namespace WL.Island
}
/// <summary>
/// 섬 씬 전용 `LoadMapMgr`. 원본은 Awake 에서 맵 프리팹을 Addressables 로 받아 오는데,
/// 섬은 씬 자체가 맵이라 받을 것이 없다 → Awake 만 갈아끼워 **주소 로드 0**·싱글턴만 채운다.
/// 🔴 원본 `LoadMapMgr.cs` 는 한 줄도 고치지 않는다(파생으로 해결).
/// 섬·던전 공용 `LoadMapMgr`. 원본은 Awake 에서 맵 프리팹을 Addressables 로 받아 오는데,
/// 섬·던전은 씬 자체가 맵이라 받을 것이 없다 → Awake 만 갈아끼워 **주소 로드 0**·싱글턴만 채운다.
/// 🔴 원본 `LoadMapMgr.cs` 는 한 줄도 고치지 않는다(파생 + private 필드 주입으로 해결).
///
/// ■ WL-816i 가 고친 것 2가지 (PD 실플레이 에러 ①)
/// ① `MissingReferenceException … WLIslandMapNodes has been destroyed` —
/// 씬마다 새로 만들던 노드가 `LoadSceneMode` 전환에서 파괴되는데 정적 `Ins` 는 그대로 남아
/// `Get_PortalPos` 의 `StartCoroutine` 이 죽은 객체를 때렸다.
/// → **DontDestroyOnLoad 단 하나**만 두고 씬이 바뀌면 시작 지점만 갈아끼운다.
/// ② `Co_Get_PortalPos` 의 `while (CheckNull(m_MapData)) yield return null;` 이 끝나지 않던 것 —
/// 우리는 맵 프리팹이 없어 `m_MapData` 가 영원히 null 이라 `Make_Actors` 의 콜백(펫·일꾼)과
/// `PCActor.Set_Warp`·`MobControlMgr` 의 도달성 검사가 **영영 안 불렸다**.
/// → 런타임 `MapData` 를 하나 만들어 넣는다. **꺼진 오브젝트**에 붙여서
/// `MapData.Awake/Start` 가 돌지 않게 한다(= `MapData.Ins` 도, RenderSettings 앰비언트·안개도 무변경).
/// </summary>
public sealed class WLIslandMapNodes : LoadMapMgr
{
public const int ZoneSlots = 16; // n_PortalIndex 가 무엇이 오든 IndexOutOfRange 가 안 나게
public const int ObjSlots = 8; // Get_tfObj(index) 여유분
static WLIslandMapNodes s_ins;
static readonly System.Reflection.FieldInfo F_MapData =
typeof(LoadMapMgr).GetField("m_MapData", System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance);
static readonly System.Reflection.FieldInfo F_GoMap =
typeof(LoadMapMgr).GetField("go_map", System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance);
Transform _start;
MapData _data;
GameObject _dummyMap;
/// <summary>현재 씬의 시작 지점(보고·진단용).</summary>
public Vector3 StartPos { get { return _start != null ? _start.position : Vector3.zero; } }
protected override void Awake() { Ins = this; isIns = true; }
/// <summary>
/// 씬을 넘어 사는 노드를 **하나만** 만들고 싱글턴 자리를 되찾는다.
/// (다른 맵의 원본 `LoadMapMgr` 가 `Ins` 를 가져갔다가 그 씬이 언로드되면 죽은 참조가 남는다 — 그것도 여기서 덮는다.)
/// </summary>
public static WLIslandMapNodes Ensure(WLIslandSettings cfg)
{
if (s_ins == null)
{
var go = new GameObject(WLIslandSceneSetup.MapNodesName);
Object.DontDestroyOnLoad(go);
s_ins = go.AddComponent<WLIslandMapNodes>();
s_ins.LoadMapPrefab = "";
s_ins.Build();
WLIslandSceneSetup.MapNodesMade++;
}
Ins = s_ins; isIns = true; // 죽은 참조로 남지 않게 매번 되찾는다
return s_ins;
}
void Build()
{
// ① 시작 지점 노드 — 포털/부활/농부/오브젝트 자리를 전부 이 하나로 답한다(던전은 아레나라 포털이 없다).
var holder = new GameObject("MapNodes");
holder.transform.SetParent(transform, false);
holder.SetActive(false); // 🔴 꺼진 채로 AddComponent → MapData.Awake/Start 가 아예 안 돈다
var startGo = new GameObject("start");
startGo.transform.SetParent(holder.transform, false);
_start = startGo.transform;
_data = holder.AddComponent<MapData>();
var objs = new Transform[ObjSlots];
for (int i = 0; i < ObjSlots; i++) objs[i] = _start;
_data.m_MapZoneData = new List<MapZoneData>(ZoneSlots);
for (int i = 0; i < ZoneSlots; i++)
{
_data.m_MapZoneData.Add(new MapZoneData
{
go_portal = startGo,
go_reincarnation = startGo,
go_mazeNextPortal = null, // Set_MazeNextPortal 은 null 이면 그냥 넘어간다(원본 확인)
tf_Farmer = _start,
tf_Obj = objs,
CamRot_Portal = 0f,
CamRot_Reincarnation = 0f,
});
}
// ② SetActive_Map(bool) 이 만지는 자리(비어 있으면 NullReference) — 아무것도 안 든 더미.
_dummyMap = new GameObject("map");
_dummyMap.transform.SetParent(transform, false);
// ③ 원본의 private 필드에 넣는다 — 원본 코드 0줄로 `Co_Get_*` 의 대기를 즉시 끝낸다.
if (F_MapData != null) F_MapData.SetValue(this, _data);
if (F_GoMap != null) F_GoMap.SetValue(this, _dummyMap);
}
/// <summary>씬이 바뀔 때마다 시작 지점과 카메라 초기 방향만 갈아끼운다(객체는 그대로 산다).</summary>
public void SetStartPos(Vector3 pos, float camYaw)
{
if (_start == null) Build();
_start.position = pos - Vector3.forward; // 원본이 +Vector3.forward 를 더해 돌려준다 → 결과가 pos 가 되게
if (_data == null || _data.m_MapZoneData == null) return;
for (int i = 0; i < _data.m_MapZoneData.Count; i++)
{
var z = _data.m_MapZoneData[i];
if (z == null) continue;
z.CamRot_Portal = camYaw;
z.CamRot_Reincarnation = camYaw;
}
}
}
/// <summary>FI 애니메이터의 파라미터를 치비 애니메이터로 그대로 흘린다(Humanoid 리타깃).</summary>

View File

@ -313,6 +313,33 @@ namespace WL.Island
"비우면 아무것도 지우지 않는다. PD 에게 새 상태를 보여 줄 때 값을 바꾼다.")]
public string saveResetToken = "816g-1";
// ───────────────────────────────── WL-816i · 카메라 구도 고정
// PD 「카메라 구도는 섬에서 조작할 때의 각도와 뷰를 말한거야. 전장에 진입해도 바뀌지 않아야 하기 때문」
// → 기준은 **섬**. 아래 기본값은 섬(FI) 카메라 실측값 그대로다.
// Level01.unity `PlayerVirtualCamera` — 회전 오일러 (45,0,0) · FramingTransposer m_CameraDistance 22
// · 렌즈 FOV 60 · 원근(orthographic 0) · Near 0.1 / Far 500 · ScreenX/Y 0.5(대상 화면 중앙)
[Header("카메라 구도 (WL-816i · 섬 기준을 던전에 그대로)")]
[Tooltip("1 이면 던전(전장)의 카메라를 아래 **섬 구도** 값으로 맞춘다. 0 이면 던전은 힘민지 기본 전투 카메라 그대로(= 816g 까지의 동작).")]
public int dungeonCameraMatchIsland = 1;
[Tooltip("섬 카메라 내려보는 각(도). 섬 실측 = 45.")]
public float camPitch = 45f;
[Tooltip("섬 카메라 수평 방향(도). 섬은 플레이어를 따라 돌지 않는 고정 구도라 0.")]
public float camYaw = 0f;
[Tooltip("섬 카메라와 대상 사이 거리(m). 섬 실측 = 22 (CinemachineFramingTransposer.m_CameraDistance).")]
public float camDistance = 22f;
[Tooltip("섬 카메라 화각(도). 섬 실측 = 60 · 원근 투영.")]
public float camFov = 60f;
[Tooltip("카메라가 바라보는 점의 높이(대상 발밑 기준 m). 섬은 대상 원점을 화면 중앙에 두므로 0.")]
public float camLookHeight = 0f;
[Tooltip("1 이면 던전에서 카메라를 드래그로 돌릴 수 없다(섬과 같은 고정 구도). 0 이면 회전 허용.")]
public int camLockYaw = 1;
// ───────────────────────────────── 던전 표
[Header("던전 표 — 한 줄 = 던전 하나")]
public WLDungeonDef[] dungeons = new WLDungeonDef[0];