游戏邦在:
杂志专栏:
gamerboom.com订阅到鲜果订阅到抓虾google reader订阅到有道订阅到QQ邮箱订阅到帮看

分享使用Box2D和Cocos2D制作弹球游戏的方法

发布时间:2012-07-27 18:12:44 Tags:,,,,

作者:Ray Wenderlich

Box2D是一个非常强大的物理库,它与Cocos2D游戏编程库的结合非常适合开发iPhone游戏。我们可以就此进行各种尝试,例如使用它创造一款简单的游戏。

在本篇教程中我们将逐步创造一款简单的回力弹球游戏,包括进行碰撞检测,创造带有物理效果的弹跳球,通过碰触而移动球拍以及设置失败/胜利屏幕。

如果你是第一次使用Cocos2D或Box2D,你最好先了解Cocos2D或Box2D的相关教程。

现在让我们开始创造回力弹球游戏!

一直弹跳的球

通过使用cocos2d-0.99.1 Box2d Application模版创造一个新项目,并将其命名为“Box2DBreakout”。清除模版代码,如此你便可以基于一个空白项目开始创造游戏了。

当你拥有了一个空白的项目时,你便可以添加如下内容到HelloWorldScene.h的最顶端:

#import “Box2D.h”

并添加以下成员变量到HelloWorld类中:

b2World *_world;
b2Body *_groundBody;
b2Fixture *_bottomFixture;
b2Fixture *_ballFixture;

然后添加以下内容到HelloWorldScene.mm顶端:

#define PTM_RATIO 32

这与我们之前所讨论的从像素转变成“参数”拥有相同的比率。

然后我们需要添加如下代码到初始方法中:

CGSize winSize = [CCDirector sharedDirector].winSize;

// Create a world
b2Vec2 gravity = b2Vec2(0.0f, 0.0f);
bool doSleep = true;
_world = new b2World(gravity, doSleep);

// Create edges around the entire screen
b2BodyDef groundBodyDef;
groundBodyDef.position.Set(0,0);
_groundBody = _world->CreateBody(&groundBodyDef);
b2PolygonShape groundBox;
b2FixtureDef groundBoxDef;
groundBoxDef.shape = &groundBox;
groundBox.SetAsEdge(b2Vec2(0,0), b2Vec2(winSize.width/PTM_RATIO, 0));
_bottomFixture = _groundBody->CreateFixture(&groundBoxDef);
groundBox.SetAsEdge(b2Vec2(0,0), b2Vec2(0, winSize.height/PTM_RATIO));
_groundBody->CreateFixture(&groundBoxDef);
groundBox.SetAsEdge(b2Vec2(0, winSize.height/PTM_RATIO), b2Vec2(winSize.width/PTM_RATIO,
winSize.height/PTM_RATIO));
_groundBody->CreateFixture(&groundBoxDef);
groundBox.SetAsEdge(b2Vec2(winSize.width/PTM_RATIO, winSize.height/PTM_RATIO),
b2Vec2(winSize.width/PTM_RATIO, 0));
_groundBody->CreateFixture(&groundBoxDef);

这与我们之前关于创造屏幕周围边界框的Box2D教程中的代码相同。但是这一次我们将重力设为0——因为在回力弹球游戏中并不存在任何重力。同时为了今后作参考(我们可能需要以此追踪球是何时撞击屏幕底端)我们将指示器安置在最底端的固定装置中。

现在我们需要下载我之前所创造的一份弹球图像,然后将其拖到项目的资源文件夹中,确保选中“将项目复制到目标群组文件夹中(如果需要的话)”。

让我们将球的精灵添加到场景中。在你所添加的代码的最后一位后添加以下内容:

// Create sprite and add it to the layer
CCSprite *ball = [CCSprite spriteWithFile:@"Ball.jpg"
rect:CGRectMake(0, 0, 52, 52)];
ball.position = ccp(100, 100);
ball.tag = 1;
[self addChild:ball];

同时需要注意的是为了更好地识别用途,我们在球上设置了一个标签。

接下来让我们创造球的形状主体:

// Create ball body
b2BodyDef ballBodyDef;
ballBodyDef.type = b2_dynamicBody;
ballBodyDef.position.Set(100/PTM_RATIO, 100/PTM_RATIO);
ballBodyDef.userData = ball;
b2Body * ballBody = _world->CreateBody(&ballBodyDef);

// Create circle shape
b2CircleShape circle;
circle.m_radius = 26.0/PTM_RATIO;

// Create shape definition and add to body
b2FixtureDef ballShapeDef;
ballShapeDef.shape = &circle;
ballShapeDef.density = 1.0f;
ballShapeDef.friction = 0.f;
ballShapeDef.restitution = 1.0f;
_ballFixture = ballBody->CreateFixture(&ballShapeDef);

这与我们之前的教程非常类似。而作为一次补习课程,并且为了创造球体我们就需要创造一个球体定义,然后创造一个球体项目,一个形状,以及固定装置定义和最后的固定装置对象。

需要注意的是我们这次所设置的参数略有不同:我们将恢复值设为1.0,也就意味着当球与对象发现碰撞时,这种碰撞是具有弹性的。说得明白点也就是球将以同等的力度进行反弹。

为了今后作参考我们保存了球体固定装置。

更新:我们必须将球设置为没有摩擦。多亏了Steve Oldmeadow指出了这点的重要性我们才能让球顺利地从墙上反弹回来,而不会在上下左右弹跳时时屡次受困。

现在我们便完成了某些设置,所以让我们将以下代码添加到上述内容之后:

b2Vec2 force = b2Vec2(10, 10);
ballBody->ApplyLinearImpulse(force, ballBodyDef.position);

这就像是对球的推动(我们可以将其想象为来自喷气背包推进器的推动力)让它开始朝着一个特定的方向移动(在这里也就是斜向右)。如此我们便可以触动球的首次移动了!

面向初始方法我们还需要做的最后一件事便是:添加时序安排标记:

[self schedule:@selector(tick:)];

然后开始标记方法本身!

- (void)tick:(ccTime) dt {
_world->Step(dt, 10, 10);
for(b2Body *b = _world->GetBodyList(); b; b=b->GetNext()) {
if (b->GetUserData() != NULL) {
CCSprite *sprite = (CCSprite *)b->GetUserData();
sprite.position = ccp(b->GetPosition().x * PTM_RATIO,
b->GetPosition().y * PTM_RATIO);
sprite.rotation = -1 * CC_RADIANS_TO_DEGREES(b->GetAngle());
}
}

}

因为这些步骤与我们之前的教程类似,所以我们对此也并不陌生。

最后我们可以开始尝试清除方法!

- (void)dealloc {

delete _world;
_groundBody = NULL;
[super dealloc];

}

让我们试试看!当你编译并运行项目时,你将能够看到一颗球不断地在屏幕上弹跳了!

BallBounce(from raywenderlich)

BallBounce(from raywenderlich)

添加球拍

没有球拍的游戏便不是回力弹球游戏了。让我们先下载我所制作的球拍图像,并将其拖到项目的资源文件夹中,确保选中“将项目复制到目标群组文件夹中(如果需要的话)”。

然后在HelloWorldScene.h中将以下成员变量添加到HelloWorld:

b2Body *_paddleBody;
b2Fixture *_paddleFixture;

在初始方法中构建球拍体:

// Create paddle and add it to the layer
CCSprite *paddle = [CCSprite spriteWithFile:@"Paddle.jpg"];
paddle.position = ccp(winSize.width/2, 50);
[self addChild:paddle];

// Create paddle body
b2BodyDef paddleBodyDef;
paddleBodyDef.type = b2_dynamicBody;
paddleBodyDef.position.Set(winSize.width/2/PTM_RATIO, 50/PTM_RATIO);
paddleBodyDef.userData = paddle;
_paddleBody = _world->CreateBody(&paddleBodyDef);

// Create paddle shape
b2PolygonShape paddleShape;
paddleShape.SetAsBox(paddle.contentSize.width/PTM_RATIO/2,
paddle.contentSize.height/PTM_RATIO/2);

// Create shape definition and add to body
b2FixtureDef paddleShapeDef;
paddleShapeDef.shape = &paddleShape;
paddleShapeDef.density = 10.0f;
paddleShapeDef.friction = 0.4f;
paddleShapeDef.restitution = 0.1f;
_paddleFixture = _paddleBody->CreateFixture(&paddleShapeDef);

我并不想对此多作解释,因为你们肯定都赞成这种创建球拍体的方法。但是我们必须清楚本次教程的不同之处:

*当我们创造了一个CCSprite时,我们并不需要明确精灵的大小(如果我们并不想这么做的话)。如果你为它设置了文件名,它便能够自动进行大小设置了。

*比起使用圆形,我们这次选择了多边形。我们通过使用一个辅助方法创造了一个盒子般的形状。

*需要注意的是有一种可替代的SetAsBox方法能够让我们明确球拍体形状的具体位置,这对于我们以后构建更加复杂的形状将非常有帮助。而现在我们并不需要这种方法,因为我们只是想围绕着球拍体简单地设置形状而已。

*与球体相比,我们在球拍上设置了更高的密度,并也相应调整了其它参数。

为了今后的参考我们保存了paddleBody和paddleFixture。

编译并运行,此时我们将能够看到场景中的球拍,而球将在此进行弹跳:

PaddleAdded(from raywenderlich)

PaddleAdded(from raywenderlich)

但是这些内容还不够有趣,因为我们还不能移动球拍!

移动球拍

让我们开始移动!球拍的移动需要借助碰触,所以让我们在初始方法中启动碰触:

self.isTouchEnabled = YES;

然后在HelloWorldScene.h中添加以下成员变量到你的HelloWorld类中:

b2MouseJoint *_mouseJoint;

让我们执行碰触方法!从ccTouchesBegan开始:

- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

if (_mouseJoint != NULL) return;

UITouch *myTouch = [touches anyObject];
CGPoint location = [myTouch locationInView:[myTouch view]];
location = [[CCDirector sharedDirector] convertToGL:location];
b2Vec2 locationWorld = b2Vec2(location.x/PTM_RATIO, location.y/PTM_RATIO);

if (_paddleFixture->TestPoint(locationWorld)) {
b2MouseJointDef md;
md.bodyA = _groundBody;
md.bodyB = _paddleBody;
md.target = locationWorld;
md.collideConnected = true;
md.maxForce = 1000.0f * _paddleBody->GetMass();

_mouseJoint = (b2MouseJoint *)_world->CreateJoint(&md);
_paddleBody->SetAwake(true);
}

}

这些代码中包含了许多内容,让我们一一进行分析:

首先,我们将碰触位置转变成我们的Cocos2D的坐标(onvertToGL),然后再转变成我们的Box2D的坐标(locationWorld)。

我们将在之前保存的球拍固定装置对象中使用一个方法以观察碰触点是否在固定装置中。

如果是的话我们将创造一个名为“鼠标点”的对象。在Box2D中,鼠标点常被用于制造朝着某一特殊点移动的主体——在这里便是用户轻敲的点。

当我们设置了一个鼠标点时,我们必须赋予它两个主体。第一个主体在这里并没有实际用途,但根据惯例它主要是接地体。而第二主体则是用户能够移动的主体,也就是这里的球拍。

然后我们需要指定目标的移动位置——也就是用户轻拍之处。

我们需要告知Box2D当主体A和主体B发生碰撞时,它应该重视这种碰撞。这点非常重要。早前因为没有相同的设置,当我用鼠标去移动球拍时它便不能与屏幕的边缘发生碰撞,甚至很多时候球拍还会飞出屏幕。而这一简单的方法便能够帮助我们有效地解决这种让人困惑且受挫的设置。

然后我们需要设定移动主体的最大力度。如果减少了力度,主体对于鼠标移动的反应也会变慢(也许有时候你便需要这种效果)。但是在此我们希望球拍能够对移动做出快速的反应。

最后我们需要在游戏世界中添加接合点,并为了今后的参考而保存指示器。我们也需要唤醒主体,因为如果主体一直沉睡着它便不可能对移动做出任何反应。

让我们添加ccTouchesMoved方法:

-(void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {

if (_mouseJoint == NULL) return;

UITouch *myTouch = [touches anyObject];
CGPoint location = [myTouch locationInView:[myTouch view]];
location = [[CCDirector sharedDirector] convertToGL:location];
b2Vec2 locationWorld = b2Vec2(location.x/PTM_RATIO, location.y/PTM_RATIO);

_mouseJoint->SetTarget(locationWorld);

}

这种方法的开始之处与ccTouchesBegan一样——我们明确了Box2D坐标中的碰触位置。而我们在此唯一需要做的便是根据当前的碰触位置而更新鼠标接合点的目标(如我们希望主体移向哪里)。

让我们添加ccTouchesEnded和ccTouchesCancelled:

-(void)ccTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {

if (_mouseJoint) {
_world->DestroyJoint(_mouseJoint);
_mouseJoint = NULL;
}

}

- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if (_mouseJoint) {
_world->DestroyJoint(_mouseJoint);
_mouseJoint = NULL;
}
}

在这些方法中我们所做的都将破坏鼠标接合点,因为当碰触结束后,我们便完成了对象的移动。

编译并运行,这时你便能够在屏幕中的任何地方移动球拍并反弹球了!

PaddleMoving(from raywenderlich)

PaddleMoving(from raywenderlich)

但是这还不算一款回力弹球游戏,我们不应该随心所欲地移动球拍,我们只能来回地移动它。

约束球拍的移动

我们可以通过添加一个名为“prismatic”的接合点轻松地约束球拍的移动。这让我们能够在特定轴上一个个地约束主体的移动。

所以我们便可以以此约束球拍在地面上只能沿着x轴进行移动。

让我们在初始方法中添加如下代码:

// Restrict paddle along the x axis
b2PrismaticJointDef jointDef;
b2Vec2 worldAxis(1.0f, 0.0f);
jointDef.collideConnected = true;
jointDef.Initialize(_paddleBody, _groundBody,
_paddleBody->GetWorldCenter(), worldAxis);
_world->CreateJoint(&jointDef);

首先我们需要做的便是明确沿着x轴延伸的轴为矢量,而沿着y轴的则不是。然后我们需要明确非常重要的collideConnected值,从而让球拍能够准确地弹回屏幕边缘而不是飞出屏幕。

然后我们需要初始化球拍和地面主体的接合点并创造接合点!

PaddleRestricted(from raywenderlich)

PaddleRestricted(from raywenderlich)

编译并运行,现在我们便只能来回移动球拍了:

完成碰触

当你围绕着这些场景不断尝试时,你应该注意到球的弹跳速度会根据球撞击球拍的方法,时而变得非常快时而又极端慢。

更新:当我第一次想要去解决这一问题时,我通过调用SetLinearVelocity而直接调整了球的速度。但是就像Steve Oldmeadow所说的,这种方法并不可行,因为它将搞乱碰撞模拟,而如果能够提高线性阻尼而间接影响球的速度或许会更加有效。所以我们便在此采取了这种方法。

在获得用户数据后将以下代码添加到标记方法中:

if (sprite.tag == 1) {
static int maxSpeed = 10;

b2Vec2 velocity = b->GetLinearVelocity();
float32 speed = velocity.Length();

if (speed > maxSpeed) {
b->SetLinearDamping(0.5);
} else if (speed < maxSpeed) {
b->SetLinearDamping(0.0);
}

}

在此我将核查精灵标签以明确它是否是球对象的标签。如果它是的话我将检查球的速度,以观察它是否过快,然后通过提高线性阻尼而为其降速。

编译并运行,你将能看到当球的速度提高时它将会再次回到正常速度上。

接下来让我们添加一些游戏逻辑,即当玩家所控制的球撞击在屏幕底端时就判定他为失败。

Box2D和碰撞

为了知道在Box2D中什么时候一个固定装置会撞上另一个固定装置,我们便需要一个接触监听器。接触监听器是面向Box2D的一个C++对象,它将通过在对象中调用方法而告知我们两个设备何时开始碰触及何时停止碰触。

接触监听器的原理是基于Box2D用户指南,我们并不能在回调中改变任何游戏物理性质。因为这是我们想要做的(游戏邦注:如在两个对象碰撞时摧毁一个对象),所以我们便会继续参考碰撞并在之后解决它们。

另外一个棘手的问题便是我们不能在接触点(发送到监听器上了)上保存参考,因为Box2D重复使用了这些参考。所以我们便只能保存它们的副本。

接下来让我们亲自尝试看看!

当球撞击了屏幕最底端

需要注意的是我们将在这一部分使用C++语言和标准模版库(STL)。如果你不熟悉C++或STL也没关系,因为你可以复制粘帖这些代码,这些都是通用的并且也能够有效地作用于你的项目中。

点击你的类文件夹并添加一个新的文件(文件/新文件),点击左边的“Cocoa Touch Class”,并选择“Objective-C class”,检查是否选中了“Subclass of NSObject”,并点击下一步。为对象MyContactListener命名,并点击完成。

右击MyContactListener.m并将文件重新命名为MyContactListener.mm。因为我们在文件中创造了一个C++类,这里的惯例是你在文件中使用C++时你必须确保文件的扩展名为mm。

然后用以下文件替代MyContactListener.h的内容:

#import “Box2D.h”
#import <vector>
#import <algorithm>

struct MyContact {
b2Fixture *fixtureA;
b2Fixture *fixtureB;
bool operator==(const MyContact& other) const
{
return (fixtureA == other.fixtureA) && (fixtureB == other.fixtureB);
}
};

class MyContactListener : public b2ContactListener {

public:
std::vector<MyContact>_contacts;

MyContactListener();
~MyContactListener();

virtual void BeginContact(b2Contact* contact);
virtual void EndContact(b2Contact* contact);
virtual void PreSolve(b2Contact* contact, const b2Manifold* oldManifold);
virtual void PostSolve(b2Contact* contact, const b2ContactImpulse* impulse);

};

在此我们定义了结构,并且我们将使用这一结构去追踪来自接触通知的相关数据。因为接触点被重复使用了,所以我们便只能保存副本。同时我们需要在此明确一个等量运算符,因为我们将使用find()方法在矢量中寻找匹配对象。

之后我们将公开接触监听器类(来自b2ContactListener)。我们只会声明我们需要实施的方法,就像我们将使用STL矢量去缓冲我们的接触点。

现在用以下代码取代MyContactListener.mm的内容:

#import “MyContactListener.h”

MyContactListener::MyContactListener() : _contacts() {
}

MyContactListener::~MyContactListener() {
}

void MyContactListener::BeginContact(b2Contact* contact) {
// We need to copy out the data because the b2Contact passed in
// is reused.
MyContact myContact = { contact->GetFixtureA(), contact->GetFixtureB() };
_contacts.push_back(myContact);
}

void MyContactListener::EndContact(b2Contact* contact) {
MyContact myContact = { contact->GetFixtureA(), contact->GetFixtureB() };
std::vector<MyContact>::iterator pos;
pos = std::find(_contacts.begin(), _contacts.end(), myContact);
if (pos != _contacts.end()) {
_contacts.erase(pos);
}
}

void MyContactListener::PreSolve(b2Contact* contact,
const b2Manifold* oldManifold) {
}

void MyContactListener::PostSolve(b2Contact* contact,
const b2ContactImpulse* impulse) {
}

我们在构造函数中初始化了我们的矢量。而实际上我们只执行了两个方法:BeginContact和EndContact。在BeginContact中我们复制了发生碰撞的固定装置,并将它们保存在我们的矢量中。而在EndContact,我们观察了接触点是否在我们的矢量中,如果在的话便删除它。

现在让我们实践这一原理。即切换到HelloWorldScene.h并进行如下修改:

// Add to top of file
#import “MyContactListener.h”

// Add inside @interface
MyContactListener *_contactListener;

然后在初始方法中添加如下代码:

// Create contact listener
_contactListener = new MyContactListener();
_world->SetContactListener(_contactListener);

在这里我们创造了接触监听器对象,并在世界对象中调用了一个方法以设置接触监听器。

让我们还未忘记前将清除代码添加到释放内存中:

delete _contactListener;

最后在标记方法的最底端添加以下代码:

std::vector<MyContact>::iterator pos;
for(pos = _contactListener->_contacts.begin();
pos != _contactListener->_contacts.end(); ++pos) {
MyContact contact = *pos;

if ((contact.fixtureA == _bottomFixture && contact.fixtureB == _ballFixture) ||
(contact.fixtureA == _ballFixture && contact.fixtureB == _bottomFixture)) {
NSLog(@”Ball hit bottom!”);
}
}

这一代码将在所有缓冲接触点上进行循环迭代,以检查是否有哪些接触点连接在球和屏幕底端之间。而现在我们只能够通过发送NSLog信息进行传达,以此检查它是否能够正常运行。

在调式模式中编译并运行,并通过点击运行\主机切换到主机上,现在当球碰触到底端时你便能够在你的记录中看到“球撞击底端”的信息了。

添加游戏结束场景

添加我们在之前的教程中所创造的GameOverScene.h和GameOverScene.mm文件。我们需要将GameOverScene.m重命名为GameOverScene.mm,因为我们现在处理的是C++代码,或者你将遇到编译错误的问题。

然后在HelloWorldScene.mm文件最顶端添加输入代码:

#import “GameOverScene.h”

用以下代码替换NSLog陈述:

GameOverScene *gameOverScene = [GameOverScene node];
[gameOverScene.layer.label setString:@"You Lose :["];
[[CCDirector sharedDirector] replaceScene:gameOverScene];

现在我们便完成设置了!但是游戏的乐趣在哪里呢?

添加一些砖块

下载我所创造的砖块图像并将其拖到项目资源文件夹中,确保选中“将项目复制到目标群组文件夹中(如果需要的话)”。

然后添加以下代码到初始方法上:

for(int i = 0; i < 4; i++) {

static int padding=20;

// Create block and add it to the layer
CCSprite *block = [CCSprite spriteWithFile:@"Block.jpg"];
int xOffset = padding+block.contentSize.width/2+
((block.contentSize.width+padding)*i);
block.position = ccp(xOffset, 250);
block.tag = 2;
[self addChild:block];

// Create block body
b2BodyDef blockBodyDef;
blockBodyDef.type = b2_dynamicBody;
blockBodyDef.position.Set(xOffset/PTM_RATIO, 250/PTM_RATIO);
blockBodyDef.userData = block;
b2Body *blockBody = _world->CreateBody(&blockBodyDef);

// Create block shape
b2PolygonShape blockShape;
blockShape.SetAsBox(block.contentSize.width/PTM_RATIO/2,
block.contentSize.height/PTM_RATIO/2);

// Create shape definition and add to body
b2FixtureDef blockShapeDef;
blockShapeDef.shape = &blockShape;
blockShapeDef.density = 10.0;
blockShapeDef.friction = 0.0;
blockShapeDef.restitution = 0.1f;
blockBody->CreateFixture(&blockShapeDef);

}

你应该非常清楚这一代码吧。我们在创造球拍体时便采用了相同的方法,只是这一次我们是在循环中进行,从而让我们能够更轻松地沿着顶部创造四个砖块。同时还需要注意的是我们将砖块精灵的标签设置为2,以备今后作参考。

编译并运行代码,现在在球的周围便围绕着一些砖块了。

摧毁砖块

为了创造出一款真正的回力弹球游戏,我们需要在球碰触到砖块时摧毁砖块。我们已经添加了代码去追踪碰撞,所以我们现在需要做的便是修改标记方法。

将我们添加到标记方法中的代码修改成:

std::vector<b2Body *>toDestroy;
std::vector<MyContact>::iterator pos;
for(pos = _contactListener->_contacts.begin();
pos != _contactListener->_contacts.end(); ++pos) {
MyContact contact = *pos;

if ((contact.fixtureA == _bottomFixture && contact.fixtureB == _ballFixture) ||
(contact.fixtureA == _ballFixture && contact.fixtureB == _bottomFixture)) {
GameOverScene *gameOverScene = [GameOverScene node];
[gameOverScene.layer.label setString:@"You Lose :["];
[[CCDirector sharedDirector] replaceScene:gameOverScene];
}

b2Body *bodyA = contact.fixtureA->GetBody();
b2Body *bodyB = contact.fixtureB->GetBody();
if (bodyA->GetUserData() != NULL && bodyB->GetUserData() != NULL) {
CCSprite *spriteA = (CCSprite *) bodyA->GetUserData();
CCSprite *spriteB = (CCSprite *) bodyB->GetUserData();

// Sprite A = ball, Sprite B = Block
if (spriteA.tag == 1 && spriteB.tag == 2) {
if (std::find(toDestroy.begin(), toDestroy.end(), bodyB)
== toDestroy.end()) {
toDestroy.push_back(bodyB);
}
}
// Sprite B = block, Sprite A = ball
else if (spriteA.tag == 2 && spriteB.tag == 1) {
if (std::find(toDestroy.begin(), toDestroy.end(), bodyA)
== toDestroy.end()) {
toDestroy.push_back(bodyA);
}
}
}
}

std::vector<b2Body *>::iterator pos2;
for(pos2 = toDestroy.begin(); pos2 != toDestroy.end(); ++pos2) {
b2Body *body = *pos2;
if (body->GetUserData() != NULL) {
CCSprite *sprite = (CCSprite *) body->GetUserData();
[self removeChild:sprite cleanup:YES];
}
_world->DestroyBody(body);
}

让我们对这些代码做出解释。我们将再次仔细检查接触点,但是这一次当我们在检查球和屏幕底端的碰撞后,我们需要明确碰撞的主体。我们可以通过在设备中调用GetBody()方法而明确碰撞主体。

当我们确认了主体,我们便需要检查它们是否拥有用户数据。如果有的话,我们便需要将它们投射到精灵中去,因为我们也是如此设置用户数据的。

随后我们需要观察哪些精灵基于自己的标签而发生了碰撞。如果精灵与砖块进行了碰撞,我们将把砖块添加到破坏对象列表中。

需要注意的是我们是将砖块添加到破坏对象列表中而不是直接破坏该主体。因为如果我们立刻摧毁了主体,游戏世界将清除大量指示器,而在我们的接触监听器中留下一大堆垃圾数据。同时如果砖块已经不存在了我们也只会将其添加到列表中!

最后我们需要浏览我们想要删除的主体列表。我们不只需要破坏来自Box2D世界的主体,同时也需要从我们的Cocos2D场景中删除精灵对象。

编译并运行,这时我们便能够摧毁砖块了!

赢得游戏

接下来我们需要添加一些逻辑让用户能够真正赢得游戏。如下修改标记方法的开头:

- (void)tick:(ccTime) dt {

bool blockFound = false;
_world->Step(dt, 10, 10);
for(b2Body *b = _world->GetBodyList(); b; b=b->GetNext()) {
if (b->GetUserData() != NULL) {
CCSprite *sprite = (CCSprite *)b->GetUserData();
if (sprite.tag == 2) {
blockFound = true;
}
//…

现在我们需要做的便是观察当我们在场景中迭代对象时我们是否会碰到砖块——如果碰到了砖块我们便需要将blockFound变量设置为“true”,否则将为“false”。

然后在功能的最后添加以下代码:

if (!blockFound) {
GameOverScene *gameOverScene = [GameOverScene node];
[gameOverScene.layer.label setString:@"You Win!"];
[[CCDirector sharedDirector] replaceScene:gameOverScene];
}

在此我们将只呈现出未发现任何砖块的游戏结束场景。编译并运行,现在我们便能够赢得游戏了!

YouWin(from raywenderlich)

YouWin(from raywenderlich)

完成碰触

现在的游戏已经很棒了,但是我们还需要添加一些音效。我们可以下载我之前所制作的背景音乐以及信号声。并在下载后将它们拖到项目的资源文件夹中。

顺便一提的是我是使用cfxr这一程序去制作音效。

总之当你在项目中添加了文件后,你便需要将如下内容添加到HelloWorldScene.mm顶端:

#import “SimpleAudioEngine.h”

并在初始方法中添加如下代码:

[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@”background-music-aac.caf”];

最后在标记方法的末尾添加以下内容:

if (toDestroy.size() > 0) {
[[SimpleAudioEngine sharedEngine] playEffect:@”blip.caf”];
}

现在你便完成了一款基于Box2D物理性质的简单回力弹球游戏了!

游戏邦注:原文发表于2010年2月25日,所涉事件和数据均以当时为准。(本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦

How To Create A Breakout Game with Box2D and Cocos2D Tutorial: Part 1

23 February 2010

Box2D is a powerful physics library that comes with the Cocos2D game programming library for the iPhone. There’s a lot you can do with it, and a great way to start learning about how it works is to create a simple game with it!

In this tutorial, we are going to create a simple Breakout game step by step, complete with collision detection, a ball bouncing with physics effects, dragging the paddle via touches, and win/lose screens. (Jump to part two of the series.)

If you are new to Cocos2D or Box2D, it may help to go through the intro to Cocos2D tutorial and/or intro to Box2D tutorial before proceeding with this tutorial.

Allright, time for some Breakout!

An Ever-Bouncing Ball

Start by creating a new project with the cocos2d-0.99.1 Box2d Application template, and name your project “Box2DBreakout”. Clear out the template code and so you have an empty project to start with – see the intro to Box2D tutorial for instructions on how to do that.

Once you have a nice clean project, add the following import to the top of HelloWorldScene.h:

And add the following member variables to the HelloWorld class:

Then add the following to the top of HelloWorldScene.mm:

This is the same ratio to convert from pixels to “meters” that we discussed in our previous Box2D tutorial.

Then add the following code to your init method:

Again, this is the same code that we had in our previous Box2D tutorial to create a bounding box around the screen. However, this time we set the gravity to zero, because in our breakout

game there will not be any gravity! Also note we store a pointer to the bottom fixture for future reference (we’ll need it to keep track of when the ball hits the bottom of the screen).

Now download a copy of the image of a bouncy ball I created and drag it into the Resources folder of your project, making sure “Copy items into destination group’s folder (if needed)” is checked.

Let’s add a sprite for the ball into the scene. Add the following right after the last bit of code you added:

There should be no surprises here, we’ve been doing this for a while now. Note that we set a tag on the ball for identification purposes (you’ll see why later in the tutorial).

Next let’s create a body for the shape:

This should look familiar as well from our last tutorial. As a refresher, to create a body we need to create a body definition, then a body object, then a shape, then a fixture definition, and finally a fixture object.

Note that we set the parameters a bit differently this time: we’ve set the restitution to 1.0, meaning the when the ball collides with an object the collision will be perfectly elastic. In plain English, this means that the ball will bounce back with equal force to the impact.

Also note that we store the ball fixture for future reference (same reason as why we stored the bottom fixture).

Update: Also note that the ball is set to have no friction. Thanks to Steve Oldmeadow for pointing out that this is important in this case so that the ball bounces nicely off the walls, preventing the ball from frequently getting stuck bouncing back and forth in a straight up-down or left-right angle.

Ok, now for something completely different. Add the following after the above:

This applies an impulse (you can think of it like a propulsion from a jet pack thruster) to the ball to get it to start moving in a particular direction (in this case, diagonally up to the right). We need this to get the ball moving in the first place!

One last thing for the init method: add the tick scheduling:

And then the tick method itself!

Again no surprises with these two since we did the same thing in the last tutorial.

One last thing and then we’re ready to try it out: the cleanup method!

Ok, let’s try it out! When you compile and run the project, you should see a ball continuously bouncing around the screen – cool!

Adding the Paddle

It wouldn’t be a breakout game if we didn’t have a paddle. Download a copy of a graphic of a paddle I made and drag it to the Resources folder of your project, making sure “Copy items into destination group’s folder (if needed)” is checked.

Then add the following member variable to HelloWorld in HelloWorldScene.h:

And then construct the paddle body in your init method:

I’m not going to explain this much because you should be a pro at creating bodies by this point. However, note a few differences this time:

When you create a CCSprite, you don’t need to specify the size of the sprite if you don’t want to. If you give it the filename, it can automatically determine the size.

Note that instead of using a circle shape, we use a polygon shape this time. We use a helper method to create the shape in the form of a box.

Note that there is an alternate SetAsBox method that allows you to specify the position of the shape relative to the body, which comes in handy when constructing complex shapes. However we don’t need to use that here, since we just want the shape centered on the body.

We make the paddle more dense than the ball, and tweak the other parameters as well.

We are storing paddleBody and paddleFixture for future reference.

If you compile and run this you’ll see our paddle in the scene, and the ball will bounce off it:

However this isn’t much fun, because we can’t move paddle yet!

Moving The Paddle

So let’s get moving! Moving the paddle is going to require touches, so enable touches in your init method:

Then add the following member variable to your HelloWorld class in HelloWorldScene.h:

Now let’s implement the touch methods! Let’s start with ccTouchesBegan:

Wow, a lot of new stuff in here. Let’s discuss it bit by bit.

First, we convert the touch location to our Cocos2D coordinates (convertToGL) and then to our Box2D coordinates (locationWorld).

Then we use a method on our paddle fixture object that we’ve stored away to see if the touch point is within the fixture.

If it is, we create something called a “mouse joint.” In Box2D, a mouse joint is used to make a body move toward a specified point – in this case where the user is tapping.

When you set up a mouse joint, you have to give it two bodies. The first isn’t actually used, but the convention is to use the ground body. The second is the body you want to move – in our case the paddle.

Then you specify where you want the target to move – in our case where the user is tapping.

Then you tell Box2D that when bodyA and bodyB collide, treat it as a collision, rather than ignoring it. This is very important. When I was trying to get this working, I didn’t have this set, so when I was moving the paddle with my mouse it wouldn’t collide with the edges of the screen, and my paddle would fly off screen sometimes! This was very confusing and frustrating until I discovered this simple way to fix it :]

You then specify the max force with which to move the body. If you reduce this amount, the body will react more slowly to your mouse movements (which may be what you want sometimes!). But here we want the paddle to respond rather quickly to movements.

Finally we add the joint to the world, and store away the pointer for future reference. We also set the body to awake. We need to do this because if the body is asleep and we don’t awaken it, it won’t respond to the movements!

Ok, next let’s add the ccTouchesMoved method:

The beginning of this method is the same as ccTouchesBegan – we get the location of the touch in Box2D coordinates. The only thing we do here is update the target of the mouse joint (i.e. where we want the body to move) to be the current location of the touch.

Let’s wrap up by adding ccTouchesEnded and ccTouchesCancelled:

All we do in these methods is destroy the mouse joint because when the touches end, we’re done moving the object.

Give it a compile and run, and you should be able to move the paddle all around the screen to bounce the ball!

Pretty cool… but wait a minute, this isn’t breakout, we shouldn’t be able to move the paddle anywhere we want, we should just be able to move it back and forth!

Restricting Movement of the Paddle

We can easily restrict movement of the paddle by adding another joint into the world called a prismatic joint. This lets us restrict the movement of one body to another along a specified axis.

So we can use this to restrict the movement of the paddle relative to the ground to only be able to move along the x-axis.

Let’s give this a shot in code. Add this to your init method:

The first thing we do is specify the axis to be a vector along the x axis, but not at all along the y axis. We then specify the ever important collideConnected value so our paddle will correctly bounce against the edge of the screen rather than flying into never-never land.

We then initialize the joint specifying the paddle and the ground body and create the joint!

Give it a compile and run and now you should only be able to move the paddle back and forth instead of anywhere you want:

Finishing Touches

Now, as you’ve been playing around with this so far you may have noticed that sometimes the ball can get super-fast or super slow, depending on how you hit it with the paddle.

Update: The first time I tried to fix this, I tried to adjust the velocity of the ball directly by calling SetLinearVelocity. However, as Steve Oldmeadow also pointed out (thanks Steve!), this is a bad idea as it messes up the collision simulations, and it’s better to indirectly affect the velocity by increasing the linear damping. So that’s what we’ll do!

Add the following code to the tick method, after getting the user data:

Here I check the tag of the sprite to see if it’s the tag for the ball object. If it is, I check the velocity and if it’s too too large, I increase the linear damping so it will eventually slow down.

If you compile and run you should see the ball goes back to a normal rate when the speed increases too much.

Gimme The Code!

Here’s the full code for the Cocos2D and Box2D Breakout Game that we’ve developed up to this point. More is coming in the next portion of the series!

What’s Next?

So far, we have a ball that bounces around the screen and a paddle we can move around via touch. In the next tutorial in the series, we pick it up from here and add some bricks that get destroyed when the ball collides into them, and some win/lose logic!

How To Create A Breakout Game with Box2D and Cocos2D Tutorial: Part 2

This is the second and final part of a tutorial on how to create a simple breakout game using the Box2D physics library that comes with Cocos2D. If you haven’t already, make sure you go through part 1 first!

We left off with a box that bounces around the screen and a paddle we could move with our fingers. Let’s start adding in some game logic by making the player lose if the ball hits the bottom of the screen!

Box2D and Collisions

To find out when a fixture collides with another fixture in Box2D, we need to register a contact listener. A contact listener is a C++ object that we give Box2D, and it will call methods on that object to let us know when two objects begin to touch and stop touching.

The trick to a contact listener, however, is according to the Box2D User Manual, you cannot perform any operation that alters game physics within the callback. Since this is something we will probably want to do (such as destroy an object when two objects collide), instead we will just keep references to the collisions so we can deal with them later.

Another tricky bit is we can’t just store references to the contact points that are sent to the listener, because they are reused by Box2D. So we have to store copies of them instead.

Ok enough talk, let’s try this out for ourselves!

When We’ve Hit Rock Bottom

Note that in this section we’re going to be using some C++ and the standard template library (STL) a bit. If you are unfamiliar with C++ or the STL, don’t worry about it too much – you can just copy and paste the code, it is general purpose and should work in your projects as well.

Ok. Click on your Classes folder and add a new file (File\New File), click “Cocoa Touch Class” on the left, and choose “Objective-C class”, verifying that “Subclass of NSObject” is selected, then click Next. Name your object MyContactListener, and click finish.

Right click on MyContactListener.m and rename the file to MyContactListener.mm. This is because we are actually creating a C++ class in this file, and the convention when you are using C++ in a file is to have the file end with mm.

Then replace the contents of MyContactListener.h with the following file:

Here we define the structure that we will use to keep track of the data we’re interested in from the contact notifications. Again, we need to store a copy because the contact points passed in are reused. note we have to declare an equality operator here, because we’re going to use a the find() method to look for matching objects in the vector, which requires this method.

After that we declare our contact listener class, which derives from b2ContactListener. We just declare the methods we need to implement, as well as a STL vector that we will use to buffer

our contact points.

Now replace the contents of MyContactListener.mm with the following:

We initialize our vector in the constructor. Then the only two methods we actually implement are BeginContact and EndContact. In BeginContact we make a copy of the fixtures that just collided, and store them in our vector. In EndContact, we look to see if the contact point is in our vector and remove it if so.

Ok, now let’s put this to use. Switch over to HelloWorldScene.h and make the following modifications:

Then add the following code to your init method:

Here we create our contact listener object, and call a method on the world object to set the contact listener.

Next add the cleanup code to dealloc before we forget:

And finally add the following code to the bottom of your tick method:

This iterates through all of the buffered contact points, and checks to see if any of them are a match between the ball and the bottom of the screen. For now, we just log this out with a NSLog message because it’s time to check if it’s working!

So compile and run in debug mode, and switch over to your console by clicking Run\Console, and whenever the ball intersects the bottom you should see a message in your log that reads “Ball hit bottom!”

Adding a game over scene

Add the GameOverScene.h and GameOverScene.mm files that we developed in the how to make a simple game with Cocos2D tutorial. Note that you’ll have to rename GameOverScene.m to GameOverScene.mm since we’re dealing with C++ code now or you will get compilation errors.

Then add the import to the top of your HelloWorldScene.mm file:

Then replace the NSLog statement with the following code:

Allright, we’re getting somewhere! But what fun is a game where you can’t win?

Adding some blocks

Download a copy of a block image I made and drag it to the Resources folder of your project, making sure “Copy items into destination group’s folder (if needed)” is checked.

Then add the following code to your init method:

You should understand this code pretty well by now. We create a body just the same way we did for the paddle, except this time we do it in a loop so we can easily create four blocks along the top. Also notice that we set the tag on the block sprite to 2, for future reference.

Compile and run this code, and you should now have blocks you can mess around with your ball!

Destroying the Blocks

To be a true breakout game, we need to destroy the blocks when the ball intersects them. Well we’ve already added the code to keep track of collisions, so all we need to do is modify the tick method!

Modify the code you added in the tick method to be the following:

Ok, let’s explain this. We go through the contact points again, but this time after we check for collisions between the ball and the bottom of the screen, we take a look at the bodies that are colliding. We can get to the bodies by calling the GetBody() method on the fixtures.

Once we have the bodies, we check to see if they have user data. If they do, we cast them to sprites – because we know that’s what we’ve set the user data to.

Then we look to see what sprites are colliding based on their tags. If a sprite is intersecting with a block, we add the block to a list of objects to destroy.

Note that we add it to a list to destroy rather than destroying the body right away. This is because if we destroy the body right away, the world will clean up a lot of pointers leaving us with garbage data in our contact listener. Also note that we only should add it to the list if it isn’t there already!

Finally, we go through the list of bodies we want to delete. Note that we not only have to destroy the body from Box2D’s world, we also have to remove the sprite object from our Cocos2D scene.

Give it a compile and run, and you should now be able to destroy bricks! Yay!

Winning the Game

Next we need to add some logic in to let the user actually win the game. Modify the beginning of your tick method to read as follows:

All we’re doing here is looking to see if we ever come across a block while we’re iterating through the objects in the scene – if we do find one we set the blockFound variable to true – otherwise it is false.

Then add the following code at the end of the function:

Here we just display a game over scene if no blocks were found. Give it a compile and run, and see if you can win the game!

Finishing Touches

The game is quite cool, but we need some sound of course! You can download the awesome background music I made and a cool blip sound I made to use. As usual, drag them to your resources folder once you’ve downloaded them.

By the way – I made the sound effect with an awesome program called cfxr that one of our commenters – Indy – pointed out. Thanks Indy this program pwns!

Anyway – once you’ve added the files to your project, add the following to the top of HelloWorldScene.mm:

And the following to your init method:

And finally the following at the end of your tick method:

And there you have it – your own simple breakout game with Box2D physics!

Gimme The Code!

Here’s the full code for the Cocos2D and Box2D Breakout Game that we’ve made in this tutorial.

Where To Go From Here?

Obviously this is a quite simple implementation of breakout, but now that you have this working there’s a lot more you can do. You could extend this code to give the blocks hit points and make the ball have to hit them a number of times before they are destroyed. You could add new blocks, let the paddle shoot lasers toward the blocks, whatever you dream up!

Let me know if you have any tips or suggestions for better ways to do things, and hope this comes in handy!(source:raywenderlich part 1,part 2)


上一篇:

下一篇: