免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播。同时,转载时不要移除本申明。如产生任何纠纷,均与本博客所有人、发表该翻译稿之人无任何关系。谢谢合作!
原文链接地址:http://www.raywenderlich.com/505/how-to-create-a-simple-breakout-game-with-box2d-and-cocos2d-tutorial-part-22
程序截图:
这是《如何使用cocos2d和box2d制作一个简单的breakout游戏》的第二部分,也是最后一部分教程。如果你还没有读过第一部分,请先阅读第一个教程!
在上一个教程中,我们创建了一个屏幕盒子,球可以在里面弹跳,同时,我们可以用手指拖着paddle移动。这部分教程中,我们将添加一些游戏逻辑,当篮球碰到屏幕底部的时候,就Gameover。
Box2D 和碰撞检测
在Box2D里面,当一个fixture和另一个fixture相互碰撞的时候,我们怎么知道呢?这就需要用到碰撞侦听器了(contact listener)。一个碰撞侦听器是一个C++对象,它继承至box2d的b2ContactListner类,并且要设置给world对象。这样,当有两个对象发生相互碰撞的时候,world对象就会回调contact listener对象的方法,这样我们就可以在那些方法里面做相应的碰撞处理了。
如何使用contact listener呢?根据BOX2D用户手册,在一个仿真周期内,你不能执行任何修改游戏物理的操作。因为,在那期间,我们可能需要做一些额外的处理(比如,当两个对象碰撞的时候销毁另一个对象)。因此, 我们需要保存碰撞的引用,这样后面就可以使用它。
另外一点值得注意的是,我们不能存储传递给contact listener的碰撞点的引用,因为,这些点被BOX2D所重用。因此,我们不得不存储这些点的拷贝。
好了,说得够多了,让我们亲手实践一下吧!
当我们碰到屏幕底部的时候
注意,在这部分里面,我们将使用一些C++代码和STL(标准模板库)。如果你还不熟悉C++或者是STL,不用太担心--你只需要复制粘贴代码就OK了,不过建议还是学习一下C++和STL,因为大部分游戏都是用C++做成的。
好。点开你的Classes文件夹,点击File\New File),选择左边的“Cocoa Touch Class”,再选择“Objective-C class”,确保“Subclass of NSObject”被选中,再点Next。把它命名为MyContactListener,然后点Finish。
右键点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算法来查找vector中的一个特定的元素,而这个标准算法find必须要求其查找的元素是可比较相等的。
在我们申明完contact listener类之后,我们只需要声明一些我们感兴趣的方法来实现就可以了。这里的vector用来缓存碰撞点信息。
现在,用下面的内容替换掉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) {
}
我们在构造函数中初使化vector。然后,我们只需要实现BeginContact和EndContact方法就可以了。这两个方法,一个是碰撞开始的时候world对象会回调,另一个就是碰撞结束的时候被回调。在BeginContact方法中,我们复制了刚刚发生碰撞的fixture的一份拷贝,并把它存储在vector中。在EndContact方法中,我们检查一下碰撞点是否在我们的vector中,如果在的话,就移除它!
好了,现在可以使用它吧。打开HelloWorldScene.h,然后做下面的修改:
//
Add to top of file
#import
"
MyContactListener.h
"
//
Add inside @interface
MyContactListener
*
_contactListener;
然后在init方法中增加下列代码:
//
Create contact listener
_contactListener
=
new
MyContactListener();
_world
->
SetContactListener(_contactListener);
这里,我们创建了contact listener对象,然后调用world对象把它设置为world的contact listener。
接下来,再我们忘记之前先做一些清理内存的操作:
最后,在tick方法底部添加下列代码:
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来打印一个消息,因为我们只想测试这样是否可行。
因此,在debug模式下编译并运行,你会发现,不管什么时候,当球和底部有碰撞的时候,你会看到控制台输出一句话“Ball hit bottom"!
添加Game Over场景
增加GameOverScene.h and GameOverScene.mm files两个文件,它们可以在《如何使用cocos2d制作一个简单的iphone游戏教程》中找到。注意,你必须把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];
好了,我们已经实现得差不多了。但是,如果你游戏你永远不能赢,那有什么意思呢?
增加一些方块
下载我制作的方块图片,然后把它拖到Resources文件夹下面,同时确保“Copy items into destination group’s folder (if needed)”被复选中。
然后往init方法中添加下列代码:
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);
}
现在,你应该可以很好地理解上面的代码了。就像之前我们为paddle创建一个body类似,这里,我们每一次也会一个方块创建一个body。注意,我们把方块精灵对象的tag设置为2,这样将来可以用到。
编译并运行,你应该可以看到篮球和方块之间有碰撞了。
销毁方块
为了使breakout游戏是一个真实的游戏,当篮球和方块有交集的时候,我们需要销毁这些方块。我们已经添加了一些代码来追踪碰撞,因此,我们对tick方法做一改动。
具体改动方式如下:
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);
}
好,让我们解释一下。我们又一次遍历所有的碰撞点,但是,这一次在我们测试完篮球和屏幕底部相撞的时候,我们将检查碰撞点。我们可以通过fixture对象的GetBody方法来找对象。
接着,我们基于精灵的tag,看看到底是哪个在发生碰撞。如果一个精灵与一个body相交的话,我们就把该body添加到待销毁的对象列表里面去。
但是也需要注意,只有确定它并不存在于销毁列表中时才把它添加进去。为什么一定要用一个list把需要销毁的存储起来而不是直接销毁。因为直接销毁会导致contact listener中留下一些已被删除指针的垃圾数据。
最后,遍历我们想要删除的body列表。
编译并运行,现在你可以销毁bricks了!
加入游戏胜利条件
接下来,我们需要添加一些逻辑,让用户能够取得游戏胜利。修改你的tick方法的开头部分,像下面一样:
-
(
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];
}
这里,如果方块都消失了,我们就会显示一个游戏结束的场景。编译并运行,看看,你的游戏现在有胜利终止条件了!
完成touch事件
这个游戏非常酷,但是,毫无疑问,我们需要音乐!你可以下载我制作的一些背景音乐,还有好听的blip声音。和之前一样,把它拖到你的resources文件夹下。
顺便提一下,我制作这些声音效果使用一个非常不错的程序,叫做cfxr.不管怎么说,你旦你加入进去之后,把HeloWorldScene.mm中加入下面的代码:
#import
"
SimpleAudioEngine.h
"
接着,在init方法中加入下列代码:
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:
@"
background-music-aac.caf
"
];
最后,在tick方法的末尾添加下面的代码:
if
(toDestroy.size()
>
0
) {
[[SimpleAudioEngine sharedEngine] playEffect:
@"
blip.caf
"
];
}
恩,终于完成了!你现在拥有一个使用Box2d物理引擎制作的breakout游戏了1
And there you have it – your own simple breakout game with Box2D physics!
给我代码!
这里是本系列教程的完整源代码。
何去何从?
很明显,这是一个非常简单的beakout游戏,但是,你还可以在此教程的基础上实现更多。我可以添加一些逻辑,比如打击一个白色块就计一分,或者有些块需要打中很多下才消失。或者你也可以添加新的不同类型的block,并且让paddle可以发射出激光等等。你可以充分发挥想象。
如果大家有什么问题可以提出来,大家一起讨论解决!如果有什么新的发现的,也欢迎贴出代码来,大家一起学习下!
PS:译者水平有限,如果有翻译的不好,或者翻译错误的,希望大家在看的时候给我指出来。谢谢!
著作权声明:本文由http://www.cnblogs.com/andyque翻译,欢迎转载分享。请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢!