Learn IPhoneand iPad Cocos2d Game Delevopment》第10章 。
相冊空間已滿,無法直接貼站外圖片。要查看圖片,請點擊鏈接。
## 使用 Tilemaps
接下來兩章,我將介紹基于貼圖的游戲世界。你也許玩過Ultima這樣的角色扮演游戲,或者剛剛把你Facebook上的朋友加進了Farmville。那么我可以肯定,你已經玩過了使用tilemap技術的游戲。
在tilemap游戲中,圖形由小圖片組成,稱作tiles(貼片),它們是緊挨在一起的;把它們放入一個個小格子里這就組成了我們的游戲世界。這個概念令人興奮,因為相比把整個世界當成一個貼圖來繪制,這樣更節約內存,同時允許更多的變化。
本章將使用所有貼圖種類中最簡單的一種貼圖:直角貼圖,來介紹一般的貼圖概念。它由正方形或矩形的貼片組成圖形,并以頂視圖的方式呈現游戲世界。例如Ultima系列一直以來都使用了貼圖技術。Ultima1-5使用正方形貼片,頂視圖視角;Ultima6-7 仍然使用直角貼圖,但使用了半等角投影透視視角。Ultima8:Pagan,是整個系列中唯一使用等角投影貼圖的游戲。等角投影貼圖在下一章討論。
我還會解釋如何滾動一個tilemap地圖,如何讓一個貼片始終保持在地圖中心,如何保持屏幕不會移出tilemap區域。觸摸你不希望聚焦的貼片會導致滾動,這意味著你會學到如何判斷被觸摸的貼片是哪一個。
## 貼圖是什么?
貼圖Tilemaps是用一個個貼片去組成2D游戲世界的技術。僅僅用幾張有著相同尺寸的圖片就可以創建出龐大的世界地圖。這意味著在大地圖中使用貼圖能有效地節省內存。這種技術應用于早期的電腦游戲。許多傳統RPG類游戲用正方形的貼片創建精彩的游戲世界。這些Tilemaps游戲看起來如圖所示:
[點擊打開鏈接](http://img.ph.126.net/mm7_kvre9APwuDkdf3Yl2A==/2749166097549894375.png)
Tilemaps通常用編輯器生成,有一種名叫Tiled(QT) 的編輯器可以直接支持cocos2d。Tiled是免費的,開源,并且允許你在多個圖層中編輯直角貼圖和等角投影貼圖。Tiled也允許你加入觸發器區域和對象,以及編輯貼片屬性——這樣你可以在代碼中判斷貼片的類型。
提示:Qt指諾基亞Qt框架,其內置了Tiled。因為還有一個Tiled的java版,因此用Tiled(Qt)加以區別。java版Tiled已不再更新,但其中包含的幾個特殊的功能仍然值得一看。但在這兩章里,我使用和討論的仍然是Tile(Qt)。
?
隨著時間的推移,方塊貼圖技術也得到一些改進,通過使用另一種貼圖技術——過渡貼片。例如,在緊挨草地貼片的地方,不直接使用水的貼片,而是使用額外的過渡貼片(例如,這個貼片中一邊包含了水,一邊包含了草地,中間是二者的分際線),這樣便可以創建出一種更平滑的過渡效果。如果不這樣做,你就要使用更多的貼片,花更多的心思考慮一個貼片如何才能過渡到另外的一個貼片,并讓貼片種類保持在一定水平。過渡貼片是值得一提的。
上圖中使用了許多過渡貼片。在其名為Desert(沙漠)的貼片集中只有4種地形的貼片:沙土、礫石(在tilemap的下半部分)、磚石(在左上部)、泥土(在右上部)。除了沙土之外的3種貼片,每一種都有12種額外的貼片用于過渡到沙土背景貼片。
貼片并不一定得是正方形;也可以創建矩形貼片的直角貼圖。在亞洲地區的RPG游戲里,經常使用這種貼圖,例如DragonQuest4-6。當使用直角透視的時候,這使設計者創建的對象看起來高比寬長。這制造并呈現了深度感。等角透視貼圖則通過斜45度透視來加深這一點。它使用偽3D風格的貼片,使游戲世界獲得視覺深度。等角透視貼圖能夠“欺騙”我們的大腦,仿佛這就是一個3D世界,盡管所有的圖片仍然是平面的。等角透視貼圖通過用一個個菱形的貼圖達到深度感,并允許距離觀察者較近的貼圖遮擋住較遠的。下圖為一個等角透視貼圖的例子。
[點擊打開鏈接](http://img.ph.126.net/QDbPuMBLKmvdzrYsvwOhbw==/2696811751881710575.png)
等角透視貼圖地圖說明tilemaps地圖不一定是平面的。使用方塊貼圖技術你可以達到這樣的效果:仿佛每個貼片天生就嚴絲合縫地放在其他貼片上面。因此,Tiled支持多圖層以創建一種類似3D的效果,如下圖所示。
[點擊打開鏈接](http://img.ph.126.net/DL-QaDaPI0VW7olY5sR9nw==/1543045822344729403.png)
在等角透視貼圖中,能夠使用分層的貼片,許多Farmville玩家視頻展現這一效果。有的Farmville玩家僅僅在莊稼地里不用一磚一石就建造出房屋甚至摩天大廈。其實就是利用了人的錯覺,用等角透視貼圖很容易做到這一點。
## 使用Zwoptex準備圖片
在本章的Tilemap01項目的Resouces/individualtile images目錄中,你會找到許多方塊狀的貼片圖像。把所有圖片加到Zwoptex中,并把畫布大小設為256*256——這個大小已經足夠。點擊Apply按鈕,Zwoptex自動把它們安排妥當。結果顯示如下:
[點擊打開鏈接](http://img.ph.126.net/wMzo5eJU7J7usoo2YifjxQ==/3092002619183471046.png)
注意,Zwoptex用隨機順序排放這些貼片。很不幸,寫這本書的時候,Zwoptex1.04還不支持按名稱排列貼片。否則,這個布局應當是按照貼片在磁盤中的文件名排放的。這個功能對許多Zwoptex用戶來說很重要,因此在以后的版本可能會支持這個功能。查看你的Zwoptex版本是否支持這個功能,如果支持,首先分別編輯你的貼片文件,然后用Zwoptex從這些貼圖文件中創建排序的貼圖集。
你仍然可以使用隨機排序的圖片,但擋你添加或刪除貼片并點擊“Apply”之后,這些貼片又恢復到原來的位置。Zwoptex好像會對貼片進行隨機重排。在使用CCSpriteBatchNode時,這根本不成為問題,因為你可以通過名稱引用某個圖片。
對于Tiled,則不一樣了。保持貼片位置不變是至關重要的,因為Tiled是通過位置+偏移來引用貼片的。
這意味著,如果貼片改變了在貼圖集中的位置,使用該貼圖集的tilemap地圖將完全變成另外一個樣子。tilemap仍然會引用貼片在貼圖集中的同一位置,但那個位置已經替換成一個水的貼片,而原來是一個草地貼片。
辦法是加一些空的貼片填充到貼圖集中(貼圖集大小至少要和你需要的一樣)。目的是簡單地做出一個繪圖空間。關鍵是把所有的空貼片加到Zwoptex以創建一個貼圖集結構,其中包含了貼片所占據的空間,但貼片實際是空的。然后關閉Zwoptex,你不再需要它了,因為你可以用任何圖片編輯程序打開這個貼圖紋理集,并且在圖片不透明的地方進行編輯。Zwoptex已經在貼圖集中標明了每個貼片所在的原始位置。
如果你比我更有藝術天分,可能會用圖形處理程序直接創建tilemap地圖。那么你需要保證圖形的背景必須是透明的。這可防止地圖顯示在游戲中時,在貼片的邊緣出現縫隙。而且,所有的貼片都應是同樣的寬和高,并且每個貼片之間的間隔也必須是固定的。
使用圖形處理程序可能比僅僅創建一些空白的貼片,然后用Zwoptex對齊要花更多的時間。后者只需處理一次,而且更加快捷。
## Tiled 地圖編輯器
創建cocos2d使用的tilemaps地圖,最常用的工具是Tiled地圖編輯器。它生成的TMX文件被cocos2d引擎所支持。Tiled的免費的,在編寫本書的時候,版本是0.5。你在它的主頁www.mapeditor.org上就可以下載它。
如果你愿意支持Tiled的開發工作,請捐助該項目:
http://sourceforge.net/donate/index.php?group_id=161281.
## 新建 Tilemap
下載Tiled后,解壓并安裝。啟動Tiled,選擇View菜單并勾選Tilesets和Layers選項。這將顯示圖層列表,并在Tiled窗口右邊顯示當前貼片集。然后選擇File ? New 創建一個 tilemap。這將彈出新地圖對話框:
[點擊打開鏈接](http://img.ph.126.net/3cVPx9OubWmGiIHL0CnG4Q==/2861474613257445171.png)
當前,Tiled支持直角貼圖和等角透視貼圖。地圖的尺寸是以貼片數為單位,而不是像素。比如這里,新地圖將包括30*20個貼片,貼片大小為32*32像素。貼片尺寸必須和你的貼片文件尺寸吻合,否則它們會被對齊。
新地圖是空的,而且也沒有加載任何貼片集。通過菜單 Map ? New 可以加載貼片集。這會打開 NewTileset dialog 對話框:
[點擊打開鏈接](http://img.ph.126.net/83h1Qgd9blBbkg6-AQixMg==/567453553065598251.png)
在其中,你可以瀏覽正確的貼片集圖片。一個貼片集是一個圖片文件名,在該圖片中包含了多個等大貼片,因此你也可以稱之為只包含等大圖片的貼圖集。
我將使用dg_grounds32.png貼片集。這些貼片由David E. Gervais 創作,并依據 Creative Commons License 發布, 這意味著你在尊重原作者的期刊下,你可以任意分享和編輯這些圖片。在[http://pousse.rapiere.free.fr/tome/index.htm](http://pousse.rapiere.free.fr/tome/index.htm) 你可以下載到他的更多作品.
在上圖中,我已經通過Browse按鈕加入了dg_grounds32.png貼片集,它就位于Tilemap01工程的Resources目錄下。如果你鉤上“Use transparent color” 勾選框, 透明區域被繪制為粉紅色(默認)。你可以保持不選擇該選項,因為目前使用的貼片沒有透明區域。
貼片的寬、高是每個貼片在貼片集中的大小。它們應當是32*32像素,等同于你創建地圖時的貼片大小。Margin和Spacing分別指定貼片邊框的寬度,以及貼片之間的間距。在這里,沒有Margin和Spacing,我都設為0。
如果你用Zwoptex對齊貼片并創建了貼圖集結構,你必須用Zwoptex的Margin和Spacing值來設定這兩個值。默認,Zwoptex使用2個像素的邊距。
載入貼片集圖片時,確保其位于項目的資源目錄下。還要確保把tilemapTMX文件保存到和貼片集文件的同一目錄。否則Cocos2d無法加載貼片集,加載TMX文件時會導致運行時錯誤。這種錯誤是由于TMX文件引用貼片集時采用了相對路徑。如果它們不在同一目錄,當程序被安裝到模擬器或設備后,cocos2d找不到圖片,因為目錄結構不存在。
## 編輯 Tilemap
貼片集加載后,你會看到一個空白地圖,激發你的創意并完成一個tilemap地圖。有一個辦法可以去掉這個空白地圖。使用一個默認的貼圖地圖是很好的開始。這里,我使用油漆桶工具(BucketFill)并選擇青草貼片,因此我的地圖現在是一片蔥蘢的草地:
[點擊打開鏈接](http://img.ph.126.net/_KKJfaXiv1M_FNgGL48TFA==/2882585236510744468.png)
Tiled有4中編輯模式,在工具欄最右邊有4個圖標:
1、Stamp Brush(快捷鍵B)
它允許你用貼片集中選擇的貼片進行繪圖;
2、Bucket Fill(快捷鍵F)
允許你用指定貼片填充區域;
3、Eraser(快捷鍵E)
擦除貼片;
4、Rectangular Select(快捷鍵R)
允許你選擇一個范圍,然后拷貝、粘貼選區內的貼片。
大部分時候,你在從貼片集中選擇貼片,然后用Stamp Brush在地圖上繪制。通過放置一個個貼片繪制基于貼片的游戲世界。
你還可以在多個圖層中編輯貼片,通過在圖層面板,你可以加入更多的圖層。選擇菜單Layer->Add Tile Layer可以創建新圖層。用多圖層的方式,你能在cocos2d中在地圖的不同區域中切換。在TileMap01項目中,我用圖層的方式,在冬夏之間進行切換。
你也可以用菜單Layer->Add Object Layer增加一個層,用于加入對象。在Tiled中對象是一些簡單的矩形,你可以通過代碼在其中繪制并讀取。你可以用它們觸發某些事件——例如,當玩家進入某個區域時產生怪。我隨機加入了幾個以顯示它們用cocos2d代碼是如何工作的。
Tiled還有一些功能是在右鍵菜單中。例如:剛才提到的矩形對象通過右鍵->RemoveObject可以刪除掉。注意,只有Layers面板中的某個圖層處于選中狀態時,右鍵菜單才有效。
通過右鍵并點擊屬性項,你也可以編輯對象、圖層、貼片的屬性。使用菜單Layer? AddTile Layer,創建一個圖層,將其命名為 GameEventLayer。選中 GameEventLayer, 選擇 Map ? New Tileset ,加載 game-events.png(和 dg_grounds32.png在同一目錄)。 其中有3個貼片。 在其中某個貼片上右鍵,選 Tile Properties, 然后添加一個isWater 屬性, 如圖所示。
[點擊打開鏈接](http://img.ph.126.net/MVBuJoI2G6VRo-aHrFs4tQ==/1024850390220412583.png)
提示: 注意每創建一個圖層都會帶來額外的開銷,尤其是你把貼片放在多個圖層的同一地方。這將導致兩個圖層都被繪制,并影響游戲性能。推薦盡可能地減少圖層的數量。對大多數游戲來說2-4個圖層足矣。加入新的tile圖層后應隨時注意游戲在設備上運行時的幀率。
現在,你可以在地圖中使用這些帶有isWater屬性的貼片了。畫出一條河吧。如果你想看看當前繪制的圖層下面是什么,可以在Layer面板中通過滑塊改變GameEventLayer的透明度,或者點擊圖層前面的“隱藏/取消隱藏”檢查框。
確認在保存TMX tilemap地圖前所有圖層的檢查框都是選中的。cocoas2d不會加載未勾選該檢查框的圖層。
最終,tilemap大概如圖所示。
[點擊打開鏈接](http://img.ph.126.net/yaaGJ_geSjsp9O79sFv8ZQ==/2772809995593586268.png)
把它保存在Resources文件夾,和貼圖集圖片放在一起。
在Cocos2d中使用直角貼圖
要在Cocos2d中使用TMX貼圖,首先要將TMX文件和相應的貼圖集圖片文件加到Xcode項目的Resources組中。在TileMap01項目中,我加入了orthogonal.tmx和 dg_grounds32.png 、game- events.png。加載和顯示tilemap地圖是很簡單的;只要在TileMapLayer類的init方法中加入以下代碼:
~~~
CCTMXTiledMap* tileMap = [CCTMXTiledMaptiledMapWithTMXFile:@"orthogonal.tmx"];
[self addChild:tileMap z:-1 tag:TileMapNode];
CCTMXLayer* eventLayer = [tileMaplayerNamed:@"GameEventLayer"]; eventLayer.visible = NO;
~~~
CCTMXTiledMap類用TMX文件名進行初始化并以tag值為標記加到了self中。你也可以把它申明為成員變量。接下來通過layerNamed方法獲得GameEventLayer對象。GameEventLayer是在Tiled中的圖層名。因為gameevents 圖層是通過代碼方式來決定某些貼片的屬性的,所以這個圖層不應當顯示出來。注意,如果你在Tiled中取消了某個圖層的選擇框,它也不會顯示,但你也無法訪問其貼片及貼片屬性。
如果現在運行該項目,你會看到如下界面:
[點擊打開鏈接](http://img.ph.126.net/ZZIvtMknkCLwWrTS6edrFw==/1328843365067921332.png)
現在你還不能用這個地圖做些什么,但我會改變這一點。在TileMap02項目,我會找到isWater貼片。我增加了ccTouchesBegan方法,如下所示,作用是判斷玩家是否碰到了某個貼片。
~~~
-(void) ccTouchesBegan:(NSSet *)toucheswithEvent:(UIEvent *)event
{
CCNode* node = [self getChildByTag:TileMapNode];
NSAssert([node isKindOfClass:[CCTMXTiledMapclass]], @"not a CCTMXTiledMap");
CCTMXTiledMap* tileMap = (CCTMXTiledMap*)node;
// 把觸摸點位置轉換為貼片坐標
CGPoint touchLocation = [selflocationFromTouches:touches];
CGPoint tilePos = [selftilePosFromLocation:touchLocation tileMap:tileMap];
// 檢查玩家是否碰到了水 (e.g., 通過貼片的 isWater 屬性)
bool isTouchOnWater = NO;
CCTMXLayer* eventLayer = [tileMaplayerNamed:@"GameEventLayer"];
int tileGID = [eventLayer tileGIDAt:tilePos];
}
if (tileGID != 0) {
NSDictionary* properties = [tileMappropertiesForGID:tileGID];
if (properties) {
NSString* isWaterProperty = [propertiesvalueForKey:@"isWater"]; isTouchOnWater = ([isWaterPropertyboolValue] == YES);
}
// 如果玩家碰到了水,進行某些動作
if (isTouchOnWater) {
}
} else {
}
[[SimpleAudioEngine sharedEngine]playEffect:@"alien-sfx.caf"];
// 取得winter圖層,并將它變成可視狀態
CCTMXLayer* winterLayer = [tileMaplayerNamed:@"WinterLayer"]; winterLayer.visible =!winterLayer.visible;
~~~
獲取CCTMXTiledMap 對象沒有什么特別的地方。觸摸位置首先轉換為屏幕坐標,然后使用tilePosFromLocation方法很快就把屏幕坐標轉換成貼片坐標(tilemap中的貼片索引)。
這里提到了全局標識GIDs的概念,它是指分配給每個貼片的唯一整型值(在一個tilemap中)。在地圖中,貼片被以從1開始的連續數字編號。GID為0,表示空貼片。CCTMXLayer的tileGIDAt方法會根據指定的貼片坐標返回貼片的GID。
然后,從tilemap獲得名為GameEventLayer的CCTMXLayer。這是那個定義了isWater貼片并以河流圖片繪制過的圖層。tileGIDAt方法返回貼片的唯一id。如果id為0,意味著在圖層的這個位置沒有任何貼片——如果這樣,說明該貼片已經移出,則觸摸到的貼片也不會是一個isWater貼片。
CCTMXTileMap有一個propertiesForGID方法,它返回一個NSDictionary,包含了該GID所代表的貼片的有效的屬性——在Tiled中我們曾經編輯過這些屬性。dictionary把所有的鍵值對都當作NSSTring儲存。如果你想看看某個NSDictionary都有些什么,可以用CCLOG語句打印出來:
~~~
CCLOG(@"NSDictionary 'properties'contains:\n%@", properties);
~~~
這將在控制臺窗口中打印類似如下的內容:
~~~
2010–08-30 19:50:52.344 Tilemap[978:207]NSDictionary 'properties' contains: {
isWater = 1;
}
~~~
你在處理tilempas的過程中,會與各種NSDictionary對象打交道。打印它們的內容可以讓你快速查看NSDictionary或其他任何iPhoneSDK集合類中的內容 。有時,這是一種有用的技巧。
NSDictionary中的每個屬性通過NSDictionary的valueForKey方法來檢索,并返回NSString。要想從NSString轉換為bool值,只需使用NSString的boolValue方法。類似地,NSString的intValue和floatValue方法可得到整數和浮點數。
ccTouchesBegan方法結尾,判斷了玩家是否觸碰到了水,是的話則發出某個聲音。然后,檢索WinterLayer圖層并讓其顯示。季節變化當然沒有這么簡單。這只是演示如何利用Tiled中的多圖層改變整個地圖,而無需單獨加載一個完整的tilemap地圖。
如果只想單個貼片,可以使用removeTileAt和setTileGID方法移除或替換某個圖層的貼片:
~~~
?[winterLayerremoveTileAt:tilePos];
[winterLayer setTileGID:tileGID at:tilePos];
~~~
定位觸摸的貼片位置
Locating Touched Tiles
在這兩行代碼中,我曾提到過tilePosFromLocation方法:
~~~
// 把觸摸點位置轉換為貼片坐標
CGPoint touchLocation = [selflocationFromTouches:touches];
CGPoint tilePos = [selftilePosFromLocation:touchLocation tileMap:tileMap];
~~~
首先,觸摸位置被轉換成屏幕坐標。這句代碼以前就學習過,但我仍然重新羅列一下具體的實現代碼以供參考:
~~~
-(CGPoint) locationFromTouch:(UITouch*)touch
{
CGPoint touchLocation = [touch locationInView:[touch view]];
return [[CCDirector sharedDirector]convertToGL:touchLocation];
}
-(CGPoint) locationFromTouches:(NSSet*)touches {
return [self locationFromTouch:[touchesanyObject]];
}
~~~
在把觸摸點位置轉換為屏幕坐標后,tilePosFromLocation方法被調用。它需要兩個參數:觸摸位置以及一個tileMap指針。這個方法包含了一些數學運算,我會作一些簡單解釋:
~~~
-(CGPoint) tilePosFromLocation:(CGPoint)locationtileMap:(CCTMXTiledMap*)tileMap
{
// 必須減去地圖的位置,因為地圖是滾動的
CGPoint pos = ccpSub(location,tileMap.position);
//必須轉換為int,因為返回結果是整數
pos.x = (int)(pos.x / tileMap.tileSize.width);
pos.y = (int)((tileMap.mapSize.height *tileMap.tileSize.height - pos.y) /tileMap.tileSize.height);
CCLOG(@"touch at (%.0f, %.0f) is attileCoord (%i, %i)", location.x, location.y, (int)pos.x, (int)pos.y);
NSAssert(pos.x >= 0 && pos.y >= 0&& pos.x < tileMap.mapSize.width && pos.y <tileMap.mapSize.height,
@"%@: coordinates (%i, %i) out ofbounds!", NSStringFromSelector(_cmd), (int)pos.x, (int)pos.y);
return pos;
}
~~~
如果你曾經用過tilemaps,這些代碼你會很熟悉,否則,你可能會一片茫然。等我來解釋一下。首先是將觸摸位置減去當前地圖的位置。在后面的Tilemap03項目中使用了貼圖滾動,因此地圖的位置很多時候并不是0,0。
為了使視角能夠向上(北)、右(東)進行滾動,你必須把地圖位置改變為負數。因為tilemap從位置0,0開始,即屏幕左下角。地圖的0,0點和屏幕的0,0點是重合的。如果你把地圖移到100,100,看起來好像是把視點向左下移。你經常會以為自己正在移動視角,其實沒有。移動的是tilemap圖層,要向地圖中心滾動,你必須把地圖坐標向負軸方向偏移。
接下來是簡單計算:要獲得tilemap的偏移量(我們知道永遠是負值),我們必須讓觸摸位置和tileMap.position相減。減去一個負數實際上是加上一個正數:
location(240, 160) – tileMap.position(-100,-100) = pos(340, 260)
因為地圖圖層從(0,0)移到了(-100,-100),而觸摸位置在(240,160),這整個偏移就應當是(340,260)。
如果考慮進滾動的偏移量,我們就能得到貼片的坐標。另外,你要知道(0,0)貼片的貼片坐標是在地圖的左上角。于此不同,屏幕坐標原點(0,0)卻位于屏幕左下角,而地圖坐標是從左上角開始。下圖顯示了一系列貼片的x,y坐標。這張截圖是在Tiledjava版中啟用 View ? Show Coordinates菜單得到的,這個功能Tiled Qt版不支持。
[點擊打開鏈接](http://img.ph.126.net/fZrjQeepMMITryFJ8TAZNw==/2534963640273081368.png)
因此為免混淆,使用下行代碼計算貼片的x坐標:
~~~
pos.x = (int)(pos.x / tileMap.tileSize.width);
?
~~~
tileMap.tileSize屬性是貼圖集中貼片大小(在這里是32*32)。如果觸摸點的x坐標是340,則上面的代碼會計算:
~~~
340 / 32 = 10.625
~~~
這當然不對,我們所有的貼片坐標都沒有小數!因為觸摸點位于貼片的內部(例如在一個32*32的方塊內)。簡單地把計算結果去除小數部分轉換成int值:
~~~
pos.x = (int)10.625 // pos.x == 10
~~~
這個轉換把小數點后面的數字消除。把小數部分消去是安全的,因為它們不但無用——反而有害。如果你不去掉小數部分,直接使用非整型的坐標檢索一個貼片,例如10.625,將導致一個運行時錯誤,因為只有x坐標為10和11的貼片,不存在貼片x坐標為10.625的貼片。
計算貼片的y坐標則更復雜一些:
~~~
pos.y = (int)((tileMap.mapSize.height *tileMap.tileSize.height - pos.y) / tileMap.tileSize.height);
~~~
注意括號的使用,這將確保最后才進行除運算。如果使用數字這個公式可能更容易理解:
~~~
pos.y = (int)((20 * 32 – 260) / 32)
~~~
在上式中,tileMap.mapsize是30*20個貼片,而每個貼片為32*32像素。
用tileMap.tileSize.height乘以tileMap.mapSize.height,得到tilemap的像素高度。這是必需的,因為tilemap的y軸是從上到下開始計算,而屏幕的y軸是從下到上的。通過計算出tilemap的最下端的y軸坐標,然后減去當前y坐標260,就能得到當前觸點在tilemap中的y坐標(像素)。由于這個結果是像素坐標,你需要除以tileSize.height然后取整,以再次折算成貼片坐標。
CCLOG和NSAssert用于在控制臺窗口查看計算結果,并確保貼片坐標不會出現不合理的值。這是一種學習手段,也是一種預防措施。
代碼優化和提高可讀性
由于地圖尺寸是固定不變的,你可以通過在類中增加一個成員變量來減少計算量,用該變量來保存地圖的像素高度:
~~~
floattileMapHeightInPixels;
~~~
在init方法中,在地圖被加載的時候,計算一次tileMapHeightInPixels就行了:
~~~
CCTMXTiledMap*tileMap=[CCTMXTiledMap tiledMapWithTMXFile:@"orthogonal.tmx"];
tileMapHeightInPixels= tileMap.mapSize.height * tileMap.tileSize.height;
~~~
現在你可以把計算公式進行重寫,這樣每次調用tilePosFromLocation方法時能夠節省一次乘法運算:
~~~
pos.y =(int)((tileMapHeightInPixels - pos.y) / tileMap.tileSize.height);
~~~
當然,這只能導致一個很小的性能改善,不能幫你贏得任何性能優化的獎項。但通過一個可讀性更好的變量名,能使計算公式更加簡單,易于閱讀。
使用 Object Layer
本章,我創建了一個包含了objectlayer(圖層名ObjectLayer)的例子:orthogonal.tmx。使用Layer->Add Object Layer菜單,可以創建Object層。然后點擊tilemap并在其中繪制一個矩形框。我覺得objectlayer這個名字有點讓人混淆,因為絕大部分游戲其實是把它當作一個“陷阱區域”使用,而不是真正意思上的對象。
在Tilemap03項目中,我在ccTouchesBegan方法中增加了許多代碼與objectlayer互動。下面列出了其中一部分代碼(在isWater判斷之后):
~~~
// 檢查是否觸摸到某個矩形對象
CCTMXObjectGroup*objectLayer = [tileMap
objectGroupNamed:@"ObjectLayer"];
boolisTouchInRectangle = NO;
?int numObjects = [objectLayer.objectscount];
for (int i =0; i < numObjects; i++) {
NSDictionary* properties = [objectLayer.objectsobjectAtIndex:i]; CGRect rect = [self getRectFromObjectProperties:properties
tileMap:tileMap];
if (CGRectContainsPoint(rect, touchLocation)) {
isTouchInRectangle = YES;
break;
}
}
~~~
因為object layers是一種特別的層,你不能用tilemap的layerNamed方法獲取objectlayer。在cocos2d,object layer其實是CCTMXObjectGroup類,這又是一個命名不當的例子,因為Tiled把它引用為objectlayer,而不是object group。通過tilemap的objectGroupNamed方法你可以獲得object layer對應的CCTMXObjectGroup,你只需要指定該objectlayer在Tiled中的名字。
緊接著,我遍歷了objectLayer的objecdts數組,它包含了由NSDictionary對象組成的列表。想起來了嗎?在前面我們曾經提到過的,tilemap的propertiesForGID方法返回的是NSDictionary屬性集,這里和它其實是同樣的東西。但有一點不同,propertiesForGID方法返回的是只讀的NSDictionary。
這些NSDictionary只是簡單地包含了每個矩形框的坐標。用getRectFromObjectProperties方法可以返回這個矩形:
~~~
-(CGRect)getRectFromObjectProperties:(NSDictionary*)dict tileMap:(CCTMXTiledMap*)tileMap{
float x, y, width, height;
x = [[dict valueForKey:@"x"] floatValue]+ tileMap.position.x;
y = [[dict valueForKey:@"y"] floatValue]+ tileMap.position.y;
width = [[dict valueForKey:@"width"]floatValue];
height = [[dict valueForKey:@"height"]floatValue];
return CGRectMake(x, y, width, height);
}
~~~
鍵x,y,width,height的值由Tiled賦值。通過valueForKey可以輕易地檢索它們的值,然后用floatValue方法把它們從NSString轉換為浮點值。x,y值需要加上tileMap的位置,因為矩形需要跟隨tilemap一起移動。最后,調用CGRectMake函數返回一個CGRect。
ccTouchesBegan方法中剩下來的代碼簡單地通過CGRectContainsPoint方法判斷觸摸點是否包含在這個矩形區域內。如果是,isTounchInRectangle標志置為true,并且退出for循環。因為沒有必要再判斷其他矩形是否包含了觸點了。在ccTouchesBegan最后,isTouchInRectangle標志被用于判斷是否在觸點位置顯示特殊效果。如果你觸摸到矩形范圍,這段代碼會產生粒子爆炸效果:
~~~
if (isTouchOnWater) {
[[SimpleAudioEnginesharedEngine] playEffect:@"alien-sfx.caf"];
} else if(isTouchInRectangle) ?{
? CCParticleSystem* system =[CCQuadParticleSystem
particleWithFile: @"fx-explosion.plist"];
system.autoRemoveOnFinish= YES;
system.position= touchLocation;
[selfaddChild:system z:1];
}
~~~
繪制Object Layer
當你運行Tilemap03項目時,你會注意到對象層的矩形框已經繪制在tilemap上了。
[點擊打開鏈接](http://img.ph.126.net/aSw2VkIgCedtQR81FnBOMg==/639229672126814676.png)
這不是tilemaps或者對象層的標準特性。這是用OpenGL ES代碼繪制的矩形框。每個CCNode都會有一個–(void)draw 方法,你可以覆蓋該方法,加入自己的OpenGL ES代碼。我習慣于用這些代碼進行調試,畫一些看得見的線、圓、或者矩形,以便于碰撞測試或者查看物體間距離。在這個例子里通過這種方法,能夠實實際際地看見對象層的位置。用可見的方式勝于在調試器中查看坐標值,因為可視化的方式要比比較和計算數值更直觀。
-(void) draw 方法會在播放幀時自動調用。但是,要有限度地使用該方法去改變節點的屬性,因為這會對節點的繪制造成影響。下面是TileMapLayer類的draw方法。
~~~
-(void) draw
{
CCNode* node = [self getChildByTag:TileMapNode];
NSAssert([node isKindOfClass:[CCTMXTiledMapclass]], @"not a CCTMXTiledMap");
CCTMXTiledMap* tileMap = (CCTMXTiledMap*)node;
// 獲取對象層
CCTMXObjectGroup* objectLayer = [tileMapobjectGroupNamed:@"ObjectLayer"];
// 線寬:3 像素
glLineWidth(3.0f);
glColor4f(1, 0, 1, 1);
int numObjects = [[objectLayer objects] count];
for (int i = 0; i < numObjects; i++) {
NSDictionary* properties = [[objectLayerobjects] objectAtIndex:i]; CGRect rect = [selfgetRectFromObjectProperties:properties
tileMap:tileMap];
[self drawRect:rect];
}
glLineWidth(1.0f);
glColor4f(1, 1, 1, 1);
}
~~~
首先,通過tag獲得一個tilemap,并調用objectGroupNamed方法獲得對象層的CCTMXObjectGroup對象。然后把線寬設為3個像素(glLineWidth方法),顏色設置為紫色(glColor4f方法)。這將影響后續的OpenGLES畫線的線寬和顏色——不僅僅是當前方法,也會對其他用OpenGL ES節點繪制的行為有影響(例如,任何定義在CCDrawingPrimitives.h頭文件中的用于畫線、圓、多邊形的方法)。這也是為什么我在畫完之后又重置glLineWidth和glColor4f的原因。在OpenGL代碼中保持使用前的狀態是一種良好的風格,否則,你可能會改變其他繪制代碼的輸出結果。OpenGL采用了狀態機制,因此你所改變的每個設置都會被記住并且會影響到下一個繪制方法。為此,你對OpenGL設置進行改變之后,應當在你繪制完畢后把它們設置回默認狀態。
注意: draw方法中的代碼總是在z順序為0的地方繪制。而且它會在所有z順序為0的其他節點之前繪制。這意味著任何OpenGLES節點都會被z順序0的其他節點所覆蓋。為此,我不得不把tileMap放在了z順序-1,因為矩形框要繪制在tilemap之上。
我遍歷了所有對象層中的對象,從他們的NSDictionary屬性集中獲得對象的CGRect,然后傳遞給drawRect方法。但不幸的是,cocos2d遺漏了這個有用的方法,因此我使用ccDrawLine簡單實現了這個方法:
~~~
drawn before all other nodes at z-order 0, whichmeans that any
-(void) drawRect:(CGRect)rect {
// 矩形由4個點線構成:pos1、pos2、pos3、pos4
pos1 = CGPointMake(rect.origin.x,rect.origin.y);
pos2 = CGPointMake(rect.origin.x, rect.origin.y+ rect.size.height);
pos3 = CGPointMake(rect.origin.x +rect.size.width, rect.origin.y +
rect.size.height);
pos4 = CGPointMake(rect.origin.x +rect.size.width, rect.origin.y);
ccDrawLine(pos1, pos2); ccDrawLine(pos2, pos3);ccDrawLine(pos3, pos4); ccDrawLine(pos4, pos1);
}
~~~
用CGPoint創建了矩形的4個頂點,然后用ccDrawLine方法把兩點連成線段。你可能需要把這個方法放在安全的地方并記住,因為很可能再次用到它。
注意,draw方法和drawRect方法用 #ifdef DEBUG和 #endif 語句包括起來。這表示在編譯發布版本時對象層的矩形不會被繪制,因為我只需要在調試時需要它們,而最終用戶并不會看見它們。
~~~
#ifdef DEBUG
-(void) drawRect:(CGRect)rect {
...
}
-(void) draw{
}
#endif
~~~
## 滾動地圖
終于來到最后的部分:滾動。實際上這很簡單,因為只需移動CCTMXTiledMap就行了。在Tilemap04工程中,我在捕捉到了觸摸點的貼片坐標之后,在ccTouchesBegan方法中調用了centerTileMapOnTileCoord方法:
~~~
ccTouchesBegan:(NSSet *)toucheswithEvent:(UIEvent *)event{
...
// 從觸摸點獲得貼片坐標
CGPoint touchLocation = [selflocationFromTouches:touches];
CGPoint tilePos = [selftilePosFromLocation:touchLocation tileMap:tileMap];
// 移動tilemap,使得觸摸點位于屏幕的中心
?[self centerTileMapOnTileCoord:tilePos tileMap:tileMap];
?...
}
~~~
下面是 centerTileMapOnTileCoord 方法, 它移動了tilemap并使觸摸到的貼片居于屏幕的中心,并且如果地圖已經到達屏幕邊緣則停止滾動。
~~~
-(void) centerTileMapOnTileCoord:(CGPoint)tilePostileMap:(CCTMXTiledMap*)tileMap{
// 把 tilemap 中心對齊指定的貼片位置
CGSize screenSize = [[CCDirector sharedDirector]winSize];
CGPoint screenCenter =CGPointMake(screenSize.width * 0.5f, screenSize.height *
0.5f);
// 貼片坐標以左上角為坐標原點
tilePos.y = (tileMap.mapSize.height - 1) -tilePos.y;
// 屏幕坐標以左下角為原點
CGPoint scrollPosition = CGPointMake(-(tilePos.x* tileMap.tileSize.width),
-(tilePos.y * tileMap.tileSize.height));
// 貼片中心和屏幕中心的偏移點
scrollPosition.x += screenCenter.x -tileMap.tileSize.width * 0.5f;
scrollPosition.y += screenCenter.y -tileMap.tileSize.height * 0.5f;
// 確保地圖滾動到地圖邊緣的時候停止
scrollPosition.x = MIN(scrollPosition.x, 0);
scrollPosition.x = MAX(scrollPosition.x,-screenSize.width);
scrollPosition.y = MIN(scrollPosition.y, 0);
scrollPosition.y = MAX(scrollPosition.y,-screenSize.height);
CCAction* move = [CCMoveToactionWithDuration:0.2f position: scrollPosition];
[tileMap stopAllActions];
[tileMap runAction:move];
}
~~~
計算出屏幕中心位置后,我改變了tilePos的y坐標,因為tilemap的y軸方向是從上到下。而屏幕的y軸方向是從下向上。實際上,我轉換了tilePos的y軸,使它的方向從下向上。另外,我把地圖的高度減去一,因為貼片坐標實際上是從0開始。也就是說,如果地圖的高度是10,它的貼片坐標只能是0-9之間。
接下來,創建了一個scrollPosition變量,用于計算地圖將移動到的位置。第1步是把貼片坐標和地圖的貼片大小相乘。你可能奇怪,為什么我讓貼片的像素坐標取負值。因為如果我想將貼片從右上端向左下運動,必須減少地圖的坐標值。
接著,修改了scrollPosition的坐標,使貼片與屏幕中心點對齊。你要考慮到貼片自己的中心是位于貼片大小一半的地方,需要從screenCenter中扣除。
通過O-C的MIN和MAX宏,我們保證了scrollPosition的位置一定在地圖的邊界范圍內,不會顯示任何地圖以外的東西。MIN和MAX返回兩個參數中最小和最大的值,它們比使用if…else語句進行條件賦值要簡練。
最后,用一個CCMoveTo動作滾動地圖節點,以使觸摸到的貼片位于屏幕中央。這將使地圖滾動到你輕擊貼片的位置。你可以用同樣的方法滾動地圖到任何貼片上——比如,玩家所在的位置。
## 小結
你現在已經對tilemaps有一個不錯的概念了,并且知道如何用Tiled地圖編輯器創建多圖層的tilemap,并在游戲中運用圖層屬性。
用cocos2d加載和顯示tilemap是件簡單的事情,但獲取貼片和對象層,讀取并修改它們的屬性則顯得有些復雜。你也學到了如何判斷觸摸點的貼片坐標,并且使用貼片坐標進行地圖的滾動,以便觸摸點貼片位于屏幕的中央。
我還講解了一點點的OpenGL ES編程知識,用它我們可以自己在tilemap上繪制對象層矩形,以便調試。