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); } }