ThreeKingdomsSlingshot/Assets/Scripts/Runtime/Gimmicks/GimmickShape.cs

193 lines
7.3 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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