Взрывы в Box2D
В этой статье мы рассмотрим несколько видов взрывов в физическом движке Box2D.Симуляция взрыва сводится к нахождению тел, которые находятся в радиусе действия взрывной волны и применении силы к ним, чтобы отбросить их от центра взрыва.Мы расмотрим три вида взрывов разной сложности:
Нахождение тел в радиусе взрыва Raycast — нахождения тел в радиусе лучей Частицы — распространение многих маленьких тел от эпицентра взрыва В случае с частицами, нам не нужно искать тела и как-то воздействовать на них напрямую: физический движок сделает все за нас. Для проверки возьмем следующую сцену, с помощью которой можно увидеть и понять все плюсы и минусы каждого из вида взрывов:
Применение импульсаВ этой теме я не буду вникать в технические подробности и формулы (например, как сила изменяется относительно расстояния от точки взрыва). Взрыв заставляет ограниченный объем газа расширяться пока давление окружающего воздуха не стабилизируется. Давление должно уменьшаться обратно пропорционально квадрату радиуса к центру взрыва.Все описанное выше можно выразить следующим кодом:
void applyBlastImpulse (b2Body* body, b2Vec2 blastCenter, b2Vec2 applyPoint, float blastPower) { b2Vec2 blastDir = applyPoint — blastCenter; float distance = blastDir.Normalize (); // Игнорирование тел, которые находятся в центре взрыва if (distance == 0) return;
float invDistance = 1 / distance; float impulseMag = blastPower * invDistance * invDistance; body→ApplyLinearImpulse (impulseMag * blastDir, applyPoint); } Нахождение тел в радиусе взрыва Самый простой метод реализации взрывов: найти все тела внутри определенного радиуса взрыва относительно его центра. Немного уточнения: нам нужны тела с ихними центрами массы в пределах указанного диапазона взрыва. Для этих целей в Box2D есть метод QueryAABB: MyQueryCallback queryCallback; b2AABB aabb; aabb.lowerBound = center — b2Vec2(blastRadius, blastRadius); aabb.upperBound = center + b2Vec2(blastRadius, blastRadius); m_world→QueryAABB (&queryCallback, aabb); // Посмотреть все найденные тела и выбрать только те, у которых центр масс входит в радиус взрыва for (int i = 0; i < queryCallback.foundBodies.size(); i++) { b2Body* body = queryCallback.foundBodies[i]; b2Vec2 bodyCom = body->GetWorldCenter (); //ignore bodies outside the blast range if ((bodyCom — center).Length () >= m_blastRadius) continue; applyBlastImpulse (body, center, bodyCom, blastPower); } Давайте посмотрим на результат от такой реализации взрыва. На картинке указаны тела, которые получат импульс после взрыва:
Для начала, неплохо, но в этом методе есть пара проблем. Самое большая из них: взрывная волна проходит через платформы и стены. Также посмотрите на большое тело слева на сцене. Оно не попадает под действие взрыва потому что его центр масс находится вне радиуса взрыва.Рассмотрим следующую ситуацию: Объекты по обеим сторонам от центра взрыва имеют одинаковую массу, но тела справа получат импульс в 4 раза больше, чем тело слева.
Raycast метод Мы можем убрать все проблемы, найденные в первом методе, используя лучи для поиска тел, с которыми будет взаимодействовать взрывная волна. for (int i = 0; i < numRays; i++) { float angle = (i / (float)numRays) * 360 * DEGTORAD; b2Vec2 rayDir( sinf(angle), cosf(angle) ); b2Vec2 rayEnd = center + blastRadius * rayDir; RayCastClosestCallback callback; m_world->RayCast (&callback, center, rayEnd); if (callback.m_body) applyBlastImpulse (callback.body, center, callback.point, (m_blastPower / (float)numRays)); } Обратите внимание, мы делим силу взрыва на количество лучей. Сделано это для того чтобы было легче подобрать количество лучей без изменения общей силы импульса взрыва. Давайте посмотрим на взрыв с использованием 32 лучей: Гораздо лучше: взрывная волна не проходит через платформы. Вторая проблема тоже решена, потому что учитывается полная поверхность с каждой стороны: Количество лучей может быть подобрано, чтобы взрыв проходил через небольшие отверствия.
Метод частиц Последний метод очень отличается от первых двух. Вместо поиска тел, находящихся в зоне действия взрыва, мы просто создадим какое-то количество маленьких тел и запустим их в разные стороны. Это наиболее реалистическое поведение относительно реального взрыва.Это метод дает не только хорошие результаты, но и значительно упрощает код, потому что большая часть работы делается физическим движком. С другой стороны, этот метод более затратный для расчетов. for (int i = 0; i < numRays; i++) { float angle = (i / (float)numRays) * 360 * DEGTORAD; b2Vec2 rayDir( sinf(angle), cosf(angle) ); b2BodyDef bd; bd.type = b2_dynamicBody; bd.fixedRotation = true; // Вращение необязательное bd.bullet = true; bd.linearDamping = 10; bd.gravityScale = 0; // Игнорирвать гравитацию bd.position = center; // Начальная точка в центре взрыва bd.linearVelocity = blastPower * rayDir; b2Body* body = m_world->CreateBody (&bd); b2CircleShape circleShape; circleShape.m_radius = 0.05; // Очень маленький радиус для тела b2FixtureDef fd; fd.shape = &circleShape; fd.density = 60 / (float)numRays; fd.friction = 0; // Трение необязательно fd.restitution = 0.99f; // Отражение от тел fd.filter.groupIndex = -1; // Частицы не должны сталкиваться друг с другом body→CreateFixture (&fd); } Так много кода только потому что мы создаем тело и добавляем ему фикстуру.Вполне очевидно, что этот метод имеет все плюсы raycast-метода. Теперь у нас есть настоящая взрывная волна, которая отражается от тел, позволяя энергии взрыва правильно проходить препятствия.
Вы можете изменить вес частиц, их начальное ускорение, трение и, конечно же, их количество. Также этот метод требует очистки мира от частиц после взрыва. Единственный и самый большой минус этого метода: нагрузка на CPU. Для мобильных платформ несколько взрывов будет проблемой, но средний компьютер легко справится с этой задачей:
Еще один побочный эффект: сила взрыва не применяется одновременно на все тела. Распространение частиц занимает некоторое время.
Автор оригинала (англ) — мой хороший знакомый iforce2d, автор физического редактора R.U. B.E. для Box2D.