Разработка игры с использованием Box2D на ActionScript 3

Доброго времени суток, дорогой читатель!
Недавно была опубликована статья, где описывалась работа с движком Box2D.
Однако, движок на сайте был версии 2.1a работа с которым, к сожалению, отличалась от работы предоставленной в статье. К сожалению скудная иностранная документация по этой версии движка заставила во многом разбираться самому. Частью своих изысканий я бы хотел поделиться с тобой, дорогой читатель.


Первым делом хотел бы рассказать о изменениях с 2.0 версии.
  • Геометрические свойства фигур были отделены от физических. Теперь физические свойства имеют тип b2FixtureDef, а геометрические остались в типе b2BodyDef. Что бы применить физические свойства к нашей геометрии надо вызвать функцию CreateFixture для нашего тела (b2Body) с параметром физических свойств.
    body.CreateFixture(fixtureDef);
  • Так же теперь надо указывать тип нашего объекта в b2BodyDef. Существуют 3 таких типа:
    1. Статический
    2. Динамический
    3. Кинематический (если нужно, что бы объект двигался, но сдвинуть другими объектами его было нельзя)
  • Теперь не нужно указывать размеры нашего мира.
  • Снято ограничение на количество полигонов.


Для освоения новой версии разберём пример.

Для начала нам нужно создать новый проект (я использую FlashDevelop) и увидеть что то в этом духе:
package {
	import flash.display.Sprite;
	import flash.events.Event;
	public class Main extends Sprite {
		public function Main():void {
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init);
		}
		public function init(e:Event = null):void {
			removeEventListener(Event.ADDED_TO_STAGE, init);
		}
	}
}

Далее мы загружаем 2.1a версию движка и кидаем её в папку с кодом нашего проекта.
Импортируем нужные нам классы:

import Box2D.Common.Math.b2Vec2;
import Box2D.Dynamics.*;
import Box2D.Collision.Shapes.b2PolygonShape;
import Box2D.Collision.Shapes.b2CircleShape;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.utils.getTimer;


Создадим нужные нам переменные:
public var PIXELS_TO_METRE:Number = new Number(30); //Количество пикселей в метре. Необходимо там, так как Box2D использует величины системы измерения СИ.
public var world:b2World; // Мир.
public var ball:b2Body; //Персонаж.
public var timer:Number = new Number(0); //Когда в последний раз была нажата клавиша.


После удаления обработчика ADDED_TO_STAGE создаём наш мир:
world = new b2World(new b2Vec2(0, 10), true);

Первый параметр, принимающий объект типа b2Vec2 — это гравитация в нашем мире. В данном случае у меня она направлена по оси y вниз на 10 пунктов, что меня устраивает.
Второй параметр — это разрешение\запрещение сна у объектов в мире.

Далее нужно создать объекты в нашем мире. Я решил создать функцию, которая создаёт прямоугольник:
public function addBox(x:Number, y:Number, width:Number, height:Number, dyn:int = 0):b2Body {
	var bodyDef:b2BodyDef = new b2BodyDef();
	bodyDef.position.Set((x + width / 2) / PIXELS_TO_METRE, (y + height / 2) / PIXELS_TO_METRE);
	if (dyn == 1) bodyDef.type = b2Body.b2_dynamicBody;
	var content:Sprite = new Sprite();
	content.graphics.beginFill(0x000000);
	content.graphics.drawRect(0 - width / 2, 0 - height / 2, width, height);
	bodyDef.userData = content;
	addChild(bodyDef.userData);
	var boxShape:b2PolygonShape = new b2PolygonShape();
	boxShape.SetAsBox(width / 2 / PIXELS_TO_METRE, height / 2 / PIXELS_TO_METRE);
	var fixtureDef:b2FixtureDef = new b2FixtureDef();
	fixtureDef.shape = boxShape;
	fixtureDef.density = dyn;
	var body:b2Body = world.CreateBody(bodyDef);
	body.CreateFixture(fixtureDef);
	return(body);
}

Объект bodyDef хранит параметры нашего прямоугольника: координаты, поворот и содержимое. Изначально точкой координат является центр нашего прямоугольника. Так как мне это неудобно я вычислил верхнюю левую точку, которая будет вводиться в качестве параметра.
У нас в проекте будут как и динамические (живые) прямоугольники, так и статические. Проверяем наш параметр (dyn) и ставим тип, если надо.
Далее я рисую сам прямоугольник, его графическую составляющую, начиная от центра.
Метод SetAsBox у объекта типа b2PolygonShape назначает физической частью объекта прямоугольник. В качестве параметров он так же принимает метры и начинает отсчёт от середины.
Объект fixtureDef — физические параметры нашего прямоугольника: динамичность, плотность, упругость и физическую модель. Такие параметры как плотность и упругость я использовать не стал.
Далее мы отправляем данные о объекте в наш мир.

Далее я создал функцию для создания шара, главного героя нашего проекта:
public function addBall(x:Number, y:Number, radius:Number):void {
	var bodyDef:b2BodyDef = new b2BodyDef();
	bodyDef.position.Set((x + radius) / PIXELS_TO_METRE, (y + radius) / PIXELS_TO_METRE);
	bodyDef.type = b2Body.b2_dynamicBody;
	var content:Sprite = new Sprite();
	content.graphics.beginFill(0x000000);
	content.graphics.drawCircle(0, 0, radius);
	bodyDef.userData = content;
	addChild(bodyDef.userData);
	var circShape:b2CircleShape = new b2CircleShape(radius / PIXELS_TO_METRE);
	var fixtureDef:b2FixtureDef = new b2FixtureDef();
	fixtureDef.shape = circShape;
	fixtureDef.density = 1;
	ball = world.CreateBody(bodyDef);
	ball.CreateFixture(fixtureDef);
}

Создание шара крайне похоже на создание прямоугольника и по этому рассматривать я его не буду.

Создаём треугольник, который у нас будет лестницей:
public function addTriangle(x:Number, y:Number, size:Number):void {
	var bodyDef:b2BodyDef = new b2BodyDef();
	bodyDef.position.Set(x / PIXELS_TO_METRE, y / PIXELS_TO_METRE);
	var content:Sprite = new Sprite();
	content.graphics.beginFill(0x000000);
	content.graphics.moveTo(size, 0);
	content.graphics.lineTo(0, size);
	content.graphics.lineTo(size, size);
	bodyDef.userData = content;
	addChild(bodyDef.userData);
	var polyDef:b2PolygonShape = new b2PolygonShape();
	polyDef.SetAsArray([
		new b2Vec2(size / PIXELS_TO_METRE, size / PIXELS_TO_METRE),
		new b2Vec2(0, size / PIXELS_TO_METRE),
		new b2Vec2(size / PIXELS_TO_METRE, 0)
	]);
	var fixtureDef:b2FixtureDef = new b2FixtureDef();
	fixtureDef.shape = polyDef;
	fixtureDef.density = 0;
	var body:b2Body = world.CreateBody(bodyDef);
	body.CreateFixture(fixtureDef);
}

Тут всё точно так же, за исключением создания физической модели. Физическая модель здесь — объект типа b2PolygonShape. Так как шаблона нету (как у addBox) мы сами рисуем треугольник с помощью массива векторов. Отсчёт начинается с последнего элемента массива до первого.

Далее мы создаём функцию с названием creteObjects, где пишем:
public function creteObjects():void {
	addBall(10, 400, 50);
	
	addBox(0, 580, 450, 20, 0);
	addBox(250, 540, 200, 40, 0);
	addTriangle(210, 540, 40);
	addBox(610, 540, 190, 60);
	addBox(-10, 0, 10, 600);
	addBox(800, 0, 10, 600);
	
	var m1:b2Body = addBox(430, 540, 20, 5, 0);
	var m2:b2Body = addBox(451, 540, 20, 5, 1);
	var m3:b2Body = addBox(472, 540, 20, 5, 1);
	var m4:b2Body = addBox(493, 540, 20, 5, 1);
	var m5:b2Body = addBox(514, 540, 20, 5, 1);
	var m6:b2Body = addBox(535, 540, 20, 5, 1);
	var m7:b2Body = addBox(556, 540, 20, 5, 1);
	var m8:b2Body = addBox(577, 540, 20, 5, 1);
	var m9:b2Body = addBox(598, 540, 20, 5, 0);
}

Последние элементы — это наш будущий мост.
Далее нам нужно соединить элементы нашего моста.
Пишем в конец creteObjects следующее:
var jointDef:b2RevoluteJointDef = new b2RevoluteJointDef();
jointDef.enableLimit = true;
jointDef.lowerAngle = 0;
jointDef.upperAngle = 0.1;
jointDef.Initialize(m1, m2, m1.GetWorldCenter());
world.CreateJoint(jointDef);
jointDef.Initialize(m2, m3, m2.GetWorldCenter());
world.CreateJoint(jointDef);
jointDef.Initialize(m3, m4, m3.GetWorldCenter());
world.CreateJoint(jointDef);
jointDef.Initialize(m4, m5, m4.GetWorldCenter());
world.CreateJoint(jointDef);
jointDef.Initialize(m5, m6, m5.GetWorldCenter());
world.CreateJoint(jointDef);
jointDef.Initialize(m6, m7, m6.GetWorldCenter());
world.CreateJoint(jointDef);
jointDef.Initialize(m7, m8, m7.GetWorldCenter());
world.CreateJoint(jointDef);
jointDef.Initialize(m8, m9, m8.GetWorldCenter());
world.CreateJoint(jointDef);

Сначала мы создаём конфигурацию нашей связки к которой применяем лимиты, чтобы сильно не тряслась.
Далее мы связываем все элементы между собой.

Все объекты в мире готовы и работают. Но если скомпилировать приложение мы ничего хорошего не увидим. Почему? Мы не выполнили рендер.
В конце функции init создаём событие ENTER_FRAME:
stage.addEventListener(Event.ENTER_FRAME, onEnterFrame);

Собственно код функции onEnterFrame:
public function onEnterFrame(e:Event = null):void {
	world.Step(1 / 30, 10, 10);
	for (var bb:b2Body = world.GetBodyList(); bb; bb = bb.GetNext()){
		if (bb.GetUserData() is Sprite) {
			var sprite:Sprite = bb.GetUserData() as Sprite;
			sprite.x = bb.GetPosition().x * PIXELS_TO_METRE;
			sprite.y = bb.GetPosition().y * PIXELS_TO_METRE;
			sprite.rotation = bb.GetAngle() * (180 / Math.PI);
		}
	}
}

Функция Step обновляет наш мир. В качестве параметра она принимает как часто обновлять объекты. В секундах.
Далее мы получаем все объекты в мире и обновляем их позицию и поворот.

Можно тестировать. Всё отлично работает, не учитывая того, что мы не можем управлять нашим персонажем. Сейчас исправим это.



Добавляем обработчик нажатия клавишь:
stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);

Создаём функцию onKeyDown:
public function onKeyDown(e:KeyboardEvent):void {
	if (e.keyCode == 39 && getTimer() - p > 200) {
		ball.ApplyImpulse(new b2Vec2(10, 0), ball.GetPosition());
		p = getTimer();
	}else if (e.keyCode == 37 && getTimer() - p > 200) {
		ball.ApplyImpulse(new b2Vec2( -10, 0), ball.GetPosition());
		p = getTimer();
	}
}

Функция ApplyImpulse приминяет толчок к объекту с заданным вектором силы и позицией.
Так же я запомнил нажатие клавиши и следующее произойдёт не ранее, чем через 200 милисекунд.

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

Похожие публикации

Комментарии 16

    –2
    Box2D не развивается и по производительно на порядок (а то и на несколько) проигрывает Nape.
      +6
      Еще раз напомню. Что есть разные движки, есть Box2D, есть Nape. Там и там есть минусы/плюсы. Если автор топика пишет про Box2D — ругать его не нужно, он просто рассказывает об этом движке. Мне, например, не интересен Nape как движок, интересен Box2D. Т.к. портирование игр это делает элементарным.

      Для автора статьи:
      Спасибо, что рассказали о новой версии движка. Я писал примеры на старом, потому, что все исходники (в т.ч. и прототип) были на старой. Мне просто было лень у меня не было времени это все переписать. А вообще, было бы хорошо, если бы рассказали, с чем вызван переход на использование фикстур и в целом: изменения в движке.
        0
        Сейчас всё распишу.
          0
          Покажете где я его ругал? Свои плюсы посту и карме он от меня получил.
          Просто везде одни и теже уроки про Box2D…

          А реально проект Box2D не развивается никак. Версия 2.1а датирована в SVN 2010-03-28.

          Объективная реальность мне подсказывает, что делать проекты на библиотеке которая не обновлялась уже более полутора лет и врядли когда обновится — рисковано… особенно в свете того что есть отличный аналог, у которго плюсов по сравнению с боксом, объективно гораздо больше чем минусов.

          Вот я и написао автору, что имеет смысл обратить внимание на аналоги. Хотя признаю, что можно было и корректнее написать…
            +1
            >> Объективная реальность мне подсказывает, что делать проекты на библиотеке которая не обновлялась уже более полутора лет и врядли когда обновится — рисковано…

            Объективно — box2d более взрослое и стабильное решение, с хорошей документацией и большим коммьюнити. Которое к тому же есть везде, уже даже на js. Для большинства проектов его производительности и возможностей хватает за глаза.

            А Nape — только для AS3, динамично развивается (napenew) => постоянно меняется. Коммьюнити не такое большое. А одной документации для создания чего-либо не хватит, придется рыться в исходниках на haxe.
              0
              Хотел возмутиться, что вот только что буквально вышла 2.2. Потом вспомнил, что топик про flash.

              Да обновят и флеш-версию, думаю :)
            0
            Мне тоже больше нравится nape, последний проект с ним делали. Дело не только в производительности, но и в более понятной структуре. Но я, например, так и не добился абсолютно жёстких соединений в nape…
            +4
            По идее, есть версия бокса собранная под алхимией. Должна работать быстрее чисто AS'ной
            • НЛО прилетело и опубликовало эту надпись здесь
              0
              мостик колбасит не по-детски :)
                0
                всегда было интересно можно ли использовать подобные движки для взгляда сверху? то есть если в вашем примере мы смотрим как бы сбоку и гравитация направлена вниз, то меня интересует гравитация направленная в сторону от экрана (аля gta 1/2)
                  0
                  Да, просто отключить гравитацию мира.
                    0
                    Угу. И выставить сопротивление среды. Будет как бы предметы лежат на столе. Ну, или плавают в воде — как сделаешь )
                    0
                    где-то даже на хабре пример был с управлением машинкой (вид сверху) и физикой на box2D
                  0
                  А нет ли у кого ссылки на статью про разрушаемые объекты? К примеру, есть планка, с такой-то жёсткостью, надо чтобы при давлении на неё с определённой силой она сломалась (можно чтобы просто испарилась). Был бы очень и очень признателен!

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

                  Самое читаемое