#unity3d
Вопрос:
Для игры, над которой я работаю, я пытаюсь написать код, который размещает игровые объекты по периметру полигонального коллидера2d. В моей игре объект может коснуться платформы, а затем начинает распространять сопротивление вокруг этой платформы. Я хочу, чтобы вещество процедурно распространялось по платформе, размещая игровые объекты каждые x
единицы. Для примера того, что я имею в виду, пожалуйста, взгляните на этот .gif, где я сделал то же самое с RayCasts.
Попытка сделать это с помощью raycasts привела к появлению множества крайних случаев. Чтобы устранить их, я хочу применить более последовательный метод.
В Unity коллайдер содержит массив Collider.points
, в котором содержатся координаты точек, составляющих коллайдер. Теоретически, если вы начнете размещать игровые объекты point[0]
, посмотрите в направлении point[1]
и начнете размещать объекты в этом направлении до тех пор , пока не достигнете point[1]
, посмотрите в направлении point[2]
и повторите, вы сможете аккуратно размещать объекты по периметру указанного коллайдера.
Моя проблема в том, что я не знал бы, должен ли мой начальный объект-распространитель запускать алгоритм размещения этого объекта между point[0]
и point[1]
или point[n]
и point[n 1]
.
Пожалуйста, взгляните на этот пример:
Если мое столкновение произойдет на красном маркере, мне нужно будет каким-то образом выяснить , что столкновение произошло на отрезке линии E между point[4]
и point[5]
, чтобы затем я мог узнать «начальную позицию» по периметру и начать писать код, который размещает объекты в обоих направлениях одновременно по периметру.
Моей первой мыслью было найти мировое положение столкновения и найти мировые положения двух ближайших точек points[]
к этой точке столкновения . Но в приведенном выше примере это не сработало бы — он нашел бы позиции 2
и 4
(что даже не является сегментом), даже если столкновение касается сегмента линии между 4
и 5
(сегмент Е).
У кого-нибудь есть какие-либо предложения о том, как это сделать?
Ответ №1:
Если у вас уже есть точка контакта, вы можете пройти через все вершины ( point
) и проверить, к какой линии точка контакта ближе всего.
Следующие два метода взяты из HandleUtility
(см. Исходный код), но он существует только в редакторе, поэтому, поскольку вы хотите использовать его во время выполнения, просто скопируйте его в пользовательский класс времени выполнения
public static class VectorUtils
{
// Project /point/ onto a line.
public static Vector3 ProjectPointLine(Vector3 point, Vector3 lineStart, Vector3 lineEnd)
{
Vector3 relativePoint = point - lineStart;
Vector3 lineDirection = lineEnd - lineStart;
float length = lineDirection.magnitude;
Vector3 normalizedLineDirection = lineDirection;
if (length > .000001f)
normalizedLineDirection /= length;
float dot = Vector3.Dot(normalizedLineDirection, relativePoint);
dot = Mathf.Clamp(dot, 0.0F, length);
return lineStart normalizedLineDirection * dot;
}
// Calculate distance between a point and a line.
public static float DistancePointLine(Vector3 point, Vector3 lineStart, Vector3 lineEnd)
{
return Vector3.Magnitude(ProjectPointLine(point, lineStart, lineEnd) - point);
}
}
Теперь, предполагая, что все ваши баллы последовательны, вы можете использовать это, например, как
// Allows to do some iteration queries on IEnumerable collections
using System.Linq;
...
public static void GetTouchSegmentEndpoints(
// The Collider.points
PolygonCollider2D collider,
// Your given collision point
Vector3 touchPoint,
// After this method call these two will be filled with the information
out Vector3 resultA, out Vector3 resultB)
{
// Assign default values
resultA = Vector3.zero;
resultB = Vector3.zero;
var localPoints = collider.points;
// First of all the PolygonCollider2D.points are in LOCAL SPACE
// so firs we need to convert them to worldSpace
// using Linq we can do this in a single line
var worldPoints = collider.points.Select(p => collider.transform.TransformPoint(p)).ToArray();
// This basically equals doing something like
//var worldPoints = new Vector3 [localPoints.Length];
//for(var i = 0; i < localPoints.Length; i )
//{
// worldPoints[i] = collider.transform.TransformPoint(localPoints[i]);
//}
// for comparing the distance to the current line
var minDistance = float.PositiveInfinity;
// Go through the world space points
for(var i = 0; i < worldPoints.Length; i )
{
// Get the next i with wrap around at the end
var nextI = i == (worldPoints.Length - 1) ? 0 : i 1;
// Get the two corner points for the current line
var pointA = worldPoints[i];
var pointB = worldPoints[nextI];
// get the distance between that line and the given touch point
var distance = VectorUtils.DistancePointLine(touchPoint, pointA, pointB);
// if it is smaller than the current minDistance
if(distance < minDistance)
{
// replace the results
resultA = pointA;
resultB = pointB;
minDistance = distance;
}
}
}
И, наконец, вы бы просто назвали это так, например
PolygonCollider2D yourCollider;
Vector3 yourWorldTouchPoint;
VectorUtils.GetTouchSegmentEndpoints(yourCollider, yourWorldTouchPoint, out var lineA, out var lineB);
// Do something with lineA and lineB
Если это самый эффективный способ, я не знаю ^^
Комментарии:
1. Мило! Я только что реализовал часть этого кода, и он выполнил свою работу!
Ответ №2:
Одним из решений было бы поиск точек для пары, линия которой ближе всего к точке столкновения. Итак, в принципе, мы хотим, чтобы наша функция CollisionEnter2D на полигоне выглядела примерно так:
private void OnCollisionEnter2D(Collision2D collision)
{
Vector2 contactPoint = collision.GetContact(0).point;
(Vector2, Vector2) closestLine = FindClosestLine(contactPoint);
if (closestLine != null)
print(closestLine);
}
Мое решение можно описать на следующей иллюстрации:
(извините за рисунок, я программист).
Как псевдоалгоритм:
define point = collisionPoint;
define pair;
define minDistance;
for pair := (p1, p2) in collider.points:
if (dist := shortestDistance(point, pair) < minDistance):
minDistance = dist
pair = (p1, p2)
В конце этого цикла у нас будут две точки, которые мы ищем. Вот примерная реализация, которую я придумал:
private (Vector2, Vector2)? FindClosestLine(Vector2 contactPoint)
{
var localScale = transform.localScale;
var points = polygonCollider.points;
(Vector2, Vector2) closestLine = (default, default);
var shortestDistance = float.MaxValue;
for (var i = 1; i < points.Length; i )
{
// We multiply the points by localScale, because the collider
// scales them to 1 internally, regardless of our size.
var line = (points[i - 1] * localScale, points[i] * localScale);
var distance = MinDistPointToLine(contactPoint, line);
if (distance < shortestDistance)
{
shortestDistance = distance;
closestLine = line;
}
}
if (shortestDistance < float.MaxValue)
return closestLine;
else return null;
}
Чтобы вычислить кратчайшее расстояние, мы можем использовать некоторую базовую тригонометрию (опять же, пожалуйста, извините за рисунок).
Код будет выглядеть примерно так:
private static float MinDistPointToLine(Vector2 point, (Vector2, Vector2) line)
{
// Calculate the shortest distance between the line (p[n 1] - p[n]) and the given point.
var (end, start) = line;
var lineLength = (start - end).magnitude;
var lineLengthSqr = lineLength * lineLength;
var distToStartSqr = (point - end).sqrMagnitude;
var distToEndSqr = (point - start).sqrMagnitude;
// Equation found by algebra.
return distToStartSqr - (distToStartSqr - distToEndSqr - lineLengthSqr) / 2 * lineLength;
}
В данном случае мы просто печатаем две точки, но вы, очевидно, используете их для реализации описанного вами алгоритма.
Комментарии:
1. Я выбрал другой подход @derHugo, но большое вам спасибо за ваш вклад, похоже, это тоже сработает.