本教程基于子龙山人翻译的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和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); }
这里,如果方块都消失了,我们就会显示一个游戏结束的场景。编译并运行,看看,你的游戏现在有胜利终止条件了!
这个游戏非常酷,但是,毫无疑问,我们需要音乐!你可以下载好听的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可以发射出激光等等。你可以充分发挥想象。
PS:令人伤心的所谓搬家工具。。。搞得显示乱七八糟的。。。。。。又得手动重来。