using UnityEngine; namespace TKS { /// /// 넓은 전장용 카메라: 화면보다 큰 전장을 빈 곳 드래그로 스크롤 · 휠로 줌 · 행동 중인 유닛 자동 추적 · 전장 밖으로 못 나가게 고정. /// public class CameraController : MonoBehaviour { public Board Board; public float ViewHeight = 18f; public float MinViewHeight = 10f; public float MaxViewHeight = 36f; public float Margin = 1f; [Tooltip("위쪽 HUD(스테이지·턴 글자) 밑에 유닛이 깔리지 않도록 전장 위 끝을 더 내려 볼 수 있는 여유")] public float TopMargin = 2.6f; [Tooltip("아래쪽 HUD(안내·유닛 정보) 여유")] public float BottomMargin = 2.2f; public float FollowSpeed = 7f; Camera cam; Vector2? focusTarget; Transform followTarget; public Camera Cam { get { if (cam == null) cam = GetComponent(); return cam; } } public void Init(Board board, float viewHeight, float minViewHeight, float maxViewHeight) { Board = board; MinViewHeight = Mathf.Max(1f, minViewHeight); MaxViewHeight = Mathf.Max(MinViewHeight, maxViewHeight); ViewHeight = Mathf.Clamp(viewHeight, MinViewHeight, MaxViewHeight); Cam.orthographicSize = ViewHeight * 0.5f; SetPosition(transform.position); } /// 화면 1 픽셀이 월드 몇 단위인가(드래그 스크롤 환산). public float WorldUnitsPerPixel { get { return Cam.orthographicSize * 2f / Mathf.Max(1, Cam.pixelHeight); } } public Rect ViewRect { get { float halfH = Cam.orthographicSize; float halfW = halfH * Cam.aspect; Vector2 c = transform.position; return new Rect(c.x - halfW, c.y - halfH, halfW * 2f, halfH * 2f); } } public void PanBy(Vector2 worldDelta) { focusTarget = null; followTarget = null; SetPosition((Vector2)transform.position + worldDelta); } public void FocusOn(Vector2 worldPos) { followTarget = null; focusTarget = worldPos; } public void SnapTo(Vector2 worldPos) { followTarget = null; focusTarget = null; SetPosition(worldPos); } public void Follow(Transform t) { focusTarget = null; followTarget = t; } public void StopFollow() { followTarget = null; focusTarget = null; } public void Zoom(float factor) { SetViewHeight(ViewHeight * factor); } public void SetViewHeight(float height) { ViewHeight = Mathf.Clamp(height, MinViewHeight, MaxViewHeight); Cam.orthographicSize = ViewHeight * 0.5f; SetPosition(transform.position); } // 화면 흔들림(타격감 · PD 2026-11-10 「타격감을 위한 화면 효과(화면 흔들림 등)도 IB 를 참고해서」) — 추적·스크롤 위치(기준)에 매 프레임 흔들림만 더하고 // 다음 프레임 처음에 빼서 기준 위치를 흔들지 않는다. 값 = 표(Resources/Skills/hit_feel.csv · HitFeel). Vector2 shakeApplied; float shakeTime; float shakeDuration; float shakeMagnitude; /// 흔들기 시작(진행 중이면 더 센 쪽을 남김) · duration 초 · magnitude = 최대 흔들림(월드 단위) · 끝으로 갈수록 약해짐. public void Shake(float duration, float magnitude) { if (duration <= 0f || magnitude <= 0f) return; float remain = shakeDuration > 0f ? shakeMagnitude * Mathf.Clamp01(1f - shakeTime / shakeDuration) : 0f; if (magnitude < remain) return; shakeDuration = duration; shakeMagnitude = magnitude; shakeTime = 0f; } public bool Shaking { get { return shakeDuration > 0f && shakeTime < shakeDuration; } } void LateUpdate() { if (shakeApplied != Vector2.zero) // 지난 프레임 흔들림 빼기(기준 위치 복원) { transform.position -= (Vector3)shakeApplied; shakeApplied = Vector2.zero; } Follow(); if (Shaking) { shakeTime += Time.unscaledDeltaTime; float k = Mathf.Clamp01(1f - shakeTime / shakeDuration); shakeApplied = Random.insideUnitCircle * (shakeMagnitude * k); transform.position += (Vector3)shakeApplied; } } void Follow() { Vector2? goal = null; if (followTarget != null && followTarget.gameObject.activeInHierarchy) goal = (Vector2)followTarget.position; else if (focusTarget.HasValue) goal = focusTarget; if (!goal.HasValue) return; Vector2 target = Clamp(goal.Value); Vector2 cur = transform.position; float k = 1f - Mathf.Exp(-FollowSpeed * Time.deltaTime); Vector2 next = Vector2.Lerp(cur, target, k); SetPosition(next); if (focusTarget.HasValue && Vector2.Distance(next, target) < 0.02f) focusTarget = null; } void SetPosition(Vector2 p) { p = Clamp(p); transform.position = new Vector3(p.x, p.y, -10f); } public Vector2 Clamp(Vector2 p) { if (Board == null) return p; float halfH = Cam.orthographicSize; float halfW = halfH * Cam.aspect; var r = Board.Rect; float minX = r.xMin - Margin + halfW; float maxX = r.xMax + Margin - halfW; float minY = r.yMin - BottomMargin + halfH; float maxY = r.yMax + TopMargin - halfH; p.x = minX > maxX ? 0f : Mathf.Clamp(p.x, minX, maxX); p.y = minY > maxY ? 0f : Mathf.Clamp(p.y, minY, maxY); return p; } } }