Обнаружение стороны столкновения между прямоугольником и прямоугольником в libgdx

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

  • Если я двигаюсь на 45 градусов вверх, я могу столкнуться с нисходящим, слева или углу стены.
  • Если я сталкиваюсь с левой, я хочу остановить движение x и двигаться только вверх. Если я покину стену, то я хочу двигаться дальше. Такой же с нижней стороной (остановка y-движения)
  • Если я сталкиваюсь с Corner, я хочу остановить движение (скользящий невозможен).

То, что я делаю на самом деле, это проверить, пересекает ли левая линия прямоугольника мой круг. Затем я проверяю пересечение между левой линией стены и моим кругом, нижней линией стены и моим кругом. В зависимости от того, какое пересечение происходит, я возвращаю x/y из моего круга и устанавливаю x/y Speed ​​на 0. Проблема заключается в том, что чаще всего не столкновение bt a перекрытие происходит. Таким образом, нижняя проверка возвращает true, даже если на самом деле круг будет только сталкиваться с правом. В этом случае оба теста пересечения вернут true, и я бы reset обе скорости, например, при столкновении углов. Как я могу решить эту проблему? Является ли лучший способ обнаружения столкновения и столкновения стороной или углом? Мне не нужна точная точка столкновения только на стороне прямоугольника.

Edit: Я должен сказать, что прямоугольники не вращаются только параллельно оси х.

Ответ 1

Ниже приведено объяснение столкновений круг/прямоугольник, но обратите внимание, что этот тип столкновения может не понадобиться для ваших нужд. Если, например, у вас была прямоугольная ограничительная рамка для вашего персонажа, алгоритм будет проще и быстрее. Даже если вы используете круг, вероятно, существует более простой подход, который достаточно хорош для ваших целей.

Я хотел бы написать код для этого, но это займет слишком много времени, поэтому вот только объяснение:

Вот пример движения вашего круга персонажей с его последними (предыдущими) и текущими позициями. Над ним отображается прямоугольник.


enter image description here


Вот то же самое движение, пунктирные линии представляют площадь, в которой этот круг перемещается. Область подметания имеет форму капсулы.


enter image description here

Было бы сложно вычислить столкновение этих двух объектов, поэтому нам нужно сделать это по-другому. Если вы посмотрите на капсулу на предыдущем изображении, вы увидите, что это просто линия движения, протянутая во всех направлениях радиусом круга. Мы можем переместить это "расширение" от линии движения к прямоугольнику стены. Таким образом, мы получаем округленный прямоугольник, как на изображении ниже.


enter image description here

Линия движения столкнется с этим расширенным (округленным) прямоугольником тогда и только тогда, когда капсула столкнется с прямоугольником стены, поэтому они как-то эквивалентны и взаимозаменяемы.

Поскольку этот расчет столкновений по-прежнему является нетривиальным и относительно дорогостоящим, вы можете сначала выполнить быструю проверку столкновения между расширенным прямоугольником стены (без округления в этот раз) и ограничивающим прямоугольником линии движения. Вы можете видеть эти прямоугольники на изображении ниже - они оба пунктирны. Это быстрый и простой расчет, и пока вы играете в игру, вероятно, НЕ будет перекрываться с конкретным прямоугольником стены > 99% времени, и здесь будет остановлен расчет столкновений.


enter image description here

Если, однако, существует перекрытие, возможно, столкновение круга символов с прямоугольником стены, но это не точно, как будет показано позже.

Теперь вам нужно рассчитать пересечение самой линии движения (а не ее ограничивающей рамки) и расширенного прямоугольника. Вероятно, вы можете найти алгоритм, как это сделать в Интернете, искать пересечение линий/прямоугольников или пересечение линий/абаб (aabb = Axis Aligned Bounding Box). Прямоугольник выравнивается по оси, что упрощает расчет. Алгоритм может дать вам точку пересечения или точки, так как возможно, что есть два - в этом случае вы выбираете ближайший к начальной точке линии. Ниже приведен пример этого пересечения/столкновения.


enter image description here

Когда вы получаете точку пересечения, вам должно быть легко подсчитать, на какой части расширенного прямоугольника это пересечение находится. Вы можете видеть эти части на изображении выше, разделенные красными линиями и отмеченные одной или двумя буквами (l - левая, r - правая, b - нижняя, t - верхняя, tl - верхняя и левая и т.д.).
Если пересечение находится на частях l, r, b или t (однобуквенные, посередине), тогда вы закончите. Существует определенно столкновение между кругом персонажей и прямоугольником стены, и вы знаете, с какой стороны. В приведенном выше примере он находится на нижней стороне. Вероятно, вы должны использовать 4 переменные, называемые как isLeftCollision, isRightCollision, isBottomCollsion и isTopCollision. В этом случае вы установите для параметра isBottomCollision значение true, а остальные 3 останутся на false.

Однако, если пересечение находится на углу, на двухбуквенных участках необходимы дополнительные вычисления, чтобы определить, существует ли фактическое столкновение между кругом персонажей и прямоугольником стены. На рисунке ниже показаны 3 таких пересечения по углам, но есть фактическое столкновение по прямоугольному прямоугольнику только на 2 из них.


enter image description here

Чтобы определить, есть ли столкновение, вам нужно найти пересечение между линией движения и окружностью, центрированной в ближайшем углу оригинального недлинного прямоугольника стены. Радиус этого круга равен радиусу круга символов. Опять же, вы можете google для алгоритма пересечения линии/круга (может быть, даже у libgdx есть один), это не сложно и не сложно найти. Не существует пересечения линий/кругов (и не сталкивается с кругом/прямоугольником) на bl-части, и есть пересечения/столкновения на br и tr частях.
В случае br вы устанавливаете как isRightCollision, isBottomCollsion в true, так и в tr-случае вы устанавливаете как isRightCollision, так и isTopCollision значение true.

Существует также один крайный кейс, на который нужно обратить внимание, и вы можете увидеть его на изображении ниже.


enter image description here

Это может произойти, если движение предыдущего шага заканчивается в углу расширенного прямоугольника, но вне радиуса внутреннего прямоугольника (не было столкновения).
Чтобы определить, действительно ли это так, просто проверьте, находится ли точка обзора движения внутри расширенного прямоугольника.
Если это так, после первоначального теста на перекрытие прямоугольника (между расширенным прямоугольником и ограничивающим прямоугольником линии движения), вы должны пропустить тест пересечения линии/прямоугольника (потому что в этом случае не может быть никакого пересечения И все равно будет столкновением круг/прямоугольник), а также просто на основе точки указания движения определяют, в каком углу вы находитесь, а затем проверяете только пересечение линии/круга с этим угловым кругом. Если есть пересечение, возникает столкновение с символом круга/стены, иначе нет.

После этого код столкновения должен быть простым:

// x, y - character coordinates
// r - character circle radius
// speedX, speedY - character speed
// intersectionX, intersectionY - intersection coordinates
// left, right, bottom, top - wall rect positions

// I strongly recomment using a const "EPSILON" value
// set it to something like 1e-5 or 1e-4
// floats can be tricky and you could find yourself on the inside of the wall
// or something similar if you don't use it :)

if (isLeftCollision) {
    x = intersectionX - EPSILON;
    if (speedX > 0) {
        speedX = 0;
    }
} else if (isRightCollision) {
    x = intersectionX + EPSILON;
    if (speedX < 0) {
        speedX = 0;
    }
}

if (isBottomCollision) {
    y = intersectionY - EPSILON;
    if (speedY > 0) {
        speedY = 0;
    }
} else if (isTopCollision) {
    y = intersectionY + EPSILON;
    if (speedY < 0) {
        speedY = 0;
    }
}

[Обновление]

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

public final class SegmentAabbIntersector {

    private static final int INSIDE = 0x0000;
    private static final int LEFT = 0x0001;
    private static final int RIGHT = 0x0010;
    private static final int BOTTOM = 0x0100;
    private static final int TOP = 0x1000;

    // Cohen–Sutherland clipping algorithm (adjusted for our needs)
    public static boolean cohenSutherlandIntersection(float x1, float y1, float x2, float y2, Rectangle r, Vector2 intersection) {

        int regionCode1 = calculateRegionCode(x1, y1, r);
        int regionCode2 = calculateRegionCode(x2, y2, r);

        float xMin = r.x;
        float xMax = r.x + r.width;
        float yMin = r.y;
        float yMax = r.y + r.height;

        while (true) {
            if (regionCode1 == INSIDE) {
                intersection.x = x1;
                intersection.y = y1;
                return true;
            } else if ((regionCode1 & regionCode2) != 0) {
                return false;
            } else {
                float x = 0.0f;
                float y = 0.0f;

                if ((regionCode1 & TOP) != 0) {
                    x = x1 + (x2 - x1) / (y2 - y1) * (yMax - y1);
                    y = yMax;
                } else if ((regionCode1 & BOTTOM) != 0) {
                    x = x1 + (x2 - x1) / (y2 - y1) * (yMin - y1);
                    y = yMin;
                } else if ((regionCode1 & RIGHT) != 0) {
                    y = y1 + (y2 - y1) / (x2 - x1) * (xMax - x1);
                    x = xMax;
                } else if ((regionCode1 & LEFT) != 0) {
                    y = y1 + (y2 - y1) / (x2 - x1) * (xMin - x1);
                    x = xMin;
                }

                x1 = x;
                y1 = y;
                regionCode1 = calculateRegionCode(x1, y1, r);
            }
        }
    }

    private static int calculateRegionCode(double x, double y, Rectangle r) {
        int code = INSIDE;

        if (x < r.x) {
            code |= LEFT;
        } else if (x > r.x + r.width) {
            code |= RIGHT;
        }

        if (y < r.y) {
            code |= BOTTOM;
        } else if (y > r.y + r.height) {
            code |= TOP;
        }

        return code;
    }
}

Вот пример использования кода:

public final class Program {

    public static void main(String[] args) {

        float radius = 5.0f;

        float x1 = -10.0f;
        float y1 = -10.0f;
        float x2 = 31.0f;
        float y2 = 13.0f;

        Rectangle r = new Rectangle(3.0f, 3.0f, 20.0f, 10.0f);
        Rectangle expandedR = new Rectangle(r.x - radius, r.y - radius, r.width + 2.0f * radius, r.height + 2.0f * radius);

        Vector2 intersection = new Vector2();

        boolean isIntersection = SegmentAabbIntersector.cohenSutherlandIntersection(x1, y1, x2, y2, expandedR, intersection);
        if (isIntersection) {
            boolean isLeft = intersection.x < r.x;
            boolean isRight = intersection.x > r.x + r.width;
            boolean isBottom = intersection.y < r.y;
            boolean isTop = intersection.y > r.y + r.height;

            String message = String.format("Intersection point: %s; isLeft: %b; isRight: %b; isBottom: %b, isTop: %b",
                    intersection, isLeft, isRight, isBottom, isTop);
            System.out.println(message);
        }

        long startTime = System.nanoTime();
        int numCalls = 10000000;
        for (int i = 0; i < numCalls; i++) {
            SegmentAabbIntersector.cohenSutherlandIntersection(x1, y1, x2, y2, expandedR, intersection);
        }
        long endTime = System.nanoTime();
        double durationMs = (endTime - startTime) / 1e6;

        System.out.println(String.format("Duration of %d calls: %f ms", numCalls, durationMs));
    }
}

Это результат, который я получаю от выполнения этого:

Intersection point: [4.26087:-2.0]; isLeft: false; isRight: false; isBottom: true, isTop: false
Duration of 10000000 calls: 279,932343 ms

Обратите внимание, что это производительность настольных ПК на процессоре i5-2400. Это, вероятно, будет намного медленнее на устройствах Android, но я считаю, что их больше, чем достаточно.
Я только тестировал это поверхностно, поэтому, если вы обнаружите какие-либо ошибки, сообщите мне.

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

Ответ 2

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

Больше - может и не понадобиться - реалистичный подход состоял бы в том, чтобы рассмотреть скорость x, y и оценить его в проверке равенства.