Box2D в картинках – Часть 2 (Привет мир!)
Итак сегодня напишем первое небольшое приложения с использованием Box2D.
Надо скачать и подключить исходники библиотеки к своему проекту (надеюсь ты знаешь как это делается ибо если нет тогда тебе рано читать эту статью
).
Но сначала надо сказать о общих положениях касающиеся Box2D. Эти положения будут предоставлены в виде тезисов:
- как единицу измерения Box2D НЕ ИСПОЛЬЗУЕТ пиксели;
- как единицу измерения Box2D ИСПОЛЬЗУЕТ международную систему С (си) – метры, килограммы, секунды (МКС);
- Box2D был настроен на работу с динамическими объектами в диапазоне от 10 сантиметров до 10 метров, то есть возможно создавать объекты от стакана до автобуса;
(еще можно встретить другую терминологию единиц измерения – это unit (юниты) – просто какая то единица. В такой терминологии Box2D работает от 1 до 100 юнитов)
- так как единицы которыми оперирует Box2D не совпадает с экранными единицами (пикселями), то надо задавать некоторый коефициент преобразования метров (юнитов) в пиксели. Например можно задать что 10 сантиметров = 30 пикселям. Или если в юнитах оперировать то 1 юнит = 30 пикселям. Это коэффициент уже каждый сам для себя определяет. (Далее буду оперировать только в метрах)
- Box2D оперирует с двумя типами объектов: динамические и статические;
- Динамические объекты участвуют в процессе анализа столкновений между собой (кирпичи, мячи, молекулы, вертолеты, машины, люди….
), статические – нет (земля, фундамент, каркас – все что абсолютно нерушимое);
- Box2D реализует столкновения с следующими фигурами: круг, квадрат, выпуклые многоугольники. Есть механизм который разрешает добавить свои фигуры, но также надо будет добавлять и свой алгоритм обработки этих фигур;
- Имена большинства структур в Box2D начинаются с префикса «b2» для того чтобы лучше визуально выделить структуры движка и сделать меньшую вероятность конфликта с структурами пользователя;
Ну вот вроде пока все. Теперь начнем создавать мир физики…
_____
1. Создадим границы мира
Первое что надо создать это границы физического мира. Представьте себе большую коробку внутри которой происходят физические процессы (столкновения, гравитация, трение и т.д.), а за рамками этой коробки физики нет. Вот так и работает Box2D. Все объекты которые вылетают за эти границы автоматически перестают просчитываться движком и они как бы “замораживаются”. Многие после этого объяснения пишут что “… потому делайте границы мира достаточно большими, чтобы ваши объекты не вылетели случайно за них и не “замерзли”, то есть не выпали из процесса просчета”.
В принципе все правильно, только если оговорится что “это надо в случае если вам действительно нужен такой огромный физический мир когда все и всегда будет просчитываться в нем”. Ведь чем больше объектов на сцене тем больше вычислений и тем больше тормоза. Потому это все очень зависит от конкретной задачи. Например, можно создать технику когда все физ объекты будут просчитываться только в том случае если они попадают в поле зрение игрока, то есть находятся на экране и с такой техникой можно создать довольно сложною, насыщенную физическими объектами игру, которая будет работать без тормозов. Но это пока для нас не актуально, потому мы сделаем классический большой квадрат
За границы мира отвечает структура b2AABB . Далее код с комментарием:
// Делаем очень большой квадрат
borderWorld.lowerBound.Set(-1000.0, -1000.0);
borderWorld.upperBound.Set(1000.0, 1000.0);
Затем нам надо создать сам мир. За мир отвечает класс b2World. Но как и в любом мире надо задать гравитацию. Гравитация может быть как вниз так и вверх так и в сторону… короче в любом направлении. Гравитация это вектор, а потому используется обычный класс вектора b2Vec2, параметры которого x и y. Они задают не только направление гравитации, а и в зависимости от величины значения, силу гравитации.
Далее создается классическая гравитация вниз с силой 10 (а вот какие единицы характеризируют это число, увы, не знаю (настоящая характ. вот так м?·с-?·кг?1))
var gravity:b2Vec2 = new b2Vec2(0.0, 10.0);
Теперь создадим сам мир:
var world:b2World = new b2World(borderWorld, gravity, true);
Третий параметр разрешает/запрещает динамическим объектам “засыпать”. Когда динамические объекты (далее ДО) успокаиваются (прекращают всякое движение, взаимодействие… ) путем этого ключа Box2D их выкидывает из обработки, тем самым оптимизируется процесс вычисления и повышается производительность. Но как только на этот ДО будет воздействовать какая то сила, движок сразу “пробудит” его, то есть включит в процесс обработки на столкновения.
(Если заметили то у Box2D очень много всяких фишек по оптимизации вычислений, как только какой то объект не нужен или может быть игнорирован Box2D сразу исключает его из процесса просчета (симуляции))
Ну вот физический мир создан! Он пока пустой, но уже полноценный
_____
2. Добавим тела в мир.
Сначала поговорим о телах. Box2D оперирует телами. А что такое тело?
Тело эта какая то абстракция которая не имеет ни отображения, ни формы, ни границ.
Тело (класс b2Body) состоит из определения тела (класс b2BodyDef) и определения формы (класс b2ShapeDef).
Создадим статическое тело, которое будет платформой или землей, как хотите называйте.
Далее создаем определение тела и задаем ему позицию, этим мы говорим где будет находится тело, в каких координатах (пусть это будет в координатах 100 х 100).
var groundDefination:b2BodyDef;
// создаем и определяем параметры стат. тела
groundDefination = new b2BodyDef();
// задаем координаты. Так как <strong>Box2D </strong>оперирует метрами (а 0.1 метр = 30 пикселям) то мы должны пиксели привести в метры,
// а это значит задаваемые значения в пикселях делим на 30.
groundDefination.position.Set(100/30, 100/30);
У нас тело пока что пустота. Чтобы оно набрало какую-то форму и свойства нам надо его наполнить. Для этого надо создать форму (фигуру), задать свойства и применить к телу.
Тогда тело уже можно будет “пощупать”. Не тело, а именно форма участвует в столкновениях и других физических просчетах. У тела может быть несколько присоединенных форм. Форма не может быть самостоятельной, без тела, если есть форма то она должна быть присоединена к телу.
Создадим форму (фигуру) (например, прямоугольник с шириной 100 и высотой 100):
var groundShapeDefination:b2PolygonDef;
// определяем полигон (форму) для нашего тела, он будет учавствовать в столкновениях.
// Используем готовый метод SetAsBox - он просто установит что наша форма прямоугольник.
// Как параметры он принимает полуширину и полувысоту. А также нам надо привести к метрам, потому делим на 30.
groundShapeDefination = new b2PolygonDef();
groundShapeDefination.SetAsBox((100/2)/30, (100/2)/30);
Тут для фигуры был использован класс b2PolygonDef, это класс наследник b2ShapeDef класса. Для прямоугольников проще использовать его так как у этого класса есть готовый метод SetAsBox, который создает фигуру прямоугольника. Как параметры метод принимает полуширину и полувысоту от требуемой величины, потому делим на 2 (ну и также не забываем поделить на коефициент преобразования в экранные координаты, то есть 30).
Ну вот вроде все объявления создали, теперь надо все их применить к телу.
groundBody = world.CreateBody(groundDefination);
// придаем ему форму (из описаной выше формы создаем форму нашего тела)
groundBody.CreateShape(groundShapeDefination);
Теперь надо задать массу тела. Это можно сделать вручную, но это не рекомендуется. Лучше создавать массу тела автоматически при помощи метода SetMassFromShapes(). Box2D автоматически рассчитает массу тела из плотности фигуры.
groundBody.SetMassFromShapes();
И вот тут надо кое что объяснить, а именно разницу в создании динамического тела и статического.
На самом деле разницы нет. Когда создаются формы, по умолчанию плотность равна 0 и соответственно масса тела при вычислении также равна 0. И если плотность не задать то она так и останется 0. А когда фигура имеет плотность 0 Box2D автоматически переводит ее в разряд статических. А это значит что это тело не принимает участие в столкновениях с другими статическими телами.
Когда создается динамическое тело то ему присваивается масса больше 0.
Вся разница будет только вот в этой строчке:
И тело сразу с статического превращается в динамическое.
И так ты можешь добавить сколько угодно статических и динамических тел на сцену.
3. Цикл симуляции
Итак, создали статические и динамические тела. И они уже существуют в мире… Но ничего не происходит, нету никаких взаимодействий.
Потому что нету процесса симуляции – Box2D ничего не просчитывает. Нам надо вызывать вручную метод, который будет говорить Box2D что надо сделать просчет физики (шаг симуляции).
Метод вызывается из объекта мира (b2World) и называется Step. Далее процитирую документацию:
Для моделирования движения тел Box2D использует численное дифференцирование (а точнее метод Эйлера). Выгляди это так: мир Box2D содержит функцию Step(float timeStep,int iterations), имеющую два аргумента: время, которое необходимо смоделировать в мире и количество итераций для разрешения взаимодействий между объектами.
Так вот, timeStep это время которое ты хочешь смоделировать в мире. Обычно это 1/20 – 1/30 секунды. Проще говоря чем это время больше тем все взаимодействия в мире происходят быстрее, чем меньше тем медленней и плавнее.
iterations – это количество итераций на каждом шаге моделирования. Далее цитата из документации:
В коде Box2D большую часть занимает constraint solver (“решальщик” ограничений). Задача Constraint solver — просчёт ограничений для правильного поведения тел при столкновениях. При просчёте одного ограничения не возникает особых трудностей. Однако при просчете нескольких ограничений, разрешение одного ограничения слегка нарушает остальные. Поэтому для получения хорошего результата необходимо произвести просчет всех ограничений несколько раз (т.е. сделать несколько итераций).
Чем больше таких итераций тем точнее происходит моделирование, НО тем больше надо времени для этого, что тянет за собой больший расход ресурсов процессора.
Потому в зависимости от критичности приложения рекомендуется использовать от 10 – 20 итераций.
Далее небольшой код как это будет выглядеть:
private var iterations:int = 20;
// Время которое надо смоделировать в мире
private var timeStep:Number = 1/20.0;
addEventListener(Event.ENTER_FRAME, updateWorld);
private funcntion updateWorld(event:Event):void
{
world.Step(timeStep, iterations);
}
Вот теперь в мире уже все моделируется – Ньютон работает по полной
4. Отображение
Хоть у нас все моделируется но ничего не видно! Естественно, ведь у нас задана только математическая модель и никакого графического представления мы не задавали.
В классе определения тела (b2BodyDef) есть поле userData типа Object. Туда мы можем сохранять любые пользовательские данные в частности и графическое отображение.
Поэтому при создании (которое было описано выше) тела сразу можно создавать его отображение (Sprite, MovieClip…) и запихивать его в поле userData.
Далее небольшой код демонстрирующий создание динамического тела, кирпича
(это код из прикрепленного исходника только немного модифицирован (упрощен)):
{
var kirpich:Sprite = new Sprite();
addChild(kirpich);
kirpich.graphics.beginFill(0x660000);
kirpich.graphics.drawRect(-25, -20, 50, 40);
kirpich.graphics.endFill();
kirpich.x = 100;
kirpich.y = 100;
var dynamicBody:b2Body;
var dynamicBodyDefination:b2BodyDef;
var dynamicShapeDefination:b2PolygonDef;
dynamicBodyDefination = new b2BodyDef();
dynamicBodyDefination.position.Set(kirpich.x / 30, kirpich.y / 30);
dynamicBodyDefination.userData = kirpich;
dynamicShapeDefination = new b2PolygonDef();
dynamicShapeDefination.SetAsBox((kirpich.width) / 30 / 2, (kirpich.height) / 30 / 2);
dynamicShapeDefination.density = 1.0; // коефициент плотности
dynamicShapeDefination.friction = 1.0; // коефициент трения
dynamicShapeDefination.restitution = 0.1; // коефициент упругости
dynamicBody = world.CreateBody(dynamicBodyDefination);
dynamicBody.CreateShape(dynamicShapeDefination);
dynamicBody.SetMassFromShapes();
}
Ну вот отображение есть.
5. Изменение позиции отображение в соответствии физической модели.
Хоть отображение у нас есть но на экране ты не увидишь чтобы это отображение куда то двигалось или хоть как то менялось.
Для этого надо каждый раз (каждый кадр) вручную получать данные от физической модели (позиция, угол наклона) и применять к своему отображению.
Далее пример кода:
{
//"делаем шаг" физического мира - тут рассчитываются положения физических тел
world.Step(timeStep, iterations);
//пробегаем по всем физическим телам и двигаем их мувики на свои места
//"их мувики" храним в пользовательских данных тела
for (bodiesList = world.GetBodyList(); bodiesList; bodiesList = bodiesList.GetNext())
{
// Извлекаем вектор позиции тела
var tVector:b2Vec2 = bodiesList.GetPosition();
// Присваиваем значения этого вектора, заранее умножив на коефициент перевода с метров в пиксели, нашему графическому отображению.
bodiesList.m_userData.x = tVector.x * 30;
bodiesList.m_userData.y = tVector.y * 30;
// Извлекаем угол (в радианах) множим на коефициент перевода в градусы и присваиваем нашему отображению
bodiesList.m_userData.rotation = bodiesList.GetAngle() * 180 / Math.PI;
}
}
6. Исходники
Исходники можете скачать вот тут.
В исходнике находится больше нежели тут было описано, но я постарался хорошо прокомментировать код, потому если ты прочитал статью и просмотрел исходники то проблем быть не должно. Весь код в статье был упрощен для лучшего понимания.
7. Заключение
В заключении еще несколько напоминаний и объяснений.
Box2D работает с радианами и потому не забывай переводить их в градусы.
Отдельно хочется сказать в методе setBullet(val:Boolean). Метод вызывается для тел и отвечает за то что тело для которого setBullet был установлен в true будет вычисляться намного точнее и соответственно ресурсов процессорных будет потреблять больше. А это значит что если тел на сцене будет много и для каждого установить этот флаг в true то будут наблюдаться тормоза.
Из названия метода можно догадаться что он был создан для быстро движущихся тел, таких как пуля, ядро (отсюда и название bullet).
Попробуй поэкспериментировать с этим флагом, заметишь что если он установлен в false то тела будут немного проскакивать друг в друга.
Кстати, на статические тела этот флаг никак не действует.
Для балансировки скорости приложения можно выделить несколько параметров с которыми следует поиграться:
- частота кадров самой флешки;
- количество итераций;
- временной шаг (Время которое надо смоделировать в мире);
- параметр setBullet;
- да и просто не забывать о оптимизациях. Например, не просчитывать позицию отображения статического тела;
Все остальное найдешь в исходнике.
В исходнике пример, который создает в случайной позиции квадратики которые падают на статическую платформу. Те из квадратиков которые уходят за пределы видимости уничтожаются.
(Если вдруг ниже в примере ничего не падает, значит уже все до падало
– просто перегрузи страницу).
Заодно ссылки:
Русская документация
Английские видео уроки
Антон on Март 10th, 2010
А где почитать как подключить библиотеки к проекту? получается нелогично как-то – изучаем базовые знания, а кусок пропущен и как мне – новичку в работе с этим движком разобраться в этом простом вопросе?