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

引用:http://www.kuqin.com/gamedev/20120728/323411.html

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日,所涉事件和数据均以当时为准。

posted @ 2012-10-17 16:47  镇水古月  阅读(507)  评论(0编辑  收藏  举报