如何制作一款像超级玛丽兄弟一样基于平台的游戏-第一部分 (xcode,物理引擎,TMXTiledMap相关应用)
這篇文章還可以在這里找到?英語
Learn how to make a game like Super Mario!
這是一篇IOS教程組的成員?Jacob Gundersen發布的教程, 他是一位獨立游戲開發者,經營著Indie Ambitions?博客。去看看他最新的app吧Factor Samurai!
對于我們中的很多人來說,超級瑪麗往往是帶我們進入激情無限的游戲世界的第一款游戲。
雖然電視游戲始于Atari(雅達利),之后擴展到很多平臺。但是隨著超級瑪麗的來臨,它直觀簡單的操作、豐富有趣的關卡設計等都是極為激動人心的進步,以致于讓人們感覺它是全新的,我們甚至幾個小時持續不斷的玩兒它!
在本篇教學中,我們將重拾超級瑪麗的魔力并制作一款你自己的平臺跳躍游戲,由于我們使用了一只考拉代替了水管工,所以我們稱其為“超級考拉兄弟”! ;]
另外,為了保持簡單性,我們將不會加入敵人,這樣不用在地面上來回躲避,過關會比較容易,同時也能專注在平臺游戲的核心部分-物理引擎。
本篇教學假設你已經熟悉Cocos2D的開發流程。如果你剛接觸Cocos2D,那么請先跟隨網站上的其他教程。
你確定你合格了嗎?(原文中是koala-fications,音似qualifications,開玩笑的目的)那么我們就開始吧!
準備工作 Getting Started
在開始之前,請先下載本篇教學的初始工程。
下載完后,解壓之,在Xcode中打開,編譯并運行。你將會在屏幕上看到以下內容:
Starter Project for Super Mario tutorial
就是它,一個沒意思的空屏幕! :]你將會在之后的教學中逐步填充它。
初始工程僅僅是一個框架,主要是將之后所需的圖片/聲音資源集成到了工程里。大致瀏覽一下,里邊都包含了以下內容:
- 游戲圖片?包含了Ray的老婆Vicki提供的一系列免費游戲圖片。
- 關卡地圖?我做了一張關卡地圖,你肯定知道它,因為它是模仿的超級瑪麗的第一關。
- 免費的音樂和音效?這怎么說也是一篇raywenderlich.com的教程啊,對吧 :]
- 一個CClayer的子類. 一個叫做GameLevelLayer的類,它將會為你處理大部分的物理引擎的工作。目前它還空空如也,等待著你去填充它!
- 一個CCSprite的子類?一個叫做Player的類,它將會包含考拉的邏輯。目前它等待著你讓它飛起來呢!(不好意思打了這么多比喻!)
當你瀏覽了項目并清楚的知道里邊都有了些什么之后,就可以繼續閱讀了,我們將會討論一些有關物理引擎的哲學。
物理引擎的本質 The Tao of Physics Engines
一個平臺游戲室基于它的物理引擎的,本篇教學中你將會從頭創建你自己的物理引擎。
我們不使用現有的物理引擎,比如Box2D或Chipmunk,有兩個主要原因決定你需要自己實現它。
一個物理引擎主要做兩件事:
Forces acting on Koalio.
舉個例子,在你的考拉游戲中你將會對其施加一個向上的力,用以是它跳躍。隨著時間變化,重力將會將它落下,于是就形成了一個經典的拋物線跳躍。
至于碰撞檢測,你將會使用它來保證你的考拉一直在地面之上,并且檢測它和地面上的障礙的碰撞。
讓我們看看這些是如何在實際中起作用的。
物理工程學 Physics Engineering
在接下來要創建的物理引擎中,用來描述考拉運動的變量有:當前速率(速度),加速度,和位置。使用這些變量,考拉每一步的運動都將遵循以下算法:
每一幀都會執行以上步驟。在本游戲中,重力的作用是持續向下推考拉,一直穿過地面,但是地面的碰撞處理會把它彈回到地面之上。你也可以通過此方法來檢測考拉是否和地面有接觸,如果沒有,那么考拉則不能起跳,因為這時它正在跳躍中或者是剛剛從突出的平臺上下來。
步驟1-5將完全的針對考拉對象。所有必要的信息都包含在這里邊,并且讓考拉自己來更新自己的變量。
但是,當你到達第六步,也就是碰撞檢測時,你需要考慮所有的關卡中的東西,比如墻,地面,敵人和其他危險的物體。碰撞檢測每一幀都會在GameLevelLayer中被執行,記住,這個類將會承擔很多物理引擎的工作。
如果你允許考拉的類更新它自己的坐標,那么當它移動到一個有碰撞的墻或者地面時,GameLevelLayer將會把他拉回,這樣就會陷入循環,考拉看起來會來回顫抖。(考拉,你是咖啡喝的有點多嗎?)
所以,你將不會讓考拉更新自己的坐標,相反的,考拉會保存一個新的變量,desiredPosition,考拉實時更新它。GameLevelLayer將會通過碰撞檢測來判斷desiredPosition是否是合理的,之后GameLevelLayer會負責更新考拉的坐標。
明白了嗎?讓我們試一下并看看代碼應該是什么樣子的!
加載TMXTiledMap Loading the TMXTiledMap
我會假設你已經熟悉如何使用tile map了。如果你不熟悉的話,請先跟隨?此篇教學?學習一些基礎。
讓我們看一下關卡里都一些什么。啟動你的Tiled地圖編輯器(如果你沒安裝請先下載),打開工程目錄里的level1.tmx,你將會看到以下內容:
在側邊欄中,你會看到有三個不同的層:
- hazards: 這個層包含了考拉需要躲避的東西。
- walls: 這個層包含了考拉不能穿越的東西,大部分是地面。
- background: 這個層僅僅是為了裝飾,比如云彩和山。
現在就來編碼!打開GameLevelLayer.m,在#import之后@implementation之前加入以下內容:
| @interface GameLevelLayer() {CCTMXTiledMap *map; }@end |
這一步在類中加入了一個tile map私有的變量。
接下來你需要在init部分加載此地圖。在init方法中加入以下代碼:
| CCLayerColor *blueSky = [[CCLayerColor alloc] initWithColor:ccc4(100, 100, 250, 255)]; [self addChild:blueSky];map = [[CCTMXTiledMap alloc] initWithTMXFile:@"level1.tmx"]; [self addChild:map]; |
首先,添加一個有顏色的背景,在這里就是一個藍天。另外兩行代碼作用是把tile map(一個 CCTMXTiledMap對象)加載到層中。
然后,在GameLevelLayer.m中,導入Player.h:
| #import "Player.h" |
同樣在GameLevelLayer.m,加入以下成員變量到@interface部分中:
| Player * player; |
然后把考拉加入到關卡中,在init中加入添加以下代碼:
| player = [[Player alloc] initWithFile:@"koalio_stand.png"]; player.position = ccp(100, 50); [map addChild:player z:15]; |
這些代碼加載了代表考拉的sprite對象,為其附一個坐標,并且添加它到地圖中。
你可能不解為什么不把考拉對象直接添加到layer中呢。原因如下,考拉對象需要和TMX layers里的對象交互,所以考拉對象應該是map的一個子節點。考拉對象應該放在最上層,所以你設置它的Z-order為15.還有,當你滾動你的tile map時,考拉是會跟著tile map一起移動的。
OK,來試試看!編譯并運行你將會看到如下內容:
看起來像個游戲了,但是考拉并沒有服從重力,是時候使用物理引擎讓它回到地面上來了,記得跟它說聲再見 :]
重力對考拉的影響 The Gravity of Koalio’s Situation
為了完成物理模擬,你可以寫一整套復雜的邏輯,根據考拉狀態的不同,對其施加不同的力,但是這樣做會很快變得復雜起來,而且這并不是真正的物理。在真實世界里,重力會把物體往地球的方向拉,所以你需要在每一幀都對考拉施加一個不變的重力。
其他的力并不是簡單的打開和關閉。在真實世界里,一個力作用到物體上產生沖量,沖量會持續的移動物體,直到有其他的力改變當前沖量。
舉例來說,一個豎直方向的力比如跳躍并不會使重力失效,只是其產生的沖量克服了重力,重力逐漸的減慢上升的速度,并最終把物理帶回到地面。類似的,一個移動的物體受到摩擦力的影響,最終會停下來。
這就是創建物理引擎模型的方法。你并不持續不斷地檢測考拉是否在地面上并根據狀態時不時的施加重力,重力是一直存在的。
扮演上帝 Playing God
物理引擎中力對于物體的作用是這樣的,當一個力作用到一個物體上后,這個物體會持續不斷地運動直到有另外的力抵消這個力。當考拉從突起的平臺上走過時,他會以一個加速度落下,直到他碰到障礙為止,當你移動考拉時,如果你不持續的施加力,那個考拉最終會因為摩擦力的作用而停止下來。
隨著你的平臺游戲的逐漸完善,你會發現這個邏輯會讓復雜的情況變得簡單,比如在一個冰面上,考拉是不可能停在一個硬幣上的,再比如貼著懸崖邊上的下落其實是一個自由落體。這種力逐漸累加的模型會讓游戲更有趣,更具動感。
這樣做也會讓實現起來容易些,因為你并不需要一直計算物體的狀態 – 他們僅僅需要遵循你的世界中的自然法則即可,他們的行為會自動由程序處理。
有些時候,你要扮演上帝!
地面的規則:CGPoints和Forces
首先定義一些術語:
- Velocity(速率)?用來描述一個物體在一個特定方向上的移動的有多快。
- Acceleration(加速度)?是速率變化的速率,用來描述物體的速度隨著時間的變化快慢。
- force(力)?是導致速率和方向變化的原因。
在物理模擬中,當一個力被施加到一個物體上,會瞬間給物體一個特定的速率,之后此物體會以這個特定的速率移動下去,直到其他的力施加其上。如果沒有外力作用,速率會在每一幀保持穩定。
你將會使用CGPoint結構來表示三個概念:速度,力/加速度(速度的變化),和位置。有兩個原因決定了使用CGPoint結構:
考拉對象在每一幀都會有一個特定的速度變量,它是由一系列力共同決定的,這些力包含重力,向前/跳躍的力和摩擦力,其中摩擦力會逐漸減慢考拉速度并最終使其停止下來。
在每一幀,你都需要將這些力累加,累加后的力會影響前一幀考拉的速度,并計算得到當前的速度。然后,當前速度需要乘上當前幀的時間系數來適當縮減,這個系數一般來說是個很小的數,最終這個速度會移動考拉。
注意:?如果以上這些讓你感到迷惑的話,那么你可以參考Dainel Shiffman寫的一篇很棒的?教學?,它基于向量解釋了力是如何累加的。這篇教學是為Processing語言設計的,雖然Processing是一種類似Java的語言,但是其中的概念是一致的。我強烈推薦你瀏覽一遍它。
讓我們從重力開始。首先創建一個可以用來施加力的循環。在GameLevelLayer.m中,向init函數if塊兒中的末尾添加以下內容:
| [self schedule:@selector(update:)]; |
然后在類中加入以下方法:
| -(void)update:(ccTime)dt {[player update:dt]; } |
打開Player.h,作如下修改:
| #import <Foundation/Foundation.h> #import "cocos2d.h"@interface Player : CCSprite @property (nonatomic, assign) CGPoint velocity;-(void)update:(ccTime)dt;@end |
然后再Player.m中添加實現部分:
| #import "Player.h"@implementation Player@synthesize velocity = _velocity;// 1 -(id)initWithFile:(NSString *)filename {if (self = [super initWithFile:filename]) {self.velocity = ccp(0.0, 0.0);}return self; }-(void)update:(ccTime)dt {// 2CGPoint gravity = ccp(0.0, -450.0);// 3CGPoint gravityStep = ccpMult(gravity, dt);// 4self.velocity = ccpAdd(self.velocity, gravityStep);CGPoint stepVelocity = ccpMult(self.velocity, dt);// 5self.position = ccpAdd(self.position, stepVelocity); }@end |
我們來一步一步的解釋以上代碼:
恭喜你!你已經準備好了寫你的第一個物理引擎了!編譯并運行,看看結果吧!
不好!考拉穿過了地面掉下去了!讓我們來修正它。
夜的顛簸 – 碰撞檢測 Bumps In The Night – Collision Detection
碰撞檢測是物理引擎的基礎。碰撞檢測的種類很多,從簡單的矩形檢測,到復雜的3D mesh碰撞檢測。幸運的是,一個類似的平臺游戲僅僅需要最簡單的碰撞檢測引擎。
為了檢測考拉的碰撞,你需要檢測所有環繞考拉的tiles。然后,你需要使用一些內置的IOS方法來判斷考拉的碰撞框是否和tile的碰撞框有接觸。
注意:?你忘記了什么是bounding box(碰撞框)了嗎?它就是包圍sprite對象最小的矩形。通常它和sprite里的frame的大小是一致的(包含透明區域),但是當一個sprite旋轉后,情況會變得略微復雜些,不過不要因此焦慮,Cocos2D有一個輔助方法來幫你解決此類問題 :]
CGRectIntersectsRect和CGRectIntersection方法正是用來解決此類問題的。CGRectIntersectsRect檢測兩個巨型是否相交,CGRectIntersection則能返回兩個相交矩形中間相交的部分。
首先,你需要找到考拉的碰撞框。每一個sprite對象都有一個和它的紋理一樣大小的碰撞框,通過boundingBox屬性可以獲取到。但是,你往往需要一個更小一些的碰撞框。
為什么呢?紋理通常都會在邊緣有一些透明的區域,就拿我們的考拉來說吧,你并不想讓它的透明區域也參與碰撞,而只想讓實際有像素的區域有碰撞。
有時候讓碰撞之間有一丁點像素重疊也是很好的。想像一下,當馬里奧碰到墻而不能移動時,他是一點兒也不能移動了,還是他的手臂和鼻子稍微陷進去一些呢?
我們這就試試。在Player.h中,添加:
| -(CGRect)collisionBoundingBox; |
在Player.m中,添加:
| -(CGRect)collisionBoundingBox {return CGRectInset(self.boundingBox, 3, 0); } |
CGRectInset方法可將一個CGRect縮減指定的像素,寬高有第二和第三個參數指定。對于我們來說,寬度將比原碰撞框縮減6像素-兩邊分別3像素。
繁重的工作 Heavy Lifting
是時候來做一些重活了。(考拉說:“你是覺得我胖的跳不起來了嗎?”(Heavy Listing有很難提起來的意思))。
為了完成碰撞檢測,你需要在GameLevelLayer中添加一系列方法,有以下這些:
- 一個返回當前考拉位置周圍8個tile的坐標的方法。
- 一個方法用來判斷這8個tile中是否包含有碰撞屬性的。有一些tile是不具有碰撞屬性的,比如背景中的云朵,這些僅僅是裝飾作用而已。
- 一個方法根據優先級來處理碰撞。
為了更輕松的實現以上方法,你還需要創建兩個輔助方法。
- 一個計算考拉當前tile坐標的方法。
- 一個根據tile坐標得到tile真實點坐標矩形框的方法。
先來處理這些輔助方法。在GameLevelLayer.m中添加一下代碼:
| - (CGPoint)tileCoordForPosition:(CGPoint)position {float x = floor(position.x / map.tileSize.width);float levelHeightInPixels = map.mapSize.height * map.tileSize.height;float y = floor((levelHeightInPixels - position.y) / map.tileSize.height);return ccp(x, y); }-(CGRect)tileRectFromTileCoords:(CGPoint)tileCoords {float levelHeightInPixels = map.mapSize.height * map.tileSize.height;CGPoint origin = ccp(tileCoords.x * map.tileSize.width, levelHeightInPixels - ((tileCoords.y + 1) * map.tileSize.height));return CGRectMake(origin.x, origin.y, map.tileSize.width, map.tileSize.height); } |
第一個方法根據你傳入的點坐標得到tile坐標。為了得到tile坐標,你只需把點坐標除以tile的大小。
你需要翻轉一下高度坐標,因為Cocos2D/OpenGL的坐標系原點是左下角,但是tile map的坐標系原點是左上角。他們使用的不同的標準。
第二個方法的工作跟第一個方法相反。它得到的是tile的真實點坐標。同樣,因為坐標系的關系,需要翻轉高度坐標。通過map.mapSize.height * map.tileSize.height計算得到地圖的總高度,然后再減去tile的高度。
為什么你需要在此多加一個tile的高度呢?請記住,tile坐標系統是從0開始算的,所以第20個tile實際上的坐標是19,如果你不多加上一個tile的坐標,實際得到的結果將會是19 * tileHeight。
我被Tile包圍啦! I’m Surrounded By Tiles!
現在把注意力放到獲取周圍tile上。在這個方法里你將會構建一個數組并將其傳遞給接下來的方法中。這個數組包含了tile的GID,tile的tilemap坐標,以及這個tile所表示的CGRect的信息。
你將要按照處理碰撞的優先級順序來安排這個數組。例如,你想要首先按照位于考拉左,右,下,上的順序來處理碰撞,之后再考慮對角線上的tile。另外,當你處理位于考拉腳下的tile時,你需要判斷此時考拉是否和地面有接觸。
還是在GameLevelLayer.m中,加入以下方法:
| -(NSArray *)getSurroundingTilesAtPosition:(CGPoint)position forLayer:(CCTMXLayer *)layer {CGPoint plPos = [self tileCoordForPosition:position]; //1NSMutableArray *gids = [NSMutableArray array]; //2for (int i = 0; i < 9; i++) { //3int c = i % 3;int r = (int)(i / 3);CGPoint tilePos = ccp(plPos.x + (c - 1), plPos.y + (r - 1));int tgid = [layer tileGIDAt:tilePos]; //4CGRect tileRect = [self tileRectFromTileCoords:tilePos]; //5NSDictionary *tileDict = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:tgid], @"gid",[NSNumber numberWithFloat:tileRect.origin.x], @"x",[NSNumber numberWithFloat:tileRect.origin.y], @"y",[NSValue valueWithCGPoint:tilePos],@"tilePos",nil];[gids addObject:tileDict]; //6}[gids removeObjectAtIndex:4];[gids insertObject:[gids objectAtIndex:2] atIndex:6];[gids removeObjectAtIndex:2];[gids exchangeObjectAtIndex:4 withObjectAtIndex:6];[gids exchangeObjectAtIndex:0 withObjectAtIndex:4]; //7for (NSDictionary *d in gids) {NSLog(@"%@", d);} //8return (NSArray *)gids; } |
呼-真是不少的代碼!不過別著急,我們會一點一點來解釋它們的。
在我們繼續之前,請先留意一下,參數里有一個layer對象,在你的tiled map中,有我們之前談到過的3個layer-harzards(危險物層),walls(墻)和backgrounds(背景)。
分層使得你可以根據不同層來分別處理碰撞檢測。
- 考拉 vs. 危險物.?如果考拉碰觸到了一個危險物層的東西,你將會殺死這只可憐的考拉(相當的殘忍,不是嗎?)
- 考拉 vs. 墻.?如果考拉碰觸到了墻層里邊的東西,那么將要停止考拉繼續像這個方向移動。“停下來,野獸!”
- 考拉 vs. 背景.?如果考拉碰觸到了背景層里的東西,你不會做任何事情,懶程序員是最好的一類程序員…或者僅僅是他們自己說的 ;]
盡管還有很多方法可以用來區分不同屬性的物體,但是對你來說,用層來區分是最具效率的。
OK,現在讓我們一步一步過一遍上面的代碼。
注意:?你僅僅需要8個tile的信息,因為永遠也不需要計算3X3塊兒最中心的那一個。
你應當總是在考拉周邊的tile處理碰撞。如果在考拉中心的tile出現了碰撞,那說明考拉在一幀中至少移動了半個他的寬度的距離。他永遠不該移動的如此之快的,至少在本游戲中是這樣的。
為了讓遍歷這8個tile更容易,我們先把考拉的中心tile加入進來,并在最后移除它。
有這樣一種很容易發生的情景,你在處理考拉正下方的tile碰撞時,也同時會處理對角線上的tile。請看右邊的示例圖。紅色的部分是考拉正下方的tile,同時你也需要處理#2用藍色標識的部分。
你的碰撞檢測子程序會按照一定邏輯來處理碰撞。通常直接連接的tile比對角線的tile更應該被檢測到,所以你盡可能的避免去檢測對角線的碰撞。
這張圖片顯示了原始的tile順序,和重新排序之后的順序,你可以看到,重排之后的順序是下,上,左,右。注意這個順序也可以幫助你在檢測的一開始就知道考拉是否和地面有接觸,這個結果決定了它是否可以跳躍,你將在之后的教程中接觸這些。
馬上就可以驗證一切都是正確的了!但是,還是有些一些事情要先做。你需要把walls layer作為成員變量加入到GameLevelLayer中,以方便以后使用。
在GameLevelLayer.m中,做如下修改:
| // Add to the @interface declaration CCTMXLayer *walls;// Add to the init method, after the map is added to the layer walls = [map layerNamed:@"walls"];// Add to the update method [self getSurroundingTilesAtPosition:player.position forLayer:walls]; |
編譯并運行!很不幸的。。。程序掛掉了,你可以在console(控制臺)中看到如下內容:
首先你得到了一些tile的位置信息,并不時的會出現一些GID的值,這些GID大多數是0,因為此時已經處于開放空間了。
最終,程序會因為TMXLayer: invalid position錯誤掛掉。這是因為tileGIDat:方法取到了tile map范圍之外的坐標。
我們稍后將使用一些措施預防這個問題,不過當前,你將通過碰撞檢測來解決它。
收回考拉的權限 Taking Away Your Koala’s Privileges
知道現在為止,考拉還自己設置自己的坐標呢。不過現在,你要收回它這樣做的權限。
如果考拉自己更新自己的坐標,那么當GameLevelLayer發現一個碰撞時,你將要拉回考拉讓其返回原處。你并不想讓你的考拉彈來彈去的,像一只亂竄的貓對吧!
所以,他需要一個新的持續更新的變量,這就是desiredPosition,它和GameLevelLayer之間有一些秘密的聯系。
我們現在讓考拉計算它自己他渴望的坐標。但是由GameLevelLayer負責更新考拉的實際坐標,考拉渴望的坐標需要經過碰撞檢測的驗證之后才會被變為它真正的坐標。同樣的策略也適用于tile的碰撞檢測循環,直到所有的tile都被檢測并處理過后,你才希望碰撞檢測器更新實際的sprite。
你需要做一些改動。首先,在Player.h中加入新的屬性:
| @property (nonatomic, assign) CGPoint desiredPosition; |
在Player.m中添加synthesize部分:
| @synthesize desiredPosition = _desiredPosition; |
現在,按如下修改Player.m中的collisionBoundingBox方法:
| -(CGRect)collisionBoundingBox {CGRect collisionBox = CGRectInset(self.boundingBox, 3, 0);CGPoint diff = ccpSub(self.desiredPosition, self.position);CGRect returnBoundingBox = CGRectOffset(collisionBox, diff.x, diff.y);return returnBoundingBox; } |
這一步根據desired坐標計算得到bounding box,這個bounding box在之后的碰撞檢測中會用到:
注意:?有很多不同的方法可以得到這個新的碰撞框。雖然你可以使用類似CCNode中的boundingBox和transform方法,但是我們目前使用的這個方法更簡單,盡管繞了些圈子。
接下來,對update方法作如下修改,我們使用desiredPosition屬性來替換掉原先的position屬性:
| // Replace this line 'self.position = ccpAdd(self.position, stepVelocity);' with: self.desiredPosition = ccpAdd(self.position, stepVelocity); |
處理碰撞 Let’s Resolve Some Collisions!
現在是時候動真格的了。你將在此把以上的內容串聯到一起。在GameLevelLayer.m中加入以下方法:
| -(void)checkForAndResolveCollisions:(Player *)p { NSArray *tiles = [self getSurroundingTilesAtPosition:p.position forLayer:walls ]; //1for (NSDictionary *dic in tiles) {CGRect pRect = [p collisionBoundingBox]; //2int gid = [[dic objectForKey:@"gid"] intValue]; //3if (gid) {CGRect tileRect = CGRectMake([[dic objectForKey:@"x"] floatValue], [[dic objectForKey:@"y"] floatValue], map.tileSize.width, map.tileSize.height); //4if (CGRectIntersectsRect(pRect, tileRect)) {CGRect intersection = CGRectIntersection(pRect, tileRect); //5int tileIndx = [tiles indexOfObject:dic]; //6if (tileIndx == 0) {//tile is directly below Koalap.desiredPosition = ccp(p.desiredPosition.x, p.desiredPosition.y + intersection.size.height);} else if (tileIndx == 1) {//tile is directly above Koalap.desiredPosition = ccp(p.desiredPosition.x, p.desiredPosition.y - intersection.size.height);} else if (tileIndx == 2) {//tile is left of Koalap.desiredPosition = ccp(p.desiredPosition.x + intersection.size.width, p.desiredPosition.y);} else if (tileIndx == 3) {//tile is right of Koalap.desiredPosition = ccp(p.desiredPosition.x - intersection.size.width, p.desiredPosition.y);} else {if (intersection.size.width > intersection.size.height) { //7//tile is diagonal, but resolving collision verticallyfloat intersectionHeight;if (tileIndx > 5) {intersectionHeight = intersection.size.height;} else {intersectionHeight = -intersection.size.height;}p.desiredPosition = ccp(p.desiredPosition.x, p.desiredPosition.y + intersection.size.height );} else {//tile is diagonal, but resolving horizontallyfloat resolutionWidth;if (tileIndx == 6 || tileIndx == 4) {resolutionWidth = intersection.size.width;} else {resolutionWidth = -intersection.size.width;}p.desiredPosition = ccp(p.desiredPosition.x , p.desiredPosition.y + resolutionWidth);} } }} }p.position = p.desiredPosition; //7 } |
好的!讓我們看看剛剛實現的代碼。
暫停并考慮個困境… Pausing to Consider a Dilemma…
這里有個棘手的問題。你需要決定如何解決碰撞。
你想到的最好的方法也許是將考拉移動出碰撞的范圍,換句話說,就是把最后一步會和tile產生碰撞的移動撤銷。這種方法是一些物理引擎所使用的,但是你將要實現一個更好的方案。
考慮一下:重力持續不斷地向下拉考拉,考拉和它腳下的tile持續產生碰撞。
如果你的考慮正在向前移動,與此同時重力把它向下拉。如果你采取上面提到的方法解決碰撞的話,那么考拉將會同時向上和向后移動,這種情況并不是你想要的!
你的考拉需要向上移動一點兒,并且仍然向前移動。
同樣的問題也會出現在墻上滑動。如果玩家讓考拉緊貼著墻,考拉渴望的運動軌跡是斜向下對著墻的方向。撤銷這個軌跡會讓考拉同時向上和向遠離墻的方向移動,同樣也不是你想要的!你想讓考拉一直貼著墻逐漸變慢的向下移動。
因此,你需要決定何時處理豎直方向的碰撞,何時處理水平方向的碰撞,每一種都應該獨占處理。一些物理引擎總是優先處理一個方向的,但是你真正做決定是要依托于考拉和tile的相對位置關系的。所以,當tile在考拉的正下方時,你總是讓考拉向上移動。
那么對角線碰撞的情況又該如何處理呢?對我們來說,你將使用相交矩形來判斷如何行動。如果相交矩形的寬度比高度大,你就假定正確的碰撞解決方式是豎直方向的,如果高度大于寬度,那么就是水平方向的。
這一過程的穩定性依賴于考拉在邊界范圍內并且有一個穩定的幀率。稍后,你將會加入一些代碼來保證考拉不會掉的太快,如果掉的太快,考拉將有可能在一幀里移動過一整個tile,而這將導致問題。
當你決定了到底是從豎直方向還是水平方向解決碰撞之后,就可以根據相交矩形的大小來把考拉移動出碰撞的范圍。根據情況使用該矩形的高或寬,把考拉移動相應距離。
到此為止,你可能懷疑過為什么需要按順序解決tile的碰撞。你總是優先解決直接接觸的tile,然后才是對角線的tile。如果你按照先檢測下邊再檢測右邊的tile的順序,你就會讓考拉向豎直方向移動。
但是,也有可能出現碰撞的CGRect的高大于寬的情況,比如考拉剛剛接觸一個tile時。
請再次參考右邊的示例圖片。藍色區域又高又窄,因為這僅僅是一部分的碰撞區域,不過,如果你已經解決了正下方紅色區域的tile的碰撞的話,就可以避免解決藍色區域的碰撞了,問題也就隨之解決了。
回到代碼! Back to the Code!
回到怪物般的checkForAndResolveCollisions:方法…
這個方法是碰撞檢測系統的本質。它是一個最基本的系統。如果你想讓你的游戲移動更快或者還有其他目標,那么你需要適當修改它以達到一致的結果。在本篇文章的最后,我羅列了一些很棒的詳細講述碰撞檢測的教程。
我們這就試試它!還是在GameLevelLayer中,對update方法做如下修改:
| // Replace this line: "[self getSurroundingTilesAtPosition:player.position forLayer:walls];" with: [self checkForAndResolveCollisions:player]; |
你可以刪除或者注釋掉getSurroundingTilesAtPosition:forLayer:里邊的log語句:
| /*for (NSDictionary *d in gids) {NSLog(@"%@", d);} //8 */ |
編譯并運行!你是否對結果感到驚奇呢?
Koalio在地板接住了,但是最終還是陷了進去!怎么回事?
你能猜到漏掉了什么嗎?回想一下,每一幀你都給考拉施加重力,這意味著考拉一直在向下加速。
你持續不斷地增加速度,直到一幀中的速度足以越過一個tile了,這是之前我們討論過的一個問題。
每當你解決一個碰撞時,你同樣需要在那個方向上重置考拉的速速!考拉停止移動了,那么它的速度理應是0。
如果你不做這一步,你就會得到奇怪的結果,比如上面見過的穿越tile,還有一種情形,當你的考拉跳上了一個短平臺時,它會滑動過長的距離,這也是你應當避免的情況。
之前提到過你需要一個好的方法來讓考拉在地面的時候不能跳躍。現在就來為其設置一個標志,在checkForAndResolveCollisions:中加入以下內容:
| -(void)checkForAndResolveCollisions:(Player *)p {NSArray *tiles = [self getSurroundingTilesAtPosition:p.position forLayer:walls ]; //1p.onGround = NO; //Herefor (NSDictionary *dic in tiles) {CGRect pRect = [p collisionBoundingBox]; //3int gid = [[dic objectForKey:@"gid"] intValue]; //4if (gid) {CGRect tileRect = CGRectMake([[dic objectForKey:@"x"] floatValue], [[dic objectForKey:@"y"] floatValue], map.tileSize.width, map.tileSize.height); //5if (CGRectIntersectsRect(pRect, tileRect)) {CGRect intersection = CGRectIntersection(pRect, tileRect);int tileIndx = [tiles indexOfObject:dic];if (tileIndx == 0) {//tile is directly below playerp.desiredPosition = ccp(p.desiredPosition.x, p.desiredPosition.y + intersection.size.height);p.velocity = ccp(p.velocity.x, 0.0); //Herep.onGround = YES; //Here} else if (tileIndx == 1) {//tile is directly above playerp.desiredPosition = ccp(p.desiredPosition.x, p.desiredPosition.y - intersection.size.height);p.velocity = ccp(p.velocity.x, 0.0); //Here} else if (tileIndx == 2) {//tile is left of playerp.desiredPosition = ccp(p.desiredPosition.x + intersection.size.width, p.desiredPosition.y);} else if (tileIndx == 3) {//tile is right of playerp.desiredPosition = ccp(p.desiredPosition.x - intersection.size.width, p.desiredPosition.y);} else {if (intersection.size.width > intersection.size.height) {//tile is diagonal, but resolving collision vertiallyp.velocity = ccp(p.velocity.x, 0.0); //Herefloat resolutionHeight;if (tileIndx > 5) {resolutionHeight = intersection.size.height;p.onGround = YES; //Here} else {resolutionHeight = -intersection.size.height;}p.desiredPosition = ccp(p.desiredPosition.x, p.desiredPosition.y + intersection.size.height );} else {float resolutionWidth;if (tileIndx == 6 || tileIndx == 4) {resolutionWidth = intersection.size.width;} else {resolutionWidth = -intersection.size.width;}p.desiredPosition = ccp(p.desiredPosition.x , p.desiredPosition.y + resolutionWidth);} } }} }p.position = p.desiredPosition; //8 } |
每當考拉腳下有tile的時候(緊貼著或者對角線都算),你就設置p.onGround為YES并把其速度置0。同樣,當考拉的上邊有tile時,也把速度置為0。這樣做能讓速率變量真實反映考拉實際的運動情況。
每次循環開始時,你都把onGround設置為NO。這樣做就可以保證僅僅在檢測到考拉腳下有tile時才把onGround置為YES。你使用這個變量決定考拉能否跳躍。你需要在在Koala類中加入此屬性。
在Player.h加入屬性的聲明:
| @property (nonatomic, assign) BOOL onGround; |
在Player.m加入synthesize部分:
| @synthesize onGround = _onGround; |
本篇字數有限,接下篇
總結
以上是生活随笔為你收集整理的如何制作一款像超级玛丽兄弟一样基于平台的游戏-第一部分 (xcode,物理引擎,TMXTiledMap相关应用)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 红帽RHCE培训-课程3笔记内容1
- 下一篇: 佛山科学技术学院计算机期末试题,高等数学