using UnityEngine; namespace TKS { /// /// 기믹 모양(circle · rect(회전) · poly) 기하 — 지대 겹침(경로 스윕) · 거리 · 진입/이탈 거리. 장애물 콜라이더도 이 모양으로 만든다. /// 경로 스윕은 0.05 간격 표본(전장 20×32 · 슬링 최대 6.5 → 한 행동 130 표본 × 기믹 수 · 충분히 가볍다). /// public struct GimmickShape { public const float SweepStep = 0.05f; public GimmickShapeType Type; public Vector2 Center; /// circle 반지름. public float R; /// rect 가로·세로. public Vector2 Size; /// rect 회전(도 · 반시계). public float Rot; /// poly 꼭짓점(월드 · Center 기준 상대 좌표가 아니다). public Vector2[] Points; public static GimmickShape From(GimmickPlacement p) { var s = new GimmickShape { Type = p.shape, Center = new Vector2(p.cx, p.cy), R = p.wOrR, Size = new Vector2(p.wOrR, p.h), Rot = p.rot }; if (p.shape == GimmickShapeType.Poly) s.Points = p.points != null ? (Vector2[])p.points.Clone() : new Vector2[0]; return s; } public static GimmickShape Circle(Vector2 c, float r) { return new GimmickShape { Type = GimmickShapeType.Circle, Center = c, R = r }; } public static GimmickShape Box(Vector2 c, Vector2 size, float rot = 0f) { return new GimmickShape { Type = GimmickShapeType.Rect, Center = c, Size = size, Rot = rot }; } public GimmickShape Translated(Vector2 d) { var s = this; s.Center += d; if (Type == GimmickShapeType.Poly && Points != null) { s.Points = new Vector2[Points.Length]; for (int i = 0; i < Points.Length; i++) s.Points[i] = Points[i] + d; } return s; } /// 가장 바깥 점까지의 거리(대략 검사용). public float BoundingRadius { get { switch (Type) { case GimmickShapeType.Circle: return R; case GimmickShapeType.Rect: return Size.magnitude * 0.5f; } float m = 0f; if (Points != null) foreach (var p in Points) m = Mathf.Max(m, Vector2.Distance(p, Center)); return m; } } public Vector2 ToLocal(Vector2 q) { Vector2 d = q - Center; float a = -Rot * Mathf.Deg2Rad; float c = Mathf.Cos(a), s = Mathf.Sin(a); return new Vector2(d.x * c - d.y * s, d.x * s + d.y * c); } public Vector2 LocalAxisX { get { float a = Rot * Mathf.Deg2Rad; return new Vector2(Mathf.Cos(a), Mathf.Sin(a)); } } /// 점 q 까지 부호 거리(안 = 음수 · 경계 = 0). public float SignedDistance(Vector2 q) { switch (Type) { case GimmickShapeType.Circle: return Vector2.Distance(q, Center) - R; case GimmickShapeType.Rect: { Vector2 l = ToLocal(q); Vector2 d = new Vector2(Mathf.Abs(l.x) - Size.x * 0.5f, Mathf.Abs(l.y) - Size.y * 0.5f); Vector2 o = new Vector2(Mathf.Max(d.x, 0f), Mathf.Max(d.y, 0f)); return o.magnitude + Mathf.Min(Mathf.Max(d.x, d.y), 0f); } } if (Points == null || Points.Length < 3) return float.MaxValue; float min = float.MaxValue; bool inside = false; for (int i = 0, j = Points.Length - 1; i < Points.Length; j = i++) { Vector2 a = Points[i], b = Points[j]; min = Mathf.Min(min, Combat.SegmentPointDistance(a, b, q)); if ((a.y > q.y) != (b.y > q.y) && q.x < (b.x - a.x) * (q.y - a.y) / (b.y - a.y) + a.x) inside = !inside; } return inside ? -min : min; } public bool Contains(Vector2 q) { return SignedDistance(q) <= 0f; } /// 선분 ab 가 모양과 겹치는가(중심 경로 · 여유 tol). public bool SegmentTouches(Vector2 a, Vector2 b, float tol = 0f) { float len = Vector2.Distance(a, b); if (SegmentPointDistanceToCenter(a, b) > BoundingRadius + tol + 0.01f) return false; int n = Mathf.Max(1, Mathf.CeilToInt(len / SweepStep)); for (int i = 0; i <= n; i++) { if (SignedDistance(Vector2.Lerp(a, b, (float)i / n)) <= tol) return true; } return false; } /// 선분 ab 중 모양 안에 든 길이. public float InsideLength(Vector2 a, Vector2 b) { float len = Vector2.Distance(a, b); if (len < 1e-4f || SegmentPointDistanceToCenter(a, b) > BoundingRadius + 0.01f) return 0f; int n = Mathf.Max(1, Mathf.CeilToInt(len / SweepStep)); float step = len / n; float inside = 0f; for (int i = 0; i < n; i++) { if (Contains(Vector2.Lerp(a, b, (i + 0.5f) / n))) inside += step; } return inside; } /// origin 에서 dir 로 dist 안에 중심이 처음 모양 안에 드는 거리(출발점이 이미 안이면 0 · 없으면 −1). public float EnterDistance(Vector2 origin, Vector2 dir, float dist) { if (Contains(origin)) return 0f; if (SegmentPointDistanceToCenter(origin, origin + dir * dist) > BoundingRadius + 0.01f) return -1f; int n = Mathf.Max(1, Mathf.CeilToInt(dist / SweepStep)); float prev = 0f; for (int i = 1; i <= n; i++) { float t = dist * i / n; if (!Contains(origin + dir * t)) { prev = t; continue; } // 이분법으로 경계를 좁힌다 float lo = prev, hi = t; for (int k = 0; k < 8; k++) { float m = (lo + hi) * 0.5f; if (Contains(origin + dir * m)) hi = m; else lo = m; } return hi; } return -1f; } /// origin(모양 안)에서 dir 로 나가는 거리(max 까지 · 출발점이 밖이면 0). public float ExitDistance(Vector2 origin, Vector2 dir, float max) { if (!Contains(origin)) return 0f; int n = Mathf.Max(1, Mathf.CeilToInt(max / SweepStep)); for (int i = 1; i <= n; i++) { float t = max * i / n; if (!Contains(origin + dir * t)) return t; } return max; } float SegmentPointDistanceToCenter(Vector2 a, Vector2 b) { return Combat.SegmentPointDistance(a, b, Center); } } }