如何製作一款像超級瑪麗兄弟一樣基於平臺的遊戲-第二部分 (xcode,物理引擎,TMXTiledMap相關應用)

這篇文章還可以在這裏找到 英語

接上篇

編譯並運行!它是否正常運作了呢?是的!太好了!

何去何從? Where to Go From Here?

恭喜你!你已經構建了屬於你自己的物理引擎了!如果你一步一步的跟着教程走到了這裏,你可以深呼吸並錘錘後背了。這是本本遊戲最難的一部分,在第2部分中將會是一馬平川!

這裏是到目前爲止的完整的工程

第2部分中, 你將會讓你的英雄考拉跑和跳。同時在地面增加一些危險物,並處理勝利/失敗的邏輯。

如果你想獲取平臺遊戲更多的信息,以下是我收集的一些資源:

你可以在留言區留言以讓我知道你的進度!

第1部分完結

Learn how to make a game like Super Mario!

Learn how to make a game like Super Mario!

這是一篇IOS教程組的成員 Jacob Gundersen發佈的教程, 他是一位獨立遊戲開發者,經營着Indie Ambitions 博客。去看看他最新的app吧Factor Samurai!

歡迎回到我們的兩部分教程 – 如何製作一款像超級瑪麗的遊戲!

在 第1部分中,你學會了如何製作一個簡單的,基於tile的物理引擎,使用這個引擎,你可以控制你的英雄考里奧在他的世界裏有所作爲。

在第2部分同時也是最後一部分中,你將學到如何控制考里奧跑和跳,這部分很有趣喲!

你還將加入一些具有碰撞的危險的地刺,處理勝利和失敗,並毫無例外的加入一些免費的音效和音樂。

第2部分和第一部分相比較而言,簡單多了,也短多了,這可是在第1部分中艱苦努力的獎勵哦!重拾你的代碼,享受之後的過程吧!

移動考里奧 Moving Koalio Around

你將要實現的控制系統相當簡單。只有前進和跳躍,很像 1-bit Ninja。如果你點擊屏幕的左半邊,考里奧會前進,如果點擊屏幕的右半邊,考里奧就跳躍。

你沒有聽錯,考里奧不能往回移動!真正的考拉是不會從危險中後退的。

因爲考里奧不是由GameLevelLayer,而是由玩家控制向前移動的,你需要在Player類中實時更新它向前的素素。在Player類中加入如下屬性(不要忘記synthesize部分!):

Player.h中:

@property (nonatomic, assign) BOOL forwardMarch;
@property (nonatomic, assign) BOOL mightAsWellJump;

Player.m中:

@synthesize forwardMarch = _forwardMarch, mightAsWellJump = _mightAsWellJump;

現在在GameLevelLayer中加入如下處理觸摸事件的代碼:

- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {  
  for (UITouch *t in touches) {
    CGPoint touchLocation = [self convertTouchToNodeSpace:t];
    if (touchLocation.x > 240) {
      player.mightAsWellJump = YES;
    } else {
      player.forwardMarch = YES;
    }
  }
}
 
- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
  for (UITouch *t in touches) {
 
    CGPoint touchLocation = [self convertTouchToNodeSpace:t];
 
    //get previous touch and convert it to node space
    CGPoint previousTouchLocation = [t previousLocationInView:[t view]];
    CGSize screenSize = [[CCDirector sharedDirector] winSize];
    previousTouchLocation = ccp(previousTouchLocation.x, screenSize.height - previousTouchLocation.y);
 
    if (touchLocation.x > 240 && previousTouchLocation.x <= 240) {
      player.forwardMarch = NO;
      player.mightAsWellJump = YES;
    } else if (previousTouchLocation.x > 240 && touchLocation.x <=240) {
      player.forwardMarch = YES;
      player.mightAsWellJump = NO;
    }
  }
}
 
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
 
  for (UITouch *t in touches) {
    CGPoint touchLocation = [self convertTouchToNodeSpace:t];
    if (touchLocation.x < 240) {
      player.forwardMarch = NO;
    } else {
      player.mightAsWellJump = NO;
    }
  }
}

這些內容相當直接。如果玩家點擊座標的x值小於240(一半的屏幕),你就把player中的forwardMarch變量置爲YES。否則(點擊座標的x值大於240)就把mightAsWellJump變量置爲YES。

touchesMoved稍微有一點複雜,因爲你只想在觸摸點穿越屏幕中心時才切換那些boolean值,所以你不得不把previousTouch座標也計算在內。除了這點之外,你僅僅需要檢測觸摸點劃過的方向來相應的設置那些boolean值。最後,如果玩家停止觸摸屏幕的任何一側時,你需要把相應的boolean值置爲NO。

爲了能夠相應觸摸事件,還需要做一些工作。首先,在init中加入這行:

	self.isTouchEnabled = YES;

然後,你還需要在AppDelegate.m打開多點觸摸(爲了檢測玩家同時發出跑和跳的指令)。在[director_ pushScene: [GameLevelLayer scene]];之前加入以下內容:

	[glView setMultipleTouchEnabled:YES];

既然你把觸摸點傳遞到了你的Player類中的一系列boolean變量中了,你便可以在update方法中加入一些代碼,來讓考里奧移動。首先先考慮前向移動,對Player.m中的update方法作如下修改:

-(void)update:(ccTime)dt {
    CGPoint gravity = ccp(0.0, -450.0);
    CGPoint gravityStep = ccpMult(gravity, dt);
 
    CGPoint forwardMove = ccp(800.0, 0.0);
    CGPoint forwardStep = ccpMult(forwardMove, dt); //1
 
    self.velocity = ccpAdd(self.velocity, gravityStep);
    self.velocity = ccp(self.velocity.x * 0.90, self.velocity.y); //2
 
    if (self.forwardMarch) {
        self.velocity = ccpAdd(self.velocity, forwardStep);
    } //3
 
    CGPoint minMovement = ccp(0.0, -450.0);
    CGPoint maxMovement = ccp(120.0, 250.0);
    self.velocity = ccpClamp(self.velocity, minMovement, maxMovement); //4
 
    CGPoint stepVelocity = ccpMult(self.velocity, dt);
 
    self.desiredPosition = ccpAdd(self.position, stepVelocity);
}

讓我們一部分一部分解讀它們:

  1. 當玩家點擊屏幕時,你需要增加了一個向前的力。和以往一樣,你把這個力根據時間戳(dt)縮減,這樣就獲得了平穩的加速。
  2. 這裏你在水平方向上增加阻尼,以模擬摩擦力。你操作物理的方式跟之前的重力沒什麼兩樣。在每一幀中,向前的力都會被計算。

    當力被撤銷時,你想要player停止,但不是立刻停止。這裏你施加一個0.90的阻尼;換句話說,每幀
    裏水平方向的速度減少百分之十。

  3. 在第3部分中,你檢測是否觸摸屏幕的變量,並根據情況施加forwardMove force。
  4. 在第4部分中,你應用了clamping。它限制了player最大的移動速度,包括水平方向的(跑的極限速度),向上的(跳躍速度)和向下的(下落速度)。

    這些damping和clamping值用來對遊戲中事件發生頻率設置上限。它還防止了你之前在第1部分中遇到過的速率過大的問題。

    你希望player需要大概1秒纔到達最大的速度。這樣做也能讓player的移動更自然,並且可控。你允許的最大的力是正120,大概是1秒鐘四分之一屏幕的距離。

    如果你想增加player的加速度,那麼可以適當增加forwardMove,也可以適當改變目前爲0.9的damping值。如果你想增加player的最大速度,那隻需要簡單地增加120那個值就行了。另外你也對跳躍250和下墜450的速度做了封頂,你同樣也可以修改它們。

編譯並運行。你將能夠通過點擊屏幕的左半邊來讓考里奧跑起來。看看它跑的樣子多瀟灑!

接下來,你將要賦予它跳躍的能力!

你的Mac將讓它跳起來! Your Mac Will Make Him… Jump, Jump!

跳躍是平臺遊戲最主要的特色,也是此類遊戲最大的樂趣來源。你肯定想把跳躍做的很流暢並且感覺很對。在本篇教學中,你將使用和刺蝟索尼克同樣的跳躍算法,在這裏有詳細說明。

update方法中if (self.forwardMarch) {行之前加入以下內容:

CGPoint jumpForce = ccp(0.0, 310.0);
 
if (self.mightAsWellJump && self.onGround) {
    self.velocity = ccpAdd(self.velocity, jumpForce);
}

如果你就到此爲止的話(你可以編譯並運行看看效果),你將會得到老學校裏雅達利遊戲機中的跳躍。每一次的跳躍都是同樣的高度。你對player施加了一個力,並等待重力把它重新拉回來。

在現代的平臺遊戲中,玩家可以在對象跳躍的過程中對其進行操作。你想要可控的,完全不真實(但是樂趣十足)的馬里奧兄弟/刺蝟索尼克中使用的跳躍,在跳躍到半空中的時候還可以改變方向,甚至終止一次跳躍。

爲了完成這個,你需要添加變量組件。有很多方法可以使用,不過你將會使用索尼克中的方法。對跳躍算法做一些修改,當玩家不再點擊屏幕右側時我們減弱向上的推力。把以上代碼替換爲一下內容:

  CGPoint jumpForce = ccp(0.0, 310.0);
  float jumpCutoff = 150.0;
 
  if (self.mightAsWellJump && self.onGround) {
    self.velocity = ccpAdd(self.velocity, jumpForce);
  } else if (!self.mightAsWellJump && self.velocity.y > jumpCutoff) {
    self.velocity = ccp(self.velocity.x, jumpCutoff);
  }

這些代碼多做了一步。當玩家不再點擊屏幕右側時(self.mightAsWellJump爲NO時),它檢測player向上的速度,如果這個速度大於cutoff(150.0),那麼就把player的速度設置爲cutoff。

這樣做能顯著地減弱跳躍。通過這種方法,你總是可以得到一個最小的跳躍(至少和jumpCutoff一樣高),但如果你持續按住屏幕,你將會得到一個完全的跳躍。

編譯並運行。現在它看起來像個遊戲了!從目前開始,你可能需要在真機上測試了(如果你之前沒這樣做過),這樣你就可以同時觸發兩個“按鈕”了(屏幕左和右)。

你的考里奧可以跑和跳了,但是最終他會跑出屏幕。是時候修正這個問題了!

基於tile的遊戲教程中的以下代碼片段加入到GameLevelLayer.m中:

-(void)setViewpointCenter:(CGPoint) position {
 
  CGSize winSize = [[CCDirector sharedDirector] winSize];
 
  int x = MAX(position.x, winSize.width / 2);
  int y = MAX(position.y, winSize.height / 2);
  x = MIN(x, (map.mapSize.width * map.tileSize.width) 
      - winSize.width / 2);
  y = MIN(y, (map.mapSize.height * map.tileSize.height) 
      - winSize.height/2);
  CGPoint actualPosition = ccp(x, y);
 
  CGPoint centerOfView = ccp(winSize.width/2, winSize.height/2);
  CGPoint viewPoint = ccpSub(centerOfView, actualPosition);
  map.position = viewPoint; 
}

這部分代碼的作用是使player不會移動出屏幕。當考里奧在關卡邊緣時,屏幕就不在追蹤它了,並把關卡的邊緣定位到屏幕的邊緣。

這裏對原版本的代碼稍作了修改。在最後一行,我們用map移動代替了原先的layer移動。因爲player是map的child,所以當player向右移動時,map向左移動,player一直會保持在屏幕的中心位置。

還有一點,touch方法需要layer的位置來做座標轉換。如果你移動了layer,你還需要另外把這些座標計算進來。所以說我們的方法更簡單。

想要完整的解釋,請參考基於tile的遊戲教程

你需要在update方法中添加如下內容:

	[self setViewpointCenter:player.position];

編譯並運行。你可以控制考里奧穿梭在整個關卡中了!

失敗之痛 The Agony of Defeat

現在你可以騰出手來處理勝利和失敗的邏輯了。

先來處理失敗的邏輯。關卡中有一些危險物。如果player碰到了它們,遊戲就結束了。

因爲它們是固定的tile,你需要像處理wall的碰撞一樣處理它們。不同的是,你需要在碰撞發生時結束,而不是處理碰撞。到此爲止,你已經在離最終完成不遠了,還有一些剩餘事項需要處理。

GameLevelLayer.m中加入如下方法:

-(void)handleHazardCollisions:(Player *)p {
  NSArray *tiles = [self getSurroundingTilesAtPosition:p.position forLayer:hazards ];
  for (NSDictionary *dic in tiles) {
    CGRect tileRect = CGRectMake([[dic objectForKey:@"x"] floatValue], [[dic objectForKey:@"y"] floatValue], map.tileSize.width, map.tileSize.height);
    CGRect pRect = [p collisionBoundingBox];
 
    if ([[dic objectForKey:@"gid"] intValue] && CGRectIntersectsRect(pRect, tileRect)) {
      [self gameOver:0];
    }
  }
}

以上這些代碼看起來很眼熟,因爲它是從checkAndResolveCollisions方法中拷貝過來的。唯一的一個新的方法是gameOver。此方法有一個參數,0代表player失敗,1代表player勝利。

由於你使用了hazards層代替了wall層,你需要在文件一開頭的@interface塊中加入一個成員變量CCTMXLayer *hazards;,在init中初始化它(在初始化wall之後的一行):

	hazards = [map layerNamed:@"hazards"];

最後一件事是你需要在update方法中調用這個方法:

-(void)update:(ccTime)dt {
  [player update:dt];
 
  [self handleHazardCollisions:player];
  [self checkForAndResolveCollisions:player];
  [self setViewpointCenter:player.position];
}

現在,如果player跑到了一個危險物層的tile上,你就會調用gameOver。那個方法會彈出失敗信息和一個重新開始的按鈕(當然也可以是勝利的):

-(void)gameOver:(BOOL)won {
	gameOver = YES;
	NSString *gameText;
 
	if (won) {
		gameText = @"You Won!";
	} else {
		gameText = @"You have Died!";
	}
 
  CCLabelTTF *diedLabel = [[CCLabelTTF alloc] initWithString:gameText fontName:@"Marker Felt" fontSize:40];
  diedLabel.position = ccp(240, 200);
  CCMoveBy *slideIn = [[CCMoveBy alloc] initWithDuration:1.0 position:ccp(0, 250)];
  CCMenuItemImage *replay = [[CCMenuItemImage alloc] initWithNormalImage:@"replay.png" selectedImage:@"replay.png" disabledImage:@"replay.png" block:^(id sender) {
    [[CCDirector sharedDirector] replaceScene:[GameLevelLayer scene]];
  }];
 
  NSArray *menuItems = [NSArray arrayWithObject:replay];
  CCMenu *menu = [[CCMenu alloc] initWithArray:menuItems];
  menu.position = ccp(240, -100);
 
  [self addChild:menu];
  [self addChild:diedLabel];
 
  [menu runAction:slideIn];
}

第一行初始化了一個新的叫做gameOver的boolean變量。你使用它來停止update方法,這樣可以組織player繼續移動並和關卡產生互動。你馬上就會遇到使用它的地方。

接下來的代碼創建了一個label,並根據玩家勝利或失敗爲賦值一個字符串。它還創建了一個重新開始的按鈕。

這些CCMenu裏基於block的方法真心好用。在此處,我們使用CCDirector的replaceScene方法複製一個同樣的場景來重新開始關卡。你還使用了CCAction,CCMoveBy來讓replay按鈕動態進入場景,沒別的目的,只是爲了好玩兒。

最後需要做的是把gameOver變量加入到GameLevelLayer類中。把它作爲成員變量就可以了,因爲你並不需要從其他類訪問它。在GameLevelLayer.m一開頭的@interface塊中加入以下內容:

CCTMXLayer *hazards;
BOOL gameOver;

並如下修改update方法:

-(void)update:(ccTime)dt {
  if (gameOver) {
    return;
  }
  [player update:dt];
  [self checkForAndResolveCollisions:player];
  [self handleHazardCollisions:player];
  [self setViewpointCenter:player.position];
}

再次編譯並運行,找到一些地刺跳上去!你會發現如下所示的場景:

這個步驟別重複太多次,否則動物保護組織會找上你的! :]

地獄深處 The Pit of Doom

現在來處理當考里奧掉落的情況。當發生這種情況時,你將結束遊戲。

目前的代碼邏輯是當發生掉落時,程序就掛掉了,錯誤是TMXLayer: invalid position error。這裏需要你解決一下。這個情況發生的是在getSurroundingTilesAtPosition:方法中調用tileGIDAt:的位置。

GameLevelLayer.m中的getSurroundingTilesAtPosition:方法裏,tileGIDat:行之前加入以下代碼:

if (tilePos.y > (map.mapSize.height - 1)) {
    //fallen in a hole
    [self gameOver:0];
    return nil;
}

這些代碼會執行gameOver並停止繼續構建tile數組。你還需要在checkForAndResolveCollisions中阻止循環遍歷tile的過程。在NSArray *tiles = [self getSurroundingTilesAtPosition:p.position forLayer:walls ];行之後,加入以下代碼塊兒:

  if (gameOver) {
    return;
  }

你將會中止循環,以此來避免試圖遍歷不完整的數組導致的程序崩潰。

編譯並運行。找一個坑掉進去,看,現在不會掛了!遊戲正常的進入gameover界面了。

勝利! Winning!

現在來處理當英雄考里奧贏得遊戲的情景!

所有需要做的就是檢測player的X座標是否觸發了勝利條件,當它穿過關卡最右端時,就勝利了。目前的關卡大約有3400像素寬。當player到達3130像素的時候,你就認爲遊戲勝利了。

GameLevelLayer.m中加入以下新方法:

-(void)checkForWin {
  if (player.position.x > 3130.0) {
    [self gameOver:1];
  }
}
-(void)update:(ccTime)dt {
  [player update:dt];
 
  [self handleHazardCollisions:player];
  [self checkForWin];
  [self checkForAndResolveCollisions:player];
  [self setViewpointCenter:player.position];
}

編譯並運行。控制你的英雄考里奧穿越整個關卡,如果你能讓它到達結束點,你將會達到這些信息:

免費的音樂和音效 Gratuitous Music and Sound Effects

是時候加些免費的音樂和音效了!

我們這就開始。在GameLevelLayer.m 和 Player.m的開頭加入以下內容:

#import "SimpleAudioEngine.h"

然後在GameLevelLayer中的init方法加入下邊一行代碼:

	[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"level1.mp3"];

這個能帶來一些不錯的遊戲音樂。感謝Incompetech.com的Kevin Macleod創做的音樂(Brittle Reel)。他有很多的CC licensed(非商業使用)的音樂!

現在來加入一個跳躍的音效。在Player.m加入以下內容到update方法中跳躍的部分:

if (self.mightAsWellJump && self.onGround) {
    self.velocity = ccpAdd(self.velocity, jumpForce);
    [[SimpleAudioEngine sharedEngine] playEffect:@"jump.wav"];
} else if (!self.mightAsWellJump && self.velocity.y > jumpCutoff) {
    self.velocity = ccp(self.velocity.x, jumpCutoff);
}

最後,當考里奧掉進一個坑或者碰到危險物時,播放一個音效。在GameLevelLayer.m中的gameOver方法中做這件事:

-(void)gameOver {
  gameOver = YES;
  [[SimpleAudioEngine sharedEngine] playEffect:@"hurt.wav"];  
  CCLabelTTF *diedLabel = [[CCLabelTTF alloc] initWithString:@"You have Died!" fontName:@"Marker Felt" fontSize:40];
  diedLabel.position = ccp(240, 200);

編譯並運行,享受屬於你的美妙的旋律吧。

到這裏就全部結束了,你已經完成了一個平臺遊戲。你.真的.很棒!

何去何從? Where to Go From Here?

這裏是最終工程的源代碼下載地址。

其實還有很多內容沒有但是可以被包含進來的:從敵人的碰撞和AI,到移動能力的加強(爬牆,雙跳,等等),再到關卡設計指導。

說到這個,有個好消息給你!

平臺遊戲Starter Kit

我非常高興地宣佈所有以上這些內容甚至更多的內容都會被包含進即將到來的平臺遊戲Starter Kit!這裏有一個關於它的預覽視頻:

以下這些是你能從這個Starter Kit學到的知識點:

  • 如何管理和讀取多個關卡
  • 如何製作一個可滑動的,帶解鎖功能的選關界面
  • 如何在Cocos2D中集成UIKit Storyboards
  • 如何高效的使用sprite sheets, animations, tilesets,並使用像素圖片!
  • 如何創建一個狀態機來處理角色/敵人的動畫和行爲
  • 更多的關於如何製作令人驚奇和遊戲的基於tile的物理引擎!
  • 如何創建一個在屏幕之上的虛擬手柄和HUD
  • 如何添加iCade支持
  • 平臺遊戲的關卡設計
  • 如何構建敵人的AI和動態行爲。
  • 如何添加一個EPIC Boss(隱藏boss)戰鬥!
  • 數個頂尖的IOS平臺遊戲開發者的採訪,分享經驗和技巧
  • . . . 還有很多,很多!

如果你對平臺遊戲Starter Kit感興趣,請確保你已經訂閱了Ray’s monthly newsletter,我將會在那裏發佈有關它的消息! :]

同時,不要忘記在第1部分中末尾推薦的那些資源。

我希望你享受制作你自己的物理引擎的過程並製作你自己的平臺遊戲!

這是一篇IOS教程組的成員 Jacob Gundersen發佈的教程, 他是一位獨立遊戲開發者,經營着Indie Ambitions 博客。去看看他最新的app吧Factor Samurai!

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