Pull to refresh

Создание очередной казуалки на Flash-платформе с физикой. Часть I

Reading time 6 min
Views 6.7K
Привет, дорогой друг.

Начну с того, что геймдевом я занимаюсь относительно недавно.
Поэтому на качество кода и информации не полагайтесь %)

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

Если повезет и вам понравится статья, то будет и вторая часть, и третья часть. В конечном итоге мы получим такую игру. (да-да, игра жутко не доделана, тоже самое, что и в статье, только с графикой и сенсорами)
Остальное под катом.



Собираемся в поход



Что нам нужно?
  1. Сам физический движок Box2D. Тут важно, чтобы версия была 2.0.2, так как он куда стабильнее. Да и производительность у них одинаковая.
    Скачать его можно тут или тут (но по второй ссылке еще и весь этот исходный код).
  2. Необходим опыт создания чего-либо на as3.
  3. Собственно то, на чем мы будем писать, иными словами IDE. Можно использовать FlashDevelop (как я), он, кстати, бесплатен; или использовать Adobe Flash CS5, например.
  4. brain.dll, соответственно и hands.dll


Предыстория



И так Box2D — это бесплатный, опенсорсный, двухмерный физический движок (для моделирования физического мира в играх и не только) разработан одним человеком, Erin Catto, для C++.

Но появился другой хороший человек, который взял и портировал его на Flash.

Кто-то говорит, что он очень «жирный», типа медленно работает, кто-то — наоборот. Знаю одно — на нем делают реальные игры, а потому, скорее всего, это зависит от рук.

Но сначала надо сказать об общих положениях, касающихся Box2D. Эти положения будут предоставлены в виде тезисов:

  1. как единицу измерения Box2D не использует пиксели
  2. как единицу измерения Box2D использует международную систему С (си) – метры, килограммы, секунды (МКС)
  3. Box2D был настроен на работу с динамическими объектами в диапазоне от 10 сантиметров до 10 метров, то есть возможно создавать объекты от стакана до автобуса
  4. так как единицы которыми оперирует Box2D не совпадает с экранными единицами (пикселями), то надо задавать некоторый коэффициент преобразования метров (юнитов) в пиксели. Например: можно задать, что 10 сантиметров = 30 пикселям.
  5. Box2D оперирует с двумя типами объектов: динамические и статические
  6. Динамические объекты участвуют в процессе анализа столкновений между собой (кирпичи, мячи, молекулы, вертолеты, машины, люди…. ), статические – нет (земля, фундамент, каркас – все что абсолютно нерушимое)
  7. Box2D реализует столкновения со следующими фигурами: круг, квадрат, выпуклые многоугольники.
  8. Имена большинства структур в Box2D начинаются с префикса «b2» для того чтобы лучше визуально выделить структуры движка и сделать меньшую вероятность конфликта со структурами пользователя


Ну вот, вроде, пока все.

Начинаем злокодить



Создаем проект, размер окна 500x500 и 30FPS, ищем наш класс Main.as.
Суем к классу Main нашу папочку с Box2D.
Импортируем нужные либы:

import Box2D.Dynamics.*;
import Box2D.Dynamics.Joints.*;
import Box2D.Collision.*;
import Box2D.Collision.Shapes.*;
import Box2D.Common.Math.*;


Создаем константы:
public static const ITERATIONS:int = 20;
public static const  TIMESTEP:Number = 1.0 / 30.0;


Что это за константы — объясню позже.

Объявляем переменные world, sprite, rotator, hero и boundsWorld:

public var world:b2World;
public var sprite:Sprite; // нужен для "псевдо вращения"
public var boundsWorld:b2Body;
public var hero:b2Body;
public var rotator:Number;


Создаем функцию:

public function InitializePhysicsWorld():void
{
	var worldAABB:b2AABB = new b2AABB(); // создание границ мира, если объект выпадает из них, то выпадает и из просчета.
	worldAABB.lowerBound.Set(-300, -300.0);
	worldAABB.upperBound.Set(300.0, 300.0);
	world = new b2World(worldAABB, new b2Vec2(0, 0), false); // создание мира, worldAABB - граница мира; new bVec2(0, 0) - вектор гравитации, почему он равен нулю объясню позже; false - запрещает спать объектам в мире.
	var dbgDraw:b2DebugDraw = new b2DebugDraw(); // для начала сделаем так, чтобы рисовался только мир, в режиме отладки, без графики.
	dbgDraw.m_sprite= sprite;
	dbgDraw.m_drawScale= 30;
	dbgDraw.m_alpha = 1;
	dbgDraw.m_fillAlpha = 0.5;
	dbgDraw.m_lineThickness = 1;
	dbgDraw.AppendFlags(b2DebugDraw.e_shapeBit); // прорисовывать сами фигуры		
	world.SetDebugDraw(dbgDraw);
}


А в main() пишем:

public function Main():void 
{
	sprite = new Sprite();
	addChild(sprite);
	sprite.x = sprite.y = 250;
	rotator = 0;
	InitializePhysicsWorld();
}


Наш мир создан, попробуем теперь заполнить его бабочками, травкой и водой чем-нибудь.
Создадим границы мира, создаем функцию:

public function CreateBoundsWorld():b2Body
{
	var body:b2Body; // исходное тело
	var bodyDef:b2BodyDef; // шаблон тела
	var polygon:b2PolygonDef; // полигон
			
	bodyDef = new b2BodyDef();
	polygon = new b2PolygonDef();
			
	bodyDef.position.Set(0, 0); // ставим на позицию x:0, y:0
			
	polygon.density = 0; // ставим на 0, т.к. если density равен нулю, то объект становится статическим.
	polygon.friction = 1.0; 
	polygon.restitution = 0.2;
			
	body = world.CreateBody(bodyDef); // регистрируем в мире шаблон
			
	// придаем форму телу
	polygon.SetAsOrientedBox(300/2/30, 1/2/30, new b2Vec2(0, -300/2/30), 0);
	body.CreateShape(polygon);
			
	polygon.SetAsOrientedBox(300/2/30, 1/2/30, new b2Vec2(0, 300/2/30), 0);
	body.CreateShape(polygon);
			
	polygon.SetAsOrientedBox(1/2/30, 300/2/30, new b2Vec2(300/2/30, 0), 0);
	body.CreateShape(polygon);
			
	polygon.SetAsOrientedBox(1/2/30, 300/2/30, new b2Vec2(-300/2/30, 0), 0);
	body.CreateShape(polygon);
						 
	body.SetMassFromShapes(); // создаем массу, хотя она тут и не нужна.

	return body; //  возвращаем тело.
}


Пишем в конец main():

boundsWorld = CreateBoundsWorld();


Теперь наш мир не пустой, но постойте, почему при компиляции ничего не видно?
Не пугайся, дорогой %username%, все нормально. Заставим наш движок рисовать и просчитывать физику.
Для этого создаем функцию:

public function enterFrameListener(event:Event):void
{
	world.Step(timestep, ITERATIONS);
	// Для моделирования движения тел Box2D использует численное дифференцирование (а точнее метод Эйлера). Выглядит это так: мир Box2D содержит функцию Step(float timeStep,int iterations), имеющую два аргумента: время, которое необходимо смоделировать в мире и количество итераций для разрешения взаимодействий между объектами.
}


Вешаем слушатель в main():

this.addEventListener(Event.ENTER_FRAME, enterFrameListener);


Запускам и видим:
image

Все бы хорошо, но теперь пришло время добавить динамические объекты, в нашем случае это герой-шар. Опять создаем функцию:

public function CreateHero(x:Number, y:Number):void
{
	var bodyDef:b2BodyDef;
	var circleDef:b2CircleDef;
			
	var x:Number;
	var y:Number;
	var r:Number;
	
	/* HERO */
	x = x / 30; // позиция героя переведенная из пикслей в метры
	y = y / 30; // 
	r = 8 / 30; // радиус

	bodyDef = new b2BodyDef();
	bodyDef.position.Set(x, y);
			
	circleDef = new b2CircleDef();
	circleDef.radius = r;
	circleDef.density = 1;
	circleDef.friction = 1;
	circleDef.restitution = 0.2;
	
	hero = world.CreateBody(bodyDef);
			
	hero.SetUserData("hero"); // будем использовать как "определитель объекта"
			
	hero.CreateShape(circleDef);
	hero.SetMassFromShapes();
			
}


Добавляем опять в main() функцию:

CreateHero(0, 0);


Компилируем, уже видим что-то новенькое, появился белый шар в центре.
Но он не двигается, если вы внимательный человек, то помните, что мы отключили гравитацию. Зачем? Чтобы сделать псевдовращение мира. Так как гравитация в нашем мире, просто обязана быть динамической.

Поэтому сделаем вращение нашего мира на клавиши «Влево» и «Вправо».
Вращать нужно будет сам спрайт с миром и менять гравитацию.
Надеюсь, вы понимаете, почему нельзя просто вращать статическую «комнату» для нашего героя %)

Добавляем две public переменные:

public var keyPressedRight:Boolean;
public var keyPressedLeft:Boolean;


Слушатели в main():

parent.addEventListener(KeyboardEvent.KEY_DOWN, keyDownListener);
parent.addEventListener(KeyboardEvent.KEY_UP, keyUpListener);


И соответственно сами функции:

public function keyDownListener(event:KeyboardEvent):void
{
	switch(event.keyCode)
	{
		case 37: keyPressedRight = true; break;
		case 39: keyPressedLeft = true; break;
	}
}
	
public function keyUpListener(event:KeyboardEvent):void
{
	switch(event.keyCode)
	{
		case 37: keyPressedRight=false; break;
		case 39: keyPressedLeft = false; break;				
	}
}


А теперь идем в наш enterFrameListener, добавляем:

if (keyPressedRight)
	rotator -= 4;
		 else 
			if(keyPressedLeft)
				rotator += 4;


Вращаем спрайт:

sprite.rotation = rotator;


Управление готово. Теперь привяжем управление к физическому движку, создадим гравитацию, которая зависит от rotator:

var angle:Number = (270 + rotator_fix) / 180 * Math.PI;
var pseudo_gravity:b2Vec2 = b2Vec2.Make(Math.cos(angle), -Math.sin(angle));


Вектор гравитации мы подсчитали, теперь нужно его применить для нашего героя.

for (var body:b2Body = world.GetBodyList(); body; body = body.GetNext()) // запускаем цикл и перебираем все тела.
{
	if (body.GetUserData() == "hero") // если userdata тела - hero, то это определенно наш герой.
	{
		pseudo_gravity.x *= 9.8 * body.GetMass();
		pseudo_gravity.y *= 9.8 * body.GetMass();
						
		body.ApplyForce(pseudo_gravity, body.GetWorldCenter()); // даем пинка
	}
}


Компилируем и ага! Добавим какую-нибудь хрень посередине. Создаем функцию:

public function CreateStaticRect(x:Number, y:Number, w:Number, h:Number):void
{
	var total_x:Number = ((x + w / 2) - 150) / 30;
	var total_y:Number = ((y + h / 2) - 150) / 30;
	var total_w:Number = w / 30;
	var total_h:Number = h / 30;
			
	var body:b2Body;
	var bodyDef:b2BodyDef;
	var polygon:b2PolygonDef;
			
	bodyDef = new b2BodyDef();
	polygon = new b2PolygonDef();
			
	bodyDef.position.Set(0, 0);
			
	bodyDef.position.Set(total_x, total_y);
	polygon.SetAsBox(total_w / 2, total_h / 2);
			
	polygon.density = 0.0;
	polygon.friction = 0.5;
	polygon.restitution = 0.2;
	body = world.CreateBody(bodyDef);
	
	body.CreateShape(polygon);
	body.SetMassFromShapes();
	body.SetUserData("box");
							
} 


И в конец main():

CreateStaticRect(0, 150, 150, 10);


Готовый «прототип» на лицо.

Итоги



Вот такой вот step-by-step. Дорогие друзья, если вам понравится, я обязательно напишу еще ряд статей, в конечном итоге мы получим что-то вроде этого.

P.S: Еще раз дам все ссылки:
Исходники | SWF'шка файлом | Embed SWF | Box2Dlib

P.S.S: Простите за ссылки на всякие фриварные хостинги. Своего сервера нет.

UPD: Часть II
Tags:
Hubs:
+66
Comments 50
Comments Comments 50

Articles