Основы Box2D во Flash

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

В статье разбираются следующие темы:

  • Что такое Box2DFlash?
  • Подключение Box2DFlash к проекту во FlashDevelop
  • Основные понятия движка
  • Создание мира Box2D во flash
  • Отрисовка физики
  • Создание статических и динамических объектов в Box2DFlash.

Что такое Box2DFlash?

Box2DFlash — это порт на ActionScript3 популярного физического 2D движка Box2D. Изначально Box2D написан на C++.

В этой статье рассматривается версия порта 2.1a.

Box2D даёт возможность создавать приложения (как правило игры) с имитацией 2D физики.

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

Подключение Box2DFlash к проекту

Сначала необходимо скачать библиотеки Box2DFlash на этом сайте: http://www.box2dflash.org. На данный момент крайняя версия v2.1a.

Далее создаём новый проект в FlashDevelop-е. Помещаем содержимое архива (а именно папку Box2D) в папку src своего проекта.

В дереве каталогов проекта это должно выглядеть так:

img01

Вот и всё подключение! Теперь box2d можно использовать в проекте.

Основные понятия движка

Следующий рисунок отображает схематически взаимосвязь основных классов движка:

base classesb2World — это основной класс который представляет из себя весь физический мир. Именно в этом классе происходит управление всей симуляцией. b2World содержит объекты, которые представляют из себя экземпляры класса b2Body. Полноценное физическое тело создаётся при помощи классов b2BodyDef, b2Shape и b2FixtureDef. В этих классах определяются свойства для физического тела, такие как форма, положение, плотность, упругость, коэф. трения и т.п. Позднее мы подробно разберём эти классы. Синяя стрелка означает, что для класса b2FixtureDef так же используется класс b2Shape.

Для начала необходимо знать следующие основные понятия:

Создание мира Box2d

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

Физический мир создаётся как экземпляр класса b2World. При создании необходимо определить вектор силы тяжести (класс b2Vec2) и задать логический флаг определяющий могут ли тела в состоянии покоя «засыпать». На тела в состоянии сна не применяются расчёты физики, что повышает производительность приложения. Состояние сна у тела продолжается до тех пор пока его не коснутся другие не спящие тела.

Класс b2Vec2 используется для хранения координат, что может представлять из себя позицию тела в мире либо какой-либо вектор.

Рассмотрим подробнее основные методы и свойства класса b2Vec2:

Свойство или метод класса b2Vec2 Описание
b2Vec2(x_:Number=0, y_:Number=0) Конструктор в который можно сразу передавать нужные значения координат.
Set(x_:Number=0, y_:Number=0) Установка координат.
Length():Number Возвращается длина вектора.
Normalize():Number Вектор нормализуется и возвращается его предыдущая длина.

Для процесса симуляции необходимо обновлять мир, вызывая метод Step(dt, vel, pos), где dt — время прошедшее между вызовами step, vel — кол-во итераций для обработки скоростей тел в мире, pos — кол-во итераций для обработки перемещения тел.

Для этого назначим слушатель события ENTER_FRAME и в его обработчике будем вызывать обновление мира. В примере я создам отдельный метод updateBox2D(dt: Number), который и будет вызываться в событии onEnterFrameHandler.

Вот что получилось:

package
{
  import Box2D.Common.Math.b2Vec2;
  import Box2D.Dynamics.b2World;
  import flash.display.Sprite;
  import flash.events.Event;

  [Frame(factoryClass="Preloader")]
  public class Main extends Sprite
  {
    private const VelIters: int = 10;
    private const PosIters: int = 10;
    private var _world: b2World;

    public function Main():void
    {
      if (stage) init() else addEventListener(Event.ADDED_TO_STAGE, init);
    }

    private function init(e:Event = null):void
    {
      removeEventListener(Event.ADDED_TO_STAGE, init);
      // entry point
      var gv: b2Vec2 = new b2Vec2(0, 9.8);
      _world = new b2World(gv, true);
      addEventListener(Event.ENTER_FRAME, onEnterFrameHandler);
    }

    private function onEnterFrameHandler(e:Event):void
    {
      updateBox2D(1 / stage.frameRate);
    }

    private function updateBox2D(aDTime: Number):void
    {
      if (_world) {
        _world.Step(aDTime, VelIters, PosIters);
        _world.ClearForces(); // необходимо вызывать после каждого вызова step()
      }
    }
  }
}

Мир создан, теперь нужно его наполнить чем-нибудь!)

Отрисовка физики

Сначала нам необходимо наладить debug-отрисовку чтобы мы могли видеть что мы создаём, т.е. физические объекты и остальные элементы мира.

Для отрисовки физики в движке присутствуют классы по умолчанию.

Создадим функцию инициализации debug-отрисовки:

private function initDebugDraw(aDDParent: DisplayObjectContainer):void
{
// создаём специальный фон для дебаг отрисовки
// это необходимо, чтобы на этапе дебаг отрисовки срабатывали клики мыши например
var ddFon: Sprite = new Sprite();
var gr: Graphics = ddFon.graphics;
gr.lineStyle(0);
gr.beginFill(0x333333, 1);
gr.drawRect(-1000, -1000, 2000, 2000);
aDDParent.addChild(ddFon);
// создаём спрайт для отрисовки физических объектов
var ddDummy: Sprite = new Sprite();
aDDParent.addChild(ddDummy);
var debugDraw: b2DebugDraw = new b2DebugDraw();
debugDraw.SetSprite(ddDummy);
debugDraw.SetDrawScale(PPM);
// указываем флаги - что необходимо отрисовывать
debugDraw.SetFlags(b2DebugDraw.e_controllerBit + b2DebugDraw.e_pairBit + b2DebugDraw.e_shapeBit + b2DebugDraw.e_jointBit);
debugDraw.SetFillAlpha(0.5); // прозрачность отрисовки
_world.SetDebugDraw(debugDraw);
}

Функцие необходимо передать 1 атрибут — родитель на котором будет происходить отрисовка, в нашем примере это будет основной класс.

Теперь в функцию updateBox2D добавляем вызов отрисовки:

private function updateBox2D(aDTime: Number):void
{
if (_world) {
_world.Step(aDTime, VelIters, PosIters);
_world.ClearForces();
// дебаг рендер физики
_world.DrawDebugData();
}
}

Теперь можно создавать объекты!)

Создание объектов Box2d

Т.к. единицы измерения движка никак не связаны с пикселями, то в приложениях использующих Box2D всегда вводится коэффициент преобразования пикселей в метры движка. Например: константа типа PixelPerMeter: int = 30. Как правило для флеш игр используют именно это значение: 30 пикселей = 1 метру в Box2D.

Box2D рассчитан на работу с телами размерами от 10 см до 10 м.

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

Тела могут быть следующих типов:

  1. статическое — тело постоянно находится в одном положении в мире и не меняет свой угол.
  2. динамическое — тело может передвигаться в мире, менять свою позицию и угол.
  3. кинематическое — очень похоже на статическое т.к. при взаимодействие с динамическим телом сохраняет своё положение и угол. Но так же может передвигаться(менять свою позицию) в мире.

По умолчанию все создаваемые тела статичны.

Для начала заготовим для создания объектов 2-е функции: createWalls() и createObjs(). В первой будут создаваться статические стены сцены, а во второй динамические объекты. Обе функции будут вызываться после создания мира и инициализации дебаг-рендера.

Статические объекты

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

Для создания статического тела, помимо самого мира b2World, нам потребуется 3 класса:

  • b2Body — определяет искомое физ. тело в мире.
  • b2BodyDef — определяет тип тела и его положение в мире;
  • b2Shape — определяет физическую форму тела.

Основные свойства и методы класса b2BodyDef:

Свойство или метод Описание
active:Boolean Определяет будет ли тело активным при создании(true) или нет(false). По умолчанию = true.
allowSleep:Boolean Будет ли тело засыпать. Можно установить в false, для определённых тел если в этом будет необходимость. По умолчанию учитываются настройки мира.
angle:Number Угол поворота тела в радианах.
angularDamping:Number Затухание угловой скорости. Если тело крутится и это свойство больше нуля, то вращение тела будет затухать.
angularVelocity:Number Угловая скорость тела.
awake:Boolean Если false, то тело при создании будет в спящем состоянии. По умолчанию = true.
bullet:Boolean Если true, то тело будет обрабатываться с большим кол-вом итераций. На это расходуется больше процессора, но тело не будет пролетать сквозь другие тела при большой скорости.
fixedRotation:Boolean Если true, то тело не будет вращаться. Полезно при создании персонажей.
linearDamping:Number Величина торможения линейной скорости. Например, если = 0, то в мире без силы тяжести тело будет лететь не замедляясь и не останавливаясь.
linearVelocity:b2Vec2 Текущая линейная скорость. Устанавливается методом linearVelocity.Set(x, y);
position:b2Vec2 Текущая позиция тела в мире. Устанавливается методом Set(x, y).
type:uint Тип тела. Может быть равен одному из 3-х типов: b2Body.b2_staticBody, b2Body.b2_dynamicBody, b2Body.b2_kinematicBody.
userData:* Указатель на объект. Это свойство очень полезно для хранения различных данных тела.

Основные свойства и методы класса b2Body:

Свойство или метод Описание
ApplyForce(force:b2Vec2, point:b2Vec2) Применение силы (force) к точке тела (point). Сила указывается в Ньютонах. Например, нам надо чтобы тело отбросило взрывной волной — можно использовать этот метод.
ApplyImpulse(impulse:b2Vec2, point:b2Vec2) Применение импульса силы к телу.
ApplyTorque(torque:Number) Применение крутящего момента к телу. Это заставляет тело вращаться (влияет на угловую скорость) не влияя на линейную скорость. Центром вращения при этом является центр масс самого тела. Атрибут torque выражается в Нм.
CreateFixture(def:b2FixtureDef):b2Fixture Создание формы тела по классу b2FixtureDef. Более гибкое создание тела касательно его параметров.
CreateFixture2(shape:b2Shape, density:Number=0.0):b2Fixture Создание формы тела по форме с указанием плотности.

Так же класс b2Body имеет множество Get и Set методов для различных параметров тела.

Экземпляр класса b2Shape создаётся при помощи двух других классов: b2PolygonShape и b2CircleShape. Оба класса являются подклассами b2Shape.

Рассмотрим основные методы этих классов:

  • b2PolygonShape.AsBox(hx:Number, hy:Number): b2PolygonShape — статический метод с помощью которого можно легко создавать форму прямоугольника. hx и hy — это половины длины и высоты в метрах.
  • b2PolygonShape.AsArray(vertices:Array, vertexCount:Number): b2PolygonShape — метод, который создаёт фигуру по массиву точек, второй атрибут — это длина массива.
  • b2CircleShape(radius:Number = 0) — конструктор, создаёт фигуру-круг с указанным радиусом.

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

  1. // создание статического прямоугольника
  2. var body: b2Body;
  3. var bDef: b2BodyDef;
  4. var shape: b2Shape;
  5. // создаём класс для определения будущего тела, пока оно ещё не создано в мире
  6. bDef = new b2BodyDef();
  7. // создаём фигуру, определяющую тело.
  8. // Используем класс примитивов b2PolygonShape и его статический метод.
  9. shape = b2PolygonShape.AsBox(640 * MPP / 2, 20 * MPP / 2);
  10. // размещаем тело сразу куда нам надо
  11. bDef.position.Set(640 / 2 * MPP, 480 * MPP);
  12. // создаём действительное тело в мире по нашему определяющему классу
  13. body = _world.CreateBody(bDef);
  14. // придаём ему физическую форму которую мы задали в классе shape
  15. body.CreateFixture2(shape);

Не забываем что тип тела статический по умолчанию, поэтому в коде мы не указали тип bDef.type = b2Body.b2_staticBody.

Можно заметить, что мы не использовали класс b2FixtureDef о котором упоминалось в начале. Для создания простого статического тела можно обойтись и без него. Это в том случае, когда нам не нужно указывать упругость или коэф. трения для тела. А так же параметр плотности у статических тел всегда равен 0, чему он и равен по умолчанию. Но если нам необходима необычайно упругая статическая поверхность, то без b2FixtureDef уже не обойтись.

Динамические объекты

Для создания динамического объекта нам дополнительно понадобится класс b2FixtureDef.

Основные свойства и методы класса b2FixtureDef:

Свойство или метод Описание
density:Number Плотность тела в кг/м^2.
friction:Number Трение. Указывается в промежутке от 0 до 1, где 0 — отсутствие трения.
restitution:Number Упругость, эластичность. Указывается от 0 до 1, где 0 — отсутсвие упругости.

У динамического объекта плотность не может быть = 0 как у статических. Поэтому нам необходимо её указывать как раз с помощью класса b2FixtureDef.

Функция создания динамических объектов получилось такой:

private function createObjs(aWorld: b2World):void
{
var i: int;
var body: b2Body;
var bDef: b2BodyDef;
var fixDef: b2FixtureDef;
// создаём круги
for (i = 0; i < 10; i++) {
bDef = new b2BodyDef();
bDef.type = b2Body.b2_dynamicBody;
bDef.position.Set(Math.random() * 640 * MPP, Math.random() * 480 * MPP);
body = aWorld.CreateBody(bDef);
fixDef = new b2FixtureDef();
fixDef.shape = new b2CircleShape(Math.random() + 0.2);
fixDef.density = Math.random() + 0.2;
body.CreateFixture(fixDef);
}
// создаём прямоугольники
for (i = 0; i < 10; i++) {
bDef = new b2BodyDef();
bDef.type = b2Body.b2_dynamicBody;
bDef.position.Set(Math.random() * 640 * MPP, Math.random() * 480 * MPP);
body = aWorld.CreateBody(bDef);
fixDef = new b2FixtureDef();
fixDef.shape = b2PolygonShape.AsBox(Math.random() + 0.5, Math.random() + 0.4);
fixDef.density = Math.random() + 0.2;
body.CreateFixture(fixDef);
}
}

 

Результат

Ещё я добавил обновление(пересоздание) всей сцены по кнопке «R». И чтобы было удобно вызывать уничтожение и создание сцены, я выделил эти действия в отдельные методы createScene() и destroyScene().

Весь код программы:

package
{
import <тут список импортов>
 
/**
  * Для статьи "Основы Box2DFlash" cyber-code.ru
  * @author Monax
  */
[Frame(factoryClass="Preloader")]
public class Main extends Sprite
{
private const PPM: Number = 30; // pixel per meter - множитель для перевода метров в пиксели
private const MPP: Number = 1 / 30; // meter per pixel - для перевода пикселей в метры
private const VelIters: int = 10;
private const PosIters: int = 10;
private var _world: b2World;
 
public function Main():void
{
if (stage) init() else addEventListener(Event.ADDED_TO_STAGE, init);
}
 
 
//////////////////////////////////////////////////////////////////////////////////////////
//{ PRIVATE
 
private function init(e:Event = null):void
{
removeEventListener(Event.ADDED_TO_STAGE, init);
// entry point
createScene();
addEventListener(Event.ENTER_FRAME, onEnterFrameHandler);
stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDownHandler);
}
 
// создание всей сцены
private function createScene():void
{
// создание мира
var gv: b2Vec2 = new b2Vec2(0, 9.8);
_world = new b2World(gv, true);
// инициализация графики
initDebugDraw(this);
// создание объектов
createWalls();
createObjs(_world);
}
 
// полное уничтожение всей сцены
private function destroyScene():void
{
// уничтожаем отображение
removeChildren();
// уничтожаем все тела в мире
if (_world)
for (var body: b2Body = _world.GetBodyList(); body; body = body.GetNext())
if (body) _world.DestroyBody(body);
// обнуляем переменную мира
_world = null;
}
 
/**
  * Инициализация debug-отрисовки
  */
private function initDebugDraw(aDDParent: DisplayObjectContainer):void
{
// создаём специальный фон для дебаг отрисовки
// это необходимо, чтобы на этапе дебаг отрисовки срабатывали клики мыши например
var ddFon: Sprite = new Sprite();
var gr: Graphics = ddFon.graphics;
gr.lineStyle(0);
gr.beginFill(0x333333, 1);
gr.drawRect(-1000, -1000, 2000, 2000);
aDDParent.addChild(ddFon);
var ddDummy: Sprite = new Sprite();
aDDParent.addChild(ddDummy);
var debugDraw: b2DebugDraw = new b2DebugDraw();
debugDraw.SetSprite(ddDummy);
debugDraw.SetDrawScale(PPM);
// указываем флаги - что необходимо отрисовывать
debugDraw.SetFlags(b2DebugDraw.e_controllerBit + b2DebugDraw.e_pairBit + b2DebugDraw.e_shapeBit + b2DebugDraw.e_jointBit);
debugDraw.SetFillAlpha(0.5);
_world.SetDebugDraw(debugDraw);
}
 
// создание стен
private function createWalls():void
{
var body: b2Body;
var bDef: b2BodyDef;
var shape: b2Shape;
// создание статического прямоугольника. которые будет в роли земли
bDef = new b2BodyDef();
shape = b2PolygonShape.AsBox(640 * MPP / 2, 20 * MPP / 2);
bDef.position.Set(640 / 2 * MPP, 480 * MPP);
body = _world.CreateBody(bDef);
body.CreateFixture2(shape);
// левая стена
bDef = new b2BodyDef();
shape = b2PolygonShape.AsBox(20 * MPP / 2, 480 * MPP / 2);
bDef.position.Set(0, 480 * MPP / 2);
body = _world.CreateBody(bDef);
body.CreateFixture2(shape);
// правая стена
bDef = new b2BodyDef();
shape = b2PolygonShape.AsBox(20 * MPP / 2, 480 * MPP / 2);
bDef.position.Set(640 * MPP, 480 * MPP / 2);
body = _world.CreateBody(bDef);
body.CreateFixture2(shape);
}
 
// создание различных объектов
private function createObjs(aWorld: b2World):void
{
var i: int;
var body: b2Body;
var bDef: b2BodyDef;
var fixDef: b2FixtureDef;
// создаём круги
for (i = 0; i < 10; i++) {
bDef = new b2BodyDef();
bDef.type = b2Body.b2_dynamicBody;
bDef.position.Set(Math.random() * 640 * MPP, Math.random() * 480 * MPP);
body = aWorld.CreateBody(bDef);
fixDef = new b2FixtureDef();
fixDef.shape = new b2CircleShape(Math.random() + 0.2);
fixDef.density = Math.random() + 0.2;
body.CreateFixture(fixDef);
}
// создаём прямоугольники
for (i = 0; i < 10; i++) {
bDef = new b2BodyDef();
bDef.type = b2Body.b2_dynamicBody;
bDef.position.Set(Math.random() * 640 * MPP, Math.random() * 480 * MPP);
body = aWorld.CreateBody(bDef);
fixDef = new b2FixtureDef();
fixDef.shape = b2PolygonShape.AsBox(Math.random() + 0.5, Math.random() + 0.4);
fixDef.density = Math.random() + 0.2;
body.CreateFixture(fixDef);
}
}
 
// обновление мира
private function updateBox2D(aDTime: Number):void
{
if (_world) {
_world.Step(aDTime, VelIters, PosIters);
_world.ClearForces();
// дебаг рендер физики
_world.DrawDebugData();
}
}
 
//} private
 
 
//////////////////////////////////////////////////////////////////////////////////////////
//{ EVENTS
 
private function onKeyDownHandler(e:KeyboardEvent):void
{
switch (e.keyCode)
{
case 82: // клавиша R
destroyScene();
createScene();
break;
}
}
 
private function onEnterFrameHandler(e:Event):void
{
updateBox2D(1 / stage.frameRate);
}
 
//} events
 
}
 
}

В результате получилась вот такая сцена (рестарт на кнопку «R»):

Скачать архив с исходниками: box2d_start.