這是一個使用Box2d和cocos2d-x從頭開始制作一個彈弓類游戲系列教材的第二部分。
(第一部分請看本博客博客列表)
在這個系列教材的第一部分,我們在場景中加入可以投射危險的橡子炸彈投射器。
在這個系列的第二也是最後部分。我們將會把游戲完善成為完整版,並且增加我們射擊的目標和游戲邏輯。
言歸正傳,開始射擊啦!
由於你已經了解了大部分相關知識,所以創造目標並不是那麼復雜。因為我們將要創建一大堆目標,那麼就讓我們先創建一個方法然後再多次調用它。
首先我們先創建一些變量用來記錄新的物體。在.h文件中加入這些變量:
- private:
- vector<b2Body*> *targets;
- vector<b2Body*> *enemies;
再來就是增加一個用來創建目標輔助方法:
- void HelloWorld::createTarget(char *imageName,
- CCPoint position,
- float rotation,
- bool isCircle,
- bool isStatic,
- bool isEnemy)
- {
- CCSprite *sprite = CCSprite::spriteWithFile(imageName);
- 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個磚頭然後再運行工程看看發生了什麼。
現在移除掉改變視角位置的那一行代碼,運行,發射一個子彈看看效果。
在我們做碰撞檢測之前,讓我們先增加釋放子彈後重新上膛代碼。
讓我們在resetGame方法中增加一個新的方法調用:
- void HelloWorld::resetBullet()
- {
- if (enemies->size() == 0)
- {
- //game over
- //we`ll do something here later
- }
- else if (attachBullet())
- {
- CCAction *action = CCMoveTo::actionWithDuration(0.2f, CCPointZero);
- runAction(action);
- }
- else
- {
- //We can reset the whole scene here
- //Alse, let`s 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.width*2.0f/PTM_RATIO,screenSize.height/PTM_RATIO), b2Vec2(screenSize.width*2.0f/PTM_RATIO,0));
- m_groundBody->CreateFixture(&groundBox, 0);
再次運行工程,應該更自然了。
現在就像憤怒的松鼠般自然了。
快要完成了!現在我們需要的就是檢測應該被摧毀的敵人。
我們使用碰撞檢測,但簡單的碰撞檢測有點小問題。我們的敵人已經和磚塊們碰撞了,那麼簡單的檢測敵人是否和其他的什麼東西碰撞已經不能滿足我們的要求了,因為那樣敵人會立刻被消除。
我們可以認為對於敵人他們應該只與子彈進行碰撞。這會很容易,但同時有些敵人將會很難摧毀。拿在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的圖。這個文件已經加到了image文件夾了。
在我們完成工程前為了防止用完所以子彈或者敵人,我們再增加一個重置所有東西的方法。這很簡單,因為我們已經幾乎完成了場景的創建。
最好的重啟我們的游戲的方法就是調用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
- //we`ll do something here later
- CCDelayTime *delayAction = CCDelayTime::actionWithDuration(2.0f);
- CCCallFunc *callSelectorAction = CCCallFunc::actionWithTarget(this, callfunc_selector(HelloWorld::resetGame));
- this->runAction(CCSequence::actions(delayAction, callSelectorAction, NULL));
- }
- else if (attachBullet())
- {
- CCAction *action = CCMoveTo::actionWithDuration(0.2f, CCPointZero);
- runAction(action);
- }
- else
- {
- //We can reset the whole scene here
- //Alse, let`s 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
本文出自 “Ghost” 博客,請務必保留此出處http://mssyy2010.blog.51cto.com/4595971/856611