📜 ⬆️ ⬇️

Little Brave Arkanoid (Part 3 - Box2D)

Today, as I promised, we will breathe life into our Arcanoid . Make the ball move, colliding with bricks, and bricks, at the same time, break. In principle, the game physics in arcanoid is not so very complex and completely realizable on its own. The only non-trivial moment in it is collision tracking. But this is exactly what “adult” physical engines are best at!

So why not use them? Moreover, if we design Box2D as a Marmalade module, subsequently, we will be able to use it in other applications, possibly requiring more sophisticated “physics”. Let's do it.

The technique of designing Box2D in the form of a subproject is completely analogous to that used in relation to LibYAML in the previous article . The only difference is that Box2D has much more source files. Therefore, if there is no desire to repeat the routine rewriting of their names in the mkf-file, already done by me, you can take the finished module directly from GitHub . The Box2D distribution is taken from here .

So, add Box2D to our project:
')
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 } 


... and we are trying to compile all this, simultaneously making cosmetic corrections to Box2D from the discharge “let's make the compiler happy”:

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; }; 


If after this you get a binding error:



... then it most likely means that you, like me, like MSVS 2003. GCC, at the same time, builds the project without errors, but we, of course, would like to be able to run it under the debugger too. Anyway, MSVS 2003 will have to be abandoned. In principle, it is enough to switch to MSVS 2005, but I immediately installed MSVS 2010, since it was at hand. Switching itself is done using the Marmalade Configuration Utility.



Well then, it's time to get down to business. If in the first article we dealt with the “world of illusions”, in the second with the “world of ideas”, now it’s time to create a “real world” that we will be responsible for the physical interactions of objects. Add new files to the project:

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 } 


The IBox2DItem interface will be responsible for sending events from Box2D to our data model. For our purposes, so far only two methods are enough:

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_ 


Yes, I know that the interface should contain only abstract methods (originally it was), but then it turned out to be more convenient to have some default implementation, and the rename class was lazy. In any case, this question is of no fundamental importance in the context of our article.

The setXY method will allow us to transfer changes in the coordinates of moving objects (so that these changes can be displayed on the screen), and the impact method will allow us to track collisions of objects a little later.

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_ 


For this module, we consider the implementation in more detail:

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; } } ... 


Methods init and release are engaged in the correct creation and destruction of the main objects of the "world". I draw attention to the fact that we set gravity to 0 (we will have weightlessness), and surround the playing field with four “walls” (one of them can then be easily removed).

Next, we define methods for creating game objects:

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; } ... 


Here we create a rectangular object (wall or brick) and a ball. In addition to the form they differ in type. Bricks are static (fixed) objects, and the ball is dynamic. Box2D requires dividing game objects into these two types, for performance reasons. Also, we set such physical properties of objects as elasticity, friction coefficient, etc. For convenience, they are defined by constants in the h-file.

In our case, absolutely elastic collisions (RESTITUTION = 1) are modeled, in the absence of friction (FRICTION = 0). We also set the linearDamping and angularDamping parameters to zero, which are responsible for the inhibition of a moving object by the “environment”. Initially, the idea was to expose a non-zero value of FRICTION so that there was an opportunity to “twist” the ball with a racket, but it had to be abandoned. When setting FRICTION to any non-zero value, the movement of the ball very quickly degenerates into a pure movement either vertically or horizontally.

In userData for body and fixture, you can store any pointer. We will store there a pointer to the IBox2DItem interface of the corresponding objects in our model.

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); } } } 


In the update method, we calculate the next iteration of the existence of the “world” by the Step method, in which three arguments are passed. The first argument is the time interval for which the calculation is made. In the Box2D user manual it is recommended to use an interval of ~ 1/60 seconds. Also, it is strongly recommended that it be constant. The following two parameters determine the number of iterations when performing calculations and directly affect the quality of the simulation. I pass the value 10 in both parameters.

When you first call the update method, we give the ball the initial speed. Since all the collisions are perfectly elastic, the speed of the ball after the collisions does not decrease and a single initial velocity is quite enough for us. If necessary, we can adjust the speed between calls to the update method (in no case should we perform any manipulations with objects in the context of the b2World.Step call, this will most likely lead to immediate memory corruption).

The task of the refresh method is to retrieve the modified coordinates of the ball (after the next calculation step) and transfer the changed coordinates to the IBox2DItem interface.

We make the necessary changes to the model:

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(px), y(py), + 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(bx, by, 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++; } } 


Here all changes are obvious. Next, make changes to 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; } 


Now the program can be launched for execution. What do we see? The ball is moving, but somehow very slowly. Rebound after collisions are not observed. Manipulations with initial speed do not change the apparent speed of the ball. All this suggests that we are doing something wrong.

Think about what it could be? We set all dimensions on the scale of screen coordinates. For myself, I usually consider the unit of measurement in Box2D to be 1 meter. Even with a screen resolution of 320x480, it turns out that we are trying to simulate an arkanoid of some absolutely unimaginably epic dimensions (moreover, the simulated physics will depend on the size of the screen of the device, and this is completely useless). In addition, Box2D does not perform very well calculations with objects of this size. Usually, the recommended size of the world should not exceed tens of meters. Make adjustments:

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)); } } } 


Now, regardless of the size of the screen, the "width" of our world will be 10 (meters). We start and make sure that the ball began to fly at normal speed and bounce off the walls. Now, we will make the "bricks" disappear after the ball collides with them.

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(px), y(py), 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(bx, by, 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)); } } } 


Here, as I said above, it is important not to try to delete an object when calculating the next iteration of b2World.Step (this is exactly what happens if you try to delete an object directly in PostSolve). Also, do not assume that PostSolve will be called once. It is quite possible that it will work, for example, twice for one “brick”. If we add an object to a broken without first checking it, we will try to destroy it twice, which inevitably leads to memory destruction. Since a large number of objects cannot accumulate in broken, we are quite happy with the performance of a linear search for an object in a vector.

There are very few. Add a racket. Initially, I wanted to make the racket a dynamic object, limiting its vertical movement with PrismaticJoint. To move it horizontally, it would be possible to temporarily create a MouseJoint. But then, I decided that it should be easier .

The fact is that the decision to make a racket a dynamic object is not very successful. Box2D will have to keep track of the collision of dynamic objects all the time, and this task is so complicated that even Box2D does not cope with it very well. Installing SetBullet helps, but there are cases when the ball will fly through the racket, which, of course, is completely unacceptable, in our case. Therefore, the racket will be a static object. We will simply destroy it between the steps of calculation and create it in a new place, if necessary. Among other things, this method is much easier to implement.

We make the necessary changes to the project:

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 } 


A slightly modified TouchPad module is taken from here :

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; } 


Now, add the module 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; } } 


And make changes to the World and 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(ps), n(pn), v(pv) {} 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(); +} 


That's all. Now, we have a working prototype of the Arcanoid game, which can be built for both Android and iPhone.

Source: https://habr.com/ru/post/166929/


All Articles