如何使用Cocos2d-x 3.0製作基於tilemap的遊戲:第二部分

如何使用Cocos2d-x 3.0製作基於tilemap的遊戲:第二部分

引言

程序截圖:

course_screenshot1 (1).png

這篇教程是《如何使用Cocos2d-x 3.0製作基於tilemap的遊戲》的第二部分。在上一個教程中,我們創建了一個簡單的基於tiled地圖的遊戲,裏面有一個忍者在沙漠裏尋找可口的西瓜!


第一部分教程中,我們介紹瞭如何基於tiled創建地圖,怎樣把地圖增加到遊戲中去,以及如何滾動地圖來跟隨主角移動、還有如何使用對象層。


在這部分教程中,我們將會介紹如何在地圖中製作可以碰撞的區域,如何使用tile屬性,如果收集遊戲物品並且動態地修改地圖、如何確保你的忍者不會喫得太飽!


因此,讓我們繼續我們上篇教程所學並且讓它更像一個真實的遊戲吧!


tiled地圖和碰撞檢測

你可能已經注意到了,目前我們的忍者可以毫無阻攔地穿過牆壁和障礙物。他是一個忍者,但是即使是真正的忍者,他也沒這麼厲害啊!


因此,我們需要找到一種方法,通過把一些tile標記成“可碰撞的”,這樣的話,我們就可以防止玩家穿過那些點的位置。有很多方法可以做得到(包括使用對象層),但是,我想向你們展示一種新的技術。我認爲它更高效,並且也是一次好的學習鍛鍊--使用一個元層(meta layer)和層屬性。


讓我們開始動手吧!再一次啓動Tiled軟件,點擊“LayerAdd tile Lyaer...”,並且命名爲“Meta”,然後選擇OK。我們將在這個層裏面加入一些假的tile代表一些“特殊tile”。


因此,現在我們需要增加我們的特殊tile。點擊“MapNew tileset...”,在你的Resources文件夾下面找到mate_tiles.png,然後選擇打開。設置Margin和Spacing都爲1並點擊OK。


這時,你可以在Tilesets區域看到一個新的標籤。打開它,而且你會看到2個tile:一個紅色的和一個綠色的。

1406536841581243.jpg


這些tile並沒有什麼特殊的東西--我只是製作了一個簡單的圖片,裏面包含了一個紅色的和一個綠色的半透明tile。接下來,我們把紅色的tile當作是“可碰撞的”(後面我們會用到綠色的),然後,合適地繪製我們的場景。


因此,確保Meta層被選中,選擇"Stamp Brush"工具(菜單上面那個像圖章一樣的工具),選擇紅色的tile,然後把任何你不想讓忍者通過的地圖都塗一遍。當你做完的時候,應該看起來像下面的圖示一樣:

1406536868423345.jpg


接下來,我們可以設置tile的屬性,這樣的話,我們在代碼中就可以識別這個tile是“可以碰撞的(穿不過去的)”。在Tileset裏面的紅色tile上在,右擊,選擇“Properties...“。增加一個新的屬性,叫做”Collidable“,並且設置成”Ture“:


保存map,並返回編譯器。在HelloWorldScene.h中做如下改動:

1
2
// Inside the HelloWorld class declaration
cocos2d::TMXLayer *_meta;


同時修改HelloWorldScene.cpp文件如下:

1
2
3
4
5
6
7
8
9
10
11
// In init, right after loading background
_meta = _tileMap->getLayer("Meta");
_meta->setVisible(false);
 
// Add new method
Point HelloWorld::tileCoordForPosition(Point position)
{
    int x = position.x / _tileMap->getTileSize().width;
    int y = ((_tileMap->getMapSize().height * _tileMap->getTileSize().height) - position.y) / _tileMap->getTileSize().height;
    return Point(x, y);
}


好了,讓我們先停一會兒。像之前一樣,我會meta層聲明瞭一個成員變量,而且從tile map中加載了一個引用。注意,我們把這個字當作是不可見的,因爲我們並不想看見這些對象,它們的存在只是爲了說明,那個區域是可以碰撞的。


接下來,我們增加一個新的幫助方法,這個方法可以幫助我們把x,y座標轉換成”tile座標“。每一個tile都有一個座標,從左上角的(0,0)開始,到右下角的(49,49)。(本例中,地圖的大小是49×49)

course_screenshot4 (1).png

上面的截屏是java版本的tiled界面。能否顯示tile的座標,我不確定這個功能在QT版本的tiled中是否存在(編者表示qt版沒有,所以上面的直接引用原教程圖)。不管怎麼說,我們將要使用的一些功能會使用tile座標,而不是x,y座標。因此,我們需要一種方式,將x,y座標轉換成tile座標。這正是那個函數所需要做的。


獲得x座標非常容易--我們只需要讓它除以一個tile的寬度就可以了。爲了得到y座標,我們不得不翻轉一些東西,因爲,在Cocos2d-x裏面(0,0)是在左下角的,而不是在左上角。


接下來,把setPlayerPosition替換成以下內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void HelloWorld::setPlayerPosition(Point position)
{
    Point tileCoord = this->tileCoordForPosition(position);
    int tileGid = _meta->getTileGIDAt(tileCoord);
    if (tileGid) {
        auto properties = _tileMap->getPropertiesForGID(tileGid).asValueMap();
        if (!properties.empty()) {
            auto collision = properties["Collidable"].asString();
            if ("True" == collision) {
                return;
            }
        }
    }
 
    _player->setPosition(position);
}


在這裏,我們把玩家的x,y座標轉換成tile座標。然後,我們使用meta層中的getTileGIDAt方法來獲取指定位置點的GID號。


對了,什麼是GID呢?GID代表”全球唯一標誌符“(我個人意見)。但是,在這個例子中,我認爲它只是我們使用的tile的一種標識,它可以是我們想要移動的紅色區域。


當我們使用GID來查找指定tile的屬性的時候。它返回一個屬性字典,因此,我們可以遍歷一下,看是否有”可碰撞的“物體被設置成”True“,或者是僅僅就是那樣。編譯並運行工程,因此還沒有設置玩家的位置。


就這麼多!編譯並運行程序,它將會向你展示,現在你不能夠通過那些紅色的tile組成的地方了吧:

course_screenshot5.png

動態修改Tiled Map

目前爲此,我們的忍者已經有一個比較有意思的冒險啦,但是,這個世界有一點點無趣。而且簡單無任務事可做!加上,我們的忍者看起來比較貪喫,而且背景將會隨着玩家移動而移動。因此,讓我們創建一些東西讓忍者來玩吧!


爲了使之可行,我將不得不創建一個前景層,這樣做可以讓用戶收集東西。那樣的話,我們僅僅從前景層中刪除不用的tile(當tile被玩角拾取的時候),這個過程中,背景將會隨之移動。


因此,打開Tiled,選擇”LayerAdd Tile Layer...“,把這個層命名爲”Foreground“,然後選擇OK。確保前景層被選擇,而且增加一對可以拾取的物品在遊戲中。我喜歡放置一些向西瓜或者別的什麼東西。

1406537076645984.jpg


現在,我們需要把這些tile標記成可以拾取的,類似的,參照我們是如何把tile標誌成可以碰撞的。選擇Meta層,轉換到Meta_tiles。現在,我們需要使這些tile可以拾取,點擊”LayerMove Layer Up“來確保你的meta層是在最頂層,並且保持綠色可見的。

1406537127598026.jpg


接下來,我們需要爲tile增加屬性,這樣把它標記成可拾取的。點鍵點擊Tilesets選項卡里的綠色的tile,然後點“Properties...”,再增加一個新的屬性,命名爲“Collectable”,值設置爲“True”。


保存地圖,然後返回到編譯器。在HelloWorldScene.h中做如下修改:

1
2
// Inside the HelloWorld class declaration
cocos2d::TMXLayer *_foreground;


同時,相應地修改HelloWorldScene.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
// In init, right after loading background
_foreground = _tileMap->getLayer("Foreground");
 
// Add to setPlayerPosition, right after the if clause with the return in it
auto collectable = properties["Collectable"].asString();
if ("True" == collectable) {
    _meta->removeTileAt(tileCoord);
    _foreground->removeTileAt(tileCoord);
 
    this->_numCollected++;
    this->_hud->numCollectedChanged(_numCollected);
}

這裏是一個常用的方法,用來保存前景層的句柄。不同之處在於,我們測試玩家正朝之移動的tile是否含有“Collectable”屬性。如果有,我們就使用removeTileAt方法來把tile從mata層和前景層中移除掉。編譯並運行工程,現在你的忍者可以嚐嚐西瓜的滋味啦!

 

創建一個計分器

我們忍者非常高興地喫西瓜啦,但是,作爲一個遊戲玩家,我們想知道自己到底吃了多少個西瓜。你懂的,我們並不想讓他喫得太胖。


通常的做法是,我們在層上面添加一個label。但是,等一下:我們在不停地移動這個層,那樣的話,label就會看不到了,怎麼辦?


這是一個非常好的機會,如果在一個場景中使用多個層--這正是我們現在面臨的難題。我們將保留HelloWorld層,但是,我們會再增加一個HelloWorldHud層來顯示我們的label。(Hud意味着Heads up display,大家可以google一下,遊戲中常用的技術)


當然,這兩個層之間需要一種方式聯繫起來--Hud層應該知道什麼時候忍者吃了一個西瓜。有許許多多的方式可以使2個不同的層相互通信,但是,我只介紹最簡單的。我們在HelloWorld層裏面保存一個HelloWorldHud層的句柄,這樣的話,當忍者吃了一個西瓜就可以調用Hud層的一個方法來進行通知。


因此,在HelloWorldScene.h裏面增加下面的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Before HelloWorld class declaration
class HelloWorldHud : public cocos2d::Layer
{
public:
    void numCollectedChanged(int numCollected);
    virtual bool init();
    CREATE_FUNC(HelloWorldHud);
 
    cocos2d::LabelTTF *label;
};
 
// Inside HelloWorld class declaration
int _numCollected;
static HelloWorldHud *_hud;


同樣的,修改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
26
27
28
29
30
31
32
33
34
35
36
37
38
// At top of file
HelloWorldHud *HelloWorld::_hud = NULL;
 
// implement some methods of HelloWorldHud
bool HelloWorldHud::init()
{
    if (!Layer::init())
    {
        return false;
    }
 
    auto visibleSize = Director::getInstance()->getVisibleSize();
    label = LabelTTF::create("0""fonts/Marker Felt.ttf", 18.0f, Size(50, 20), TextHAlignment::RIGHT);
    label->setColor(Color3B(0, 0, 0));
    int margin = 10;
    label->setPosition(visibleSize.width - (label->getDimensions().width / 2) - margin,
        label->getDimensions().height / 2 + margin);
    this->addChild(label);
 
    return true;
}
 
void HelloWorldHud::numCollectedChanged(int numCollected)
{
    char showStr[20];
    sprintf(showStr, "%d", numCollected);
    label->setString(showStr);
}
 
// Add to the HelloWorld::createScene() method, right before the return
auto hud = HelloWorldHud::create();
_hud = hud;
 
scene->addChild(hud);
 
// Add inside setPlayerPosition, in the case where a tile is collectable
this->_numCollected++;
this->_hud->numCollectedChanged(_numCollected);


一切很明瞭。我們的第二個層從Layer派生,只是在它的右下角加了一個label。我們修改scene把第二個層也添加進去,然後傳遞一個Hud類的引用給HelloWorld層。然後修改HelloWorldLayer層,當計數器改變的時候,就調用Hud類的方法,這樣就可以相應地更新Hud類了。


編譯並運行,如果一切ok,你將會在屏幕右下角看到統計忍者喫西瓜的Label。


來點音效和音樂

如果沒有很cool的音效和背景音樂的話,這就不能算作是一個完整的遊戲教程了。


增加音效和音樂非常簡單,只需在HelloWolrdScene.cpp作如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// At top of file
#include "SimpleAudioEngine.h"
using namespace CocosDenshion;
 
// At top of init for HelloWorld layer
SimpleAudioEngine::getInstance()->preloadEffect("pickup.mp3");
SimpleAudioEngine::getInstance()->preloadEffect("hit.mp3");
SimpleAudioEngine::getInstance()->preloadEffect("move.mp3");
SimpleAudioEngine::getInstance()->playBackgroundMusic("TileMap.mp3"true);
 
// In case for collidable tile
SimpleAudioEngine::getInstance()->playEffect("hit.mp3");
 
// In case of collectable tile
SimpleAudioEngine::getInstance()->playEffect("pickup.mp3");
 
// Right before setting player position
SimpleAudioEngine::getInstance()->playEffect("move.mp3");

現在,我們的忍者可以開懷大吃了!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章