260 lines
16 KiB
C#
260 lines
16 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 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;
|
|
bool _leadTouched; float _oLead = 1f;
|
|
|
|
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;
|
|
}
|
|
|
|
// 이동 방향 리드(#755) 배율 — 기본 1 = 아무것도 만지지 않는다(다른 팀 기능 그대로).
|
|
// 0 으로 두면 던전에서 화면 중심이 흔들리지 않아 섬과 완전히 같은 고정 구도가 된다.
|
|
if (_cfg.camLeadMultiplier >= 0f && !Mathf.Approximately(_cfg.camLeadMultiplier, 1f))
|
|
{
|
|
if (!_leadTouched) { _leadTouched = true; _oLead = CameraMoveLead.LeadMultiplier; }
|
|
CameraMoveLead.LeadMultiplier = _cfg.camLeadMultiplier;
|
|
}
|
|
|
|
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 (_leadTouched) { CameraMoveLead.LeadMultiplier = _oLead; _leadTouched = false; }
|
|
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];
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// WL-816i 에러 ② — "Screen position out of view frustum" 감시·수리
|
|
//
|
|
// 🔴 실측(이 프로젝트 에디터 · 같은 문구 그대로 재현):
|
|
// 이 에러는 **좌표가 화면 밖이라서** 나는 것이 아니다. 화면 밖·클립 밖 좌표(뷰포트 1,0 · z 1000)를
|
|
// 넣어도 정상 카메라에서는 에러가 **나지 않는다**. 에러가 나는 유일한 조건은 **카메라의 투영이 깨진 것**이다:
|
|
// · near == far → 에러
|
|
// · 직교인데 orthographicSize ≤ 0 → 에러
|
|
// · aspect ≤ 0 → 에러
|
|
// · 카메라를 꺼 두거나 오브젝트를 꺼 둔 것 → 에러 아님(정상 동작)
|
|
// PD 콘솔의 「screen pos 1080, 0, 1000」 = (pixelRect.xMax, pixelRect.yMin, 대상 깊이) —
|
|
// NGUI `UIAnchor.Update`(UIAnchor.cs:206-207)가 화면 우하단 앵커를 계산할 때 넣는 값 모양 그대로다.
|
|
// → 그래서 값을 넣는 쪽이 아니라 **깨진 카메라**를 찾아 되살린다. 깨진 카메라는 어차피 아무것도 못 그린다.
|
|
// ─────────────────────────────────────────────────────────────────
|
|
public static int Repairs;
|
|
public static string LastRepair = "";
|
|
|
|
/// <summary>투영이 깨진 카메라를 찾아 되살린다. 되살린 수를 돌려준다(정상이면 0 · 로그도 없다).</summary>
|
|
public static int RepairDegenerateCameras(WLIslandSettings cfg)
|
|
{
|
|
if (cfg == null || cfg.fixDegenerateCameras == 0) return 0;
|
|
var cams = Object.FindObjectsByType<Camera>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
|
int n = 0;
|
|
for (int i = 0; i < cams.Length; i++)
|
|
{
|
|
var c = cams[i];
|
|
if (c == null) continue;
|
|
string why = null;
|
|
if (c.nearClipPlane >= c.farClipPlane) { why = "near " + c.nearClipPlane + " ≥ far " + c.farClipPlane; c.nearClipPlane = 0.3f; c.farClipPlane = 500f; }
|
|
if (c.orthographic && c.orthographicSize <= 0f) { why = (why == null ? "" : why + " · ") + "직교 size " + c.orthographicSize; c.orthographicSize = 5f; }
|
|
if (c.aspect <= 0f || float.IsNaN(c.aspect)) { why = (why == null ? "" : why + " · ") + "aspect " + c.aspect; c.ResetAspect(); }
|
|
if (why == null) continue;
|
|
n++; Repairs++;
|
|
LastRepair = c.name + "(" + (c.gameObject.scene.IsValid() ? c.gameObject.scene.name : "-") + ") — " + why;
|
|
WLIslandBridge.Log(cfg, "🔴 투영이 깨진 카메라를 되살렸다 — " + LastRepair);
|
|
}
|
|
return n;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// 실측 보고용 — 지금 화면의 구도를 잰다(섬·던전 공통 · 캡처와 함께 표로 남긴다)
|
|
// ─────────────────────────────────────────────────────────────────
|
|
/// <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");
|
|
}
|
|
}
|
|
}
|