Маленький отважный арканоид (часть 3 — Box2D)

  • Tutorial
Сегодня, как я и обещал, мы вдохнем в наш Arcanoid жизнь. Заставим шарик двигаться, сталкиваясь с кирпичами, а кирпичи, при этом, разбиваться. В принципе, игровая физика в arcanoid не так чтобы очень сложна и вполне реализуема собственными силами. Единственный нетривиальный момент в ней — отслеживание столкновений. Но это именно то, что «взрослые» физические движки умеют лучше всего!

Так почему бы их не использовать? К тому-же, если мы оформим Box2D в виде модуля Marmalade, впоследствии, мы сможем использовать его и в других приложениях, возможно требующих более изощренной «физики». Давайте этим займемся.

Методика оформления Box2D в виде подпроекта совершенно аналогична той, которую мы использовали по отношению к LibYAML в предыдущей статье. Единственное отличие в том, что в Box2D гораздо больше исходных файлов. Поэтому, если нет желания повторять рутинное переписывание их имен в mkf-файл, уже выполненное мной, можно взять готовый модуль непосредственно с GitHub. Дистрибутив Box2D взят отсюда.

Итак, добавляем Box2D в наш проект:

arcanoid.mkb
#!/usr/bin/env mkb
options
{
      module_path="../yaml"
+     module_path="../box2d"
}
subprojects
{
    iwgl
    yaml
+   box2d
}

includepath
{
    ./source/Main
    ./source/Model
}
files
{
    [Main]
    (source/Main)
    Main.cpp
    Main.h
    Quads.cpp
    Quads.h       
    Desktop.cpp
    Desktop.h
    IO.cpp
    IO.h

    [Model]
    (source/Model)
    Bricks.cpp
    Bricks.h
    Ball.cpp
    Ball.h
    Board.cpp
    Board.h
}
assets
{
    (data)
    level.json
}


… и пытаемся все это скомпилировать, попутно внося в Box2D косметические исправления из разряда «сделаем компилятор счастливым»:

Collision\b2BroadPhase.h
-      for (int32 i = 0; i < m_moveCount; ++i)
+      for (int32 j = 0; j < m_moveCount; ++j)
	{
-		m_queryProxyId = m_moveBuffer[i];
+		m_queryProxyId = m_moveBuffer[j];
                ...
	}

        ...
	while (i < m_pairCount)
	{
           ...
	}


Common\b2Math.h
/// A 2D column vector.
struct b2Vec2
{
	/// Default constructor does nothing (for performance).
-	b2Vec2() {}
+	b2Vec2(): x(0.0f), y(0.0f) {}

	/// Construct using coordinates.
	b2Vec2(float32 x, float32 y) : x(x), y(y) {}

	...
	float32 x, y;
};


Если после этого вы получаете ошибку связывания:



… то это, скорее всего означает, что вам также как и мне, нравится MSVS 2003. GCC, при этом, собирает проект без ошибок, но нам, конечно, хотелось бы иметь возможность запускать его и под отладчиком тоже. Как бы там ни было, от MSVS 2003 придется отказаться. В принципе, достаточно переключиться на MSVS 2005, но я сразу поставил MSVS 2010, благо она была под рукой. Само переключение осуществляется при помощи Marmalade Configuration Utility.



Ну что-же, пора браться за дело. Если в первой статье мы имели дело с «миром иллюзий», во втором с «миром идей», то теперь пришла пора создать «реальный мир», который у нас будет отвечать за физические взаимодействия объектов. Добавим новые файлы в проект:

arcanoid.mkb
#!/usr/bin/env mkb
options
{
	module_path="../yaml"
	module_path="../box2d"
}
subprojects
{
    iwgl
    yaml
    box2d
}

includepath
{
    ./source/Main
    ./source/Model
}
files
{
    [Main]
    (source/Main)
    Main.cpp
    Main.h
    Quads.cpp
    Quads.h       
    Desktop.cpp
    Desktop.h
    IO.cpp
    IO.h
+   World.cpp
+   World.h       

    [Model]
    (source/Model)
    Bricks.cpp
    Bricks.h
    Ball.cpp
    Ball.h
    Board.cpp
    Board.h
+   IBox2DItem.h
}
assets
{
    (data)
    level.json
}


Интерфейс IBox2DItem будет отвечать за передачу событий из Box2D в нашу модель данных. Для наших целей, пока достаточно всего двух методов:

IBox2DItem.h
#ifndef _I_BOX2D_ITEM_H_
#define _I_BOX2D_ITEM_H_

#include <Box2D.h>

class IBox2DItem {
	public:
		virtual void setXY(int X, int Y) {}
		virtual bool impact(b2Body* b) {return false;}
};

#endif	// _I_BOX2D_ITEM_H_


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

Метод setXY позволит нам передавать изменения координат движущихся объектов (для того, чтобы эти изменения можно было отобразить на экране), а метод impact позволит нам отслеживать соударения объектов, чуть позже.

World.h
#ifndef _WORLD_H_
#define _WORLD_H_

#include <vector>
#include <Box2D.h>
#include "Desktop.h"
#include "IBox2DItem.h"

const int HALF_MARGIN   = 10;
const int V_ITERATIONS  = 10;
const int P_ITERATIONS  = 10;

const float FRICTION    = 0.0f;
const float RESTITUTION = 1.0f;
const float DYN_DENSITY = 0.0f;
const float R_INVIS     = 0.0f;
const float EPS         = 1.0f;
const float SPEED_SQ    = 10.0f;

using namespace std;

class World {
	private:
		bool isStarted;
		int  HandleX, HandleH, HandleW;
		uint64 timestamp;
		int  width, height;
		b2World* wp;
		b2Body* ground;
		b2Body* ball;
		b2Body* handle;
		b2Body* createBox(int x, int y, int hw, int hh, IBox2DItem* userData = NULL);
		float32 getTimeStep();
		void  start();
	public:
		World(): width(0), height(0), wp(NULL) {}
		void init();
		void release();
		void update();
		void refresh();
		b2Body* addBrick(int x, int y, int hw, int hh, IBox2DItem* userData) {return createBox(x, y, hw, hh, userData);}
		b2Body* addBall(int x, int y, int r, IBox2DItem* userData);
		b2Body* addHandle(int x, int y, int hw, int hh, IBox2DItem* userData);
		void    moveHandle(int x, int y);

		typedef vector<b2Body*>::iterator BIter;
};

extern World world;

#endif	// _WORLD_H_


Для этого модуля, рассмотрим реализацию подробнее:

World.cpp
#include "s3e.h"
#include "World.h"
#include "Ball.h"

World world;

void World::init() {
	isStarted = false;
	width  = desktop.getWidth();
	height = desktop.getHeight();
	b2Vec2 gravity(0.0f, 0.0f);
	wp = new b2World(gravity);
	ground = createBox(width/2, -HALF_MARGIN, width/2, HALF_MARGIN);
	createBox(-HALF_MARGIN, height/2, HALF_MARGIN, height/2);
	createBox(width/2, height + HALF_MARGIN, width/2, HALF_MARGIN);
	createBox(width + HALF_MARGIN, height/2, HALF_MARGIN, height/2);
	ball = NULL;
	handle = NULL;
}

void World::release() {
	if (wp != NULL) {
		delete wp;
		wp = NULL;
		ball = NULL;
		handle = NULL;
	}
}
...


Методы init и release занимаются корректным созданием и уничтожением основных объектов «мира». Обращаю внимание, что гравитацию мы выставляем в 0 (у нас будет невесовмость), а игровое поле окружаем четырьмя «стенами» (одну из них потом можно будет легко убрать).

Далее определяем методы для создания игровых объектов:

World.cpp
...
b2Body* World::createBox(int x, int y, int hw, int hh, IBox2DItem* userData) {
	b2BodyDef def;
	def.type = b2_staticBody;
	def.position.Set(x, y);
	b2Body* r = wp->CreateBody(&def);
	b2PolygonShape box;
	box.SetAsBox(hw, hh);
	b2FixtureDef fd;
	fd.shape = &box;
	fd.density = 0;
	fd.friction = FRICTION;
	fd.restitution = RESTITUTION;
	r->CreateFixture(&fd);
	r->SetUserData(userData);
	return r;
}

b2Body* World::addBall(int x, int y, int r, IBox2DItem* userData) {
	if (ball != NULL) {
		wp->DestroyBody(ball);
	}
	b2BodyDef def;
	def.type = b2_dynamicBody;
	def.linearDamping = 0.0f;
	def.angularDamping = 0.0f;
	def.position.Set(x, y);
	ball = wp->CreateBody(&def);
	b2CircleShape shape;
	shape.m_p.SetZero();
	shape.m_radius = r + R_INVIS;
	b2FixtureDef fd;
	fd.shape = &shape;
	fd.density = DYN_DENSITY;
	fd.friction = FRICTION;
	fd.restitution = RESTITUTION;
	ball->CreateFixture(&fd);
	ball->SetBullet(true);
	ball->SetUserData(userData);
	return ball;
}
...


Здесь мы создаем прямоугольный объект (стена или кирпич) и шарик. Кроме формы они отличаются типом. Кирпичи являются статическими (неподвижными) объектами, а шарик динамическим. Box2D требует разделять игровые объекты на два этих типа, из соображений производительности. Также, мы задаем такие физические свойства объектов как упругость, коэффициент трения и т.п. Для удобства, они определены константами в h-файле.

В нашем случае, моделируются абсолютно упругие соударения (RESTITUTION = 1), при отсутствии трения (FRICTION = 0). Также в ноль выставляем параметры linearDamping и angularDamping, отвечающие за торможение движущегося объекта «средой». Первоначально, была идея выставить ненулевое значение FRICTION, чтобы была возможность «подкручивать» шарик ракеткой, но от нее пришлось отказаться. При установке FRICTION в любое ненулевое значение, движение шарика очень быстро вырождается в чистое движение по вертикали или горизонтали.

В userData для body и fixture можно хранить любой указатель. Мы будем хранить там указатель на интерфейс IBox2DItem соответствующих объектов в нашей модели.

World.cpp
...
float32 World::getTimeStep() {
	uint64 t = s3eTimerGetMs();
	int r = (int)(t - timestamp);
	timestamp = t;
	return (float32)r / 1000.0f;
}

void World::start() {
	if (ball != NULL) {
		ball->ApplyLinearImpulse(ball->GetWorldVector(b2Vec2(-10.0f, -10.0f)), 
			                     ball->GetWorldPoint(b2Vec2(0.0f, 0.0f)));
	}
}

void World::update() {
	if (!isStarted) {
		isStarted = true;
		start();
		timestamp = s3eTimerGetMs();
		srand((unsigned int)timestamp);
	} else {
		float32 timeStep = getTimeStep();
		wp->Step(timeStep, V_ITERATIONS, P_ITERATIONS);
	}
}

void World::refresh() {
	if (ball != NULL) {
		b2Vec2 pos = ball->GetPosition();
		Ball* b = (Ball*)ball->GetUserData();
		if (b != NULL) {
			b->setXY(pos.x, pos.y);
		}
	}
}


В методе update мы рассчитываем очередную итерацию существования «мира» методом Step, в который передается три аргумента. Первый аргумент — интервал времени на который производится рассчет. В руководстве пользователя Box2D рекомендуется использовать интервал ~1/60 секунды. Также, настоятельно рекомендуется чтобы он был константным. Следующие два параметра определяют количество итераций при выполнении расчетов и напрямую влияют на качество моделирования. Я передаю в оба параметра значение 10.

При первом вызове метода update, мы придаем шарику начальную скорость. Поскольку все соударения идеально упруги, скорость движения шарика после соударений не уменьшается и однократного задания начальной скорости нам вполне достаточно. При необходимости, мы можем скорректировать скорость между вызовами метода update (ни в коем случае не следует выполнять каких либо манипуляций с объектами в контексте вызова b2World.Step, это, скорее всего, приведет к немедленному разрушению памяти).

Задачей метода refresh является получение измененных координат шарика (после очередного шага расчетов) и передача измененных координат интерфейсу IBox2DItem.

Внесем необходимые изменения в модель:

Bricks.h
#ifndef _BRICKS_H_
#define _BRICKS_H_

#include "IwGL.h"
#include "s3e.h"
#include "Desktop.h"
+#include "World.h"
+#include "IBox2DItem.h"

#define BRICK_COLOR_1      0xffffff00
#define BRICK_COLOR_2      0xff50ff00
#define BRICK_HALF_WIDTH   20
#define BRICK_HALF_HEIGHT  10

#include <vector>

using namespace std;

-class Bricks {
+class Bricks: public IBox2DItem {
    private:
        struct SBrick {
            SBrick(int x, int y): x(x), 
                                          y(y), 
+                                         body(NULL), 
+                                         isBroken(false),
                                          hw(BRICK_HALF_WIDTH), 
                                          hh(BRICK_HALF_HEIGHT), 
                                          ic(BRICK_COLOR_1), 
                                          oc(BRICK_COLOR_2) {}
            SBrick(const SBrick& p): x(p.x), 
                                          y(p.y), 
+                                         body(p.body), 
+                                         isBroken(p.isBroken),
                                          hw(p.hw), 
                                          hh(p.hh), 
                                          ic(p.ic), 
                                          oc(p.oc) {}
            int x, y, hw, hh, ic, oc;
+            int isBroken;
+            b2Body* body;
        };
        vector<SBrick> bricks;
    public:
        Bricks(): bricks() {}
+		void init() {}
+		void release() {}
        void refresh();
        void clear(){bricks.clear();}
        void add(SBrick& b);

    typedef vector<SBrick>::iterator BIter;

	friend class Board;
};

#endif	// _BRICKS_H_


Bricks.cpp
#include "Bricks.h"
#include "Quads.h"

void Bricks::refresh() {
    for (BIter p = bricks.begin(); p != bricks.end(); ++p) {
+	    if (p->isBroken) continue;
            CIwGLPoint point(p->x, p->y);
            point = IwGLTransform(point);

            int16* quadPoints = quads.getQuadPoints();
            uint32* quadCols = quads.getQuadCols();
            if ((quadPoints == NULL) || (quadCols == NULL)) break;

            *quadPoints++ = point.x - p->hw;
            *quadPoints++ = point.y + p->hh;
            *quadCols++   = p->ic;

            *quadPoints++ = point.x + p->hw;
            *quadPoints++ = point.y + p->hh;
            *quadCols++   = p->oc;

            *quadPoints++ = point.x + p->hw;
            *quadPoints++ = point.y - p->hh;
            *quadCols++   = p->ic;

            *quadPoints++ = point.x - p->hw;
            *quadPoints++ = point.y - p->hh;
            *quadCols++   = p->oc;
    }
}

void Bricks::add(SBrick& b) { 
+   b.body = world.addBrick(b.x, b.y, b.hw, b.hh, (IBox2DItem*)this);
    bricks.push_back(b);
}


Ball.h
#ifndef _BALL_H_
#define _BALL_H_

#include <vector>
#include "IwGL.h"
#include "s3e.h"
#include "Desktop.h"
+#include "World.h"
+#include "IBox2DItem.h"

#define MAX_SEGMENTS       7
#define BALL_COLOR_1       0x00000000
#define BALL_COLOR_2       0xffffffff
#define BALL_RADIUS        15

using namespace std;

-class Ball {
+class Ball: public IBox2DItem {
    private:
        struct Offset {
            Offset(int dx, int dy): dx(dx), dy(dy) {}
            Offset(const Offset& p): dx(p.dx), dy(p.dy) {}
            int dx, dy;
        };
        vector<Offset> offsets;
        int     x;
        int     y;
+       b2Body*  body;
    public:
        void init();
        void release() {}
        void refresh();
        virtual void setXY(int X, int Y);

    typedef vector<Offset>::iterator OIter;
};

#endif	// _BALL_H_


Ball.cpp
#include "Ball.h"
#include "Quads.h"
#include "Desktop.h"
#include <math.h>

#define PI 3.14159265f

void Ball::init(){
    x = desktop.getWidth() / 2;
    y = desktop.getHeight()/ 2;

    float delta = PI / (float)MAX_SEGMENTS;
    float angle = delta / 2.0f;
    float r = (float)desktop.toRSize(BALL_RADIUS);
    for (int i = 0; i < MAX_SEGMENTS; i++) {
        offsets.push_back(Offset((int16)(cos(angle) * r), (int16)(sin(angle) * r)));
        angle = angle + delta;
        offsets.push_back(Offset((int16)(cos(angle) * r), (int16)(sin(angle) * r)));
        angle = angle + delta;
        offsets.push_back(Offset((int16)(cos(angle) * r), (int16)(sin(angle) * r)));
    }
+   body = world.addBall(x, y, (int)r, (IBox2DItem*)this);
}

void Ball::setXY(int X, int Y) {
    x = X;
    y = Y;
}

void Ball::refresh() {
    CIwGLPoint point(x, y);
    point = IwGLTransform(point);
    OIter o = offsets.begin();
    int r = desktop.toRSize(BALL_RADIUS);
    for (int i = 0; i < MAX_SEGMENTS; i++) {

            int16* quadPoints = quads.getQuadPoints();
            uint32* quadCols = quads.getQuadCols();
            if ((quadPoints == NULL) || (quadCols == NULL)) break;

            *quadPoints++ = point.x + (r / 4);
            *quadPoints++ = point.y + (r / 4);
            *quadCols++   = BALL_COLOR_2;

            *quadPoints++ = point.x + o->dx;
            *quadPoints++ = point.y + o->dy;
            *quadCols++   = BALL_COLOR_1;
            o++;

            *quadPoints++ = point.x + o->dx;
            *quadPoints++ = point.y + o->dy;
            *quadCols++   = BALL_COLOR_1;
            o++;

            *quadPoints++ = point.x + o->dx;
            *quadPoints++ = point.y + o->dy;
            *quadCols++   = BALL_COLOR_1;
            o++;
    }
}


Здесь все изменения очевидны. Далее вносим изменения в Main:

Main.cpp
#include "Main.h"

#include "s3e.h"
#include "IwGL.h"

#include "Desktop.h"
+#include "World.h"
#include "IO.h"
#include "Quads.h"
#include "Board.h"

Board board;

void init() {
    desktop.init();
    io.init();
    quads.init();
+   world.init();
    board.init();
}

void release() {
+   world.release();
    io.release();
    desktop.release();
}

int main() {
    init(); {
        while (!s3eDeviceCheckQuitRequest()) {
            io.update();
            if (io.isKeyDown(s3eKeyAbsBSK) || io.isKeyDown(s3eKeyBack)) break;
+           world.update();
            quads.update();
            desktop.update();
            board.update();
            board.refresh();
+           world.refresh();
            quads.refresh();
            io.refresh();
            desktop.refresh();
        }
    }
    release();
    return 0;
}


Теперь программу можно запустить на выполнение. Что мы видим? Шарик движется, но как-то очень медленно. Отскоков после соударений не наблюдается. Манипуляции с начальной скоростью не изменяют видимой скорости движения шарика. Все это говорит о том, что мы что-то делаем не так.

Подумаем, что бы это могло быть? Мы задаем все размеры в масштабе экранных координат. Для себя, я обычно считаю, единицу измерения в Box2D равной 1 метру. Даже при разрешении экрана 320x480, получается, что мы пытаемся смоделировать арканоид каких-то совершенно невообразимо эпических размеров (более того, моделируемая физика будет зависеть от размеров экрана устройства, а это уже совсем никуда не годится). Кроме того, Box2D не очень хорошо производит рассчеты с объектами таких размеров. Обычно, рекомендуемые размеры мира не должны превышать десятков метров. Внесем коррективы:

World.h
#ifndef _WORLD_H_
#define _WORLD_H_

#include <vector>
#include <Box2D.h>
#include "Desktop.h"
#include "IBox2DItem.h"

+const float W_WIDTH     = 10.0f;

const int HALF_MARGIN   = 10;
const int V_ITERATIONS  = 10;
const int P_ITERATIONS  = 10;

const float FRICTION    = 0.0f;
const float RESTITUTION = 1.0f;
const float DYN_DENSITY = 0.0f;
const float R_INVIS     = 0.0f;
const float EPS         = 1.0f;
const float SPEED_SQ    = 10.0f;

using namespace std;

class World {
	private:
		bool isStarted;
		int  HandleX, HandleH, HandleW;
		uint64 timestamp;
		int  width, height;
		b2World* wp;
		b2Body* ground;
		b2Body* ball;
		b2Body* handle;
		b2Body* createBox(int x, int y, int hw, int hh, IBox2DItem* userData = NULL);
		float32 getTimeStep();
		void  start();
+	float toWorld(int x);
+	int   fromWorld(float x);
	public:
		World(): width(0), height(0), wp(NULL) {}
		void init();
		void release();
		void update();
		void refresh();
		b2Body* addBrick(int x, int y, int hw, int hh, IBox2DItem* userData) {return createBox(x, y, hw, hh, userData);}
		b2Body* addBall(int x, int y, int r, IBox2DItem* userData);

		typedef vector<b2Body*>::iterator BIter;
};

extern World world;

#endif	// _WORLD_H_


World.cpp
#include "s3e.h"
#include "World.h"
#include "Ball.h"

World world;

void World::init() {
	isStarted = false;
	width  = desktop.getWidth();
	height = desktop.getHeight();
	b2Vec2 gravity(0.0f, 0.0f);
	wp = new b2World(gravity);
	ground = createBox(width/2, -HALF_MARGIN, width/2, HALF_MARGIN);
	createBox(-HALF_MARGIN, height/2, HALF_MARGIN, height/2);
	createBox(width/2, height + HALF_MARGIN, width/2, HALF_MARGIN);
	createBox(width + HALF_MARGIN, height/2, HALF_MARGIN, height/2);
	ball = NULL;
	handle = NULL;
}

void World::release() {
	if (wp != NULL) {
		delete wp;
		wp = NULL;
		ball = NULL;
		handle = NULL;
	}
}

+float World::toWorld(int x) {
+	return ((float)x * W_WIDTH) / (float)desktop.getWidth();
+}

+int World::fromWorld(float x) {
+	return (int)((x * (float)desktop.getWidth()) / W_WIDTH);
+}

b2Body* World::createBox(int x, int y, int hw, int hh, IBox2DItem* userData) {
	b2BodyDef def;
	def.type = b2_staticBody;
-   def.position.Set(x, y);
+   def.position.Set(toWorld(x), toWorld(y));
	b2Body* r = wp->CreateBody(&def);
	b2PolygonShape box;
-   box.SetAsBox(hw, hh);
+   box.SetAsBox(toWorld(hw), toWorld(hh));
	b2FixtureDef fd;
	fd.shape = &box;
	fd.density = 0;
	fd.friction = FRICTION;
	fd.restitution = RESTITUTION;
	r->CreateFixture(&fd);
	r->SetUserData(userData);
	return r;
}

b2Body* World::addBall(int x, int y, int r, IBox2DItem* userData) {
	if (ball != NULL) {
		wp->DestroyBody(ball);
	}
	b2BodyDef def;
	def.type = b2_dynamicBody;
	def.linearDamping = 0.0f;
	def.angularDamping = 0.0f;
-   def.position.Set(x, y);
+   def.position.Set(toWorld(x), toWorld(y));
	ball = wp->CreateBody(&def);
	b2CircleShape shape;
	shape.m_p.SetZero();
-   shape.m_radius = r + R_INVIS;
+   shape.m_radius = toWorld(r) + R_INVIS;
	b2FixtureDef fd;
	fd.shape = &shape;
	fd.density = DYN_DENSITY;
	fd.friction = FRICTION;
	fd.restitution = RESTITUTION;
	ball->CreateFixture(&fd);
	ball->SetBullet(true);
	ball->SetUserData(userData);
	return ball;
}

float32 World::getTimeStep() {
	uint64 t = s3eTimerGetMs();
	int r = (int)(t - timestamp);
	timestamp = t;
	return (float32)r / 1000.0f;
}

void World::start() {
	if (ball != NULL) {
		ball->ApplyLinearImpulse(ball->GetWorldVector(b2Vec2(-10.0f, -10.0f)), 
			                     ball->GetWorldPoint(b2Vec2(0.0f, 0.0f)));
	}
}

void World::update() {
	if (!isStarted) {
		isStarted = true;
		start();
		timestamp = s3eTimerGetMs();
		srand((unsigned int)timestamp);
	} else {
		float32 timeStep = getTimeStep();
		wp->Step(timeStep, V_ITERATIONS, P_ITERATIONS);
	}
}

void World::refresh() {
	if (ball != NULL) {
		b2Vec2 pos = ball->GetPosition();
		Ball* b = (Ball*)ball->GetUserData();
		if (b != NULL) {
-		b->setXY(pos.x, pos.y);
+		b->setXY(fromWorld(pos.x), fromWorld(pos.y));
		}
	}
}


Теперь, независимо от размеров экрана, «ширина» нашего мира будет составлять 10 (метров). Запускаем и убеждаемся, что шарик начал летать с нормальной скоростью и отскакивать от стен. Теперь, добьемся того, чтобы «кирпичи» исчезали после столкновения с ними шарика.

Bricks.h
#ifndef _BRICKS_H_
#define _BRICKS_H_

#include "IwGL.h"
#include "s3e.h"
#include "Desktop.h"
#include "World.h"
#include "IBox2DItem.h"

#define BRICK_COLOR_1      0xffffff00
#define BRICK_COLOR_2      0xff50ff00
#define BRICK_HALF_WIDTH   20
#define BRICK_HALF_HEIGHT  10

#include <vector>

using namespace std;

class Bricks: public IBox2DItem {
    private:
        struct SBrick {
            SBrick(int x, int y): x(x), 
                                          y(y), 
                                          body(NULL), 
										  isBroken(false),
                                          hw(BRICK_HALF_WIDTH), 
                                          hh(BRICK_HALF_HEIGHT), 
                                          ic(BRICK_COLOR_1), 
                                          oc(BRICK_COLOR_2) {}
            SBrick(const SBrick& p): x(p.x), 
                                          y(p.y), 
                                          body(p.body), 
										  isBroken(p.isBroken),
                                          hw(p.hw), 
                                          hh(p.hh), 
                                          ic(p.ic), 
                                          oc(p.oc) {}
            int x, y, hw, hh, ic, oc;
			int isBroken;
			b2Body* body;
        };
        vector<SBrick> bricks;
+       virtual bool impact(b2Body* b);
    public:
        Bricks(): bricks() {}
		void init() {}
		void release() {}
        void refresh();
        void clear(){bricks.clear();}
        void add(SBrick& b);

    typedef vector<SBrick>::iterator BIter;

	friend class Board;
};

#endif	// _BRICKS_H_


Bricks.cpp
#include "Bricks.h"
#include "Quads.h"

void Bricks::refresh() {
    for (BIter p = bricks.begin(); p != bricks.end(); ++p) {
	    if (p->isBroken) continue;
            CIwGLPoint point(p->x, p->y);
            point = IwGLTransform(point);

            int16* quadPoints = quads.getQuadPoints();
            uint32* quadCols = quads.getQuadCols();
            if ((quadPoints == NULL) || (quadCols == NULL)) break;

            *quadPoints++ = point.x - p->hw;
            *quadPoints++ = point.y + p->hh;
            *quadCols++   = p->ic;

            *quadPoints++ = point.x + p->hw;
            *quadPoints++ = point.y + p->hh;
            *quadCols++   = p->oc;

            *quadPoints++ = point.x + p->hw;
            *quadPoints++ = point.y - p->hh;
            *quadCols++   = p->ic;

            *quadPoints++ = point.x - p->hw;
            *quadPoints++ = point.y - p->hh;
            *quadCols++   = p->oc;
    }
}

+bool Bricks::impact(b2Body* b) {
+    for (BIter p = bricks.begin(); p != bricks.end(); ++p) {
+		if (p->body == b) {
+			p->isBroken = true;
+			return true;
+		}
+	}
+	return false;
+}

void Bricks::add(SBrick& b) { 
	b.body = world.addBrick(b.x, b.y, b.hw, b.hh, (IBox2DItem*)this);
    bricks.push_back(b);
}


World.h
#ifndef _WORLD_H_
#define _WORLD_H_

#include <vector>
#include <Box2D.h>
#include "Desktop.h"
#include "IBox2DItem.h"

const float W_WIDTH     = 10.0f;

const int HALF_MARGIN   = 10;
const int V_ITERATIONS  = 10;
const int P_ITERATIONS  = 10;

const float FRICTION    = 0.0f;
const float RESTITUTION = 1.0f;
const float DYN_DENSITY = 0.0f;
const float R_INVIS     = 0.0f;
const float EPS         = 1.0f;
const float SPEED_SQ    = 10.0f;

using namespace std;

-class World {
+class World: public b2ContactListener {
	private:
		bool isStarted;
		int  HandleX, HandleH, HandleW;
		uint64 timestamp;
		int  width, height;
		b2World* wp;
		b2Body* ground;
		b2Body* ball;
		b2Body* handle;
		b2Body* createBox(int x, int y, int hw, int hh, IBox2DItem* userData = NULL);
		float32 getTimeStep();
+	vector<b2Body*>* broken;
		void  start();
+	void  impact(b2Body* b);
+	virtual void PostSolve(b2Contact* contact, const b2ContactImpulse* impulse);
		float toWorld(int x);
		int   fromWorld(float x);
	public:
		World(): broken(), width(0), height(0), wp(NULL) {}
		void init();
		void release();
		void update();
		void refresh();
		b2Body* addBrick(int x, int y, int hw, int hh, IBox2DItem* userData) {
                      return createBox(x, y, hw, hh, userData);
                }
		b2Body* addBall(int x, int y, int r, IBox2DItem* userData);

+	typedef vector<b2Body*>::iterator BIter;
};

extern World world;

#endif	// _WORLD_H_


World.cpp
#include "s3e.h"
#include "World.h"
#include "Ball.h"

World world;

void World::init() {
+   broken = new vector<b2Body*>();
	isStarted = false;
	width  = desktop.getWidth();
	height = desktop.getHeight();
	b2Vec2 gravity(0.0f, 0.0f);
	wp = new b2World(gravity);
+   wp->SetContactListener(this);
	ground = createBox(width/2, -HALF_MARGIN, width/2, HALF_MARGIN);
	createBox(-HALF_MARGIN, height/2, HALF_MARGIN, height/2);
	createBox(width/2, height + HALF_MARGIN, width/2, HALF_MARGIN);
	createBox(width + HALF_MARGIN, height/2, HALF_MARGIN, height/2);
	ball = NULL;
	handle = NULL;
}

void World::release() {
	if (wp != NULL) {
		delete wp;
		wp = NULL;
		ball = NULL;
		handle = NULL;
	}
+   delete broken;
}

float World::toWorld(int x) {
	return ((float)x * W_WIDTH) / (float)desktop.getWidth();
}

int World::fromWorld(float x) {
	return (int)((x * (float)desktop.getWidth()) / W_WIDTH);
}

b2Body* World::createBox(int x, int y, int hw, int hh, IBox2DItem* userData) {
	b2BodyDef def;
	def.type = b2_staticBody;
	def.position.Set(toWorld(x), toWorld(y));
	b2Body* r = wp->CreateBody(&def);
	b2PolygonShape box;
	box.SetAsBox(toWorld(hw), toWorld(hh));
	b2FixtureDef fd;
	fd.shape = &box;
	fd.density = 0;
	fd.friction = FRICTION;
	fd.restitution = RESTITUTION;
	r->CreateFixture(&fd);
	r->SetUserData(userData);
	return r;
}

b2Body* World::addBall(int x, int y, int r, IBox2DItem* userData) {
	if (ball != NULL) {
		wp->DestroyBody(ball);
	}
	b2BodyDef def;
	def.type = b2_dynamicBody;
	def.linearDamping = 0.0f;
	def.angularDamping = 0.0f;
	def.position.Set(toWorld(x), toWorld(y));
	ball = wp->CreateBody(&def);
	b2CircleShape shape;
	shape.m_p.SetZero();
	shape.m_radius = toWorld(r) + R_INVIS;
	b2FixtureDef fd;
	fd.shape = &shape;
	fd.density = DYN_DENSITY;
	fd.friction = FRICTION;
	fd.restitution = RESTITUTION;
	ball->CreateFixture(&fd);
	ball->SetBullet(true);
	ball->SetUserData(userData);
	return ball;
}

float32 World::getTimeStep() {
	uint64 t = s3eTimerGetMs();
	int r = (int)(t - timestamp);
	timestamp = t;
	return (float32)r / 1000.0f;
}

void World::start() {
	if (ball != NULL) {
		ball->ApplyLinearImpulse(ball->GetWorldVector(b2Vec2(-10.0f, -10.0f)), 
			                     ball->GetWorldPoint(b2Vec2(0.0f, 0.0f)));
	}
}

+void World::impact(b2Body* b) {
+	IBox2DItem* it = (IBox2DItem*)b->GetUserData();
+	if (it != NULL) {
+		if (it->impact(b)) {
+			for (BIter p = broken->begin(); p != broken->end(); ++p) {
+				if (*p == b) return;
+			}
+			broken->push_back(b);
+		}
+	}
+}

+void World::PostSolve(b2Contact* contact, const b2ContactImpulse* impulse) {
+	impact(contact->GetFixtureA()->GetBody());
+	impact(contact->GetFixtureB()->GetBody());
+}

void World::update() {
	if (!isStarted) {
		isStarted = true;
		start();
		timestamp = s3eTimerGetMs();
		srand((unsigned int)timestamp);
	} else {
		float32 timeStep = getTimeStep();
		wp->Step(timeStep, V_ITERATIONS, P_ITERATIONS);
	}
}

void World::refresh() {
+   for (BIter p = broken->begin(); p != broken->end(); ++p) {
+   	wp->DestroyBody(*p);
+   }
+   broken->clear();
	if (ball != NULL) {
		b2Vec2 pos = ball->GetPosition();
		Ball* b = (Ball*)ball->GetUserData();
		if (b != NULL) {
			b->setXY(fromWorld(pos.x), fromWorld(pos.y));
		}
	}
}


Здесь, как я уже говорил выше, важно не пытаться удалить объект при рассчете очередной итерации b2World.Step (именно это и произойдет если попытаться удалить объект непосредственно в PostSolve). Также, не следует рассчитывать на то, что PostSolve будет вызыван однократно. Вполне возможна ситуация когда он сработает, например, дважды для одного «кирпича». Если мы внесем объект в broken без предварительной проверки, мы попытаемся разрушить его дважды, что неизбежно приведет к разрушению памяти. Поскольку в broken не может накопиться большого количества объектов, линейный поиск объекта в векторе нас вполне устраивает по производительности.

Осталось совсем немного. Добавим ракетку. Первоначально, я хотел сделать ракетку динамическим объектом, ограничив ее движение по вертикали при помощи PrismaticJoint. Перемещать ее по горизонтали, можно было-бы временно создавая MouseJoint. Но потом, я решил, что надо быть проще.

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

Внесем в проект необходимые изменения:

arcanoid.mkb
#!/usr/bin/env mkb
options
{
     module_path="../yaml"
     module_path="../box2d"
}
subprojects
{
    iwgl
    yaml
    box2d
}

includepath
{
    ./source/Main
    ./source/Model
}
files
{
    [Main]
    (source/Main)
    Main.cpp
    Main.h
    Quads.cpp
    Quads.h       
    Desktop.cpp
    Desktop.h
    IO.cpp
    IO.h
+   TouchPad.cpp
+   TouchPad.h

    [Model]
    (source/Model)
    Bricks.cpp
    Bricks.h
    Ball.cpp
    Ball.h
    Board.cpp
    Board.h
+   Handle.cpp
+   Handle.h
}
assets
{
    (data)
    level.json
}


Немного измененный модуль TouchPad возьмем отсюда:

TouchPad.h
#ifndef _TOUCHPAD_H_
#define _TOUCHPAD_H_

#include "s3ePointer.h"
#include "Desktop.h"

#define MAX_TOUCHES	3

enum EMessageType {
	emtNothing                                                      = 0x00,
	emtTouchEvent                                                   = 0x10,
	emtTouchIdMask                                                  = 0x03,
	emtTouchMask                                                    = 0x78,
	emtMultiTouch                                                   = 0x14,
	emtTouchOut                                                     = 0x18,
	emtTouchDown                                                    = 0x30,
	emtTouchUp                                                      = 0x50,
	emtTouchOutUp                                                   = 0x58,
	emtTouchMove                                                    = 0x70,
	emtSingleTouchDown                                              = 0x30,
	emtSingleTouchUp                                                = 0x50,
	emtSingleTouchMove                                              = 0x70,
	emtMultiTouchDown                                               = 0x34,
	emtMultiTouchUp                                                 = 0x54,
	emtMultiTouchMove                                               = 0x74
};

struct Touch {
   int   x, y;
   bool  isActive, isPressed, isMoved;
   int   id;	
};

class TouchPad {
  private:
    bool   IsAvailable;
    bool   IsMultiTouch;
    Touch  Touches[MAX_TOUCHES];
    Touch* findTouch(int id);								
    static void HandleMultiTouchButton(s3ePointerTouchEvent* event);
    static void HandleMultiTouchMotion(s3ePointerTouchMotionEvent* event);
  public:
    static bool isTouchDown(int eventCode);
    static bool isTouchUp(int eventCode);
    bool   isAvailable() const { return IsAvailable; }
    bool   isMultiTouch() const { return IsMultiTouch; }
    Touch* getTouchByID(int id);
    Touch* getTouch(int index) { return &Touches[index]; }	
    Touch* getTouchPressed();
    int	   getTouchCount() const;

    bool   init();
    void   release();
    void   update();
    void   clear();
};

extern TouchPad touchPad;

#endif	// _TOUCHPAD_H_


TouchPad.cpp
#include "TouchPad.h"

TouchPad touchPad;

bool TouchPad::isTouchDown(int eventCode) {
    return (eventCode & emtTouchMask) == emtTouchDown;
}
 
bool TouchPad::isTouchUp(int eventCode) {
    return (eventCode & emtTouchMask) == emtTouchUp;
}

void TouchPad::HandleMultiTouchButton(s3ePointerTouchEvent* event) {
	Touch* touch = touchPad.findTouch(event->m_TouchID);
    if (touch != NULL) {
        touch->isPressed = event->m_Pressed != 0; 
        touch->isActive  = true;
        touch->x  = event->m_x;
        touch->y  = event->m_y;
		touch->id = event->m_TouchID;
    }
}

void TouchPad::HandleMultiTouchMotion(s3ePointerTouchMotionEvent* event) {
	Touch* touch = touchPad.findTouch(event->m_TouchID);
    if (touch != NULL) {
		if (touch->isActive) {
			touch->isMoved = true;
		}
        touch->isActive  = true;
        touch->x = event->m_x;
        touch->y = event->m_y;
    }
}

void HandleSingleTouchButton(s3ePointerEvent* event) {
	Touch* touch = touchPad.getTouch(0);
    touch->isPressed = event->m_Pressed != 0;
    touch->isActive  = true;
    touch->x  = event->m_x;
    touch->y  = event->m_y;
	touch->id = 0;
}

void HandleSingleTouchMotion(s3ePointerMotionEvent* event) {
	Touch* touch = touchPad.getTouch(0);
	if (touch->isActive) {
		touch->isMoved = true;
	}
    touch->isActive  = true;
    touch->x = event->m_x;
    touch->y = event->m_y;
}

Touch* TouchPad::getTouchByID(int id) {
	for (int i = 0; i < MAX_TOUCHES; i++) {
		if (Touches[i].isActive && Touches[i].id == id)
			return &Touches[i];
	}
	return NULL;
}

Touch* TouchPad::getTouchPressed() {
	for (int i = 0; i < MAX_TOUCHES; i++) {
		if (Touches[i].isPressed && Touches[i].isActive)
			return &Touches[i];
	}
	return NULL;
}

Touch* TouchPad::findTouch(int id) {
	if (!IsAvailable)
		return NULL;
	for (int i = 0; i < MAX_TOUCHES; i++) {
		if (Touches[i].id == id)
			return &Touches[i];
    }
	for (int i = 0; i < MAX_TOUCHES; i++) {
		if (!Touches[i].isActive)	{
            Touches[i].id = id;
			return &Touches[i];
		}
	}
	return NULL;
}

int	TouchPad::getTouchCount() const {
	if (!IsAvailable)
		return 0;
	int r = 0;
	for (int i = 0; i < MAX_TOUCHES; i++) {
		if (Touches[i].isActive) {
            r++;
		}
	}
	return r;
}

void TouchPad::update() {
	for (int i = 0; i < MAX_TOUCHES; i++) {
		Touches[i].isMoved = false;
	}
	if (IsAvailable) {
		s3ePointerUpdate();
	}
}

void TouchPad::clear() {
	for (int i = 0; i < MAX_TOUCHES; i++) {
		if (!Touches[i].isPressed) {
			Touches[i].isActive = false;
		}
		Touches[i].isMoved = false;
	}
}

bool TouchPad::init() {
    IsAvailable = s3ePointerGetInt(S3E_POINTER_AVAILABLE) ? true : false;
	if (!IsAvailable) return false;
	for (int i = 0; i < MAX_TOUCHES; i++) {
		Touches[i].isPressed = false;
		Touches[i].isActive = false;
		Touches[i].isMoved = false;
		Touches[i].id = 0;
	}
    IsMultiTouch = s3ePointerGetInt(S3E_POINTER_MULTI_TOUCH_AVAILABLE) ? true : false;
    if (IsMultiTouch) {
        s3ePointerRegister(S3E_POINTER_TOUCH_EVENT, 
             (s3eCallback)HandleMultiTouchButton, NULL);
        s3ePointerRegister(S3E_POINTER_TOUCH_MOTION_EVENT, 
             (s3eCallback)HandleMultiTouchMotion, NULL);
    } else {
        s3ePointerRegister(S3E_POINTER_BUTTON_EVENT, 
             (s3eCallback)HandleSingleTouchButton, NULL);
        s3ePointerRegister(S3E_POINTER_MOTION_EVENT, 
             (s3eCallback)HandleSingleTouchMotion, NULL);
    }
	return true;
}

void TouchPad::release() {
	if (IsAvailable) {
		if (IsMultiTouch) {
			s3ePointerUnRegister(S3E_POINTER_TOUCH_EVENT, 
			   (s3eCallback)HandleMultiTouchButton);
			s3ePointerUnRegister(S3E_POINTER_TOUCH_MOTION_EVENT, 
			   (s3eCallback)HandleMultiTouchMotion);
		} else {
			s3ePointerUnRegister(S3E_POINTER_BUTTON_EVENT, 
			   (s3eCallback)HandleSingleTouchButton);
			s3ePointerUnRegister(S3E_POINTER_MOTION_EVENT, 
			   (s3eCallback)HandleSingleTouchMotion);
		}
	}
}


IO.h
#ifndef _IO_H_
#define _IO_H_

#include "TouchPad.h"

class IO {
    private:
        bool KeysAvailable;
    public:
        void init();
        void release();
        void update();
        void refresh();
	    bool isKeyDown(s3eKey key) const;
};

extern IO io;

#endif	// _IO_H_


IO.cpp
#include "s3e.h"
#include "IO.h"

IO io;

void IO::init() {
	touchPad.init();
}

void IO::release() {
	touchPad.release();
}

void IO::update() {
	touchPad.update();
    s3eKeyboardUpdate();
}

void IO::refresh() {
	touchPad.clear();
}

bool IO::isKeyDown(s3eKey key) const {
    return (s3eKeyboardGetState(key) & S3E_KEY_STATE_DOWN) == S3E_KEY_STATE_DOWN;
}


Теперь, добавим модуль Handle:

Handle.h
#ifndef _HANDLE_H_
#define _HANDLE_H_

#include "IwGL.h"
#include "s3e.h"

#include "Desktop.h"
#include "World.h"
#include "IBox2DItem.h"

#define HANDLE_COLOR        0xffff3000
#define HANDLE_H_WIDTH      40
#define HANDLE_H_HEIGHT     10
#define HANDLE_H_POS        50

class Handle: public IBox2DItem {
	private:
        int     x;
        int     y;
		int     touchId;
	public:
        void init();
        void release() {}
        void refresh();
        void update();
		virtual void setXY(int X, int Y);
};

#endif	// _HANDLE_H_


Handle.cpp
#include "Handle.h"
#include "Quads.h"
#include "TouchPad.h"

void Handle::init() {
    x = desktop.getWidth() / 2;
	y = desktop.getHeight();
	touchId = -1;
}

void Handle::setXY(int X, int Y) {
	x = X;
	y = Y;
}

void Handle::refresh() {
    CIwGLPoint point(x, y);
    point = IwGLTransform(point);

    int16* quadPoints = quads.getQuadPoints();
    uint32* quadCols = quads.getQuadCols();
    if ((quadPoints == NULL) || (quadCols == NULL)) return;

    *quadPoints++ = point.x - desktop.toRSize(HANDLE_H_WIDTH);
    *quadPoints++ = point.y + desktop.toRSize(HANDLE_H_HEIGHT);
    *quadCols++   = HANDLE_COLOR;

    *quadPoints++ = point.x + desktop.toRSize(HANDLE_H_WIDTH);
    *quadPoints++ = point.y + desktop.toRSize(HANDLE_H_HEIGHT);
    *quadCols++   = HANDLE_COLOR;

    *quadPoints++ = point.x + desktop.toRSize(HANDLE_H_WIDTH);
    *quadPoints++ = point.y - desktop.toRSize(HANDLE_H_HEIGHT);
    *quadCols++   = HANDLE_COLOR;

    *quadPoints++ = point.x - desktop.toRSize(HANDLE_H_WIDTH);
    *quadPoints++ = point.y - desktop.toRSize(HANDLE_H_HEIGHT);
    *quadCols++   = HANDLE_COLOR;

	world.addHandle(x, y, desktop.toRSize(HANDLE_H_WIDTH), desktop.toRSize(HANDLE_H_HEIGHT), (IBox2DItem*)this);
}

void Handle::update() {
	Touch* t = NULL;
	if (touchId > 0) {
		t = touchPad.getTouchByID(touchId);
	} else {
		t = touchPad.getTouchPressed();
	}
	if (t != NULL) {
		touchId = t->id;
		world.moveHandle(t->x, t->y);
	} else {
		touchId = -1;
	}
}


И внесем изменения в World и Board:
World.h
#ifndef _WORLD_H_
#define _WORLD_H_

#include <vector>
#include <Box2D.h>
#include "Desktop.h"
#include "IBox2DItem.h"

const float W_WIDTH     = 10.0f;

const int HALF_MARGIN   = 10;
const int V_ITERATIONS  = 10;
const int P_ITERATIONS  = 10;

const float FRICTION    = 0.0f;
const float RESTITUTION = 1.0f;
const float DYN_DENSITY = 0.0f;
const float R_INVIS     = 0.0f;
const float EPS         = 1.0f;
const float SPEED_SQ    = 10.0f;

using namespace std;

class World: public b2ContactListener {
	private:
		bool isStarted;
+	bool isHandleCreated;
		int  HandleX, HandleH, HandleW;
		uint64 timestamp;
		int  width, height;
		b2World* wp;
		b2Body* ground;
		b2Body* ball;
		b2Body* handle;
		b2Body* createBox(int x, int y, int hw, int hh, IBox2DItem* userData = NULL);
		float32 getTimeStep();
		vector<b2Body*>* broken;
		void  start();
		void  impact(b2Body* b);
	    virtual void PostSolve(b2Contact* contact, const b2ContactImpulse* impulse);
		float toWorld(int x);
		int   fromWorld(float x);
	public:
		World(): broken(), width(0), height(0), wp(NULL) {}
		void init();
		void release();
		void update();
		void refresh();
		b2Body* addBrick(int x, int y, int hw, int hh, IBox2DItem* userData) {return createBox(x, y, hw, hh, userData);}
		b2Body* addBall(int x, int y, int r, IBox2DItem* userData);
+	b2Body* addHandle(int x, int y, int hw, int hh, IBox2DItem* userData);
+	void    moveHandle(int x, int y);

		typedef vector<b2Body*>::iterator BIter;
};

extern World world;

#endif	// _WORLD_H_


World.cpp
#include "s3e.h"
#include "World.h"
#include "Ball.h"

World world;

void World::init() {
	broken = new vector<b2Body*>();
	isStarted = false;
	width  = desktop.getWidth();
	height = desktop.getHeight();
	b2Vec2 gravity(0.0f, 0.0f);
	wp = new b2World(gravity);
	wp->SetContactListener(this);
	ground = createBox(width/2, -HALF_MARGIN, width/2, HALF_MARGIN);
	createBox(-HALF_MARGIN, height/2, HALF_MARGIN, height/2);
	createBox(width/2, height + HALF_MARGIN, width/2, HALF_MARGIN);
	createBox(width + HALF_MARGIN, height/2, HALF_MARGIN, height/2);
	ball = NULL;
	handle = NULL;
}

void World::release() {
	if (wp != NULL) {
		delete wp;
		wp = NULL;
		ball = NULL;
		handle = NULL;
	}
    delete broken;
}

float World::toWorld(int x) {
	return ((float)x * W_WIDTH) / (float)desktop.getWidth();
}

int World::fromWorld(float x) {
	return (int)((x * (float)desktop.getWidth()) / W_WIDTH);
}

b2Body* World::createBox(int x, int y, int hw, int hh, IBox2DItem* userData) {
	b2BodyDef def;
	def.type = b2_staticBody;
	def.position.Set(toWorld(x), toWorld(y));
	b2Body* r = wp->CreateBody(&def);
	b2PolygonShape box;
	box.SetAsBox(toWorld(hw), toWorld(hh));
	b2FixtureDef fd;
	fd.shape = &box;
	fd.density = 0;
	fd.friction = FRICTION;
	fd.restitution = RESTITUTION;
	r->CreateFixture(&fd);
	r->SetUserData(userData);
	return r;
}

b2Body* World::addBall(int x, int y, int r, IBox2DItem* userData) {
	if (ball != NULL) {
		wp->DestroyBody(ball);
	}
	b2BodyDef def;
	def.type = b2_dynamicBody;
	def.linearDamping = 0.0f;
	def.angularDamping = 0.0f;
	def.position.Set(toWorld(x), toWorld(y));
	ball = wp->CreateBody(&def);
	b2CircleShape shape;
	shape.m_p.SetZero();
	shape.m_radius = toWorld(r) + R_INVIS;
	b2FixtureDef fd;
	fd.shape = &shape;
	fd.density = DYN_DENSITY;
	fd.friction = FRICTION;
	fd.restitution = RESTITUTION;
	ball->CreateFixture(&fd);
	ball->SetBullet(true);
	ball->SetUserData(userData);
	return ball;
}

+b2Body* World::addHandle(int x, int y, int hw, int hh, IBox2DItem* userData) {
+	HandleW = hw; HandleH = hh;
+	if (handle != NULL) {
+		wp->DestroyBody(handle);
+	}
+	b2BodyDef def;
+	def.type = b2_staticBody;
+	def.position.Set(toWorld(x), toWorld(y));
+	handle = wp->CreateBody(&def);
+	b2PolygonShape box;
+	box.SetAsBox(toWorld(hw), toWorld(hh));
+	b2FixtureDef fd;
+	fd.shape = &box;
+	fd.density = DYN_DENSITY;
+	fd.friction = FRICTION;
+	fd.restitution = RESTITUTION;
+	handle->CreateFixture(&fd);
+	handle->SetUserData(userData);
+	return handle;
+}

+void World::moveHandle(int x, int y) {
+	isHandleCreated = true;
+	HandleX = x;
+}

float32 World::getTimeStep() {
	uint64 t = s3eTimerGetMs();
	int r = (int)(t - timestamp);
	timestamp = t;
	return (float32)r / 1000.0f;
}

void World::start() {
	if (ball != NULL) {
		ball->ApplyLinearImpulse(ball->GetWorldVector(b2Vec2(-10.0f, -10.0f)), 
			                     ball->GetWorldPoint(b2Vec2(0.0f, 0.0f)));
	}
}

void World::impact(b2Body* b) {
	IBox2DItem* it = (IBox2DItem*)b->GetUserData();
	if (it != NULL) {
		if (it->impact(b)) {
			for (BIter p = broken->begin(); p != broken->end(); ++p) {
				if (*p == b) return;
			}
			broken->push_back(b);
		}
	}
}

void World::PostSolve(b2Contact* contact, const b2ContactImpulse* impulse) {
	impact(contact->GetFixtureA()->GetBody());
	impact(contact->GetFixtureB()->GetBody());
}

void World::update() {
	if (!isStarted) {
		isStarted = true;
		start();
		timestamp = s3eTimerGetMs();
		srand((unsigned int)timestamp);
	} else {
		float32 timeStep = getTimeStep();
		wp->Step(timeStep, V_ITERATIONS, P_ITERATIONS);
	}
}

void World::refresh() {
	for (BIter p = broken->begin(); p != broken->end(); ++p) {
		wp->DestroyBody(*p);
	}
	broken->clear();
+	if (isHandleCreated) {
+		if (handle != NULL) {
+			int y = fromWorld(handle->GetPosition().y);
+			IBox2DItem* data = (IBox2DItem*)handle->GetUserData();
+			if (HandleX < HandleW) {
+				HandleX = HandleW;
+			}
+			if (HandleX > desktop.getWidth() - HandleW) {
+				HandleX = desktop.getWidth() - HandleW;
+			}
+			handle = addHandle(HandleX, y, HandleW, HandleH, data);
+			b2Vec2 pos = handle->GetPosition();
+			data->setXY(fromWorld(pos.x), fromWorld(pos.y));
+		}
+	}
	if (ball != NULL) {
		b2Vec2 pos = ball->GetPosition();
		Ball* b = (Ball*)ball->GetUserData();
		if (b != NULL) {
			b->setXY(fromWorld(pos.x), fromWorld(pos.y));
		}
	}
}


Board.h
#ifndef _BOARD_H_
#define _BOARD_H_

#include <yaml.h>
#include <vector>
#include <String>

#include "Bricks.h"
#include "Ball.h"
+#include "Handle.h"

#define MAX_NAME_SZ   100

using namespace std;

enum EBrickMask {
    ebmX            = 0x01,
    ebmY            = 0x02,
    ebmComplete     = 0x03,
    ebmWidth        = 0x04,
    ebmHeight       = 0x08,
    ebmIColor       = 0x10,
    ebmOColor       = 0x20
};

class Board {
    private:
        struct Type {
            Type(const char* s, const char* n, const char* v): s(s), n(n), v(v) {}
            Type(const Type& p): s(p.s), n(p.n), v(p.v) {}
            string s, n, v;
        };
        Bricks bricks;
        Ball ball;
+       Handle handle;
        yaml_parser_t parser;
        yaml_event_t event;
        vector<string> scopes;
        vector<Type> types;
        char currName[MAX_NAME_SZ];
        int  brickMask;
        int  brickX, brickY, brickW, brickH, brickIC, brickOC;
        bool isTypeScope;
        void load();
        void clear();
        void notify();
        const char* getScopeName();
        void setProperty(const char* scope, const char* name, const char* value);
        void closeTag(const char* scope);
        int  fromNum(const char* s);
    public:
        Board(): scopes(), types() {}
        void init();
        void release();
        void refresh();
        void update();

    typedef vector<string>::iterator SIter;
    typedef vector<Type>::iterator TIter;
};

#endif	// _BOARD_H_


Board.cpp
#include "Board.h"
#include "Desktop.h"

const char* BOARD_SCOPE      = "board";
const char* LEVEL_SCOPE      = "level";
const char* TYPE_SCOPE       = "types";

const char* TYPE_PROPERTY    = "type";
const char* WIDTH_PROPERTY   = "width";
const char* HEIGHT_PROPERTY  = "height";
const char* IC_PROPERTY      = "inner_color";
const char* OC_PROPERTY      = "outer_color";
const char* X_PROPERTY       = "x";
const char* Y_PROPERTY       = "y";

void Board::init() {
    ball.init();
    bricks.init();
+   handle.init();
    load();
}

void Board::release() {
+   handle.release();
    bricks.release();
    ball.release();
}

...
void Board::refresh() {
    bricks.refresh();
    ball.refresh();
+   handle.refresh();
}

+void Board::update() {
+   handle.update();
+}


На этом все. Теперь, у нас есть работающий прототип игры Arcanoid, который можно собрать как для Android, так и под iPhone.
Поделиться публикацией

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

    +6
    можно собрать как для Android, так и под iPhone.

    Не хочу ничего собирать, хочу попробовать!
      0
      Действительно! Кагда уже Халк играть?
        0
        Для желающих, могу выложить apk или ipa, на выбор. Вечером, когда доберусь до лицензированной версии мармелада.
          0
          Как и обещал, загрузил версии для iPhone и Android.
            +2
            А почему ваше приложение запрашивает все возможные права? К чему там sms и запись аудио/видео?
              0
              Это вопрос к разработчикам Marmalade. Коротко говоря, запрашиваются все права, которые МОГУТ понадобиться приложению, разработанному под Marmalade. Я специально интересовался этим вопросом, есть возможность подсунуть при сборке свой манифест, менее требовательный по правам, но, как правило, приложение после этого не работает (поскольку Marmalade не только требует эти права, но но по большей части, также, активно их использует). Поскольку эта сборка тестовая, я не занимался урезанием прав и выполнил сборку по умолчанию.
              0
              Жаль, айфон-версия не загружается…
                0
                Я тестировал ее на 4-ом iPhone. 5-го под рукой не было.
                  0
                  Я о том, что попытка загрузить файл по ссылке dfiles.ru/files/d8hrb81xi завершается выдачей сообщения:
                  «Такого файла не существует, доступ к нему ограничен или он был удален из-за нарушения авторских прав.»
                    0
                    О, ну это как-раз не беда. Если не успеете забрать, сообщите мне. Я перевыложу или отошлю по почте (объем 6M)
        +5
        Не слишком ли избыточно использовать Box2D для арканоида?
          0
          Это смотря как понимать избыточность. Мы можем использовать Box2D, либо писать свой (очень простой) физический движок. Я думаю, что даже в случае арканоида, второй путь будет более трудоемок.
            0
            Вы не правы. Попробуйте — вам понравится.

            P.S. PapaBubaDiop правильно отметил ниже: в вашей задаче нет ничего специфического, поэтому использование бокса очень даже избыточно для такой обыденной задачи.
              0
              Я пробовал, мне нравится (еще в конце 90-ых дело было).
              Ниже я ответил почему я использовал Box2D.
                0
                Да я понимаю, почему вы использовали его. И мне этот топик интересен и он мне нравится, но по возможности — приводите примеры посложнее, если можно.
                  0
                  Даже такая простая игра как арканоид выливается в неимоверные портянки кода. Сложные примеры объективно сложно воспринимаются. Но я постараюсь учесть Ваше пожелание впоследствии. Оно вполне резонно.
            +1
            Именно, если бы кирпичи были со Смоленского завода и со сторонами непараллельными координатным осям, можно было потратить время на подключения чужого мира, но не для классического арканоида, кой любой школьник- участник физических олимпиад уместит в 100 строк.
              0
              Понимаете в чем дело, цель была — не арканоид, а обкатка Box2D под Marmalade на чем нибудь простом, типа арканоида, с целью использования впоследствии в более сложных проектах.
                0
                Надо было пример посложнее привести.
                0
                И кстати, кирпичи таки можно сделать разного размера (да хоть и со Смоленского) и даже непрямоугольными а трапециевидными. Не вижу в этом каких-то технических сложностей.
                  0
                  Ну так сделайте. Ждём подобного примера ;)
                    0
                    Возможно, когда нибудь я займусь «уродливым» арканоидом с кирпичами разных размеров и форм, но на ближайшее время, фронт работ определен. Например, следующую статью я собираюсь писать про SoundEngine.

                    И да, чуть не забыл. На слабо я не ведусь.
                      0
                      Да причём тут это. Слабо, не слабо — детский сад. Думал, мало ли. Отказаться ваше право. Извините, если не так выразился. :)
                        0
                        Я тоже прошу прощения за резкость. Дело в том, что арканоид в том виде, в котором он есть гениален, в своем роде. Все на своем месте. На 4pda.ru я видел эксперименты над арканоидом. С ненулевой гравитацией, неупругими отскоками, неодинаковыми блоками… Все это не то. В том то и дело, что дьявольски трудно придумать что-то новое. Дело не в наворотах, а в идее.

                        Так что, если я не вижу в чем-то технических сложностей, это вовсе не означает, что именно это и надо делать.
                          0
                          Допускаю впрочем, что я излишне консервативен
                  +1
                  А вы гляньте чуть глубже: это туториал по совместному использованию Box2D к Marmalade. Хоть и на примере арканоида, где он нафиг не нужен.
                  0
                  В наши времена арканоиды писали без Box 2d :)
                    0
                    Не могу с Вами не согласиться :)
                      0
                      Они и сейчас без него пишутся. Внезапно! :)
                        0
                        А кто спорит???
                          0
                          Да пока не видать камикадзе. :D

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

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