Туториал по Box2DFlash

Все больше и больше игр добавляют к геймплею динамическую физику или используют её как основной элемент. Box2D — это популярная и мощная библиотека физики, которая считается лучшей библиотекой 2D физики. Её используют такие выдающиеся игры, как Angry Birds и Crayon Physics Deluxe. Этот туториал ориентирован на Flash версии Box2D и предполагает знание азов Flash и ActionScript 3. Если вы плохо знакомы с Flash, обратите внимание на отличный туториал по Flash от Nandrew.

Начало

Box2D портирована на многие языки, и что более сбивает с толку, в сети доступно множество Flash портов. В этом туториале мы будем использовать версию 2.1a Box2DFlash, которую считают самым официальным Flash портом Box2D. Скачайте Box2DFlash2.1a для Flash 10 с сайта Box2DFlash.

Разархивируйте скаченный файл и скопируйте папку Box2D в папку src вашего проекта, как здесь:

Краткий обзор Box2D

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

В основе стоит мир, который содержит наши объекты и управляет моделированием динамической физики. Эти объекты нам известны как тела, и мир может иметь 0 или более тел. Каждое тело состоит из 0 и более форм. Форма, такая как прямоугольник и круг, используется для обнаружения столкновений. Каждая из этих форм связана с телом через физические параметры и добавляет свойства, такие как плотность и трение.

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

Привет мир физики

В этом туториале мы будем создавать это:

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

package
{
  import flash.display.Sprite;
  import flash.events.Event;
  import Box2D.Common.Math.*;
  import flash.events.TimerEvent;
  import flash.utils.Timer;
  import Box2D.Dynamics.*;
  import Box2D.Collision.Shapes.*;

  public class Main extends Sprite
  {
    private var world:b2World;
    private var timestep:Number;
    private var iterations:uint;
    private var pixelsPerMeter:Number = 30;
    private var genBodyTimer:Timer;
    private var sideWallWidth:int = 20;
    private var bottomWallHeight:int = 25;

    public function Main():void
    {
      this.initWorld();
      this.createWalls();
      this.createStaticBodies();
      this.setupDebugDraw();

      this.genBodyTimer = new Timer(500);
      this.genBodyTimer.addEventListener(TimerEvent.TIMER, this.genRandomBody);

      if (stage) init();
      else addEventListener(Event.ADDED_TO_STAGE, init);
    }

  //...здесь будут параметры функции

  }
}

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

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

private function initWorld():void
{
  var gravity:b2Vec2 = new b2Vec2(0.0, 9.8);
  var doSleep:Boolean = true;

  // Создаем мир
  this.world = new b2World(gravity, doSleep);
  this.world.SetWarmStarting(true);
  this.timestep = 1.0 / 30.0;
  this.velocityIterations = 6;
  this.positionIterations = 4;
}

Мир — это центр нашей Box2D среды. Он содержит все наши объекты и управляет моделированием динамической физики. Устанавливаем силу тяжести (gravity) и даем установку миру прекратить вычисление физики тел, которые находятся в состоянии покоя (doSleep). «Теплый старт» (SetWarmStarting) говорит миру, что тела должны стартовать (начинаться) активно. Если бы он был ложным, тела бы оставались на месте до тех пор, пока что-то не столкнулось с ними. Переменная временного шага (timestep) определяет, как часто мир должен просчитывать физику (в данном случае, каждую 1/30 секунды или 30 Hz — тридцать раз в секунду). Итерация определяет сколько раз за временной шаг рассчитать положение (positionIterations) и скорость тела (velocityIterations) прежде, чем перейти к следующему в списке очередности, компромисс между производительностью и точностью.

private function createWalls():void
{
  var wallShape:b2PolygonShape = new b2PolygonShape();
  var wallBd:b2BodyDef = new b2BodyDef();
  var wallB:b2Body;

  wallShape.SetAsBox(
    sideWallWidth / pixelsPerMeter / 2,
    this.stage.stageHeight / pixelsPerMeter / 2);

  //Левая стена
  wallBd.position.Set(
    (sideWallWidth / 2) / pixelsPerMeter,
    this.stage.stageHeight / 2 / pixelsPerMeter);
  wallB = world.CreateBody(wallBd);
  wallB.CreateFixture2(wallShape);

  //Правая стена
  wallBd.position.Set(
    (this.stage.stageWidth - (sideWallWidth / 2)) / pixelsPerMeter,
    this.stage.stageHeight / 2 / pixelsPerMeter);
  wallB = world.CreateBody(wallBd);
  wallB.CreateFixture2(wallShape);

  //Нижняя стенка
  wallShape.SetAsBox(
    this.stage.stageWidth / pixelsPerMeter / 2,
    bottomWallHeight / pixelsPerMeter / 2);
  wallBd.position.Set(
    this.stage.stageWidth / 2 / pixelsPerMeter,
    (this.stage.stageHeight - (bottomWallHeight / 2)) / pixelsPerMeter);
  wallB = world.CreateBody(wallBd);
  wallB.CreateFixture2(wallShape);
}

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

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

Чтобы использовать единицы измерения в Box2D, вы должны понимать некоторые ключевые моменты:

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

box2DMeters = pixels / pixelsPerMeter
pixels = box2DMeters * pixelsPerMeter

Начало координат Box2D тел находится в их центрах. Это значит, что расстояние между двумя телами, на самом деле — расстояние между их центрами. Также это значит, что мы должны пропустить половину ширины и высоты по размеру формы, потому как форма создается из её центра наружу.

Теперь, с этим все понятно, давайте прервем создание левой стенки.

wallShape.SetAsBox(
  sideWallWidth / pixelsPerMeter / 2,
  this.stage.stageHeight / pixelsPerMeter / 2);

Чтобы установить размеры формы стены, мы разделим желаемую ширину стены на pixelsPerMeter, что перевести её в метры, а затем разделим на 2, чтобы получить половину ширины. Тот же принцип используем с высотой.

wallBd.position.Set(
  (sideWallWidth / 2) / pixelsPerMeter,
  this.stage.stageHeight / 2 / pixelsPerMeter);;

Следующий шаг — установка положения тела. Стена находится слева, так что x-координата будет равна 0. Затем добавляем половину ширины и высоты стены — запомните, что начало координат в центре — а затем мы переводим координаты в метры. Мы хотим, чтобы y-координата стены была в центре мира, поэтому мы делим пополам высоту стадии, и переводим её в метры.

wallB = world.CreateBody(wallBd);
wallB.CreateFixture2(wallShape);

Тело создано и добавлено в мир. Затем мы придаем телу форму с помощью вспомогательного устройства.

private function createStaticBodies():void
{
  var blockBody:b2Body;
  var blockBd:b2BodyDef = new b2BodyDef();
  var blockShape:b2PolygonShape = new b2PolygonShape();
  var rectHeight:int = 30;  

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

  blockBd.position.Set(
    this.stage.stageWidth / 2 / pixelsPerMeter,
    (this.stage.stageHeight - this.bottomWallHeight - (rectHeight / 2))
          / pixelsPerMeter);
  blockShape.SetAsBox(320 / pixelsPerMeter / 2, rectHeight / pixelsPerMeter / 2);
  blockBody = world.CreateBody(blockBd);
  blockBody.CreateFixture2(blockShape);  

  blockBd.position.Set(
    this.stage.stageWidth / 2 / pixelsPerMeter,
    (this.stage.stageHeight - (this.bottomWallHeight + rectHeight)
          - (rectHeight / 2)) / pixelsPerMeter);
  blockShape.SetAsBox(240 / pixelsPerMeter / 2, rectHeight / pixelsPerMeter / 2);
  blockBody = world.CreateBody(blockBd);
  blockBody.CreateFixture2(blockShape); 

  blockBd.position.Set(
    this.stage.stageWidth / 2 / pixelsPerMeter,
    (this.stage.stageHeight - (this.bottomWallHeight + 2 * rectHeight)
          - (rectHeight / 2)) / pixelsPerMeter);
  blockShape.SetAsBox(140 / pixelsPerMeter / 2, rectHeight / pixelsPerMeter / 2);
  blockBody = world.CreateBody(blockBd);
  blockBody.CreateFixture2(blockShape);
}

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

private function setupDebugDraw():void
{
  var debugDraw:b2DebugDraw = new b2DebugDraw();
  var debugSprite:Sprite = new Sprite();

  addChild(debugSprite);
  debugDraw.SetSprite(debugSprite);
  debugDraw.SetDrawScale(30.0);
  debugDraw.SetFillAlpha(0.3);
  debugDraw.SetLineThickness(1.0);
  debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit);
  world.SetDebugDraw(debugDraw);
}

Это говорит нашему Box2D миру начертить отладочную информацию для наших тел. Это позволит нам увидеть соединения и формы тел. Так же это показывает изменение цвета, когда тело находится в режиме ожидания. И меняет его снова, когда оно активируется.

private function init(e:Event = null):void
{
  this.removeEventListener(Event.ADDED_TO_STAGE, init);
  this.addEventListener(Event.ENTER_FRAME, update);
  this.genBodyTimer.start();
}

Наша установка практически завершена! Мы запускаем игровой цикл и таймер для генерации тел в случайном порядке.

private function genCircle():void
{
  var body:b2Body;
  var fd:b2FixtureDef;
  var bodyDefC:b2BodyDef = new b2BodyDef();
  bodyDefC.type = b2Body.b2_dynamicBody; 

  var circShape:b2CircleShape
     = new b2CircleShape((Math.random() * 7 + 10) / pixelsPerMeter);

  fd = new b2FixtureDef();
  fd.shape = circShape;
  fd.density = 1.0;
  fd.friction = 0.3;
  fd.restitution = 0.1;
  bodyDefC.position.Set(
    (Math.random() * (this.stage.stageWidth - sideWallWidth - 20)
         + sideWallWidth + 20) / pixelsPerMeter,
    (Math.random() * 80 + 40) / pixelsPerMeter);
  bodyDefC.angle = Math.random() * Math.PI;
  body = world.CreateBody(bodyDefC);
  body.CreateFixture(fd);
}

private function genRectangle():void
{
  var body:b2Body;
  var fd:b2FixtureDef = new b2FixtureDef();
  var rectDef:b2BodyDef = new b2BodyDef();

  rectDef.type = b2Body.b2_dynamicBody;

  var polyShape:b2PolygonShape = new b2PolygonShape();

  fd.shape = polyShape;
  fd.density = 1.0;
  fd.friction = 0.3;
  fd.restitution = 0.1;
  polyShape.SetAsBox(
    (Math.random() * 16 + 20) / pixelsPerMeter / 2,
    (Math.random() * 16 + 20) / pixelsPerMeter / 2);

  rectDef.position.Set(
    (Math.random() * (this.stage.stageWidth - 2 * (sideWallWidth + 20))
         + (sideWallWidth + 20)) / pixelsPerMeter,
    (Math.random() * 80 + 40) / pixelsPerMeter);

  rectDef.angle = Math.random() * Math.PI;
  body = world.CreateBody(rectDef);
  body.CreateFixture(fd);
}

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

private function genRandomBody(e:TimerEvent):void
{
  var bodyType:Number = Math.random();
  (bodyType < 0.5) ? this.genCircle() : this.genRectangle();
}

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

private function update(e:Event = null):void
{
  world.Step(timestep, velocityIterations, positionIterations);
  world.ClearForces();
  world.DrawDebugData();
}

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

Вот и все! Теперь у нас есть мир, заполненный телами, которые демонстрируют динамичность и физику в реальном времени. Теперь, когда все основные моменты рассмотрены, можно попробовать что-то другое. Как насчет создания стека с динамическими блоками по которым можно стрелять шарами? Или как насчет того, чтобы создать ряд форм и прикрепить их к одному телу? Удачи в экспериментах.

Скачать

Исходный код этого проекта: Box2D_DevMag_Tut.zip (370 KB).

Дополнительные материалы и литература