Туториал по 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).

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

    • Немного полезной информации можно почерпнуть из Box2DFlash FAQs, и масса полезной информации располагается в документации к API.
    • Также прочтите интересный исчерпывающий туториал о том, как создать клон игры Peggle в Box2D.
    • Набор для разработки мира — мощный набор инструментов для создания Box2D игр в Flash IDE.
    • Инструмент для создания уровней на Box2DFlash — BisonKick