Как предотвратить ошибку с плавающей точкой при обнаружении скольжения точки-полигона - PullRequest
7 голосов
/ 10 мая 2019

Я пытаюсь написать функцию для обработки движения в игре, которую я программирую.То, что у меня есть, почти работает, но есть пара ситуаций, когда оно ломается.

Я кодировал минимальный демонстрационный пример, представленный ниже.В этом примере я пытаюсь вычислить перемещение объекта, представленного точкой, и вектор движения.Траектория движения этого объекта проверяется по совокупности полигонов, которые разбиваются на отрезки для проверки.Когда этот объект сталкивается с отрезком линии, я хочу, чтобы он скользил вдоль этого отрезка (а не останавливался или отскакивал).

Для этого я проверяю свой предполагаемый путь на наличие столкновений, и если я нахожупересечение, я делаю новый тест из этой точки пересечения вдоль пути отрезка линии, с которым я столкнулся, с величиной остатка движения.

enter image description here

Проблема возникает, когда мы скользим вдоль отрезка в «карман».Часто проверка столкновения будет проходить по обоим отрезкам, образующим карман, и объект будет проскальзывать.Поскольку я путешествую параллельно одному из отрезков и пересекаю оба отрезка в конечных точках, я считаю, что эта проблема вызвана ошибкой с плавающей запятой.Проскальзывает ли он, перехватывается или перехватывается один раз, а затем проскальзывает во второй проверке, кажется совершенно случайным.

Я вычисляю пересечение, используя простой алгоритм, который я нашел здесь: https://stackoverflow.com/a/20679579/4208739, но я пробовал и много других алгоритмов.Все они имеют одинаковые проблемы.

(Vector2 - это класс, предоставляемый библиотекой Unity, он просто хранит координаты x и y в виде чисел с плавающей точкой. Функция Vector2.Dot просто вычисляет произведение точек).

//returns the final destination of the intended movement, given the starting position, intended direction of movement, and provided collection of line segments
//slideMax provides a hard cap on number of slides allowed before we give up
Vector2 Move(Vector2 pos, Vector2[] lineStarts, Vector2[] lineEnds, Vector2 moveDir, int slideMax)
{
    int slideCount = 0;
    while (moveDir != Vector2.zero && slideCount < slideMax)
    {
        pos = DynamicMove(pos, lineStarts, lineEnds, moveDir, out moveDir);
        slideCount++;
    }
    return pos;
}

//returns what portion of the intended movement can be performed before collision, and the vector of "slide" that the object should follow, if there is a collision
Vector2 DynamicMove(Vector2 pos, Vector2[] lineStarts, Vector2[] lineEnds, Vector2 moveDir, out Vector2 slideDir)
{
    slideDir = Vector2.zero;
    float moveRemainder = 1f;
    for (int i = 0; i < lineStarts.Length; i++)
    {
        Vector2 tSlide;
        float rem = LineProj(pos, moveDir, lineStarts[i], lineEnds[i], out tSlide);
        if (rem < moveRemainder)
        {
            moveRemainder = rem;
            slideDir = tSlide;
        }
    }
    return pos + moveDir * moveRemainder;
}

//Calculates point of collision between the intended movement and the passed in line segment, also calculate vector of slide, if applicable
float LineProj(Vector2 pos, Vector2 moveDir, Vector2 lineStart, Vector2 lineEnd, out Vector2 slideDir)
{
    slideDir = new Vector2(0, 0);
    float start = (lineStart.x - pos.x) * moveDir.y - (lineStart.y - pos.y) * moveDir.x;
    float end = (lineEnd.x - pos.x) * moveDir.y - (lineEnd.y - pos.y) * moveDir.x;
    if (start < 0 || end > 0)
        return 1;
    //https://stackoverflow.com/a/20679579/4208739
    //Uses Cramer's Rule
    float L1A = -moveDir.y;
    float L1B = moveDir.x;
    float L1C = -(pos.x *(moveDir.y + pos.y) - (moveDir.x + pos.x)*pos.y);
    float L2A = lineStart.y - lineEnd.y;
    float L2B = lineEnd.x - lineStart.x;
    float L2C = -(lineStart.x * lineEnd.y - lineEnd.x * lineStart.y);
    float D = L1A * L2B - L1B * L2A;
    float Dx = L1C * L2B - L1B * L2C;
    float Dy = L1A * L2C - L1C * L2A;
    if (D == 0)
        return 1;
    Vector2 inter = new Vector2(Dx / D, Dy / D);
    if (Vector2.Dot(inter - pos, moveDir) < 0)
        return 1;
    float t = (inter - pos).magnitude / moveDir.magnitude;
    if (t > 1)
        return 1;
    slideDir = (1 - t) * Vector2.Dot((lineEnd - lineStart).normalized, moveDir.normalized) * (lineEnd - lineStart).normalized;
    return t;
}

Есть ли способ подсчета столкновений, который не подвержен подобным проблемам?Я полагаю, что не могу полностью искоренить ошибку с плавающей запятой, но есть ли способ проверить, который, по крайней мере, гарантирует, что я столкнусь с ОДНИМ из двух отрезков в кармане?Или есть что-то более принципиально неправильное в том, чтобы поступать таким образом?

Если что-то неясно, я могу нарисовать диаграммы или написать примеры.

РЕДАКТИРОВАТЬ: Обдумав этот вопрос больше, иВ ответ на ответ Эрика, мне интересно, может ли преобразование моей математики из плавающей запятой в фиксированную точку решить эту проблему?На практике я действительно просто конвертировал бы мои значения (которые можно удобно разместить в диапазоне от -100 до 100) в целые, а затем выполнял бы математику с учетом этих ограничений?Я еще не собрал все вопросы вместе, но я мог бы попробовать.Если у кого-то есть информация о чем-либо подобном, я буду признателен.

1 Ответ

1 голос
/ 11 мая 2019

У вас есть линия, которая в идеале нацелена точно на точку, конечную точку сегмента. Это означает, что любая ошибка в вычислении, независимо от того, насколько она мала, может сказать, что линия не соответствует точке. Я вижу три возможных решения:

  • Проанализируйте арифметику и спроектируйте ее, чтобы убедиться, что она выполнена без ошибок, возможно, с использованием методов расширенной точности.
  • Проанализируйте арифметику и спроектируйте ее, чтобы убедиться, что она выполнена с небольшой ошибкой в ​​пользу столкновения, возможно, путем добавления небольшого смещения к столкновению.
  • Слегка удлинить отрезок.

Кажется, что третий будет проще всего - два отрезка, образующих карман, можно просто немного удлинить, чтобы они пересеклись. Тогда путь скольжения не будет нацелен на точку; он будет нацелен на внутреннюю часть сегмента, и будет допускать ошибку.

...