[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:
parent
9aaddae676
commit
f2e89653ed
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4b386eb2f387c634992d62d3591735a9
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
Loading…
Reference in New Issue