如何使用cocos2d和box2d来制作一个Breakout游戏:PART-2

泰然作为官方翻译团队的成员,本文章作已经为官方译本更新到Ray的网站,转载请注明出处。大家也可以到Ray的网站查看本教程的中文版本。

 

[caption id="attachment_483" align="alignright" width="250" caption="Simple Breakout Game Screenshot"]Simple Breakout Game Screenshot[/caption]

这是《如何使用cocos2d和box2d制作一个简单的breakout游戏》的第二部分,也是最后一部分教程。如果你还没有读过第一部分,请先阅读第一部分教程

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!
在上一篇教程中,我们创建了一个方框,球可以在里面弹跳,以及一个paddle,可以随着我们的手指移动。我们首先添加一些游戏逻辑,使得如果篮球碰到屏幕底部的时候,就会判断玩家失败。

Box2D 和碰撞检测

为了知道一个fixture和另一个fixture相互碰撞,我们需要注册一个碰撞侦听器(contact listener)。一个碰撞侦听器是一个C++对象,它继承至box2d的b2ContactListener。当两个物体开始接触和停止接触时,它会调用b2ContactListener的方法来让我们知道。

碰撞侦听器的诀窍是什么? 不管怎么样, 根据 Box2D用户手册, 在回调中你不能进行任何更改游戏物理世界的操作。既然我们可能想要做些事情(比如,当两个对象碰撞的时候销毁另一个对象),所以我们仅仅是保留碰撞的引用,然后待会处理它们。

另外一个棘手的问题是,我们不能存储传递给contact listener的碰撞点的引用,因为,这些点被Box2D重用。因此,我们不得不存储这些点的拷贝。

好了,说得够多了,让我们亲手实践一下吧!

当我们碰到屏幕底部

注意,在这部分,我们将使用一些C++代码和STL(标准模板库)。不用太担心--你只需要复制粘贴代码就OK了,这是通用的,在你的项目中也能运行。

好了,点开你的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 
#import 

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

这里,我们定义了一个数据结构,当碰撞通知到达的时候,用来保存我们感兴趣的数据。再说一遍,我们需要存储其拷贝,因为碰撞点会被重用。注意,这里一定要声明判断相等的变量operator,因为,我们将使用find()方法来查找向量中的一个特定的元素,它需要operator来判断相等。

在声明完继承自b2ContactListener的contact listener类之后,我们只需要声明一些需要实现的方法就可以了。这里的STL 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::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方法就可以了。在BeginContact方法中,我们复制了刚刚发生碰撞的fixtures的一份拷贝,并把它存储在vector中。在EndContact方法中,我们检查一下碰撞点是否在我们的vector中,如果在的话,就移除它!

好了,现在可以使用它吧。切换到HelloWorldScene.h,然后做下面的修改:

// 添加到文件顶部
#import "MyContactListener.h"

// 添加到@interface中
MyContactListener *_contactListener;

然后在init方法中增加下列代码:

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

这里,我们创建了contact listener对象,然后调用world方法把它设置为world的contact listener。

接下来,再我们忘记之前先做一些清理内存的操作:

delete _contactListener;

最后,在tick方法底部添加下列代码:

std::vector::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 和 GameOverScene.mm files 两个文件,我们是在 how to make a simple game with Cocos2D tutorial中开发的这2个文件. 注意,你必须把GameOverScene.m改成GameOverScene.mm,因为我们要在里面使用一些C++,如果你不改的话,那么编译就会报错。

然后在HelloWorldScene.mm文件顶部加入下面import代码:

#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;
    
    // 创建方块并添加到层中
    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];
    
    // 创建方块body
    b2BodyDef blockBodyDef;
    blockBodyDef.type = b2_dynamicBody;
    blockBodyDef.position.Set(xOffset/PTM_RATIO, 250/PTM_RATIO);
    blockBodyDef.userData = block;
    b2Body *blockBody = _world->CreateBody(&blockBodyDef);
    
    // 创建方块形状
    b2PolygonShape blockShape;
    blockShape.SetAsBox(block.contentSize.width/PTM_RATIO/2,
                        block.contentSize.height/PTM_RATIO/2);
    
    // 创建形状定义并加入到body中
    b2FixtureDef blockShapeDef;
    blockShapeDef.shape = &blockShape;
    blockShapeDef.density = 10.0;
    blockShapeDef.friction = 0.0;
    blockShapeDef.restitution = 0.1f;
    blockBody->CreateFixture(&blockShapeDef);
    
}

现在,你应该可以很好地理解上面的代码了。就像之前我们为paddle所做的那样创建一个body,除了这里使用了一个循环,使我们能够很容易的沿着顶部创建4个方块。注意,我们把方块精灵对象的tag设置为2,作为将来参考使用。

编译并运行代码,你现在应该有方块来跟篮球玩了!

销毁方块

为了使breakout游戏是一个真实的游戏,当篮球和方块有交集的时候,需要销毁这些方块。我们已经添加了一些代码来追踪碰撞,因此,所以要做的仅仅是修改tick方法

按照下面修改tick方法中你增加的代码:

std::vectortoDestroy;
std::vector::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::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()方法来得到body。

一旦得到bodies,检查它们是否有user data.如果有,把user data转为精灵 - 因为我们知道user data是我们设置的精灵。

接着,基于精灵的tag,可以知道什么精灵在发生碰撞。如果一个精灵与一个方块相交的话,我们就把该方块添加到待销毁的对象列表里面去。

注意我们是把它加到列表销毁而不是立即销毁。这是因为立即销毁会导致contact listener中留下一些已被world删除指针的垃圾数据,同时也要注意只有确定它并不存在于销毁列表中时才把它添加进去。

最后,遍历我们想要删除的body列表。注意我们不仅仅销毁Box2D的body,我们也需要移除Cocos2D场景中的精灵对象

编译并运行,现在你可以销毁砖块了!

加入游戏胜利条件

接下来,我们需要添加一些逻辑,让用户能够取得游戏胜利。修改你的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];
}

这里,如果方块都消失了,我们就会显示一个游戏结束的场景。编译并运行,看看是否能赢得游戏。

You Win Screenshot

完成touch事件

这个游戏非常酷,但是,毫无疑问,我们需要音乐!你可以下载 我制作的一些背景音乐 和 a 好听的blip声音 来使用。和之前一样,在下载后把它们拖到你的resources文件夹下。

顺便提一下,我制作这些声音效果使用一个非常不错的程序,叫做 cfxr that one of our commenters - Indy - pointed out. Thanks Indy this program pwns!是由我们得评论员之一-Indy提出来的,感谢Indy。

不管怎么说,你加入文件之后,在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游戏了!

给我代码!

这里是本系列教程的 Cocos2D and Box2D Breakout Game完整源代码

何去何从?

很明显,这是一个非常简单的beakout游戏,但是,你还可以在此教程的基础上实现更多。你可以扩展一些代码,比如打击一个白色块就计一分,或者有些块需要打中很多下才消失。或者你也可以添加新的不同类型的砖块,并且让paddle可以发射出激光等等。你可以充分发挥想象。

如果你有更好的意见和方法,请提出来。 希望这个能派上用场!

标签: cocos2d教程

?>