Advertisement
  1. Game Development
  2. Game Physics

Основы физики 2D платформера, часть 6: Ответная реакция на столкновение объекта с объектом

by
Difficulty:BeginnerLength:LongLanguages:
This post is part of a series called Basic 2D Platformer Physics .
Basic 2D Platformer Physics, Part 5: Object vs. Object Collision Detection
Basic 2D Platformer Physics, Part 7: Slopes Groundwork

Russian (Pусский) translation by Alexey Malyutin (you can also view the original English article)

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

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

Демо было опубликовано под Unity 5.4.0f3, и исходный код также совместим с этой версией Unity.

Ответная реакция на столкновение

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

Добавим дополнительные данные

Нам понадобятся дополнительные данные для класса MovingObject, чтобы управлять ответной реакцией "объект против объекта". Прежде всего, неплохо было бы иметь булеву переменную, чтобы помечать объект, как кинематический - т.е. этот объект не будет приводиться в движение никаким другим объектом.

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

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

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

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

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

Перемещение из пересечения

Давайте создадим функцию UpdatePhysicsResponse, в которой мы будем управлять ответной реакцией объекта на объект.

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

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

Теперь давайте начнем работу с переменными, которые мы недавно объявили.

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

Давайте теперь начнем обход всех наших данных в цикле.

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

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

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

Наконец, если он справа, тогда установим mPushesRightObject в истину и установим скорость в значение не больше нуля, потому что наш объект не может больше двигаться вправо, так как путь заблокирован.

Давайте обработаем левую сторону таким же способом.

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

Давайте обработаем ось y таким же образом.

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

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

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

When we bump into another object the other object should not move

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

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

When we bump into another object the other object should not move even if its coming vertically

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

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

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

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

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

Другой случай, когда speedSum по оси x равна нулю. В этом случае мы вычисляем соответствующий коэффициент для оси y, и задаем, что мы должны переместить 50% перекрытия по оси x. 

Аналогично мы обрабатываем случай, когда speedSum равна нулю только по оси y, и для последнего варианта мы вычисляем оба соответствующих коэффициента.

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

Сейчас, перед тем, как мы решим, должны ли мы перемещать объект из состояния столкновения по оси x или по оси y, давайте вычислим направление, с которого произошло пересечение. Здесь есть три возможности: когда мы врезались в другой объект по горизонтали, вертикали или диагонали. 

В первом случае нам нужно двигаться из пересечения по оси x, во втором случае нам нужно двигаться из пересечения по оси y, и в последнем случае нам нужно двигаться из пересечения по той оси, которая наименее перекрыта.

Looking at our paths coming from each direction

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

Во-первых, давайте вычислим, пересекались ли мы с другим AABB в предыдущем кадре.

Теперь давайте установим условие для выхода из пересечения горизонтально. Как объяснялось ранее, на нужно было не пересекаться по оси y и не пересекаться по оси x в предыдущем кадре.

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

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

Но нам нужно выходить из пересечения по оси y в случае диагонального удара, только если пересечение по оси x меньше, чем пересечение по оси y.

Вот и решены все случаи. Теперь нам нужно фактически переместить объект из пересечения.

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

Вертикальная корректировка делается так же.

Вот почти и все; есть еще один нюанс, который надо учесть. Представьте сценарий, при котором мы приземляемся сразу на два объекта одновременно. У нас возникает два почти идентичных набора данных столкновения. По мере того, как мы перебираем в цикле все столкновения, мы корректируем позицию столкновения с первым объектом, слега перемещая нас вверх.

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

Vertically landing on a group of objects

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

Теперь давайте убедимся, что мы складываем все смещения, которые мы применили к нашему объекту в этом векторе.

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

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

Vertically landing on a group of objects

Теперь, когда наша функция готова, давайте убедимся, что мы ее используем. Хорошее место для вызова этой функции будет после вызова CheckCollisions. Это потребует от нас разделить нашу функцию UpdatePhysics на две части, поэтому давайте создадим вторую часть прямо сейчас, в классе MovingObject.

Во второй части мы вызываем нашу свежезаконченную функцию UpdatePhysicsResponse и обновляем основные переменные толкания слива, справа, снизу и сверху. После этого нам нужно только применить позицию.

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

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

Заключение

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

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

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

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

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.