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

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

引言

程序截圖:

1406535642116249.png

本教程將會教大家如何使用Cocos2d-x來做一個基於tile地圖的遊戲,當然還有Tiled地圖編輯器。(我們小時候玩的小霸王小學機裏面的遊戲,大部分都是基於tile地圖的遊戲,如坦克大戰、冒險島、吞食天地等)我們將會創建一個忍者在沙漠中找西瓜喫的小遊戲。


在第一部分教程中,我將教大家如何使用Tile來創建地圖,怎樣把地圖加到遊戲中,怎麼讓地圖跟隨玩家滾動,以及怎樣使用對象層。

在第二部分教程中,我將介紹如何在地圖中創建可碰撞的區域,如何使用tile屬性,如何製作可拾取的物體和動態修改地圖,還有確保忍者不要喫撐了!


如果你還沒有準備好的話,你可能需要先從《如何用Cocos2d-x3.0製作一款簡單的遊戲》系列教程開始學起,因爲我們這個教程使用了大量的基本概念,而這些概念都可以從上面的教程中獲取。


好了,讓我們玩一玩tile地圖吧!


創建工程骨架

讓我們首先創建整個工程的骨架,這樣可以確保今後我們需要的文件都包含進來了,並且能夠跑起來。


首先工程命名爲TileGame。


接下來,下載遊戲資源文件。這個資源文件包裏包含了以下內容:

  • 玩家sprite。這個圖片和《如何用Cocos2d-x3.0製作一款簡單的遊戲》差不多。

  • 我使用cxfr這個工具製作的一些音效。

  • 我使用Garage Band製作的一些背景音樂。(查看這篇博文獲得更多的信息)

  • 我們將會使用的tile集合--它實際上會和tile地圖編輯器一塊兒使用,但是,我想把它放在這裏,餘下的事情會變得更容易。

  • 一些額外的“特殊”的tile,我將會在後面加以說明。


一旦你獲得了這些資源,解壓並把它拖到你的工程的“Resources”分組下面。(編者的話:上面的音頻資源都被編者轉成了mp3格式)


如果一切順利,所有的文件應該都在你的工程裏了。是時候製作我們的地圖了!


使用Tile來製作地圖

Cocos2d-x支持使用Tile地圖編輯器創建的TMX格式的地圖。(建議大家在安裝的時候選擇英文,本教程的Tile採用英文的)


下載完之後,直接雙擊運行。點擊File\New,然後會出現以下對話框:

1406535817577032.jpg

在 orientation部分,你可以選擇Orthogonal。Layer format我們也選默認的 Base64(zlib compressed)。


接下來,設置地圖的大小。記住,這個大小是以tile爲單位的,而不是以像素爲單位。我們將創建一個儘量小的地圖,因此選擇50*50。


最後,你指定每個tile的寬度和高度。你這裏選擇的寬度和高度要根據你的實際的tile圖片的尺寸來做。這個教程使用的樣例tile的尺寸是32*32,所以在上面的選項中選擇32*32.


接下來,我們把製作地圖所需要的tile集合導入進來。點擊菜單欄上面的“map”菜單,“New Tileset...”,然後會出現下面的窗口:

1406535828655253.jpg

爲了獲得圖片,點擊“Browse...”按鈕,然後定位到工程的的Resources文件夾,選擇 tmw_desert_spacing.png文件(我們剛纔解壓進去的),然後加到工程中去。它會基於文件名自動填充名稱。然後把新圖快 名稱命名爲“tmw_desert_spacing.png”.同時,設置下面的Tile spacing和Margin都爲1。


你可以保留寬度和高度爲32*32,因爲tile的實際大小也是這麼多。至於margin和spacing,我還沒找到任何好的文檔解釋如何設置這兩個值,下面是我的個人看法:

  • Margin就是當前的tile計算自身的像素的時候,它需要減去多少個像素(寬度和高度都包含在內)。(類比word、css的margin)

  • Spacing 就是相鄰兩個tile之間的間隔(同時考慮寬度和高度)(類比word、css的spacing)

  

如果你看看 tmw_desert_spacing.png,你將會看見每一個tile都有一個像素的空白邊界圍繞着,這意味着我們需要把margin和spacing設置爲1。

course_screenshot4.png

一旦你選擇ok,你將會看到Tilesets窗口中顯示了一些tiles。現在,你可以製作地圖了!在Tilesets小窗口,選擇一個tile,然後再在地圖上的任意位置單擊,你就會看到你選中的tile出現在點中的地方了。

1406535943531495.jpg

因此,繼續製作地圖吧---充分發揮你的聰明才智!確保增加至少一對建築物在地圖上,因爲後面我們需要一些東西來做碰撞。

1406535964366046.jpg


記住一些方便的快捷方式:

  • 你可以在Tileset拾取器中拖出一個方框,一次選取多個tile。

  • 你可以使用工具欄上的“Bucket Fill Tools”按鈕(就是一個桶那個)來基於一個基準tile繪製整個地圖。

  • 你可以使用“View\Zoom In...”和“View\Zoom out...”來放大和縮小地圖。


一旦你完成了地圖的繪製工作,在Layers選項卡的層上面雙擊(現在可以說是“Layer1”),然後重命名爲“Background”。然後點擊“File\Save”並且保存文件到你的工程的資源文件夾中,並且命名爲“TileMap.tmx”。


後面我們將會使用這個tmx來做一些有趣的事情,好了,讓我們把地圖加載到遊戲中去吧!


把tile地圖添加到Cocos2d-x的場景中

打開HelloWorldScene.h,然後添加一些成員變量:

1
2
3
4
cpp
private:
    cocos2d::TMXTiledMap *_tileMap;
    cocos2d::TMXLayer *_background;


然後在HelloWorldScene.cpp文件中做如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cpp
// Replace the init method with the following
bool HelloWorld::init()
{
    if ( !Layer::init() )
    {
        return false;
    }
 
    std::string file = "TileMap.tmx";
    auto str = String::createWithContentsOfFile(FileUtils::getInstance()->fullPathForFilename(file.c_str()).c_str());
    _tileMap = TMXTiledMap::createWithXML(str->getCString(),"");
    _background = _tileMap->layerNamed("Background");
 
    addChild(_tileMap, -1);
     
    return true;
}


這裏,我們調用TMXTiledMap類的一些方法,把我們剛剛創建的地圖文件加載進去。


一些簡明的TMXTiledMap的背景知識。它是一個Node,你可以設置它的位置和比例等。這個地圖的孩子是一些層,而且提供了一個幫助函數可以讓你通過層的名字得到層對象--我們上面就是通過這種方面獲得地圖背景的。每一個層都是一個SpriteSheet的子類,這裏考慮了性能的原因--但是這也意味着每一個層只能有一個tile集。


因此,我們這裏做的所有這些,就是指向一個tile地圖,然後保存背景層的引用,並且把tile地圖加到HelloWorld層中。


好了,就這麼多!編譯並運行工程,你將會看到地圖的左下角出現在窗口中。

1406536058606554.png


還不錯!但是,這還不是一個遊戲!我們還需要三個東西:a)遊戲主角,b)主角初使位置和c)能夠移動視圖,這樣就好像是第一視角了。


好了,接下來讓我們來解決這些問題。


tiled對象層和設置tile地圖位置

tiled支持兩類層--tile層(就是我們目前使用的層),還有對象層。


對象層允許你在地圖上圈出一些區域,來指定一些事件的發生。比如,你可能想製作一個區域,在那裏怪物將會跳出來,或者是一個區域,只要進入就會死掉。這我們這個例子中,我們將創建一個區域來顯示我們的遊戲主角。


因此,找到Tiled的菜單,點擊” Layer\Add Object Group…”,命名爲“Objects”,然後選擇Ok。如下圖所示,首先在圖層出,把背景層前的勾選去除。在工具欄上方選擇矩形,畫一個小矩形,你將會注意到,它並沒有繪製一個tile,而是畫了一個很難看的灰色矩形,這個矩形我們之後可以擴展,使之能夠包含多個tiles或者移動它。我們只想要選擇一個tile來讓主角顯示。因此,在你的地圖上選擇一個tile。這個區域(下圖畫出的矩形)的大小實際上並沒有關係,因爲我們僅僅使用x、y座標。

1406536122569547.jpg


然後,在矩形上面點右鍵, 取名爲“SpawnPoint",然後選擇確定:

course_screenshot9.jpg

(下面給出一些技巧。如何把一個對象準確放置到Background的空白區域,只需要調整背景層的opacity就可以了)

1406536163368547.jpg

我們僅僅把這個類型設置爲空就行了,最後Cocos2d-x會爲我們創建ValueMap保存相關數據,我們可以從中獲得對象的各種屬性,包含x,y座標。


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

1
2
3
cpp
// Inside the HelloWorld class declaration
cocos2d::Sprite *_player;


同樣,修改HelloWorldScene.cpp,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
cpp
// Inside the init method, after setting "_background =" 
TMXObjectGroup *objects = _tileMap->getObjectGroup("Objects");
CCASSERT(NULL != objects, "'Objects' object group not found");
auto spawnPoint = objects->getObject("SpawnPoint");
CCASSERT(!spawnPoint.empty(), "SpawnPoint object not found");
int x = spawnPoint["x"].asInt();
int y = spawnPoint["y"].asInt();
_player = Sprite::create("Player.png");
_player->setPosition(x, y);
addChild(_player);
setViewPointCenter(_player->getPosition());


好了,讓我們先歇會兒,來解釋一下對象層和對象組。首先,注意你通過TMXTiledMap對象的getObjectGroup方法來獲得對象層(而不是getObject方法)。它返回一個特殊的TMXObjectGroup對象。


我們然後調用TMXObjectGroup類的getObject方法來獲得一個ValueMap,這個map包含了關於對象的大量信息,包括x和y座標值,寬度和高度。在這個例子中,我們只關心x和y座標,因此,我們提取出這兩個信息,並且設置player的位置。


最後,我想設置這個視圖爲玩家所在的位置。因此,添加下面一個新方法到文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cpp
void HelloWorld::setViewPointCenter(Point position) {
    auto winSize = Director::getInstance()->getWinSize();
 
    int x = MAX(position.x, winSize.width / 2);
    int y = MAX(position.y, winSize.height / 2);
    x = MIN(x, (_tileMap->getMapSize().width * this->_tileMap->getTileSize().width) - winSize.width / 2);
    y = MIN(y, (_tileMap->getMapSize().height * _tileMap->getTileSize().height) - winSize.height / 2);
    auto actualPosition = Point(x, y);
 
    auto centerOfView = Point(winSize.width / 2, winSize.height / 2);
    auto viewPoint = centerOfView - actualPosition;
    this->setPosition(viewPoint);
}


好了,讓我解釋一下。假設這個函數是設置camera的中心。我們允許用戶傳入地圖上任何x、y座標值--但是如果你仔細想一下,有些東西我們並不想讓它顯示出來--比如,我們不想讓屏幕超過地圖的邊界(那些區域僅僅是一個空白區域!)


比如,看看下面這幅圖:

course_screenshot11.png

看一下,什麼時候camera的中心會小於winSize.width/2或者winSize.height/2,部分視圖將會在屏幕之外?類似的,我們需要檢查上面的界限區間,也和我們這裏的情形一樣。


因此,我們把這個函數看作是設置camera的視角中心點。然而。。。那不完全是我們想要的。在Cocos2d-x裏面有一種方式可以直接操作一個Node的camera,但是那會使事情變得更復雜。我們需要另一種替代方法,那就是移動整個層。


看看下面的圖:

course_screenshot12.png

想像一個大的地圖,我們查看從0到winSize.height/width的座標。我們的視圖的中心點是centerOfView,而且我們知道我們要把這個中心設置到哪裏(actualPositon)。因此,爲了使實際的位置和視圖中心相吻合,我們只需要把地圖往左下角移動即可!


這個可以通過使實際的位置減去視圖的中心位置來實現,然後設置HelloWorld層到那個點。


唉!太多理論了--讓我們看點實際的吧!編譯並運行項目,如果一切順利,你將會看到忍者在場景當中,然而視角也移過來了。


使忍者移動

我們已經有一個好的開端了,但是我們的忍者只是站在那兒不動!這可不像真正的忍者!


讓我們使忍者動起來吧,只需要讓忍者移動到用戶點擊的地方就行了。在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
39
40
41
42
43
44
45
46
47
48
cpp
// Inside init method
auto listener = EventListenerTouchOneByOne::create();
//lambda expression: advanced feature in C++ 11
listener->onTouchBegan = [&](Touch *touch, Event *unused_event)->bool return true; };
listener->onTouchEnded = CC_CALLBACK_2(HelloWorld::onTouchEnded, this);
this->_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
 
//add following method
void HelloWorld::setPlayerPosition(Point position)
{
    _player->setPosition(position);
}
 
void HelloWorld::onTouchEnded(Touch *touch, Event *unused_event)
{
    auto touchLocation = touch->getLocation();
    touchLocation = this->convertToNodeSpace(touchLocation);
 
    auto playerPos = _player->getPosition();
    auto diff = touchLocation - playerPos;
    if (abs(diff.x) > abs(diff.y)) {
        if (diff.x > 0) {
            playerPos.x += _tileMap->getTileSize().width;
        }
        else {
            playerPos.x -= _tileMap->getTileSize().width;
        }
    }
    else {
        if (diff.y > 0) {
            playerPos.y += _tileMap->getTileSize().height;
        }
        else {
            playerPos.y -= _tileMap->getTileSize().height;
        }
    }
 
    if (playerPos.x getMapSize().width * _tileMap->getMapSize().width) &&
        playerPos.y getMapSize().height * _tileMap->getMapSize().height) &&
        playerPos.y >= 0 &&
        playerPos.x >= 0)
    {
        this->setPlayerPosition(playerPos);
    }
 
    this->setViewPointCenter(_player->getPosition());
}


首先,在init方法中設置事件監聽器,讓監聽器的onTouchesBegan和onTouchEnded綁定不同的方法。因爲要想onTouchEnded能用,onTouchesBegan必須返回true。我們這裏只需onTouchesBegan發揮返回true的作用,所以就直接寫c++11的新特徵支持的也是Cocos2d-x3.0 支持的lambda表達式,不是太懂的話搜索一下。這樣發生觸摸事件,監聽器就會調用onTouchBegan匿名方法和onTouchEnded方法(注意是單數形式,而不是複數形式的onTouchesBegan和onTouchesEnded方法)


你可能會問,爲什麼我要講這個,因爲我們在 《如何用Cocos2d-x3.0製作一款簡單的遊戲》裏面使用的是onTouchesBegan和onTouchesEnded方法。那兩個方法可以,在這個教程裏用兩種方法都可以。但是,我想向大家介紹一個新方法,因爲它有兩個優點:

  • “你不需要處理std::vector&,劃分Touch並調度的工作全部由Cocos2d-x框架來完成。每一次方法調用,你只獲得了一個Touch。“

  • “你可以在onTouchBegan中返回true,這樣當前的層就可以接收touch事件回調。而且,只有當你返回true的時候,纔會響應move/ended/cancelled回調. 這個就使你從一些複雜的多觸摸判斷中解放出來了。


不管怎麼說,在我們的onTouchEnded裏面,我們轉換屏幕touch座標爲本地座標。


這是因爲,touch位置只是告訴我們屏幕視口的座標(比如100,100)。但是,我們我們滾動了地圖,這個位置實際可能對應地圖的(800,800)。因此,調用這個方法基於我們當前層的位置來決定touch的偏移。


接下來,計算出touch點和player的位置之差。我們必須基於touch位置選擇一個方向,因此,首先,我們需要計算出是上下移動還是左右移動。然後,我們比較正負值,決定具體的方向。


相應的,我們再調整player的位置,並且設置player的位置爲視口的中心位置,這個在上一節中已經用到了。


更新:注意,我們不得不添加一個安全檢查,來確保我們的player不會移到地圖之外!這一點,是Geek&Dad指出來的,謝謝你!


編譯並運行!你現在可以點擊鼠標,想讓盡者移到哪,它就移到哪兒!


何去何從?

這只是這個教程的一部分。此時,你應該瞭解一些創建tile地圖的基礎了,而且知道如何把它導入到遊戲當中。

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