Все больше и больше игр добавляют к геймплею динамическую физику или используют её как основной элемент. 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