193 lines
7.3 KiB
C#
193 lines
7.3 KiB
C#
using UnityEngine;
|
||
|
||
namespace TKS
|
||
{
|
||
/// <summary>
|
||
/// 기믹 모양(circle · rect(회전) · poly) 기하 — 지대 겹침(경로 스윕) · 거리 · 진입/이탈 거리. 장애물 콜라이더도 이 모양으로 만든다.
|
||
/// 경로 스윕은 0.05 간격 표본(전장 20×32 · 슬링 최대 6.5 → 한 행동 130 표본 × 기믹 수 · 충분히 가볍다).
|
||
/// </summary>
|
||
public struct GimmickShape
|
||
{
|
||
public const float SweepStep = 0.05f;
|
||
|
||
public GimmickShapeType Type;
|
||
public Vector2 Center;
|
||
/// <summary>circle 반지름.</summary>
|
||
public float R;
|
||
/// <summary>rect 가로·세로.</summary>
|
||
public Vector2 Size;
|
||
/// <summary>rect 회전(도 · 반시계).</summary>
|
||
public float Rot;
|
||
/// <summary>poly 꼭짓점(월드 · Center 기준 상대 좌표가 아니다).</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>가장 바깥 점까지의 거리(대략 검사용).</summary>
|
||
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));
|
||
}
|
||
}
|
||
|
||
/// <summary>점 q 까지 부호 거리(안 = 음수 · 경계 = 0).</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>선분 ab 가 모양과 겹치는가(중심 경로 · 여유 tol).</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>선분 ab 중 모양 안에 든 길이.</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>origin 에서 dir 로 dist 안에 중심이 처음 모양 안에 드는 거리(출발점이 이미 안이면 0 · 없으면 −1).</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>origin(모양 안)에서 dir 로 나가는 거리(max 까지 · 출발점이 밖이면 0).</summary>
|
||
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);
|
||
}
|
||
}
|
||
}
|