这是一个使用Box2d和cocos2d-x从头开始制作一个弹弓类游戏系列教材的第二部分。
(第一部分请看本博客博客列表)
在这个系列教材的第一部分,我们在场景中加入可以投射危险的橡子×××投射器。
在这个系列的第二也是最后部分。我们将会把游戏完善成为完整版,并且增加我们射击的目标和游戏逻辑。
言归正传,开始射击啦!
创造目标
由于你已经了解了大部分相关知识,所以创造目标并不是那么复杂。因为我们将要创建一大堆目标,那么就让我们先创建一个方法然后再多次调用它。
首先我们先创建一些变量用来记录新的物体。在.h文件中加入这些变量:private: vector<b2Body> targets; vector<b2Body*> enemies; 再来就是增加一个用来创建目标辅助方法:void HelloWorld::createTarget(char p_w_picpathName, CCPoint position, float rotation, bool isCircle, bool isStatic, bool isEnemy) { CCSprite *sprite = CCSprite::spriteWithFile(p_w_picpathName); this->addChild(sprite, 1); b2BodyDef bodyDef; bodyDef.type = isStatic ? b2_staticBody : b2_dynamicBody; bodyDef.position.Set((position.x+sprite->getContentSize().width/2.0f)/PTM_RATIO, (position.y+sprite->getContentSize().height/2.0f)/PTM_RATIO); bodyDef.angle = CC_DEGREES_TO_RADIANS(rotation); bodyDef.userData = sprite; b2Body body = m_world->CreateBody(&bodyDef); b2FixtureDef boxDef; b2Fixture fixtureTemp; if (isCircle){ b2CircleShape circle; boxDef.shape = &circle; circle.m_radius = sprite->getContentSize().width/2.0f/PTM_RATIO; fixtureTemp = body->CreateFixture(&circle, 0.5f); targets->push_back(body); } else{ b2PolygonShape box; boxDef.shape = &box; box.SetAsBox(sprite->getContentSize().width/2.0f/PTM_RATIO, sprite->getContentSize().height/2.0f/PTM_RATIO); body->CreateFixture(&box, 0.5f); targets->push_back(body); } if (isEnemy){ fixtureTemp->SetUserData((void*)1); // boxDef.userData = (void)1; enemies->push_back(body); } } (夹具创建我按原博客里使用body - >CreateFixture ( & boxDef ) ; 会出现未知错误,感觉有可能是bug) 由于我们将会拥有大量不同类型的目标,并使用不同的方法方法来将他们增加到场景中,所以这个方法会有很多参数。不过别担心,这很简单,让我们一点一点来分析它。
首先我们读取从函数传递的文件名来读取精灵。为了让放置对象更加容易,我们传递给方法的坐标是我们想要放置目标坐标的左下角的值。由于Box2d的坐标是取中心位置,我们不得不使用精灵的尺寸来设置物体(body)的坐标。
我们接下来根据我们希望的形状定义物体的夹具。它可以是一个圆(主要是敌人的形状)或者是个矩形。同样的夹具(fixture)的尺寸由精灵的尺寸可得知。
接下来,如果这是一个敌人(敌人之后会“爆炸”并且我想要通过记录他们来得知关卡是否结束)我将敌人加入到enemies集合并将夹具的userData设置为1。userData通常设置为结构体或者指向另一个对象的指针,但即使这样的话我们只是想标记这些夹具作为敌人夹具。当我向你展示如何检测敌人是否应该被摧毁时你就会很清楚为什么要这样做。
我们接下来创建物体的夹具让后把它加入到目标数组中一边记录使用它们。
现在是时候调用这个方法几次来完成我们的场景。这是个很大的方法因为我不得不调用createTarget方法来创造每个我们想加入到场景中的对象。
这些是我们将会使用的精灵。
现在我们需要做的就是把这个精灵放到正确的位置。我测试了多次来获得正确的坐标,而你直接使用这是坐标就可以了!:]增加下面的方法在createTarget方法:void HelloWorld::createTarget() { createTarget("brick_2.png", CCPointMake(675.0, FLOOR_HEIGHT), 0.0f, false, false, false); createTarget("brick_1.png", CCPointMake(741.0, FLOOR_HEIGHT), 0.0f, false, false, false); createTarget("brick_1.png", CCPointMake(741.0, FLOOR_HEIGHT+23.0), 0.0f, false, false, false); createTarget("brick_3.png", CCPointMake(673.0, FLOOR_HEIGHT+46.0), 0.0f, false, false, false); createTarget("brick_1.png", CCPointMake(707.0, FLOOR_HEIGHT+58.0), 0.0f, false, false, false); createTarget("brick_1.png", CCPointMake(707.0, FLOOR_HEIGHT+81.0), 0.0f, false, false, false); createTarget("head_dog.png", CCPointMake(702.0, FLOOR_HEIGHT), 0.0f, true, false, true); createTarget("head_cat.png", CCPointMake(680.0, FLOOR_HEIGHT+58.0), 0.0f, true, false, true); createTarget("head_dog.png", CCPointMake(740.0, FLOOR_HEIGHT+58.0), 0.0f, true, false, true); // 2 bricks at the right of the first block createTarget("brick_2.png", CCPointMake(770.0, FLOOR_HEIGHT), 0.0f, false, false, false); createTarget("brick_2.png", CCPointMake(770.0, FLOOR_HEIGHT+46.0), 0.0f, false, false, false); // The dog between the blocks createTarget("head_dog.png", CCPointMake(830.0, FLOOR_HEIGHT), 0.0f, true, false, true); // Second block createTarget("brick_platform.png", CCPointMake(839.0, FLOOR_HEIGHT), 0.0f, false, true, false); createTarget("brick_2.png", CCPointMake(854.0, FLOOR_HEIGHT+28.0), 0.0f, false, false, false); createTarget("brick_2.png", CCPointMake(854.0, FLOOR_HEIGHT+28.0+46.0), 0.0f, false, false, false); createTarget("head_cat.png", CCPointMake(881.0, FLOOR_HEIGHT+28.0), 0.0f, true, false, true); createTarget("brick_2.png", CCPointMake(909.0, FLOOR_HEIGHT+28.0), 0.0f, false, false, false); createTarget("brick_1.png", CCPointMake(909.0, FLOOR_HEIGHT+28.0+46.0), 0.0f, false, false, false); createTarget("brick_1.png", CCPointMake(909.0, FLOOR_HEIGHT+28.0+46.0+23.0), 0.0f, false, false, false); createTarget("brick_2.png", CCPointMake(882.0, FLOOR_HEIGHT+108.0), 90.0f, false, false, false); } 很简单吧,只需要调用我们的辅助方法来生成我们想要的目标。在resetGame方法末尾增加这个方法:this->createTarget(); 运行工程你将不会看到这部分场景,除非你投射出一颗×××。因此现在的方法就是在init方法末尾加一行代码来检查我们生成的场景是否正确从而让事情变得简单。setPosition(ccp(-480, 0)); 这会让我们看到场景的右半部分,而不是左半部分。再次运行工程看看目标位置是否正确。
你可以在下一步之前先试玩下游戏,比如注释掉右侧的2个砖头然后再运行工程看看发生了什么。
现在移除掉改变视角位置的那一行代码,运行,发射一个×××看看效果。
Rapid Fire
在我们做碰撞检测之前,让我们先增加释放×××后重新上膛代码。
让我们在resetGame方法中增加一个新的方法调用:void HelloWorld::resetBullet() { if (enemies->size() == 0) { //game over //well do something here later </span><span> </span></span></li><li class="alt"><span> } </span></li><li><span> <span class="keyword">else</span><span> </span><span class="keyword">if</span><span> (attachBullet()) </span></span></li><li class="alt"><span> { </span></li><li><span> CCAction </em>action = CCMoveTo::actionWithDuration(0.2f, CCPointZero); </span></li><li class="alt"><span> runAction(action); </span></li><li><span> } </span></li><li class="alt"><span> <span class="keyword">else</span><span> </span></span></li><li><span> { </span></li><li class="alt"><span> <span class="comment">//We can reset the whole scene here</span><span> </span></span></li><li><span> <span class="comment">//Alse, lets do this later } } 在这个方法中,我们首先检测是否我们已经消灭了所有敌人。目前而言这并不会发生,因为我们并不会产生破坏,但我们已经假设了这种情况。
如果仍然剩有敌人,我们会试着重新上膛。记住如果仍然剩余×××attachBullets方法返回真否则返回假。因此,如果如果仍然还有×××,我则运行一个将视角位置重置为场景左半部分cocos2d-x动作,这样我又能看见我的投射器了。
如果没有×××了我们将不得不重启整个场景,但这个一会再处理。让我们首先找点乐子。
我们现在不得不在合适的时机调用这个方法。但是什么时候是合适的时机呢?也许是当×××被释放后最后停止运动的时候?也许是当撞击到第一个目标后的几秒?好吧,这些想法都有道理。为了让事情变得简单,我们在释放×××后的几秒钟后调用这个方法。
正如你记得的,我们在摧毁连接关节(weld joint)时候在tick方法里完成了这件事。所以找到那个方法,再调用摧毁关节的后面加入:CCDelayTime *delayAction = CCDelayTime::actionWithDuration(5.0f); CCCallFunc callSelectorAction = CCCallFunc::actionWithTarget(this, callfunc_selector(HelloWorld::resetBullet)); this->runAction(CCSequence::actions(delayAction, callSelectorAction, NULL)); 这会等待5秒钟,然后在调用resetBullet方法。现在运行工程然后观看蓄意的破坏。这部分最后的注意,在我看来这蓄意的破坏太不自然了。
因为一个细节:撞到了右侧边界的目标全都仍然存在,好像倚靠着一面在我们的世界中根本不存在的墙。目标应该向右侧倾倒,但事实上它们没有。
这之所以会发生就是因为我们创建世界的时候加上了4条边界,我们现在希望右侧边界不要存在。
所以嘛,回到init方法移除这几行代码:groundBox.Set(b2Vec2(screenSize.width2.0f/PTM_RATIO,screenSize.height/PTM_RATIO), b2Vec2(screenSize.width*2.0f/PTM_RATIO,0)); m_groundBody->CreateFixture(&groundBox, 0); 再次运行工程,应该更自然了。
现在就像愤怒的松鼠般自然了。
It`s Raining Cats and Dogs
快要完成了!现在我们需要的就是检测应该被摧毁的敌人。
我们使用碰撞检测,但简单的碰撞检测有点小问题。我们的敌人已经和砖块们碰撞了,那么简单的检测敌人是否和其他的什么东西碰撞已经不能满足我们的要求了,因为那样敌人会立刻被消除。
我们可以认为对于敌人他们应该只与×××进行碰撞。这会很容易,但同时有些敌人将会很难摧毁。拿在2个砖块之间的狗来说。×××很难撞到它,但用周围的砖块砸它并不难。但是我们已经确定了一个简单的碰撞,那么砖块就无效了。
我们能做的就是决定碰撞的力的大小,然后再决定最后敌人能承受最小的力。
为了完成这个我们需要创建一个contact listener。Ray已经在撞球游戏教程2中解释了如何创建。如果你没读过那个教程或者你不记得了,那就去读读那个教程,我在这等你。。。
这之间会有些不同,首先我们将用std::set代替std::vector。不同点就是set不允许重复放置所以当对目标多次碰撞我们不用担心序列中加入2次。
另一个不同就是我们将使用postSolve方法,因为这是我们能够检索碰撞的力来决定是否摧毁敌人。
创建一个MyContackLister.h:#ifndef _MYCONTACT_LISTENER_H_ #define _MYCONTACT_LISTENER_H_ #include "Box2D/Box2D.h" #include <set> #include <algorithm> class MyContactListener : public b2ContactListener { public: std::set<b2Body>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); }; #endif 在创建一个MyContackLister.cpp:#include "MyContactListener.h" MyContactListener::MyContactListener() : contacts() { } MyContactListener::~MyContactListener() { } void MyContactListener::BeginContact(b2Contact contact) { } void MyContactListener::EndContact(b2Contact* contact) { } void MyContactListener::PreSolve(b2Contact contact, const b2Manifold oldManifold) { } void MyContactListener::PostSolve(b2Contact* contact, const b2ContactImpulse impulse) { bool isAEnemy = (contact->GetFixtureA()->GetUserData() != NULL); bool isBEnemy = (contact->GetFixtureB()->GetUserData() != NULL); if (isAEnemy || isBEnemy) { // Should the body break? int32 count = contact->GetManifold()->pointCount; float32 maxImpulse = 0.0f; for (int32 i = 0; i < count; ++i) { maxImpulse = b2Max(maxImpulse, impulse->normalImpulses[i]); } if (maxImpulse > 1.0f) { // Flag the enemy(ies) for breaking. if (isAEnemy) contacts.insert(contact->GetFixtureA()->GetBody()); if (isBEnemy) contacts.insert(contact->GetFixtureB()->GetBody()); } } } 如我所提的,我们只实现了PostSolve方法。
首先我们决定我们处理的碰撞包含至少1个敌人。记得创建目标方法中我们用夹具的用户信息标记了敌人?那就是我们为什么要这么干,看到没,我说过你会理解的。
每次碰撞可以有超过1个碰撞点和1个推动力,这个推动力就是碰撞的基础力。我们接下来决定碰撞力的最大值,然后决定是否应该摧毁敌人。
如果我们决定摧毁敌人,我们将这个物体加入到我们的set中这样我们之后就可以摧毁它。记住,正如RAY说的,我们不能在碰撞过程中摧毁敌人物体。因此我们保留它在我们的set中,在之后在摧毁它。
摧毁敌人的力的大小应该是你自己测试的值,这个值可能根据不同的物体的质量,×××的速度的力的效果和其他因素变化很大。我的建议是开始让它很小,然后再慢慢增大,直到确定个合适的值。
现在我们有了我们的监听代码。让我们举例应用它。回到.h文件加入语句:#include "MyContactListener.h" 增加一个指针来指向我们的监听器:MyContactListener contactListener; 回到实现文件在init方法末尾:contactListener = new MyContactListener(); m_world->SetContactListener(contactListener); 现在我们有些代码来删除敌人。在tick方法的末尾:// Check for impacts std::set<b2Body*>::iterator pos; for(pos = contactListener->contacts.begin(); pos != contactListener->contacts.end(); ++pos) { b2Body body = pos; for (vector<b2Body*>::iterator iter = targets->begin(); iter !=targets->end(); ++iter) { if (body == iter)//////////////////////////////////////////// { iter = targets->erase(iter); break; } } for (vector<b2Body>::iterator iter = enemies->begin(); iter !=enemies->end(); ++iter) { if (body == *iter) { iter = enemies->erase(iter); break; } } CCNode contactNode = (CCNode)body->GetUserData(); // CCPoint position = contactNode->getPosition(); removeChild(contactNode, true); m_world->DestroyBody(body); 我们简单地迭代了碰撞监听器的set并且摧毁了所有物体和相关精灵。我们还将他们从enemies和targets中移除,因此我们可以判定我们是否消除了所有敌人。
最后我们清空碰撞将听器的set,这样让他准备好应对下次的tick调用,并且这样我们不用再试着重复删除那些物体。
运行游戏,很cool!
使用cocos2d-x的粒子效果(particle)会让事情变得简单。我可不会讨论一大堆粒子的细节,那样教程就跑题了。如果你想深入了解粒子系统,那就去读读ray的cocos2d的书的14章。这期间我会做的就是演示给你如何使用预置的粒子。
在循环中增加下列代码:CCParticleSun *explosion = CCParticleSun::node(); explosion->retain(); explosion->setTexture(CCTextureCache::sharedTextureCache()->addImage("fire.png")); explosion->initWithTotalParticles(200); explosion->setIsAutoRemoveOnFinish(true); explosion->setStartSizeVar(10.0f); explosion->setSpeed(70.0f); explosion->setAnchorPoint(ccp(0.5f, 0.5f)); explosion->setPosition(position); explosion->setDuration(1.0f); addChild(explosion, 11); explosion->release(); 我们先存储敌人精灵的位置,因此我们知道在哪里增加粒子。接下来增加CCParticleSun粒子实例。很简单是吧?
再次运行。
很不错,是吧?
CCParticleSun是cocos2d-x粒子系统预置的类之一。
CCParticleExplostion可能看起来更好,但是你错了,至少我这么认为的。试试看各种粒子来看看效果。有一件事我要悄悄告诉你,那就是粒子用的纹理,如果你看到CCParticleSun的代码你会注意到它用了一个叫做fire.png的图。这个文件已经加到了p_w_picpath文件夹了。
完成触摸
在我们完成工程前为了防止用完所以×××或者敌人,我们再增加一个重置所有东西的方法。这很简单,因为我们已经几乎完成了场景的创建。
最好的重启我们的游戏的方法就是调用resetGame。但如果你只是简单地调用那样你将会遗留很多之前的敌人和目标。所以我们要加些清除代码,用来处理上述情况。幸运的是我们已经留下了所有的东西的引用,这样会很简单。
回到resetGame方法的首部:if (m_bullets.size() != 0) { for (vector<b2Body>::iterator bulletPointer = m_bullets.begin(); bulletPointer != m_bullets.end(); ++bulletPointer) { b2Body bullet = (b2Body*)bulletPointer; CCNode node = (CCNode*)bullet->GetUserData(); removeChild(node, true); m_world->DestroyBody(bullet); // bulletPointer= m_bullets.erase(bulletPointer); } // [bullets release]; m_bullets.clear(); } if (targets->size() !=0) { for (vector<b2Body>::iterator targetPointer = (targets).begin(); targetPointer != (*targets).end(); targetPointer++) { b2Body target = (b2Body)*targetPointer; CCNode node = (CCNode)target->GetUserData(); removeChild(node, true); m_world->DestroyBody(target); } // [bullets release]; (*targets).clear(); (enemies).clear(); } 这很简单。我们只是遍历了sets,并且移除了物体和相关精灵。条件语句是为了防止我们在sets中没有我们创建的东西的情况下访问。
现在让我们再合适的时间调用resetGame。如果你记得我们留下了些条件的空白区域在resetBUllet方法中。好吧,那就是合适的位置。回到那里我们注释掉的位置,加入代码:void HelloWorld::resetBullet() { if (enemies->size() == 0) { //game over //well do something here later </span><span> </span></span></li><li class="alt"><span> CCDelayTime </em>delayAction = CCDelayTime::actionWithDuration(2.0f); </span></li><li><span> CCCallFunc *callSelectorAction = CCCallFunc::actionWithTarget(<span class="keyword">this</span><span>, callfunc_selector(HelloWorld::resetGame)); </span></span></li><li class="alt"><span> <span class="keyword">this</span><span>->runAction(CCSequence::actions(delayAction, callSelectorAction, NULL)); </span></span></li><li><span> </span></li><li class="alt"><span> } </span></li><li><span> <span class="keyword">else</span><span> </span><span class="keyword">if</span><span> (attachBullet()) </span></span></li><li class="alt"><span> { </span></li><li><span> CCAction <em>action = CCMoveTo::actionWithDuration(0.2f, CCPointZero); </span></li><li class="alt"><span> runAction(action); </span></li><li><span> } </span></li><li class="alt"><span> <span class="keyword">else</span><span> </span></span></li><li><span> { </span></li><li class="alt"><span> <span class="comment">//We can reset the whole scene here</span><span> </span></span></li><li><span> <span class="comment">//Alse, lets do this later CCDelayTime delayAction = CCDelayTime::actionWithDuration(2.0f); CCCallFunc *callSelectorAction = CCCallFunc::actionWithTarget(this, callfunc_selector(HelloWorld::resetGame)); this->runAction(CCSequence::actions(delayAction, callSelectorAction, NULL)); } } 运行游戏你将会看到当敌人或者×××都耗光的时候,游戏就会重启,你就可以在玩这个游戏了,而不用重新启动工程。
让我们增加另外一个细节。当游戏开始的时候你看不到目标,所以你不知道该摧毁什么。让我们修正这个,再一次,更改resetGame方法。
在resetGame方法我们调用这3个方法:this->createBullets(3); this->createTarget(); this->attachBullet(); CCFiniteTimeAction action1 = CCMoveTo::actionWithDuration(1.5f, ccp(-480.0f, 0.0f)); CCCallFuncN action2 = CCCallFuncN::actionWithTarget(this, callfuncN_selector(HelloWorld::attachBullet)); CCDelayTime *action3 = CCDelayTime::actionWithDuration(1.0f); CCFiniteTimeAction action4 = CCMoveTo::actionWithDuration(1.5f, CCPointZero); runAction(CCSequence::actions(action1, /action2, */action3, action4, NULL)); 现在我们将创造×××和目标,然后开始一系列动作。这些动作会一个接一个的运行。
首先我们将向右移动场景以便我们可以看到我们的目标。
当这个动作完成时我们将调用这个方法粘连×××。在我们没有看到它的时候这将使×××粘连住,所以我们会避免我们现在已经使用的非常野蛮的粘连方法。
最后…我们的视角会回到左边,这样你就可以开始毁灭啦!
资源文件看上个教程。
原文链接:http://www.raywenderlich.com/4787/how-to-make-a-catapult-shooting-game-with-cocos2d-and-box2d-part-2
|