使用cocos2d-x和BOX2D来制作一个BreakOut(打砖块)游戏(二)

本教程基于子龙山人翻译的cocos2d的IPHONE教程,用cocos2d-x for XNA引擎重写,加上我一些加工制作。教程中大多数文字图片都是原作者和翻译作者子龙山人,还有不少是我自己的理解和加工。感谢原作者的教程和子龙山人的翻译。本教程仅供学习交流之用,切勿进行商业传播。

子龙山人翻译的Iphone教程地址:http://www.cnblogs.com/zilongshanren/archive/2011/05/29/2059467.html

Iphone教程原文地址:http://www.raywenderlich.com/505/how-to-create-a-simple-breakout-game-with-box2d-and-cocos2d-tutorial-part-22

程序截图:

<cocos2d-x for wp7>使用cocos2d-x和BOX2D来制作一个BreakOut(打砖块)游戏(二)_第1张图片

这是《如何使用cocos2d和box2d制作一个简单的breakout游戏》的第二部分,也是最后一部分教程。如果你还没有读过第一部分,请先阅读《 <cocos2d-x for wp7>使用cocos2d-x和BOX2D来制作一个BreakOut(打砖块)游戏(一)》。

在上一个教程中,我们创建了一个屏幕盒子,球可以在里面弹跳,同时,我们可以用手指拖着paddle移动。这部分教程中,我们将添加一些游戏逻辑,当篮球碰到屏幕底部的时候,就Gameover。

Box2D 和碰撞检测

  在Box2D里面,当一个fixture和另一个fixture相互碰撞的时候,我们怎么知道呢?这就需要用到碰撞侦听器了(contact listener)。一个碰撞侦听器是一个对象,它继承至box2d的IContactListner接口的实现,并且要设置给world对象。这样,当有两个对象发生相互碰撞的时候,world对象就会回调contact listener对象的方法,这样我们就可以在那些方法里面做相应的碰撞处理了。

  如何使用contact listener呢?根据BOX2D用户手册,在一个仿真周期内,你不能执行任何修改游戏物理的操作。因为,在那期间,我们可能需要做一些额外的处理(比如,当两个对象碰撞的时候销毁另一个对象)。因此, 我们需要保存碰撞的引用,这样后面就可以使用它。

  另外一点值得注意的是,我们不能存储传递给contact listener的碰撞点的引用,因为,这些点被BOX2D所重用。因此,我们不得不存储这些点的拷贝。

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

当我们碰到屏幕底部的时候

这里我们添加一个类到Classes文件夹。并且命名为MyContactListener.cs。并且使之继承于接口IContactListener。

并修改这个命名空间内的代码为:

class MyContact
    {
        public Fixture fixtureA;
        public Fixture fixtureB;

    }
    class MyContactListener : IContactListener
    {
        public List<MyContact> contacts = new List<MyContact>();

        public void BeginContact(Contact contact)
        {
            MyContact myContact = new MyContact()
            {
                fixtureA = contact.GetFixtureA(),
                fixtureB = contact.GetFixtureB()
            };
            contacts.Add(myContact);

        }

        public void EndContact(Contact contact)
        {
            contacts.Clear();
        }

        public void PostSolve(Contact contact, ref ContactImpulse impulse)
        {
        }
        public void PreSolve(Contact contact, ref Manifold oldManifold)
        {
        }
    }

 这里,我们定义了一个类MyContact来保存数据,当碰撞通知到达的时候,用来保存碰撞点信息。再说一遍,我们需要存储其拷贝,因为它们会被重用,所以不能保存指针。这里有一个问题,就是如果在EndContact的时候要从list里面找到那个点的话,基本是找不到的。我经过多次试验,每次在EndContact的时候,只有一个点在list里面,如果不移除的话就会出问题。但是用什么IndexOf方法压根就找不到。所以这里用了一个很郁闷的方法,直接将其清空。关于为什么找不到的问题。我想了想,估计是在GetFixtureA和GetFixtureB这两个方法的问题,感觉可能是返回的是引用,并不是拷贝。如果是这样的话,我们在这里做的仅仅是Contact的浅拷贝。这样的话,应该做个深拷贝才行。不过我也没有看源码中GetFixtureA是怎么实现的。所以这里仅仅是猜测。有兴趣的朋友可以去看看GetFixtureA是怎么实现的。然后来解决这个问题吧。

PS:C#中的引用,浅拷贝,深拷贝要特别注意,不然会发现很多很奇怪的问题。


好了,现在可以使用它吧。打开BreakOutLayer类,然后添加一个声明:

MyContactListener contactListener;


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

            //Create contact listener
            contactListener = new MyContactListener();
            world.ContactListener = contactListener;

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

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

            foreach (var item in contactListener.contacts)
            {
                if ((item.fixtureA == bottomFixture && item.fixtureB == ballFixture) ||
                (item.fixtureA == ballFixture && item.fixtureB == bottomFixture))
                {
                    Debug.WriteLine("Ball hit the bottom!");
                }
            }

这里遍历所有缓存的碰撞点,然后看看是否有一个碰撞点,它的两个碰撞体分别是篮球和屏幕底部。目前为止,我们只是使用NSLog来打印一个消息,因为我们只想测试这样是否可行。

  因此,在debug模式下编译并运行,你会发现,不管什么时候,当球和底部有碰撞的时候,你会看到控制台输出一句话“Ball hit the bottom"!


添加Game Over场景

新建一个类添加到Classes文件夹,命名为GameOverScene.cs。并且使之继承于CCScene

修改代码为:

    class GameOverScene:CCScene
    {
        public GameOverScene(bool isWin)
        {
            string msg;
            if (isWin)
                msg = "YOU WIN";
            else
                msg = "YOU LOSE";
            CCLabelTTF label = CCLabelTTF.labelWithString(msg, "Arial", 24);
            label.position = new CCPoint(CCDirector.sharedDirector().getWinSize().width / 2, CCDirector.sharedDirector().getWinSize().height - 100);
            this.addChild(label);
        }
    }

然后,把Debug语句替换成下列代码:

                        GameOverScene pScene = new GameOverScene(false);
                        CCDirector.sharedDirector().replaceScene(pScene);

好了,我们已经实现得差不多了。但是,如果你游戏你永远不能赢,那有什么意思呢?

增加一些方块

  下载我制作的方块图片,然后把它添加到images文件夹下面.

  然后往init方法中添加下列代码:

            for (int i = 0; i < 4; i++)
            {
                int padding = 20;
                
                //Create block and add it to the layer
                CCSprite block = CCSprite.spriteWithFile(@"images/Block");
                float xOffset = padding + block.contentSize.width / 2 + (block.contentSize.width + padding) * i;
                block.position = new CCPoint(xOffset, 400);
                block.tag = 2;
                this.addChild(block);

                //Create block body
                BodyDef blockBodyDef = new BodyDef();
                blockBodyDef.type = BodyType.Dynamic;
                blockBodyDef.position = new Vector2((float)(xOffset / PTM_RATIO), (float)(400 / PTM_RATIO));
                blockBodyDef.userData = block;
                Body blockBody = world.CreateBody(blockBodyDef);

                //Create block shape
                PolygonShape blockShape = new PolygonShape();
                blockShape.SetAsBox((float)(block.contentSize.width / PTM_RATIO / 2), (float)(block.contentSize.height / PTM_RATIO / 2));
                
                //Create shape definition and add to body
                FixtureDef blockShapeDef = new FixtureDef();
                blockShapeDef.shape = blockShape;
                blockShapeDef.density = 10.0f;
                blockShapeDef.friction = 0.0f;
                blockShapeDef.restitution = 0.1f;
                blockBody.CreateFixture(blockShapeDef);
            }

  现在,你应该可以很好地理解上面的代码了。就像之前我们为paddle创建一个body类似,这里,我们每一次也会一个方块创建一个body。注意,我们把方块精灵对象的tag设置为2,这样将来可以用到。

  编译并运行,你应该可以看到篮球和方块之间有碰撞了。

销毁方块

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

  具体改动方式如下:

            List<Body> toDestroy = new List<Body>();
            foreach (var item in contactListener.contacts)
            {
                if ((item.fixtureA == bottomFixture && item.fixtureB == ballFixture) ||
                (item.fixtureA == ballFixture && item.fixtureB == bottomFixture))
                {
                    GameOverScene pScene = new GameOverScene(false);
                    CCDirector.sharedDirector().replaceScene(pScene);

                }
                Body bodyA = item.fixtureA.GetBody();
                Body bodyB = item.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 (toDestroy.IndexOf(bodyB) == -1)
                        {
                            toDestroy.Add(bodyB);
                        }
                    }
                    //Sprite B = block ,Sprite A = ball
                    else if (spriteA.tag == 2 && spriteB.tag == 1)
                    {
                        if (toDestroy.IndexOf(bodyA) == -1)
                        {
                            toDestroy.Add(bodyA);
                        }
                    }
                }
            }

            foreach (var item in toDestroy)
            {
                if (item.GetUserData() != null)
                {
                    CCSprite sprite = (CCSprite)item.GetUserData();
                    this.removeChild(sprite, true);
                }
                world.DestroyBody(item);
            }

  好,让我们解释一下。我们又一次遍历所有的碰撞点,但是,这一次在我们测试完篮球和屏幕底部相撞的时候,我们将检查碰撞点。我们可以通过fixture对象的GetBody方法来找对象。

 接着,我们基于精灵的tag,看看到底是哪个在发生碰撞。如果一个精灵与一个body相交的话,我们就把该body添加到待销毁的对象列表里面去。

  但是也需要注意,只有确定它并不存在于销毁列表中时才把它添加进去。为什么一定要用一个list把需要销毁的存储起来而不是直接销毁。因为直接销毁会导致contact listener中留下一些已被删除指针的垃圾数据。 

 最后,遍历我们想要删除的body列表。

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

加入游戏胜利条件

  接下来,我们需要添加一些逻辑,让用户能够取得游戏胜利。修改你的tick方法的开头部分,像下面一样:

 bool blockFind = false;
            world.Step(dt, 10, 10);
            for (Body b = world.GetBodyList(); b != null;b = b.GetNext() )
            {
                if (b.GetUserData() != null)
                {
                    CCSprite sprite = (CCSprite)b.GetUserData();
                    if (sprite.tag == 1)
                    {
                        int maxSpeed = 10;
                        Vector2 velocity = b.GetLinearVelocity();
                        float speed = velocity.Length();

                        if (speed > maxSpeed)
                        {
                            b.SetLinearDamping(0.5f);
                        }
                        else if (speed < maxSpeed)
                        {
                            b.SetLinearDamping(0.0f);
                        }
                    }
                    else if (sprite.tag == 2)
                    {
                        blockFind = true;
                    }

  我们需要做的,仅仅是遍历一下场景中的所有对象,看看是否还有一个方块----如果我们确实找到了一个,那么就把blockFound变量设置为true,否则就设置为false.

  然后,在这个函数的末尾添加下面的代码:

if (!blockFind)
            {
                GameOverScene gameOverScene = new GameOverScene(true);
                CCDirector.sharedDirector().replaceScene(gameOverScene);
            }

这里,如果方块都消失了,我们就会显示一个游戏结束的场景。编译并运行,看看,你的游戏现在有胜利终止条件了!

完成touch事件

  这个游戏非常酷,但是,毫无疑问,我们需要音乐!你可以下载好听的blip声音。(背景音乐自己弄,MP3格式),和之前一样,在Content工程新建一个resources文件夹,把它添加到你的resources文件夹下。

添加CocosDenshion.dll这个DLL的引用。

在tick方法的末尾添加下面的代码:

            if (toDestroy.Count > 0)
            {
                SimpleAudioEngine.sharedEngine().playEffect(@"resources/blip");
            }

终于完成了!你现在拥有一个使用Box2d物理引擎制作的breakout游戏了

本次工程下载:http://dl.dbank.com/c0a0a5ze3q


何去何从?

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




你可能感兴趣的:(游戏,vector,velocity,null,float,resources)