Статьи

Разработка игр для Android с помощью libgdx — обнаружение столкновений, часть 4

Это четвертая часть руководства по libgdx, в котором мы создаем прототип 2d-платформера по образцу Star Guard . Вы можете прочитать предыдущие статьи, если вам интересно, как мы сюда попали.

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

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

Быстрый и грязный путь

Самый простой и быстрый способ сделать это — перебрать все блоки в мире и проверить, не сталкиваются ли блоки с текущим ограничивающим прямоугольником Боба. Это хорошо работает в нашем крошечном мире 10 × 7, но если у нас огромный мир с тысячами блоков, обнаружение каждого кадра становится невозможным без ущерба для производительности.

Лучший способ

Чтобы оптимизировать вышеупомянутое решение, мы будем выборочно выбирать плитки, которые являются потенциальными кандидатами на столкновение с Бобом.
По замыслу игровой мир состоит из блоков, ограничивающие рамки которых выровнены по оси, а их ширина и высота равны 1 единице.
В этом случае наш мир выглядит следующим образом (все блоки / плитки находятся в единичных блоках):

Блоки

Красные квадраты представляют границы, где блоки были бы размещены, если таковые имеются. Желтые — это блоки.
Теперь мы можем выбрать простой 2-мерный массив (матрицу) для нашего мира, и каждая ячейка будет содержать Block или null если его нет. Это контейнер с картой. Мы всегда знаем, где находится Боб, поэтому легко определить, в какой ячейке мы находимся. Простой и ленивый способ получить кандидатов в блоки, с которыми Боб может столкнуться, состоит в том, чтобы выбрать все окружающие ячейки и проверить, перекрывает ли текущий ограничивающий прямоугольник Боба одну из плиток, которая имеет блок.

столкновения возвеличил
Поскольку мы также контролируем движение Боба, у нас есть доступ к его направлению и скорости движения. Это сужает наши возможности еще больше. Например, если Боб направляется налево, у нас есть следующий сценарий:

коллизионные-кандидатов
Изображение выше дает нам 2 клетки-кандидата (плитки), чтобы проверить, сталкиваются ли объекты в этих ячейках с Бобом. Помните, что гравитация постоянно тянет Боба вниз, поэтому мы всегда должны проверять плитки на оси Y. По знаку вертикальной скорости мы знаем, когда Боб прыгает или падает. Если Боб прыгает, кандидат будет клеткой (клеткой) над ним. Отрицательная вертикальная скорость означает, что Боб падает, поэтому мы выбираем плитку из-под него как кандидата. Если он движется влево (его скорость <0), тогда мы выбираем кандидата слева от него. Если он движется направо (скорость> 0), тогда мы выбираем плитку справа от него. Если горизонтальная скорость равна 0, это означает, что нам не нужно беспокоиться о горизонтальных кандидатах. Нам нужно сделать его оптимальным, потому что мы будем делать это каждый кадр, и мы должны будем делать это для каждого врага, пули и любых встречных объектов, которые будут в игре.

Что происходит при столкновении?

Это очень просто в нашем случае. Движение Боба по этой оси прекращается. Его скорость на этой оси будет установлена ​​на 0. Это можно сделать, только если 2 оси проверены отдельно. Сначала мы проверим горизонтальное столкновение, и если Боб столкнется, мы остановим его горизонтальное движение.
Мы делаем то же самое на вертикальной оси (Y). Это так просто.

Сначала смоделируйте, а после

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

коллизий положение
Зеленый ящик — это то место, где сейчас стоит Боб. Смещенная синяя коробка — то, где Боб должен быть после этого кадра. Фиолетовый — это то, как много Боб в стене. Это расстояние, на которое нам нужно оттолкнуть Боба, чтобы он стоял рядом со стеной. Мы просто установили его положение рядом со стеной, чтобы достичь этого без особых вычислений. Код для обнаружения столкновений на самом деле очень прост. Все это находится в BobController.java . Также есть несколько других изменений, о которых я должен упомянуть до появления контроллера. World.java имеет следующие изменения:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class World {
 
    /** Our player controlled hero **/
    Bob bob;
    /** A world has a level through which Bob needs to go through **/
    Level level;
 
    /** The collision boxes **/
    Array<Rectangle> collisionRects = new Array<Rectangle>();
 
    // Getters -----------
 
    public Array<Rectangle> getCollisionRects() {
        return collisionRects;
    }
    public Bob getBob() {
        return bob;
    }
    public Level getLevel() {
        return level;
    }
    /** Return only the blocks that need to be drawn **/
    public List<Block> getDrawableBlocks(int width, int height) {
        int x = (int)bob.getPosition().x - width;
        int y = (int)bob.getPosition().y - height;
        if (x < 0) {
            x = 0;
        }
        if (y < 0) {
            y = 0;
        }
        int x2 = x + 2 * width;
        int y2 = y + 2 * height;
        if (x2 > level.getWidth()) {
            x2 = level.getWidth() - 1;
        }
        if (y2 > level.getHeight()) {
            y2 = level.getHeight() - 1;
        }
 
        List<Block> blocks = new ArrayList<Block>();
        Block block;
        for (int col = x; col <= x2; col++) {
            for (int row = y; row <= y2; row++) {
                block = level.getBlocks()[col][row];
                if (block != null) {
                    blocks.add(block);
                }
            }
        }
        return blocks;
    }
 
    // --------------------
    public World() {
        createDemoWorld();
    }
 
    private void createDemoWorld() {
        bob = new Bob(new Vector2(7, 2));
        level = new Level();
    }
}

# 09collisionRects — это простой массив, в который я помещу прямоугольники, с которыми сталкивается Боб в этом конкретном кадре. Это только для целей отладки и для отображения полей на экране. Это может и будет удалено из финальной игры.
№ 13 — Просто обеспечивает доступ к ящикам столкновения
# 23getDrawableBlocks(int width, int height) — это метод, который возвращает список объектов Block которые находятся в окне камеры и будут отображены. Этот метод просто готовит приложение для рендеринга огромных миров без потери производительности. Это очень простой алгоритм. Получите блоки, окружающие Боба на расстоянии, и верните их для рендеринга. Это оптимизация.
# 61 — Создает Level объявленный в строке # 06 . Это хорошо, чтобы вывести уровень из мира, так как мы хотим, чтобы наша игра имела несколько уровней. Это очевидный первый шаг. Level.java можно найти здесь .

Как я упоминал ранее, фактическое обнаружение столкновений находится в BobController.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public class BobController {
    // ... code omitted ... //
    private Array<Block> collidable = new Array<Block>();
    // ... code omitted ... //
 
    public void update(float delta) {
        processInput();
        if (grounded && bob.getState().equals(State.JUMPING)) {
            bob.setState(State.IDLE);
        }
        bob.getAcceleration().y = GRAVITY;
        bob.getAcceleration().mul(delta);
        bob.getVelocity().add(bob.getAcceleration().x, bob.getAcceleration().y);
        checkCollisionWithBlocks(delta);
        bob.getVelocity().x *= DAMP;
        if (bob.getVelocity().x > MAX_VEL) {
            bob.getVelocity().x = MAX_VEL;
        }
        if (bob.getVelocity().x < -MAX_VEL) {
            bob.getVelocity().x = -MAX_VEL;
        }
        bob.update(delta);
    }
 
    private void checkCollisionWithBlocks(float delta) {
        bob.getVelocity().mul(delta);
        Rectangle bobRect = rectPool.obtain();
        bobRect.set(bob.getBounds().x, bob.getBounds().y, bob.getBounds().width, bob.getBounds().height);
        int startX, endX;
        int startY = (int) bob.getBounds().y;
        int endY = (int) (bob.getBounds().y + bob.getBounds().height);
        if (bob.getVelocity().x < 0) {
            startX = endX = (int) Math.floor(bob.getBounds().x + bob.getVelocity().x);
        } else {
            startX = endX = (int) Math.floor(bob.getBounds().x + bob.getBounds().width + bob.getVelocity().x);
        }
        populateCollidableBlocks(startX, startY, endX, endY);
        bobRect.x += bob.getVelocity().x;
        world.getCollisionRects().clear();
        for (Block block : collidable) {
            if (block == null) continue;
            if (bobRect.overlaps(block.getBounds())) {
                bob.getVelocity().x = 0;
                world.getCollisionRects().add(block.getBounds());
                break;
            }
        }
        bobRect.x = bob.getPosition().x;
        startX = (int) bob.getBounds().x;
        endX = (int) (bob.getBounds().x + bob.getBounds().width);
        if (bob.getVelocity().y < 0) {
            startY = endY = (int) Math.floor(bob.getBounds().y + bob.getVelocity().y);
        } else {
            startY = endY = (int) Math.floor(bob.getBounds().y + bob.getBounds().height + bob.getVelocity().y);
        }
        populateCollidableBlocks(startX, startY, endX, endY);
        bobRect.y += bob.getVelocity().y;
        for (Block block : collidable) {
            if (block == null) continue;
            if (bobRect.overlaps(block.getBounds())) {
                if (bob.getVelocity().y < 0) {
                    grounded = true;
                }
                bob.getVelocity().y = 0;
                world.getCollisionRects().add(block.getBounds());
                break;
            }
        }
        bobRect.y = bob.getPosition().y;
        bob.getPosition().add(bob.getVelocity());
        bob.getBounds().x = bob.getPosition().x;
        bob.getBounds().y = bob.getPosition().y;
        bob.getVelocity().mul(1 / delta);
    }
 
    private void populateCollidableBlocks(int startX, int startY, int endX, int endY) {
        collidable.clear();
        for (int x = startX; x <= endX; x++) {
            for (int y = startY; y <= endY; y++) {
                if (x >= 0 && x < world.getLevel().getWidth() && y >=0 && y < world.getLevel().getHeight()) {
                    collidable.add(world.getLevel().get(x, y));
                }
            }
        }
    }
    // ... code omitted ... //
}

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

# 03 — в массиве collidable каждый кадр будет содержать блоки, которые являются кандидатами на столкновение с Бобом.
Метод update теперь более лаконичен.
# 07 — обрабатывает ввод как обычно и там ничего не меняется
# 08 — # 09 — сбрасывает состояние Боба, если он не в воздухе.
# 12 — Ускорение Боба преобразуется во время кадра. Это важно, поскольку кадр может быть очень маленьким (обычно 1/60 секунды), и мы хотим выполнить это преобразование только один раз в кадре.
# 13 — вычислить скорость за время кадра
# 14 — выделен, потому что именно здесь происходит обнаружение столкновений. Я пройдусь по этому методу чуть позже.
# 15 — # 22 — Применяет DAMP к Бобу, чтобы остановить его, и следит за тем, чтобы Боб не превышал свою максимальную скорость.
# 25 — метод checkCollisionWithBlocks(float delta) который устанавливает состояния, положение и другие параметры Боба на основе его столкновения или без checkCollisionWithBlocks(float delta) блоков на уровне.
# 26 — преобразовать скорость во время кадра
# 27 — # 28 — Мы используем Pool для получения Rectangle, который является копией текущего ограничивающего прямоугольника Боба. Этот прямоугольник будет смещен там, где bob должен быть этим кадром, и проверен на соответствие блокам-кандидатам
# 29 — # 36 — Эти строки определяют начальную и конечную координаты в матрице уровней, которые должны быть проверены на столкновение. Матрица уровня — это просто двумерный массив, и каждая ячейка представляет одну единицу, поэтому может содержать один блок. Проверьте Level.java
# 31 — Координата Y установлена, поскольку пока мы ищем только горизонталь.
# 32 — проверяет, движется ли Боб влево, и если да, то определяет плитку слева от него. Математика проста, и я использовал этот подход, поэтому, если я решу, что мне нужны другие измерения для клеток, это все равно будет работать.
# 37 — заполняет collidable массив блоками в пределах указанного диапазона. В этом случае либо плитка слева или справа, в зависимости от подшипника Боба. Также обратите внимание, что если в этой ячейке нет блока, результат будет нулевым.
# 38 — это где мы смещаем копию ограничивающего прямоугольника Боба. На новой позиции bobRec Боб должен находиться в нормальных условиях. Но только по оси X.
# 39 — помните коллизиюРекты из мира для отладки? Теперь мы очищаем этот массив, чтобы заполнить его прямоугольниками, с которыми сталкивается Боб.
# 40 — # 47 — Здесь происходит фактическое обнаружение столкновения на оси X. Мы перебираем все блоки-кандидаты (в нашем случае это будет 1) и проверяем, пересекает ли ограничивающая рамка блока смещенную ограничивающую рамку Боба. Мы используем метод bobRect.overlaps который является частью класса Rectangle в libgdx и возвращает true, если 2 прямоугольника перекрываются. Если есть совпадение, у нас есть столкновение, поэтому мы устанавливаем скорость Боба в 0 (строка # 43 добавляет прямоугольник к world.collisionRects и выходит из обнаружения.
# 48 — Мы сбрасываем положение ограничивающего прямоугольника, потому что мы перемещаемся, чтобы проверить столкновение по оси Y, не обращая внимания на X.
# 49 — # 68 — точно так же, как и раньше, но это происходит на оси Y. Есть одна дополнительная инструкция # 61 — # 63, которая устанавливает grounded состояние в true если столкновение было обнаружено, когда Боб падал.
# 69 — Копия прямоугольника Боба сбрасывается
# 70 — Устанавливается новая скорость Боба, которая будет использоваться для вычисления новой позиции Боба.
# 71 — # 72 — Положение реальных границ Боба обновлено
# 73 — Мы преобразуем скорость обратно в базовые единицы измерения. Это очень важно.

И это все для столкновения Боба с плитками. Конечно, мы будем развивать это по мере добавления новых сущностей, но пока все хорошо. Мы немного обманули, как на диаграмме, которую я заявил, что я буду помещать Боба рядом с Блоком при столкновении, но в коде я полностью игнорирую замену. Поскольку расстояние настолько мало, что мы его даже не видим, все в порядке. Это может быть добавлено, это не будет иметь большого значения. Если вы решите добавить его, убедитесь, что вы установили позицию Боба рядом с блоком, чуть-чуть дальше, чтобы получилась функция перекрытия
false В WorldRenderer.java есть небольшое дополнение.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class WorldRenderer {
    // ... code omitted ... //
    public void render() {
        spriteBatch.begin();
            drawBlocks();
            drawBob();
        spriteBatch.end();
        drawCollisionBlocks();
        if (debug)
            drawDebug();
    }
 
    private void drawCollisionBlocks() {
        debugRenderer.setProjectionMatrix(cam.combined);
        debugRenderer.begin(ShapeType.FilledRectangle);
        debugRenderer.setColor(new Color(1, 1, 1, 1));
        for (Rectangle rect : world.getCollisionRects()) {
            debugRenderer.filledRect(rect.x, rect.y, rect.width, rect.height);
        }
        debugRenderer.end();
    }
    // ... code omitted ... //
}

Добавление метода drawCollisionBlocks() который рисует белое поле, где происходит столкновение. Это все для вашего удовольствия от просмотра. Результат работы, которую мы проделали до сих пор, должен быть похож на это видео:

Эта статья должна завершить базовое обнаружение столкновений. Далее мы рассмотрим расширение мира, движение камеры, создание врагов, использование оружия, добавление звука. Пожалуйста, поделитесь своими идеями, что должно быть на первом месте, так как все это важно. Исходный код этого проекта можно найти здесь: https://github.com/obviam/star-assault . Вам нужно оформить заказ на часть 4 . Чтобы проверить это с помощью git: git clone -b part4 [email protected]:obviam/star-assault.git . Вы также можете скачать его в виде zip-файла . В каталоге libgdx tests есть хороший платформер. СуперКалио . Он демонстрирует много вещей, которые я уже рассмотрел, и он намного короче, а для тех, у кого есть опыт работы с libgdx, это очень полезно.

Ссылка: Разработка игр для Android с помощью libgdx — обнаружение столкновений, часть 4 от нашего партнера JCG Impaler в блоге Against the Grain .