注:本文譯自Sprite Kit Tutorial for Beginners
目錄
Sprite Kit的優點和缺點
Sprite Kit vs Cocos2D-iPhone vs Cocos2D-X vs Unity
Hello, Sprite Kit!
橫屏顯示
移動怪獸
發射炮彈
碰撞檢測和物理特性: 概述
碰撞檢測和物理特性: 實現
收尾
何去何從?
碰撞檢測和物理特性: 概述
至此我們已經可以讓炮彈任意的發射了——現在我們要讓忍者利用炮彈來消滅這些怪物。下面就添加一些代碼來給炮彈與怪物相交做檢測。
Sprite Kit內置了一個物理引擎,這非常的棒!該物理引擎不僅可以模擬現實運動,還能進行碰撞檢測。
下面我們就在游戲中使用Sprite Kit的物理引擎來檢測炮彈與怪物的碰撞。首先,我們來看看需要做些神馬事情:
物理世界的配置。物理世界是一個模擬的空間,用來進行物理計算。默認情況下,在場景(scene)中已經創建好了一個,我們可以對其做一些屬性配置,例如重力感應。
為精靈(sprite)創建對應的物體(physics bodies)。在Sprite Kit中,為了碰撞檢測,我們可以為每個精靈創建一個相應的形狀,並設置一些屬性,這就稱為物體(physics body)。注意:圖文的形狀不一定跟精靈的外形一模一樣。一般情況,這個形狀都是簡單的、大概的(而不用精確到像素級別)——畢竟這已經足以夠大多數游戲使用了。
將精靈分類。在物體(physics body)上可以設置的一個屬性是category,該屬性是一個位掩碼(bitmask)。通過該屬性可以將精靈分類。在本文的游戲中,有兩個類別——一類是炮彈,另一類則是怪物。設置之後,當兩種物體相互碰撞時,就可以很容易的通過類別對精靈做出相應的處理。
設置一個contact(觸點) delegate。還記得上面提到的物理世界嗎?我們可以在物理世界上設置一個contact delegate,通過該delegate,當兩個物體碰撞時,可以收到通知。收到通知後,我們可以通過代碼檢查物體的類別,如果是怪物和炮彈,那麼就做出相應的動作!
上面大致介紹了一下游戲策略,下面就來看看如何實現!
碰撞檢測和物理特性: 實現
首先在MyScene.m文件頂部添加如下兩個常量:
1
2
static const uint32_t projectileCategory = 0x1 << 0;
static const uint32_t monsterCategory = 0x1 << 1;
上面設置了兩個類別,記住需要用位(bit)的方式表達——一個用於炮彈,另一個則是怪物。
注意:看到上面的語法你可能感到奇怪。在Sprite Kit中category是一個32位整數,當做一個位掩碼(bitmask)。這種表達方法比較奇特:在一個32位整數中的每一位表示一種類別(因此最多也就只能有32類)。在這裡,第一位表示炮彈,下一位表示怪獸。
接著,在initWithSize中,將下面的代碼添加到位置:添加player到場景涉及代碼的後面。
1
2
self.physicsWorld.gravity = CGVectorMake(0,0);
self.physicsWorld.contactDelegate = self;
上面的代碼將物理世界的重力感應設置為0,並將場景設置位物理世界的代理(當有兩個物體碰撞時,會受到通知)。
在addMonster方法中,將如下代碼添加創建怪獸相關代碼後面:
monster.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:monster.size]; // 1
monster.physicsBody.dynamic = YES; // 2
monster.physicsBody.categoryBitMask = monsterCategory; // 3
monster.physicsBody.contactTestBitMask = projectileCategory; // 4
monster.physicsBody.collisionBitMask = 0; // 5
來看看上面代碼意思:
為怪獸創建一個對應的物體。此處,物體被定義為一個與怪獸相同尺寸的矩形(這樣與怪獸形狀比較接近)。
將怪獸設置位dynamic。這意味著物理引擎將不再控制這個怪獸的運動——我們自己已經寫好相關運動的代碼了。
將categoryBitMask設置為之前定義好的monsterCategory。
contactTestBitMask表示與什麼類型對象碰撞時,應該通知contact代理。在這裡選擇炮彈類型。
collisionBitMask表示物理引擎需要處理的碰撞事件。在此處我們不希望炮彈和怪物被相互彈開——所以再次將其設置為0。
接著在touchesEnded:withEvent:方法中設置炮彈位置的代碼後面添加如下代碼。
projectile.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:projectile.size.width/2];
projectile.physicsBody.dynamic = YES;
projectile.physicsBody.categoryBitMask = projectileCategory;
projectile.physicsBody.contactTestBitMask = monsterCategory;
projectile.physicsBody.collisionBitMask = 0;
projectile.physicsBody.usesPreciseCollisionDetection = YES;
在上面的代碼中跟之前的類似,只不過有些不同,我們來看看: 1. 為了更好的效果,炮彈的形狀是圓形的。 2. usesPreciseCollisionDetection屬性設置為YES。這對於快速移動的物體非常重要(例如炮彈),如果不這樣設置的話,有可能快速移動的兩個物體會直接相互穿過去,而不會檢測到碰撞的發生。
接著,添加如下方法,當炮彈與怪物發生碰撞時,會被調用。注意這個方法是不會被自動調用,稍後會看到我們如何調用它。
- (void)projectile:(SKSpriteNode *)projectile didCollideWithMonster:(SKSpriteNode *)monster {
NSLog(@"Hit");
[projectile removeFromParent];
[monster removeFromParent];
}
當怪物和炮彈發生碰撞,上面的代碼會將他們從場景中移除。很簡單吧!
下面該實現contact delegate方法了。將如下方法添加到文件中:
1
- (void)didBeginContact:(SKPhysicsContact *)contact
{
// 1
SKPhysicsBody *firstBody, *secondBody;
if (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask)
{
firstBody = contact.bodyA;
secondBody = contact.bodyB;
}
else
{
firstBody = contact.bodyB;
secondBody = contact.bodyA;
}
// 2
if ((firstBody.categoryBitMask & projectileCategory) != 0 &&
(secondBody.categoryBitMask & monsterCategory) != 0)
{
[self projectile:(SKSpriteNode *) firstBody.node didCollideWithMonster:(SKSpriteNode *) secondBody.node];
}
}
還記得之前給物理世界設置的contactDelegate嗎?當兩個物體發生碰撞之後,就會調用上面的方法。
在上面的方法中,可以分為兩部分來理解:
該方法會傳遞給你發生碰撞的兩個物體,但是並不一定符合特定的順序(如炮彈在前,或者炮彈在後)。所以這裡的代碼是通過物體的category bit mask來對其進行排序,以便後續做出正確的判斷。注意,這裡的代碼來自蘋果提供的Adventure示例。
最後,檢測一下這兩個碰撞的物體是否就是炮彈和怪物,如果是的話就調用之前的方法。
最後一步,為了編譯器沒有警告,確保private interface 中添加一下SKPhysicsContactDelegate:
1
@interface MyScene () <SKPhysicsContactDelegate>
現在編譯並運行程序,可以發現,當炮彈與怪物接觸時,他們就會消失!
收尾
現在,本文的游戲快完成了。接下來我們就來為游戲添加音效和音樂,以及一些簡單的游戲邏輯吧。
蘋果提供的Sprite Kit裡面並沒有音頻引擎(Cocos2D中是有的),不過我們可以通過action來播放音效,並且可以使用AVFoundation播放後台音樂。
在工程中我已經准備好了一些音效和很酷的後台音樂,在本文開頭已經將resources添加到工程中了,現在只需要播放它們即可!
首先在ViewController.m文件頂部添加如下import:
1
@import AVFoundation;
上面的語法是iOS 7中新的modules功能 —— 只需要使用新的關鍵字@import,就可以框架的頭文件和庫文件添加到工程中,這功能非常方便。要了解更多相關內容,請看到iOS 7 by Tutorials中的第十章內容中的:What’s New with Objective-C and Foundation。
接著添加一個新的屬性和private interface:
@interface ViewController ()
@property (nonatomic) AVAudioPlayer * backgroundMusicPlayer;
@end
接著將下面的代碼添加到viewWillLayoutSubviews方法中(在[super viewWillLayoutSubviews]後面):
NSError *error;
NSURL * backgroundMusicURL = [[NSBundle mainBundle] URLForResource:@"background-music-aac" withExtension:@"caf"];
self.backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:backgroundMusicURL error:&error];
self.backgroundMusicPlayer.numberOfLoops = -1;
[self.backgroundMusicPlayer prepareToPlay];
[self.backgroundMusicPlayer play];
上面的代碼會開始無限循環的播放後台音樂。
下面我們來看看如何處理音效。切換到MyScene.m文件中,並將下面這行代碼添加到touchesEnded:withEvent:方法的頂部:
1
[self runAction:[SKAction playSoundFileNamed:@"pew-pew-lei.caf" waitForCompletion:NO]];
如上,一行代碼就可以播放音效了,很簡單吧!
下面,我們創建一個新的創建和layer,用來顯示你贏了(You Win)或你輸了(You Lose)。用模板iOS\Cocoa Touch\Objective-C class創建一個新的文件,將其命名為GameOverScene,並讓其繼承自SKScene,然後點擊Next和Create。
接著用如下代碼替換GameOverScene.h中的內容:
#import <SpriteKit/SpriteKit.h>
@interface GameOverScene : SKScene
-(id)initWithSize:(CGSize)size won:(BOOL)won;
@end
在上面的代碼中導入了Sprite Kit頭文件,並聲明了一個特定的初始化方法,該方法的第一個參數用來定位顯示的位置,第二個參數won用來判斷用戶是否贏了。
接著用下面的代碼替換GameOverLayer.m中的內容:
#import "GameOverScene.h"
#import "MyScene.h"
@implementation GameOverScene
-(id)initWithSize:(CGSize)size won:(BOOL)won {
if (self = [super initWithSize:size]) {
// 1
self.backgroundColor = [SKColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
// 2
NSString * message;
if (won) {
message = @"You Won!";
} else {
message = @"You Lose :[";
}
// 3
SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
label.text = message;
label.fontSize = 40;
label.fontColor = [SKColor blackColor];
label.position = CGPointMake(self.size.width/2, self.size.height/2);
[self addChild:label];
// 4
[self runAction:
[SKAction sequence:@[
[SKAction waitForDuration:3.0],
[SKAction runBlock:^{
// 5
SKTransition *reveal = [SKTransition flipHorizontalWithDuration:0.5];
SKScene * myScene = [[MyScene alloc] initWithSize:self.size];
[self.view presentScene:myScene transition: reveal];
}]
]]
];
}
return self;
}
@end
上面的代碼可以分為4部分內容,我們來分別看看:
將背景色設置為白色(與主場景一樣顏色)。
根據won參數,將信息設置為”You Won”或”You Lose”。
這裡的代碼是利用Sprite Kit將一個文本標簽顯示到屏幕中。如代碼所示,只需要選擇一個字體,並設置少量的參數即可,也非常簡單。
設置並運行有個有兩個action的sequence。為了看起來方便,此處我將它們放到一塊(而不是為每個action創建單獨的一個變量)。首先是等待3秒,然後是利用runBlockaction來運行一些代碼。
演示了在Sprite Kit中如何過渡到新的場景。首先可以選擇任意的一種不同的動畫過渡效果,用於場景的顯示,在這裡選擇了翻轉效果(持續0.5秒)。然後是創建一個想要顯示的場景,接著使用self.view的方法presentScene:transition:來顯示出場景。
OK,萬事俱備,只欠東風了!現在只需要在主場景中,適當的情況下加載game over scene就可以了。
首先,在MyScene.m中導入新的場景:
1
#import "GameOverScene.h"
然後,在addMonster中,用下面的代碼替換最後一行在怪物上運行action的代碼:
SKAction * loseAction = [SKAction runBlock:^{
SKTransition *reveal = [SKTransition flipHorizontalWithDuration:0.5];
SKScene * gameOverScene = [[GameOverScene alloc] initWithSize:self.size won:NO];
[self.view presentScene:gameOverScene transition: reveal];
}];
[monster runAction:[SKAction sequence:@[actionMove, loseAction, actionMoveDone]]];
上面創建了一個”lose action”,當怪物離開屏幕時,顯示game over場景。
在這裡為什麼loseAction要在actionMoveDone之前運行呢? 原因在於如果將一個精靈從場景中移除了,那麼它就不在處於場景的層次結構中了,也就不會有action了。所以需要過渡到lose場景之後,才能將精靈移除。不過,實際上actionMoveDone永遠都不會被調用——因為此時已經過渡到新的場景中了,留在這裡就是為了達到教學的目的。
現在,需要處理一下贏了的情況。在private interface中添加一個新的屬性:
1
@property (nonatomic) int monstersDestroyed;
然後將如下代碼添加到projectile:didCollideWithMonster:的底部:
self.monstersDestroyed++;
if (self.monstersDestroyed > 30) {
SKTransition *reveal = [SKTransition flipHorizontalWithDuration:0.5];
SKScene * gameOverScene = [[GameOverScene alloc] initWithSize:self.size won:YES];
[self.view presentScene:gameOverScene transition: reveal];
}
編譯並運行程序,嘗試一下贏了和輸了會看到的畫面!