Вычисление, какой из сегментов линии коллайдера был поражен в Единстве? (Размещение игровых объектов по периметру коллайдера)

#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, но большое вам спасибо за ваш вклад, похоже, это тоже сработает.