ThreeKingdomsSlingshot/Assets/Scripts/Runtime/CameraController.cs

182 lines
6.3 KiB
C#
Raw Permalink Normal View History

using UnityEngine;
namespace TKS
{
/// <summary>
/// 넓은 전장용 카메라: 화면보다 큰 전장을 빈 곳 드래그로 스크롤 · 휠로 줌 · 행동 중인 유닛 자동 추적 · 전장 밖으로 못 나가게 고정.
/// </summary>
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<Camera>();
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);
}
/// <summary>화면 1 픽셀이 월드 몇 단위인가(드래그 스크롤 환산).</summary>
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);
}
스킬 이펙트 1차 + IB(쿠로네코) 연출 시험 — 돌진·조준 UI·타격감 (PD 2026-11-10) - 기본 공격(돌진) = IB 이펙트 그대로 시험: 앞 빛 물결 · 속도 꼬리(진영색 4색 · 멈춘 자리에서 0.12초 흐려짐) · WL 흙먼지 · 기준점 dash(아래로 갈수록 발 쪽) · 히트 이펙트 제거(PD 지정 대기) - 슬링샷 방향 결정 UI = IB GameTouchArrow 이식(IbAimArrow · IB 유닛 반지름 0.496 기준 비율) + 진행 중 캐릭터 연출(IbUnitMarks: 발밑 원 · 몸 빛 · 조준 중 속성 방울) · 값 표 Resources/Skills/ib_ui.csv - 기병: 복귀 영역(채운 원 · PreviewCavalryReturn) · 복귀 때 돌아서기(UnitView.TurnAround) - 타격감: HitFeel + CameraController.Shake · 표 hit_feel.csv(IB 흔들림 값은 디컴파일에 없어 추정값) - 액티브 스킬 1차 연출(Fable 제안 56 스킬 · 사본 Resources/Fx/TKS_Fx_Sk_*) · SkillVfx 확장(s_camp · f_fwd · f_drift · f_fade · s_over · b_flat · 트레일 페이드) · 셀화 셰이더 포트 - 2D 돌진 시험본(Dash2D · Aura2D test_aura) · 이전 시험 행은 skill_vfx.csv 에 # 주석 - 여포 크기: dz_boss_lvbu 몸 높이 819 → 718(깃털 제외 · 말 탄 유닛 머리 끝 상한 때문에 작아졌음) - 원본 = Assets/VFX/IB(쿠로네코 · GUID 유지 · Tools/wl_vfx_import.py --skip-scripts) · 사본 도구 Tools/make_ib_fx_copies.cs · make_ib_camp_textures.py · build_dash2d.cs - 테스트 PlayMode 118/118 통과 · 자동 재직렬화(png meta · UITheme · ProjectSettings) 제외 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 04:19:44 +00:00
// 화면 흔들림(타격감 · PD 2026-11-10 「타격감을 위한 화면 효과(화면 흔들림 등)도 IB 를 참고해서」) — 추적·스크롤 위치(기준)에 매 프레임 흔들림만 더하고
// 다음 프레임 처음에 빼서 기준 위치를 흔들지 않는다. 값 = 표(Resources/Skills/hit_feel.csv · HitFeel).
Vector2 shakeApplied;
float shakeTime;
float shakeDuration;
float shakeMagnitude;
/// <summary>흔들기 시작(진행 중이면 더 센 쪽을 남김) · duration 초 · magnitude = 최대 흔들림(월드 단위) · 끝으로 갈수록 약해짐.</summary>
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()
스킬 이펙트 1차 + IB(쿠로네코) 연출 시험 — 돌진·조준 UI·타격감 (PD 2026-11-10) - 기본 공격(돌진) = IB 이펙트 그대로 시험: 앞 빛 물결 · 속도 꼬리(진영색 4색 · 멈춘 자리에서 0.12초 흐려짐) · WL 흙먼지 · 기준점 dash(아래로 갈수록 발 쪽) · 히트 이펙트 제거(PD 지정 대기) - 슬링샷 방향 결정 UI = IB GameTouchArrow 이식(IbAimArrow · IB 유닛 반지름 0.496 기준 비율) + 진행 중 캐릭터 연출(IbUnitMarks: 발밑 원 · 몸 빛 · 조준 중 속성 방울) · 값 표 Resources/Skills/ib_ui.csv - 기병: 복귀 영역(채운 원 · PreviewCavalryReturn) · 복귀 때 돌아서기(UnitView.TurnAround) - 타격감: HitFeel + CameraController.Shake · 표 hit_feel.csv(IB 흔들림 값은 디컴파일에 없어 추정값) - 액티브 스킬 1차 연출(Fable 제안 56 스킬 · 사본 Resources/Fx/TKS_Fx_Sk_*) · SkillVfx 확장(s_camp · f_fwd · f_drift · f_fade · s_over · b_flat · 트레일 페이드) · 셀화 셰이더 포트 - 2D 돌진 시험본(Dash2D · Aura2D test_aura) · 이전 시험 행은 skill_vfx.csv 에 # 주석 - 여포 크기: dz_boss_lvbu 몸 높이 819 → 718(깃털 제외 · 말 탄 유닛 머리 끝 상한 때문에 작아졌음) - 원본 = Assets/VFX/IB(쿠로네코 · GUID 유지 · Tools/wl_vfx_import.py --skip-scripts) · 사본 도구 Tools/make_ib_fx_copies.cs · make_ib_camp_textures.py · build_dash2d.cs - 테스트 PlayMode 118/118 통과 · 자동 재직렬화(png meta · UITheme · ProjectSettings) 제외 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 04:19:44 +00:00
{
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;
}
}
}