Pull to refresh

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

Reading time26 min
Views13K
Сегодня, как я и обещал, мы вдохнем в наш 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.
Tags:
Hubs:
+13
Comments31

Articles

Change theme settings