using UnityEngine;
///
/// 캐릭터가 화면 밖으로 나가 잘리지 않도록 화면 좌표를 제한한다.
///
/// 드래그와 창 올라타기 양쪽에서 쓴다. 캐릭터를 화면 가장자리 창의 타이틀바에
/// 올려놓을 때도 같은 제한이 필요하기 때문에 별도 유틸로 분리했다.
///
/// 캐릭터는 고정된 깊이 평면 위에서만 움직이므로, 화면상 크기는 드래그 중에
/// 변하지 않는다. 그래서 시작할 때 한 번만 재면 된다.
///
public static class CharacterScreenBounds
{
///
/// 캐릭터 원점(transform.position) 기준으로, 화면상 어디까지 뻗어 있는지 잰다.
/// 반환되는 offMin/offMax 는 원점의 화면 좌표에 대한 상대 오프셋이다.
///
public static bool TryMeasure(Camera cam, Transform character, out Vector2 offMin, out Vector2 offMax)
{
offMin = offMax = Vector2.zero;
if (cam == null || character == null) return false;
var renderers = character.GetComponentsInChildren(true);
if (renderers.Length == 0) return false;
Bounds world = renderers[0].bounds;
for (int i = 1; i < renderers.Length; i++) world.Encapsulate(renderers[i].bounds);
Vector3 c = world.center;
Vector3 e = world.extents;
// AABB 를 화면에 투영하면 축 정렬이 깨지므로 8개 꼭짓점을 모두 변환해 감싼다.
var min = new Vector2(float.MaxValue, float.MaxValue);
var max = new Vector2(float.MinValue, float.MinValue);
for (int i = 0; i < 8; i++)
{
var corner = new Vector3(
c.x + ((i & 1) == 0 ? -e.x : e.x),
c.y + ((i & 2) == 0 ? -e.y : e.y),
c.z + ((i & 4) == 0 ? -e.z : e.z));
Vector3 sp = cam.WorldToScreenPoint(corner);
min = Vector2.Min(min, sp);
max = Vector2.Max(max, sp);
}
Vector2 origin = cam.WorldToScreenPoint(character.position);
offMin = min - origin;
offMax = max - origin;
return true;
}
///
/// 캐릭터 원점의 화면 좌표를, 캐릭터 전체가 화면 안에 들어오도록 제한한다.
///
/// 가장자리에서 추가로 띄울 여백(픽셀)
public static Vector2 Clamp(Vector2 origin, Vector2 offMin, Vector2 offMax, float margin = 0f)
{
return new Vector2(
ClampAxis(origin.x, offMin.x, offMax.x, Screen.width, margin),
ClampAxis(origin.y, offMin.y, offMax.y, Screen.height, margin));
}
static float ClampAxis(float value, float offMin, float offMax, float screenSize, float margin)
{
float lo = margin - offMin;
float hi = screenSize - margin - offMax;
// 캐릭터가 화면보다 큰 경우 범위가 뒤집힌다. 그때는 가운데로 보낸다.
if (lo > hi) return (lo + hi) * 0.5f;
return Mathf.Clamp(value, lo, hi);
}
}