Часть №7: Обработка столкновений в игре «Breakout» на C++/OpenGL

  Дмитрий Бушуев  | 

  Обновл. 20 Дек 2020  | 

 683

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

Всякий раз, когда происходит фиксирование факта столкновения, необходимо выполнить две вещи: во-первых, переместить мяч так, чтобы он больше не находился внутри другого объекта; во-вторых, изменить направление скорости мяча, чтобы он выглядел так, будто отскакивает от объекта.

Изменение положения мяча при столкновении

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

На ней видно, как мяч слегка заступил в область AABB, и было обнаружено столкновение. Теперь мы хотим переместить мяч обратно так, чтобы он просто касался AABB, как будто никакого столкновения не произошло. Чтобы выяснить, какое расстояние нам нужно пройти, для перемещения мяча из AABB обратно, необходимо получить вектор R, который является величиной проникновения в AABB. Для получения данного вектора, вычтем вектор V из радиуса мяча. Вектор V — это разность между ближайшей точкой P и центром мяча C.

Зная вектор R, мы смещаем положение мяча так, чтобы он только лишь касался AABB; теперь мяч расположен правильно.

Направление столкновения


Далее нам нужно выяснить, как обновить скорость мяча после столкновения. Для игры «Breakout» мы используем следующие правила изменения скорости мяча:

   если мяч сталкивается с правой или левой стороной AABB, то значение горизонтальной составляющей его скорости (x) меняется на противоположное;

   если мяч сталкивается с нижней или верхней стороной AABB, то значение вертикальной составляющей его скорости (y) меняется на противоположное.

Но как мы определим направление, в котором мяч произвел соударение с AABB? Существует несколько подходов к решению данной задачи. Один из них заключается в том, что вместо одного AABB мы используем по четыре AABB на кирпич, располагая их на каждой стороне кирпича. Таким образом, мы можем определить, в какую AABB и, следовательно, в какой край произошло попадание мяча. Однако существует более простой подход — использование скалярного произведения.

Вы, наверняка, помните из урока о трансформациях в OpenGL, что скалярное произведение дает нам угол между двумя нормализованными векторами. Что, если мы определим 4 вектора, указывающие на Север, Юг, Запад и Восток, и будем вычислять скалярное произведение между каждым из них и заданным вектором? Результирующее скалярное произведение будет наибольшим в том случае, когда один из векторов направления сонаправлен с заданным вектором (максимальное значение скалярного произведения равно 1.0f, если угол между векторами равен 0 градусов):

Функция VectorDirection() сравнивает вектор target с каждым из векторов направления массива compass и возвращает тот из них, который составляет наименьший угол с заданным вектором target. Переменная Direction — это перечисление, которое мы поместим в заголовочный файл игрового класса (game.h):

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

Обработка столкновения вида «AABB-Окружность»

Чтобы вычислить необходимые значения для обработки столкновения, нам потребуется немного больше информации от функции(й) столкновения, чем просто true или false. Теперь мы собираемся вернуть кортеж (tuple) информации, который сообщит нам, произошло ли столкновение, в каком направлении оно произошло, и вектор разницы R. Контейнер tuple можно найти в заголовочном файле <tuple>.

Чтобы сохранить код немного более организованными, мы определим новый псевдоним Collision:

Затем мы подредактируем код функции CheckCollision(), чтобы она возвращала не только true или false, но и векторы направления и разности:

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

Не пугайтесь сложности данной функции, поскольку она в основном является всего лишь прямым переложением рассмотренных до сего момента идей. Сначала мы производим проверку на предмет столкновения, и, если оно произошло, разрушаем блок (блок не должен быть твердым). Затем мы получаем вектор направления столкновения dir и вектор V в виде переменной diff_vector из кортежа и, наконец, выполняем обработку столкновения.

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

Столкновения вида «Игрок-Мяч»


Столкновения между мячом и ракеткой игрока обрабатываются несколько иначе, чем мы обсуждали ранее, так как на этот раз горизонтальная скорость мяча должна быть обновлена в зависимости от того, в какую точку ракетки относительно её центра попал мяч. Чем дальше от центра ракетки он ударяется, тем сильнее должно быть изменение его горизонтальной скорости:

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

Обратите внимание, что старая скорость сохраняется в переменной oldVelocity. Это делается потому, что мы обновляем горизонтальную составляющую вектора скорости мяча, сохраняя вертикальную составляющую постоянной. Это будет означать, что длина вектора скорости мяча постоянно изменяется, увеличиваясь в зависимости от того, насколько далеко от центра ракетки происходит соударение. По этой причине новый вектор скорости нормализуется и умножается на длину старого вектора скорости. Таким образом, скорость мяча всегда постоянна, независимо от того, в какой точке ракетки происходит соударение с мячом.

Проблема «липкой ракетки»

Когда вы запускали код, то, возможно, заметили, что в механизме обработки столкновения ракетки и мяча присутствует некоторый недочет, а именно:

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

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

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

Нижняя граница окна

Единственное, чего все еще не хватает — это некоторого условия, при котором будет происходить сброс уровня и положения ракетки игрока. В функции Game::Update() мы добавим проверку, достиг ли мяч нижнего края окна, и если — да, то необходимо «сбросить игру»:

Функции ResetLevel() и ResetPlayer() перезагружают уровень и сбрасывают значения объектов до их первоначальных значений. Теперь игра должна выглядеть примерно так:

  GitHub / Часть №7: Обработка столкновений в игре «Breakout» на C++/OpenGL — Исходный код

И вот оно, мы только что закончили создавать клон классической игры «Breakout» с аналогичной механикой.

Некоторые примечания

Обнаружение столкновений — трудная тема разработки игр и, возможно, самая сложная. Большинство схем обнаружения и обработки столкновений совмещены с физическими движками современных игр. Схема столкновения, которую мы использовали для игры «Breakout», очень проста и предназначена специально для данного типа игр.

Следует подчеркнуть, что описанный способ обнаружения и обработки столкновений не является совершенным. Он вычисляет возможные столкновения только один раз за каждый кадр, и только для тех позиций, в которых в этот момент были запечатлены игровые объекты; это означает, что если объект будет иметь такую скорость, что он пройдет над другим объектом в течение одного кадра, то всё будет выглядеть так, будто столкновения и не было. Поэтому, если в приложении появятся просадки FPS или вы сможете достичь достаточно высоких скоростей, представленная схема обнаружения столкновений работать не будет.

Список проблем, которые все еще могут возникнуть:

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

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

   Попадание в угол кирпича может изменить скорость мяча в неправильном направлении, так как расстояние, которое он проходит за время одного кадра, может повлиять на то, какое направление будет возвращено функцией VectorDirection(): вертикальное или горизонтальное.

Как бы там ни было, данные уроки направлены на то, чтобы научить вас основам некоторых аспектов программирования графики и разработки игр. Именно по этой причине мы и использовали рассматриваемую схему; она понятна и довольно хорошо работает в стандартных сценариях. Просто имейте в виду, что существуют лучшие (более сложные) схемы столкновений (например, теорема о разделяющей оси (сокр. «SAT» от англ. «Separating Axis Theorem»)), которые хорошо работают почти во всех сценариях (включая подвижные объекты).

К счастью, есть большие, практичные и довольно эффективные физические движки для использования в ваших собственных играх. Если вы хотите углубиться в такие системы или нуждаетесь в более продвинутой физике и у вас есть проблемы с математикой, то Box2D — это идеальная библиотека 2D-физики для реализации физики и обнаружения столкновений в ваших приложениях.


Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (1 оценок, среднее: 5,00 из 5)
Загрузка...

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *