如何使用Cocos2d-x 3.0製作基於tilemap的遊戲:第一部分
引言
程序截圖:
本教程將會教大家如何使用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,然後會出現以下對話框:
在 orientation部分,你可以選擇Orthogonal。Layer format我們也選默認的 Base64(zlib compressed)。
接下來,設置地圖的大小。記住,這個大小是以tile爲單位的,而不是以像素爲單位。我們將創建一個儘量小的地圖,因此選擇50*50。
最後,你指定每個tile的寬度和高度。你這裏選擇的寬度和高度要根據你的實際的tile圖片的尺寸來做。這個教程使用的樣例tile的尺寸是32*32,所以在上面的選項中選擇32*32.
接下來,我們把製作地圖所需要的tile集合導入進來。點擊菜單欄上面的“map”菜單,“New Tileset...”,然後會出現下面的窗口:
爲了獲得圖片,點擊“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。
一旦你選擇ok,你將會看到Tilesets窗口中顯示了一些tiles。現在,你可以製作地圖了!在Tilesets小窗口,選擇一個tile,然後再在地圖上的任意位置單擊,你就會看到你選中的tile出現在點中的地方了。
因此,繼續製作地圖吧---充分發揮你的聰明才智!確保增加至少一對建築物在地圖上,因爲後面我們需要一些東西來做碰撞。
記住一些方便的快捷方式:
-
你可以在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層中。
好了,就這麼多!編譯並運行工程,你將會看到地圖的左下角出現在窗口中。
還不錯!但是,這還不是一個遊戲!我們還需要三個東西:a)遊戲主角,b)主角初使位置和c)能夠移動視圖,這樣就好像是第一視角了。
好了,接下來讓我們來解決這些問題。
tiled對象層和設置tile地圖位置
tiled支持兩類層--tile層(就是我們目前使用的層),還有對象層。
對象層允許你在地圖上圈出一些區域,來指定一些事件的發生。比如,你可能想製作一個區域,在那裏怪物將會跳出來,或者是一個區域,只要進入就會死掉。這我們這個例子中,我們將創建一個區域來顯示我們的遊戲主角。
因此,找到Tiled的菜單,點擊” Layer\Add Object Group…”,命名爲“Objects”,然後選擇Ok。如下圖所示,首先在圖層出,把背景層前的勾選去除。在工具欄上方選擇矩形,畫一個小矩形,你將會注意到,它並沒有繪製一個tile,而是畫了一個很難看的灰色矩形,這個矩形我們之後可以擴展,使之能夠包含多個tiles或者移動它。我們只想要選擇一個tile來讓主角顯示。因此,在你的地圖上選擇一個tile。這個區域(下圖畫出的矩形)的大小實際上並沒有關係,因爲我們僅僅使用x、y座標。
然後,在矩形上面點右鍵, 取名爲“SpawnPoint",然後選擇確定:
(下面給出一些技巧。如何把一個對象準確放置到Background的空白區域,只需要調整背景層的opacity就可以了)
我們僅僅把這個類型設置爲空就行了,最後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座標值--但是如果你仔細想一下,有些東西我們並不想讓它顯示出來--比如,我們不想讓屏幕超過地圖的邊界(那些區域僅僅是一個空白區域!)
比如,看看下面這幅圖:
看一下,什麼時候camera的中心會小於winSize.width/2或者winSize.height/2,部分視圖將會在屏幕之外?類似的,我們需要檢查上面的界限區間,也和我們這裏的情形一樣。
因此,我們把這個函數看作是設置camera的視角中心點。然而。。。那不完全是我們想要的。在Cocos2d-x裏面有一種方式可以直接操作一個Node的camera,但是那會使事情變得更復雜。我們需要另一種替代方法,那就是移動整個層。
看看下面的圖:
想像一個大的地圖,我們查看從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地圖的基礎了,而且知道如何把它導入到遊戲當中。