深入Sprite Kit
學習Sprite Kit最好的方法是在實踐中觀察它。此示例創建一對場景和各自的動畫內容。通過這個例子,你將學習使用Sprite Kit內容的一些基礎技術,包括:
· 場景在一個基於Sprite Kit的游戲中的角色。
· 如何組織節點樹來繪制內容。
· 使用動作讓場景內容動起來。
· 如何添加交互到場景。
· 場景之間的過渡。
· 在一個場景裡模擬物理。
一旦你完成這個項目,你可以用它來試驗其他Sprite Kit概念。你可以在這個例子的結尾找到一些建議。
你應該已經熟悉創建iOS應用程序之前通過這個項目工作。欲了解更多信息,請參閱今天開始開發iOS應用程序的。大多數Sprite Kit在這個例子中的代碼是相同的OS X。
讓我們開始吧
本次練習需要Xcode 5.0。使用的單一視圖的應用程序模板創建一個新的iOS應用程序的Xcode項目。
在創建項目時,請使用以下值:
· 產品名稱:SpriteWalkthrough
· ClassPrefix:Sprite
· 設備:iPad
添加Sprite Kit框架到項目中。
創建你的第一個場景
Sprite Kit內容被放置在一個窗口中,就像其他可視化內容那樣。Sprite Kit內容由SKView類渲染呈現。SKView對象渲染的內容稱為一個場景,它是一個SKScene對象。場景參與響應鏈,還有其他使它們適合於游戲的功能。
因為Sprite Kit內容由視圖對象渲染,你可以在視圖層次組合這個視圖與其他視圖。例如,你可以使用標准的按鈕控件,並把它們放在你的Sprite Kit視圖上面。或者,你可以添加交互到精靈來實現自己的按鈕,選擇權在你。在這個例子中,稍候你會看到如何實現場景交互。
配置視圖控制器來使用Sprite Kit
1. 打開項目的storyboard。它有一個單一的視圖控制器(SpriteViewController)。選擇視圖控制器的view對象並把它的類改成SKView。
2. 在視圖控制器的實現文件添加一個導入行。
[cpp]
#import <SpriteKit/SpriteKit.h>
#import <SpriteKit/SpriteKit.h>3. 實現視圖控制器的viewDidLoad方法來配置視圖。
[cpp]
- (void)viewDidLoad
{
[super viewDidLoad];
SKView * spriteView =(SKView *)self.view;
spriteView.showsDrawCount = YES;
spriteView.showsNodeCount = YES;
spriteView.showsFPS = YES;
}
- (void)viewDidLoad
{
[super viewDidLoad];
SKView * spriteView =(SKView *)self.view;
spriteView.showsDrawCount = YES;
spriteView.showsNodeCount = YES;
spriteView.showsFPS = YES;
}
4. 代碼開啟了描述場景如何渲染視圖的診斷信息。最重要的一塊信息是幀率(spriteView.showsFPS),你希望你的游戲盡可能在一個恆定的幀率下運行。其他行展示了在視圖中顯示了多少個節點,以及使用多少繪畫傳遞來渲染內容(越少越好)的詳情。
接下來,添加第一個場景。
創建Hello場景
1. 創建一個名為HelloScene新類並讓它作為SKScene類的子類。
2. 在你的視圖控制器導入場景的頭文件。
[cpp]
#import “HelloScene.h”
#import “HelloScene.h”
3. 修改視圖控制器來創建場景,並在視圖中呈現場景。
[cpp]
- (void)viewWillAppear:(BOOL)animated
HelloScene *hello = [[HelloScene alloc] initWithSize:CGSizeMake(768,1024)];
SKView *spriteView =(SKView *)self.view;
[spriteView presentScene:hello];
- (void)viewWillAppear:(BOOL)animated
{
HelloScene *hello = [[HelloScene alloc] initWithSize:CGSizeMake(768,1024)];
SKView *spriteView =(SKView *)self.view;
[spriteView presentScene:hello];
}
現在,構建並運行項目。該應用程序應該啟動並顯示一個只有診斷信息的空白屏幕。
將內容添加到場景
當設計一個基於Sprite Kit的游戲,你要為你的游戲界面各主要大塊(chuck)設計不同的場景類。例如,你可以為主菜單創建一個場景而為游戲設置創建另一個單獨的場景。在這裡,你會遵循類似的設計。這第一個場景顯示了傳統的“Hello World”文本。
大多數情況下,你可以配置一個場景在它被視圖首次呈現時的內容。這跟視圖控制器只在視圖屬性被引用時加載他們的視圖的方式是類似的。在這個例子中,代碼在didMoveToView:方法內部,每當場景在視圖中顯示時該方法會被調用。
在場景中顯示Hello文本
1. 添加一個新的屬性到場景的實現文件中來跟蹤場景是否已創建其內容。
[cpp]
@interface HelloScene()
@property BOOL contentCreated;
@end
@interface HelloScene()
@property BOOL contentCreated;
@end
該屬性跟蹤並不需要向客戶端公開的狀態,所以,在實現文件中它一個私有接口聲明裡實現。
2. 實現場景的didMoveToView:方法。
[cpp]
- (self)didMoveToView:(SKView *)view
{
if(!self.contentCreated)
{
[self createSceneContents];
self.contentCreated = YES;
}
}
- (self)didMoveToView:(SKView *)view
{
if(!self.contentCreated)
{
[self createSceneContents];
self.contentCreated = YES;
}
}每當視圖呈現場景時,didMoveToView:方法都會被調用。但是,在這種情況下,場景的內容應只在場景第一次呈現時進行配置。因此,這段代碼使用先前定義的屬性(contentCreated)來跟蹤場景的內容是否已經被初始化。
3. 實現場景的createSceneContents方法。
[cpp]
- (void)createSceneContents
self.backgroundColor = [SKColor blueColor];
self.scaleMode = SKSceneScaleModeAspectFit;
[self AddChild:[self newHelloNode];
- (void)createSceneContents
{
self.backgroundColor = [SKColor blueColor];
self.scaleMode = SKSceneScaleModeAspectFit;
[self AddChild:[self newHelloNode];
}
場景在繪制它的子元素之前用背景色繪制視圖的區域。注意使用SKColor類創建color對象。事實上,SKColor不是一個類,它是一個宏,在iOS上映射為UIColor而在OS X上它映射為NSColor。它的存在是為了使創建跨平台的代碼更容易。
場景的縮放(scale)模式決定如何進行縮放以適應視圖。在這個例子中,代碼縮放視圖,以便你可以看到場景的所有內容,如果需要使用寬屏(letterboxing)。
4. 實現場景的newHelloNode方法。
[cpp]
- (SKLabelNode *)newHelloNode
{
SKLabelNode * helloNode = [SKLabelNode labelNodeWithFontNamed:@“Chalkduster”];
@helloNode.text =“Hello, World!”
helloNode.fontSize = 42;
helloNode.position = CGPointMake(CGRectGetMidX(self.frame),CGRectGetMidY(self.frame));
return helloNode;
}
- (SKLabelNode *)newHelloNode
{
SKLabelNode * helloNode = [SKLabelNode labelNodeWithFontNamed:@“Chalkduster”];
@helloNode.text =“Hello, World!”
helloNode.fontSize = 42;
helloNode.position = CGPointMake(CGRectGetMidX(self.frame),CGRectGetMidY(self.frame));
return helloNode;
}你永遠不用編寫顯式執行繪圖命令的代碼,而如果你使用OpenGL ES或Quartz 2D你就需要。在Sprite Kit中,你通過創建節點對象並把它們添加到場景中來添加內容。所有繪制必須由Sprite Kit中提供的類來執行。你可以自定義這些類的行為來產生許多不同的圖形效果。然而,通過控制所有的繪圖,Sprite Kit可以對如何進行繪圖應用許多優化。
現在構建並運行該項目。你現在應該看到一個藍色屏幕上面有“Hello, World!”。現在,你已經學會了繪制Sprite Kit內容的所有基礎知識。
使用動作讓場景動起來
靜態文本很友好,但如果文字可以動起來,它會更有趣。大多數的時候,你通過執行動作(action)移動場景周圍的東西。Sprite Kit中的大多數動作對一個節點應用變化。創建action對象來描述你想要的改變,然後告訴一個節點來運行它。然後,當渲染場景時,動作被執行,在幾個幀上發生變化直到它完成。
當用戶觸摸場景內容,文字動起來然後淡出。
讓文本動起來
1. 添加以下代碼到newHelloNode方法:
[cpp]
helloName.name = @“helloNode”;
helloName.name = @“helloNode”;
所有節點都有一個名稱屬性,你可以設置它來描述節點。當你想能夠在稍後找到它,或當你想構建基於節點名稱的行為時,你應該命名一個節點。稍後,你可以搜索樹中與名稱相匹配的節點。
在這個例子中,你給標簽的一個名稱以便稍後可以找到它。在實際的游戲中,你可能會得給呈現相同類型的內容的任何節點以相同的名稱。例如,如果你的游戲把每個怪物呈現為一個節點,你可能會命名節點為monster。
2. 重載場景類的touchesBegan:withEvent方法。當場景接收到觸摸事件,它查找名為helloNode的節點,並告訴它要運行一個簡短的動畫。
所有節點對象都是iOS上UIResponder 或OS X上NSResponder的 的子類。這意味著你可以創建Sprite Kit節點類的子類來添加交互到場景中的任何一個節點。
[cpp]
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
SKNode *helloNode = [self childNodeWithName:@“helloNode”];
If(helloNode != nil)
{
helloNode.name = nil;
SKAction *moveUp = [SKAction moveByX:0 y:100.0 duration:0.5];
SKAction *zoom = [SKAction scaleTo:2.0 duration:0.25];
SKAction *pause = [SKAction waitForDuration:0.5];
SKAction *fadeAway = SKAction fadeWithDuration:0.25];
SKAction *remove = [SKAction removeFromParent];
SKAction * moveSequence = [SKAction sequence:@[moveUp, zoom, pause, fadeAway, remove];
[helloNode runAction:moveSequence];
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
SKNode *helloNode = [self childNodeWithName:@“helloNode”];
If(helloNode != nil)
{
helloNode.name = nil;
SKAction *moveUp = [SKAction moveByX:0 y:100.0 duration:0.5];
SKAction *zoom = [SKAction scaleTo:2.0 duration:0.25];
SKAction *pause = [SKAction waitForDuration:0.5];
SKAction *fadeAway = SKAction fadeWithDuration:0.25];
SKAction *remove = [SKAction removeFromParent];
SKAction * moveSequence = [SKAction sequence:@[moveUp, zoom, pause, fadeAway, remove];
[helloNode runAction:moveSequence];
}
}
為了防止節點響應重復按壓,代碼會清除節點的名稱。然後,它構建動作對象來執行各種操作。最後,它組合這些動作創建一個動作序列;序列運行時,按順序執行每個動作。最後,它告訴標簽節點執行序列動作。
運行的應用程序。你應該看到像之前那樣的文字。在屏幕的底部,節點計數應該是1。現在,點擊視圖內部。你應該看到文字動畫並淡出。在它淡出後,節點計數應該變為0,因為節點已從父節點中刪除。
場景之間的轉換
Sprite Kit讓場景之間的過渡變得很容易。場景之間的過渡時,你可以堅持保留它們,或清除它們。在這個例子中,你將創建第二個場景類,來學習一些其他的游戲行為。“Hello, World!”文字從屏幕上消失時,代碼創建一個新的場景並過渡到它。Hello場景過渡在後會被丟棄。
創建飛船場景
1. 創建一個名為SpaceshipScene的新類並讓它成為SKScene類的子類。
2. 實現代碼來初始化飛船場景的內容。此代碼類似於你為HelloScene類實現的代碼。
[cpp]
@interface SpaceshipScene()
@property BOOL contentCreated;
@end
@implementation SpaceshipScene
- (void)didMoveToView:(SKView *)view
{
If(!self.contentCreated)
{
[self createSceneContents];
self.contentCreated = YES;
}
}
- (void)createSceneContents
{
self.backgroundColor = [SKColor blackColor];
self.scaleMode = SKSceneScaleModeAspectFit;
}
@interface SpaceshipScene()
@property BOOL contentCreated;
@end
@implementation SpaceshipScene
- (void)didMoveToView:(SKView *)view
{
If(!self.contentCreated)
{
[self createSceneContents];
self.contentCreated = YES;
}
}
- (void)createSceneContents
{
self.backgroundColor = [SKColor blackColor];
self.scaleMode = SKSceneScaleModeAspectFit;
}3. 在HelloScene.m文件中導入SpaceshipScene.h頭。
[cpp]
#import "SpaceshipScene.h"
#import "SpaceshipScene.h"
4. 在touchesBegan:withEvent方法中,更改runAction:的調用為新的調用runAction:completion:。實現完成處理來創建並呈現一個新的場景。
[cpp]
[helloNode runAction:moveSequence completion:^ {
SKScene * spaceshipScene = [[SpaceshipScene alloc] initWithSize:self.size];
SKTransition *doors= [SKTransition doorsOpenVerticalWithDuration:0.5];
[self.view presentScene:spaceshipScene transition:doors];
}];
[helloNode runAction:moveSequence completion:^ {
SKScene * spaceshipScene = [[SpaceshipScene alloc] initWithSize:self.size];
SKTransition *doors= [SKTransition doorsOpenVerticalWithDuration:0.5];
[self.view presentScene:spaceshipScene transition:doors];
}];構建並運行該項目。當你觸摸場景內部時,文字淡出,然後在視圖過渡到新的場景。你應該看到一個黑色的屏幕。
使用節點構建復雜的內容
新的場景還沒有任何內容,所以你准備要添加一個飛船到場景。要構建這個太空飛船,你需要使用多個SKSpriteNode對象來創造了飛船和它表面的燈光。每個精靈節點都將執行動作。
精靈節點是在一個Sprite Kit應用程序中最常見用於創建內容的類。他們可以繪制無紋理或紋理的矩形。在這個例子中,你要使用無紋理對象。稍後,這些占位符(placeholder)可以很容易地用紋理精靈進行替換,而不改變它們的行為。在實際的游戲中,你可能需要幾十個或上百個節點來創建你的游戲的可視化內容。但是,從本質上說,那些精靈將使用與這個簡單的例子相同的技術。
雖然你可以直接添加所有三個精靈到場景,但這並不是Sprite Kit的方式。閃爍的燈光是飛船的一部分!如果飛船移動,燈光應該和它一起移動。解決的辦法是使飛船節點成為它們的父節點,同樣地場景將是飛船的父節點。光的坐標將要相對於父節點的位置來指定,而父節點是在子精靈圖像的中心。
添加飛船
1. 在SpaceshipScene.m中,添加代碼到createSceneContents方法來創建飛船。
[cpp]
SKSpriteNode *spaceship = [self newSpaceship];
spaceship.position = CGPointMake(CGRectGetMidX(self.frame),CGRectGetMidY(self.frame)-150);
[self addChild:spaceship];
SKSpriteNode *spaceship = [self newSpaceship];
spaceship.position = CGPointMake(CGRectGetMidX(self.frame),CGRectGetMidY(self.frame)-150);
[self addChild:spaceship];
2. 實現newSpaceship的方法。
[cpp]
- (SKSpriteNode *)newSpaceship
{
SKSpriteNode *hull= [[SKSpriteNode alloc] initWithColor:[SKColor grayColor] size:CGSizeMake(64,32);
SKAction *hover= [SKAction sequence:@[
[SKAction waitForDuration:1.0]
[SKAction moveByX:100 y:50.0 duration:1.0]
[SKAction waitForDuration:1.0]
[SKAction moveByX:-100.0 y:-50 duration:1.0]];
[hull runAction:[SKAction repeatActionForever:hover];
return hull;}
- (SKSpriteNode *)newSpaceship
{
SKSpriteNode *hull= [[SKSpriteNode alloc] initWithColor:[SKColor grayColor] size:CGSizeMake(64,32);
SKAction *hover= [SKAction sequence:@[
[SKAction waitForDuration:1.0]
[SKAction moveByX:100 y:50.0 duration:1.0]
[SKAction waitForDuration:1.0]
[SKAction moveByX:-100.0 y:-50 duration:1.0]];
[hull runAction:[SKAction repeatActionForever:hover];
return hull;}
此方法創建飛船的船體,並添加了一個簡短的動畫。需要注意的是引入了一種新的動作。一個重復的動作不斷地重復的傳遞給它的動作。在這種情況下,序列一直重復。
現在構建並運行應用程序來看當前的行為,你應該看到一個矩形。
在建立復雜的有孩子的節點時,把用來在構造方法後面或者甚至是在子類中創建節點的代碼分離出來,是一個很好的主意。這使得它更容易改變精靈的組成和行為,而無需改變使用精靈的客戶端(client)。
3. 添加代碼到newSpaceship方法來添加燈光。
[cpp]
SKSpriteNode *light1= [self newLight];
light1.position = CGPointMake(-28.0,6.0);
[hull addChild:light1];
SKSpriteNode *light2= [self newLight];
Light2.position = CGPointMake(28.0,6.0);
[hull addChild:light2];
SKSpriteNode *light1= [self newLight];
light1.position = CGPointMake(-28.0,6.0);
[hull addChild:light1];
SKSpriteNode *light2= [self newLight];
Light2.position = CGPointMake(28.0,6.0);
[hull addChild:light2];4. 實現newLight方法。
[cpp]
- (SKSpriteNode *)newLight
{
SKSpriteNode *light = [[SKSpriteNode alloc] initWithColor:[SKColor yellowColor] size:CGSizeMake(8,8)];
SKAction *blink= [SKAction sequence:@ [
[SKAction fadeOutWithDuration:0.25]
[SKAction fadeInWithDuration:0.25]];
SKAction * blinkForever = [SKAction repeatActionForever:blink];
[light runAction:blinkForever];
return light;
}
- (SKSpriteNode *)newLight
{
SKSpriteNode *light = [[SKSpriteNode alloc] initWithColor:[SKColor yellowColor] size:CGSizeMake(8,8)];
SKAction *blink= [SKAction sequence:@ [
[SKAction fadeOutWithDuration:0.25]
[SKAction fadeInWithDuration:0.25]];
SKAction * blinkForever = [SKAction repeatActionForever:blink];
[light runAction:blinkForever];
return light;
}當你運行應用程序時,你應該看到一對燈在飛船上。當飛船移動,燈光和它一起移動。這三個節點全都是連續動畫。你可以添加額外的動作,讓燈光在船的周圍移動,它們總是相對船體移動。
創建能交互的節點
在實際的游戲中,你通常需要節點之間能交互。把行為添加給精靈的方法有很多,所以這個例子僅展示其中之一。你將添加新節點到場景,使用物理子系統模擬它們的運動並實現碰撞效果。
Sprite Kit提供了一個完整的物理模擬,你可以使用它添加自動行為到節點。也就是說,物理在使其移動的節點上自動模擬,而不是在節點上執行動作。當它與物理系統一部分的其他節點交互時,碰撞自動計算並執行。
添加物理模擬到飛船場景
1. 更改newSpaceship方法來添加一個物理體到飛船。
[cpp]
hull.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:hull.size];
hull.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:hull.size];
構建並運行應用程序。等一下!飛船垂直墜落到屏幕下方。這是因為重力施加到飛船的物理體。即使移動動作仍在運行,物理效果也被應用到飛船上。
2. 更改的newSpaceship方法來防止飛船受物理交互影響。
[cpp]
hull.physicsBody.dynamic = NO;
hull.physicsBody.dynamic = NO;當你現在運行它時,應用程序像之前那樣運行。飛船不再受重力影響。稍後,這也意味著飛船的速度將不會受到碰撞的影響,。
3. 添加代碼到createSceneContents方法來生成大量巖石。
[cpp]
SKAction * makeRocks = [SKAction sequence:@ [
[SKAction performSelector:@selector(addRock) onTarget:self]
[SKAction waitForDuration:0.10 withRange:0.15]
]];
[self runAction:[SKAction repeatActionForever:makeRocks];
SKAction * makeRocks = [SKAction sequence:@ [
[SKAction performSelector:@selector(addRock) onTarget:self]
[SKAction waitForDuration:0.10 withRange:0.15]
]];
[self runAction:[SKAction repeatActionForever:makeRocks];場景也是一個節點,因此它也可以運行動作。在這種情況下,自定義操作調用場景上的方法來創建巖石。序列創建一個巖石,然後等待一段隨機時間。重復這個動作,場景不斷產生大量新的巖石。
4. 實現addRock方法。
[cpp]
static inline:CGFloat skRandf() {
return rand()/(CGFloat)RAND_MAX;
}
static inline CGFloat skRand(CGFloat low, CGFloat high) {
return skRandf()*(high - low) + low;
}
- (void)addRock
{
SKSpriteNode *rock = [[SKSpriteNode alloc] initWithColor:[SKColor brownColor] size:CGSizeMake(8,8)];
rock.position = CGPointMake(skRand(0, self.size.width),self.size.height-50);
rock.name = @“rock”;
rock.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:rock.size];
rock.physicsBody.usesPreciseCollisionDetection = YES;
[self addChild:rock];
}
static inline:CGFloat skRandf() {
return rand()/(CGFloat)RAND_MAX;
}
static inline CGFloat skRand(CGFloat low, CGFloat high) {
return skRandf()*(high - low) + low;
}
- (void)addRock
{
SKSpriteNode *rock = [[SKSpriteNode alloc] initWithColor:[SKColor brownColor] size:CGSizeMake(8,8)];
rock.position = CGPointMake(skRand(0, self.size.width),self.size.height-50);
rock.name = @“rock”;
rock.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:rock.size];
rock.physicsBody.usesPreciseCollisionDetection = YES;
[self addChild:rock];
}構建並運行該項目。巖石現在應該從場景上方落下來。當一塊石頭擊中了船,巖石從船上反彈。沒有添加動作來移動巖石。巖石下落並與船碰撞完全是由於物理子系統的作用。
巖石都很小且移動速度非常快,所以代碼指定精確的碰撞,以確保所有的碰撞都檢測到。
如果你讓應用程序運行了一段時間,幀率會開始下降,即使節點計數仍然很低。這是因為節點的代碼僅顯示出場景中可見的節點。然而,當巖石落下到場景的底部時,它們繼續存在於場景中,這意味著物理還在對它們模擬。最終,有如此多的節點正在處理以致Sprite Kit減慢了。
5. 實現場景中的didSimulatePhysics方法來當巖石移動到屏幕之外時移除它們。
[cpp]
- (void)didSimulatePhysics
{
[self enumerateChildNodesWithName:@“rock” usingBlock:^(SKNode *node, BOOL *stop){
if (node.position.y <0)
[node removeFromParent];
}];
}
- (void)didSimulatePhysics
{
[self enumerateChildNodesWithName:@“rock” usingBlock:^(SKNode *node, BOOL *stop){
if (node.position.y <0)
[node removeFromParent];
}];
}每次場景處理一幀,都運行動作和模擬物理。你的游戲可以掛接到這個過程中來執行其他自定義代碼。在每一幀,場景將處理物理,然後移除移出屏幕底部的所有巖石。當你運行應用程序時,幀率保持不變。
在場景中,預處理及後處理與動作和物理結合的地方,就是你建立你的游戲的行為的地主。
這就是你第一次體驗Sprite Kit!其它一切都是你在這裡看到的基本技術的細化。
試試這個!
這裡有一些東西,你可以嘗試:
· 做一個OS X版本的這個例子。你在視圖控制器寫的代碼,在OS X上通常是在一個應用程序委托中實現。響應代碼需要改變來使用鼠標事件而不是觸摸事件。但是,代碼的其余部分應是相同的。
· 使用紋理精靈呈現船和巖石。(提示:“使用精靈”)
· 嘗試在觸摸事件的響應中移動飛船。(提示:“添加動作節點”和“構建場景”)。
· 添加額外的圖形效果到場景(提示:“使用其他節點類型”)
· 巖石與船舶碰撞時添加其他行為。例如,使巖石發生爆炸。(提示:“模擬物理”)