Так почему бы их не использовать? К тому-же, если мы оформим Box2D в виде модуля Marmalade, впоследствии, мы сможем использовать его и в других приложениях, возможно требующих более изощренной «физики». Давайте этим займемся.
Методика оформления Box2D в виде подпроекта совершенно аналогична той, которую мы использовали по отношению к LibYAML в предыдущей статье. Единственное отличие в том, что в Box2D гораздо больше исходных файлов. Поэтому, если нет желания повторять рутинное переписывание их имен в mkf-файл, уже выполненное мной, можно взять готовый модуль непосредственно с GitHub. Дистрибутив Box2D взят отсюда.
Итак, добавляем Box2D в наш проект:
#!/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 косметические исправления из разряда «сделаем компилятор счастливым»:
- 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) { ... }
/// 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.
Ну что-же, пора браться за дело. Если в первой статье мы имели дело с «миром иллюзий», во втором с «миром идей», то теперь пришла пора создать «реальный мир», который у нас будет отвечать за физические взаимодействия объектов. Добавим новые файлы в проект:
#!/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 в нашу модель данных. Для наших целей, пока достаточно всего двух методов:
#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 позволит нам отслеживать соударения объектов, чуть позже.
#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_
Для этого модуля, рассмотрим реализацию подробнее:
#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 (у нас будет невесовмость), а игровое поле окружаем четырьмя «стенами» (одну из них потом можно будет легко убрать).
Далее определяем методы для создания игровых объектов:
... 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 соответствующих объектов в нашей модели.
... 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.
Внесем необходимые изменения в модель:
#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_
#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); }
#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_
#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:
#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 метру. Даже при разрешении экрана 320×480, получается, что мы пытаемся смоделировать арканоид каких-то совершенно невообразимо эпических размеров (более того, моделируемая физика будет зависеть от размеров экрана устройства, а это уже совсем никуда не годится). Кроме того, Box2D не очень хорошо производит рассчеты с объектами таких размеров. Обычно, рекомендуемые размеры мира не должны превышать десятков метров. Внесем коррективы:
#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_
#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 (метров). Запускаем и убеждаемся, что шарик начал летать с нормальной скоростью и отскакивать от стен. Теперь, добьемся того, чтобы «кирпичи» исчезали после столкновения с ними шарика.
#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_
#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); }
#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_
#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 будет вызыван однократно. Вполне возможна ситуация когда он сработает, например, дважды для одного «кирпича». Если мы внесем объект в briken без предварительной проверки, мы попытаемся разрушить его дважды, что неизбежно приведет к разрушению памяти. Поскольку в broken не может накопиться большого количества объектов, линейный поиск объекта в векторе нас вполне устраивает по производительности.
Осталось совсем немного. Добавим ракетку. Первоначально, я хотел сделать ракетку динамическим объектом, ограничив ее движение по вертикали при помощи PrismaticJoint. Перемещать ее по горизонтали, можно было-бы временно создавая MouseJoint. Но потом, я решил, что надо быть проще.
Дело в том, что решение сделать ракетку динамическим объектом не очень удачно. Box2D придется все время отслеживать столкновение динамических объектов, а задача эта настолько сложная, что даже Box2D не очень хорошо с ней справляется. Установка SetBullet помогает, но возможны случаи когда шарик будет пролетать сквозь ракетку, что, естественно, совершенно недопустимо, в нашем случае. Поэтому, ракетка будет статическим объектом. Мы просто будем уничтожать ее между шагами расчета и создавать в новом месте, при необходимости. Помимо прочего, этот способ гораздо проще в реализации.
Внесем в проект необходимые изменения:
#!/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 возьмем отсюда:
#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_
#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); } } }
#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_
#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:
#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_
#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:
#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_
#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)); } } }
#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_
#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.
ссылка на оригинал статьи http://habrahabr.ru/post/166929/
Добавить комментарий