如何使用Cocos2d-x 3.0製作基於tilemap的遊戲:第三部分(完)
引言
程序截圖:
在第二部分教程中,Ray教大家如何在地圖中製作可碰撞的區域,如何使用tile屬性,如何製作可以拾取的物品以及如何動態修改地圖、如何使用“Heads up display”來顯示分數。
在這個教程中,我們將加入敵人,這樣的話,你的忍者就可以向它們扔飛鏢啦,同時還增加了勝利和失敗的遊戲邏輯。但是,首先,你得下載一些相關的資源文件。
這個zip文件裏面包含以下內容:
1. 一個敵人精靈
2. 一個忍者飛鏢,直接從《如何使用Cocos2d製作一個簡單的iPhone遊戲》中拿過來的。
3. 兩張按鈕的圖片,在教程的後面有使用。
在繼續學習之前,不要忘了把這些資源(Added_Resources.zip)加入到你的工程中。
增加敵人
到第二部分教程結束的時候,遊戲已經很酷了,但是它還不是一個完整的遊戲。你的忍者可以輕而易舉地四處遊蕩,想吃就吃。但是,什麼時候玩家會勝利或者失敗呢。我們不妨想象一下,有2個敵人在追殺你的忍者,那麼這個遊戲會顯得更加有趣。
敵人出現的位置點
好了,回到Tiled軟件,然後打開你的Tile地圖(TileMap.tmx)。
往對象層中加入一個對象,在player附近就行,但是不要太近,否則敵人一出現玩家就Game Over了。這個位置將成爲敵人出現的位置點,把它命名爲“EnemySpawn1”。
對象組(對象層中的所有對象組成一個對象組)中的對象被存儲在一個TMXObjectGroup中,同時使用對象名字作爲key。這意味着每一個位置點必須有一個唯一的名字。儘管我們可以遍歷所有的key來比較哪個是以“EnemySpawn”開頭,但是這樣做效率很低下。相反,我們採用的做法是,使用一個屬性來表示,每個給定的對象代表一個敵人出現的位置點。
給這個對象一個屬性“Enemy”,同時賦一個值1。如果你想在這個教程的基礎上擴展,並且增加其它的不同類型的敵人,你可以使用這些敵人的屬性值來表示不同類型的敵人。現在,製作6-10個這種敵人出現位置點對象,相應的它們離player的距離也要有一些不同。爲每一個對象定義一個“Enemy”屬性,並且賦值爲1。保存這張地圖並且回到編譯器。
開始創建敵人
好了,現在我們將把敵人實際顯示到地圖上來。首先在HelloWorldScene.cpp中添加如下代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
//in the HelloWorld class void HelloWorld::addEnemyAtPos(Point pos) { auto enemy = Sprite::create( "enemy1.png" ); enemy->setPosition(pos); this ->addChild(enemy); } // in the init method - after creating the player // iterate through objects, finding all enemy spawn points // create an enemy for each one for (auto& eSpawnPoint : objects->getObjects()) { ValueMap& dict = eSpawnPoint.asValueMap(); if (dict[ "Enemy" ].asInt() == 1) { x = dict[ "x" ].asInt(); y = dict[ "y" ].asInt(); this ->addEnemyAtPos(Point(x,y)); } } |
第一個循環遍歷對象列表,判斷它是否是一個敵人出現的位置點。如果是,則獲得它的x和y座標值,然後調用addEnemyAtPos方法把它們加入到合適的地方去。
這個addEnemyAtPos方法非常直白,它僅僅是在傳入的Point座標值處創建一個敵人精靈。如果你編譯並運行,你會看到這些敵人出現在你之前在Tiled工具中設定的位置處,很酷吧!
但是,這裏有一個問題,這些敵人很傻瓜,它們並不會追殺你的忍者。
使它們移動
因此,現在我們將添加一些代碼,使這些敵人會追着我們的player跑。因爲,player肯定會移動,我們必須動態地改變敵人的運動方向。爲了實現這個目的,我們讓敵人每次移動10個像素,然後在下一次移動之前,先調整它們的方向。在HelloWorldScene.cpp中加入如下代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
// callback. starts another iteration of enemy movement. void HelloWorld::enemyMoveFinished(Object *pSender) { Sprite *enemy = (Sprite *)pSender; this ->animateEnemy(enemy); } // a method to move the enemy 10 pixels toward the player void HelloWorld::animateEnemy(Sprite *enemy) { // speed of the enemy float actualDuration = 0.3f; // Create the actions auto position = (_player->getPosition() - enemy->getPosition()).normalize()*10; auto actionMove = MoveBy::create(actualDuration, position); auto actionMoveDone = CallFuncN::create(CC_CALLBACK_1(HelloWorld::enemyMoveFinished, this )); enemy->runAction(Sequence::create(actionMove, actionMoveDone, NULL)); } // add this at the end of addEnemyAtPos // Use our animation method and // start the enemy moving toward the player this ->animateEnemy(enemy); |
animateEnemy:方法創建兩個action。第一個action使之朝敵人移動10個像素,時間爲0.3秒。你可以改變這個時間使之移動得更快或者更慢。第二個action將會調用enemyMoveFinished:方法。我們使用Sequence create來把它們組合起來,這樣的話,當敵人停止移動的時候就立馬可以執行enemyMoveFinished:方法就可以被調用了。在addEnemyAtPos:方法裏面,我們調用animateEnemy:方法來使敵人朝着玩家(player)移動。(其實這裏是個遞歸的調用,每次移動10個像素,然後又調用enemyMoveFinished:方法)
很簡潔!但是,但是,如果敵人每次移動的時候面部都對着player那樣是不是更逼真呢?只需要在animateEnemy:方法中加入下列語句即可:
1
2
3
4
5
6
7
8
9
10
|
//immediately before creating the actions in animateEnemy //rotate to face the player auto diff = ccpSub(_player->getPosition(), enemy->getPosition()); float angleRadians = atanf(( float )diff.y / ( float )diff.x); float angleDegrees = CC_RADIANS_TO_DEGREES(angleRadians); float cocosAngle = -1 * angleDegrees; if (diff.x < 0) { cocosAngle += 180; } enemy->setRotation(cocosAngle); |
這個代碼計算每次玩家相對於敵人的角度,然後旋轉敵人來使之面朝玩家。
忍者飛鏢
已經很不錯了,但是玩家是一個忍者啊!他應該要能夠保護他自己!
我們將向遊戲中添加模式(modes)。模式並不是實現這個功能的最好方式,但是,它比其他的方法要簡單,而且這個方法在模擬器下也能運行(因爲並不需要多點觸摸)。因爲這些優點,所以這個教程裏面,我們使用這種方法。首先將會建立UI,這樣的話玩家可以方便地在“移動模式”和“擲飛鏢”模式之間進行切換。我們將增加一個按鈕來使用這個功能的轉換。(即從移動模式轉到擲飛鏢模式)。
現在,我們將增加一些屬性,使兩個層之間可以更好的通信。在HelloWorldScene.h裏面增加如下代碼:
1
2
3
4
5
6
7
8
9
10
|
// at the top of the file add a forward declaration for HelloWorld, // because our two layers need to reference each other class HelloWorld; // inside the HelloWorldHud class declaration HelloWorld *_gameLayer; // Inside the HelloWorld class declaration CC_SYNTHESIZE( int , _mode, Mode); |
同時修改HelloWorldScene.cpp文件
1
2
3
4
5
6
7
8
9
10
11
|
// in HelloWorld's init method _mode =0; //replace following two lines with CC_SYNTHESIZE //private: // HelloWorld *_gameLayer; CC_SYNTHESIZE(HelloWorld *, _gameLayer, GameLayer); // in HelloWorld's createScene method // after _hud = hud; _hud->setGameLayer(layer); |
在HelloWorldScene.cpp中添加下面的代碼,這段代碼定義了一個按鈕。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// in HelloWorldHud's init method // define the button MenuItem *on = MenuItemImage::create( "projectile-button-on.png" , "projectile-button-on.png" ); MenuItem *off = MenuItemImage::create( "projectile-button-off.png" , "projectile-button-off.png" ); auto toggleItem = MenuItemToggle::createWithCallback(CC_CALLBACK_1(HelloWorldHud::projectileButtonTapped, this ), off, on, NULL); auto toggleMenu = Menu::create(toggleItem, NULL); toggleMenu->setPosition(on->getContentSize().width * 2, on->getContentSize().height / 2); this ->addChild(toggleMenu); // in HelloWorldHud //callback for the button //mode 0 = moving mode //mode 1 = ninja star throwing mode void HelloWorldHud::projectileButtonTapped(Object *pSender) { if (_gameLayer->getMode() == 1) { _gameLayer->setMode(0); } else { _gameLayer->setMode(1); } } |
編譯並運行。這時會在左下角出現一個按鈕,並且你可以打開或者關閉之。但是這並不會對遊戲造成任何影響。我們的下一步就是增加飛鏢的發射。
發射飛鏢
接下來,我們將添加一些代碼來檢查玩家當前處於哪種模式下面,並且在用戶點擊屏幕的時候影響不同的事件。如果是移動模式則移動玩家,如果是射擊模式,則擲飛鏢。在onTouchEnded方法裏面增加下面代碼:
1
2
3
4
5
|
if (_mode ==0) { // old contents of onTouchEnded } else { // code to throw ninja stars will go here } |
這樣可以使得移動模式下,玩家只能移動。下一步就是要添加代碼使忍者能夠發射飛鏢。在else部分增加,在增加之前,先在HelloWorld.cpp中添加一些清理代碼:
1
2
3
4
5
|
void HelloWorld::projectileMoveFinished(Object *pSender) { Sprite *sprite = (Sprite *)pSender; this ->removeChild(sprite); } |
好了,看到上面的else部分的註釋了嗎:
1
|
// code to throw ninja stars will go here |
在上面的註釋後面添加下面的代碼:
這段代碼會在用戶點擊屏幕的方向發射飛鏢。對於這段代碼的完整的細節,可以查看我改編的另一個文章《如何用Cocos2d-x 3.0製作一款簡單的遊戲:第一部分》。當然,查看原作者的文章後面的註釋會更加清楚明白一些。
projectileMoveFinished:方法會在飛鏢移動到屏幕之外的時候移除。這個方法非常關鍵。一旦我們開始做碰撞檢測的時候,我們將要循環遍歷所有的飛鏢。如果我們不移除飛出屏幕範圍之外的飛鏢的話,這個存儲飛鏢的列表將會越來越大,而且遊戲將會越來越慢。編譯並運行工程,現在,你的忍者可以向敵人投擲飛鏢了。
碰撞檢測
接下來,就是當飛鏢擊中敵人的時候,要把敵人銷燬。在HelloWorld Class類中增加以下變量(在HelloWorldScene.h文件中):
1
2
|
cocos2d::Vector<cocos2d::Sprite *> _enemies; cocos2d::Vector<cocos2d::Sprite *> _projectiles; |
然後初使化_projectiles數組:
1
2
3
4
5
|
// at the end of the launch projectiles section of onTouchEnded: _projectiles.pushBack(projectile); // at the end of projectileMoveFinished: _projectiles.eraseObject(sprite); |
然後在addEnemyAtPos方法的結尾添加如下代碼:
1
|
_enemies.pushBack(enemy); |
接着,在HelloWorld類中添加如下代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
void HelloWorld::testCollisions( float dt) { Vector<Sprite*> projectilesToDelete; // iterate through projectiles for (Sprite *projectile : _projectiles) { auto projectileRect = Rect( projectile->getPositionX() - projectile->getContentSize().width / 2, projectile->getPositionY() - projectile->getContentSize().height / 2, projectile->getContentSize().width, projectile->getContentSize().height); Vector<Sprite*> targetsToDelete; // iterate through enemies, see if any intersect with current projectile for (Sprite *target : _enemies) { auto targetRect = Rect( target->getPositionX() - target->getContentSize().width / 2, target->getPositionY() - target->getContentSize().height / 2, target->getContentSize().width, target->getContentSize().height); if (projectileRect.intersectsRect(targetRect)) { targetsToDelete.pushBack(target); } } // delete all hit enemies for (Sprite *target : targetsToDelete) { _enemies.eraseObject(target); this ->removeChild(target); } if (targetsToDelete.size() > 0) { // add the projectile to the list of ones to remove projectilesToDelete.pushBack(projectile); } targetsToDelete.clear(); } // remove all the projectiles that hit. for (Sprite *projectile : projectilesToDelete) { _projectiles.eraseObject(projectile); this ->removeChild(projectile); } projectilesToDelete.clear(); } |
最後,調度testCollisions:方法,把這些代碼加在HelloWorld類的init方法中。
1
|
this ->schedule(schedule_selector(HelloWorld::testCollisions)); |
上面的所有的代碼,關於具體是如何工作的,可以在本站查找《如何用Cocos2d-x3.0製作一款簡單的遊戲:第一部分》教程。當然,原作者的文章註釋部分的討論更加清晰。代碼儘量自己用手敲進去,不要爲了省事,alt+c,alt+v,這樣不好,真的!
好了,現在可以用飛鏢打敵人,而且打中之後它們會消失。現在讓我們添加一些邏輯,使得遊戲可以勝利或者失敗吧!
勝利和失敗
The Game Over Scene
好了,讓我們創建一個新的場景,來作爲我們的“You Win”或者“You Lose”指示器吧。在vs裏創建GameOverScene類。
然後用下面的代碼替換掉模板生成的GameOverScene.h的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
#include "cocos2d.h" class GameOverLayer : public cocos2d::LayerColor { public : GameOverLayer() :_label(NULL) {}; virtual ~GameOverLayer(); bool init(); CREATE_FUNC(GameOverLayer); void gameOverDone(); CC_SYNTHESIZE_READONLY(cocos2d::LabelTTF*, _label, Label); }; class GameOverScene : public cocos2d::Scene { public : GameOverScene() :_layer(NULL) {}; ~GameOverScene(); bool init(); CREATE_FUNC(GameOverScene); CC_SYNTHESIZE_READONLY(GameOverLayer*, _layer, Layer); }; |
相應地修改GameOverScene.cpp文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
#include "GameOverScene.h" #include "HelloWorldScene.h" USING_NS_CC; bool GameOverScene::init() { if (Scene::init()) { this ->_layer = GameOverLayer::create(); this ->_layer->retain(); this ->addChild(_layer); return true ; } else { return false ; } } GameOverScene::~GameOverScene() { if (_layer) { _layer->release(); _layer = NULL; } } bool GameOverLayer::init() { if (LayerColor::initWithColor(Color4B(255, 255, 255, 255))) { auto winSize = Director::getInstance()->getWinSize(); this ->_label = LabelTTF::create( "" , "Artial" , 32); _label->retain(); _label->setColor(Color3B(0, 0, 0)); _label->setPosition(Point(winSize.width / 2, winSize.height / 2)); this ->addChild(_label); this ->runAction(Sequence::create( DelayTime::create(3), CallFunc::create(CC_CALLBACK_0(GameOverLayer::gameOverDone, this )), NULL)); return true ; } else { return false ; } } void GameOverLayer::gameOverDone() { Director::getInstance()->replaceScene(HelloWorld::createScene()); } GameOverLayer::~GameOverLayer() { if (_label) { _label->release(); _label = NULL; } } |
GameOverLayer僅僅只是在屏幕中間旋轉一個label,然後調度一個transition隔3秒後回到HelloWorld場景中。
勝利場景
現在,讓我們添加一些代碼,使得玩家吃完所有的西瓜的時候,遊戲會結束。在HelloWorld類的setPlayerPositoin:方法中添加以下代碼,(位於HelloWorldScene.cpp中,就是_numCollected++後面:)
1
2
3
4
|
// put the number of melons on your map in place of the '2' if (_numCollected == 2) { this ->win(); } |
然後,在HelloWorld類中創建win方法:
1
2
3
4
5
6
|
void HelloWorld::win() { GameOverScene *gameOverScene = GameOverScene::create(); gameOverScene->getLayer()->getLabel()->setString( "You Win!" ); Director::getInstance()->replaceScene(gameOverScene); } |
不要忘了包含頭文件:
1
|
#include "GameOverScene.h" |
編譯並運行,當你吃完所有的西瓜後,就會出現如下畫面:
失敗場景
就這個教程而言,我們的玩家只要有一個敵人碰到他,遊戲是結束了。在HelloWorld類的testCollision方法中添加以列循環:
1
2
3
4
5
6
7
8
9
10
|
for (Sprite *target : _enemies) { auto targetRect = Rect( target->getPosition().x - (target->getContentSize().width / 2), target->getPosition().y - (target->getContentSize().height / 2), target->getContentSize().width, target->getContentSize().height); if (targetRect.containsPoint(_player->getPosition())) { this ->lose(); } } |
這個循環遍歷所有的敵人,只要有一個敵人精靈的圖片所在的矩形和玩家接觸到了,那麼遊戲就失敗了。接下,再創建lose方法:
1
2
3
4
5
6
|
void HelloWorld::lose() { GameOverScene *gameOverScene = GameOverScene::create(); gameOverScene->getLayer()->getLabel()->setString( "You Lose!" ); Director::getInstance()->replaceScene(gameOverScene); } |
編譯並運行,一旦有一個敵人碰到你,你就會看到下面的場景:
完整源代碼
這裏有這個教程的完整源代碼:TileGame3.zip。謝謝你們有耐心看到這裏。
接下來怎麼做?
建議:
-
增加多個關卡
-
增加不同類型的敵人
-
在Hud層中顯示血條和玩家生命
-
製作更多的道具,比如加血的,武器等等
-
一個菜單系統,可以選擇關卡,關閉音效,等等
-
使用更好的用戶界面,來使遊戲畫面更加精美,投擲飛鏢更加瀟灑。
-