1. 程式人生 > >11. 碰撞檢測和收集物品:如何使用cocos2d製作基於tiled地圖的遊戲:第二部分

11. 碰撞檢測和收集物品:如何使用cocos2d製作基於tiled地圖的遊戲:第二部分

免責申明(必讀!):本部落格提供的所有教程的翻譯原稿均來自於網際網路,僅供學習交流之用,切勿進行商業傳播。同時,轉載時不要移除本申明。如產生任何糾紛,均與本部落格所有人、發表該翻譯稿之人無任何關係。謝謝合作!

程式截圖:

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

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

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

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

tiled地圖和碰撞檢測

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

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

讓我們開始動手吧!再一次啟動Tiled軟體,點選“Layer\Add tile Lyaer…”,並且命名為“Meta”,然後選擇OK。我們將在這個層裡面加入一些假的tile代表一些“特殊tile”。

因此,現在我們需要增加我們的特殊tile。點選“Map\New tileset…”,在你的Resources資料夾下面找到mate_tiles.png,然後選擇開啟。設定Margin和Spacing都為1並點選OK。

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

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

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

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

(由於版本的關係,我這裡補充我上傳的Tiled編輯器(java版本)如何設定屬性!!!)

首先,選擇TileSets–>TileSetManager,並選中meta_tiles,出現如下所示圖:

然後點選右下角的“Edit”按鈕(就是垃圾回收站下面那個圖示),接下來會出現下圖所示:(接著就選中紅色tile和綠色tile,然後新增Collidable屬性並設定為True就ok啦)

儲存map,並返回Xcode。在HelloWorldScene.h中做如下改動:

// Inside the HelloWorld class declaration
CCTMXLayer *_meta;

// After the class declaration
@property (nonatomic, retain) CCTMXLayer *meta;

同時修改HelloWorldScene.m檔案如下:

// Right after the implementation section
@synthesize meta = _meta;

// In dealloc
self.meta = nil;

// In init, right after loading background
self.meta = [_tileMap layerNamed:@"Meta"];
_meta.visible = NO;

// Add new method
- (CGPoint)tileCoordForPosition:(CGPoint)position {
int x = position.x / _tileMap.tileSize.width;
int y = ((_tileMap.mapSize.height * _tileMap.tileSize.height) - position.y) / _tileMap.tileSize.height;
return ccp(x, y);
}

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

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

上面的截圖是java版本的tiled介面。能否顯示tile的座標,我不確定這個功能在QT版本的tiled中是否存在。不管怎麼說,我們將要使用的一些功能會使用tile座標,而不是x,y座標。因此,我們需要一種方式,將x,y座標轉換成tile座標。這正是那個函式所需要做的。

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

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

CGPoint tileCoord = [self tileCoordForPosition:position];
int tileGid = [_meta tileGIDAt:tileCoord];
if (tileGid) {
NSDictionary *properties = [_tileMap propertiesForGID:tileGid];
if (properties) {
NSString *collision = [properties valueForKey:@"Collidable"];
if (collision && [collision compare:@"True"] == NSOrderedSame) {
return;
}
}
}
_player.position = position;

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

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

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

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

動態修改Tiled Map

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

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

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

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

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

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

// Inside the HelloWorld class declaration
CCTMXLayer *_foreground;

// After the class declaration
@property (nonatomic, retain) CCTMXLayer *foreground;

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

// Right after the implementation section
@synthesize foreground = _foreground;

// In dealloc
self.foreground = nil;

// In init, right after loading background
self.foreground = [_tileMap layerNamed:@"Foreground"];

// Add to setPlayerPosition, right after the if clause with the return in it
NSString *collectable = [properties valueForKey:@"Collectable"];
if (collectable && [collectable compare:@"True"] == NSOrderedSame) {
[_meta removeTileAt:tileCoord];
[_foreground removeTileAt:tileCoord];
}

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

建立一個計分器

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

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

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

當然,這兩個層之間需要一種方式聯絡起來–Hud層應該知道什麼時候忍者吃了一個西瓜。有許許多多的方式可以使2個不同的層相互通訊,但是,我只介紹最簡單的。我們在HelloWorld層裡面儲存一個HelloWorldHud層的控制代碼,這樣的話,當忍者吃了一個西瓜就可以呼叫Hud層的一個方法來進行通知。

因此,在HelloWorldScene.h裡面增加下面的程式碼:

// Before HelloWorld class declaration
@interface HelloWorldHud : CCLayer
{
CCLabel *label;
}

- (void)numCollectedChanged:(int)numCollected;
@end

// Inside HelloWorld class declaration
int _numCollected;
HelloWorldHud *_hud;

// After the class declaration
@property (nonatomic, assign) int numCollected;
@property (nonatomic, retain) HelloWorldHud *hud;

同樣的,修改HelloWorldScene.m檔案:

// At top of file
@implementation HelloWorldHud

-(id) init
{
if ((self = [super init])) {
CGSize winSize = [[CCDirector sharedDirector] winSize];
label = [CCLabel labelWithString:@"0" dimensions:CGSizeMake(50, 20)
alignment:UITextAlignmentRight fontName:@"Verdana-Bold"
fontSize:18.0];
label.color = ccc3(0,0,0);
int margin = 10;
label.position = ccp(winSize.width - (label.contentSize.width/2)
- margin, label.contentSize.height/2 + margin);
[self addChild:label];
}
return self;
}

- (void)numCollectedChanged:(int)numCollected {
[label setString:[NSString stringWithFormat:@"%d", numCollected]];
}

@end

// Right after the HelloWorld implementation section
@synthesize numCollected = _numCollected;
@synthesize hud = _hud;

// In dealloc
self.hud = nil;

// Add to the +(id) scene method, right before the return
HelloWorldHud *hud = [HelloWorldHud node];
[scene addChild: hud];

layer.hud = hud;

// Add inside setPlayerPosition, in the case where a tile is collectable
self.numCollected++;
[_hud numCollectedChanged:_numCollected];

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

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

來點音效和音樂

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

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

// At top of file
#import ”SimpleAudioEngine.h”

// At top of init for HelloWorld layer
[[SimpleAudioEngine sharedEngine] preloadEffect:@”pickup.caf”];
[[SimpleAudioEngine sharedEngine] preloadEffect:@”hit.caf”];
[[SimpleAudioEngine sharedEngine] preloadEffect:@”move.caf”];
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@”TileMap.caf”];

// In case for collidable tile
[[SimpleAudioEngine sharedEngine] playEffect:@”hit.caf”];

// In case of collectable tile
[[SimpleAudioEngine sharedEngine] playEffect:@”pickup.caf”];

// Right before setting player position
[[SimpleAudioEngine sharedEngine] playEffect:@”move.caf”];

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

何去何從?

這個系列的教程,就此完結了。距離上次翻譯時間長了點。通過這個教程的學習,你對cocos2d裡面的tiled map的使用,應該有一個非常好的理解了。這裡有這個教程的完整原始碼。

如果你看了這個教程,有什麼好的意見或建議,可以自由發言,謝謝!