當前位置:首頁 >教程首頁 > 华体会hth体育app在线登录 > 3D模型大師班 >如何用Sprite Kit製作《太空入侵者》(1)

如何用Sprite Kit製作《太空入侵者》(1)

發布時間:2018-11-17 19:57:24
  《太空入侵者》是遊戲史上具有重大意義的一款遊戲。這款由西角友宏(Tomohiro Nishikado)製作的遊戲於1978年由Taito Corporation發行,收益達數十億美元。它成為一種文化符號,吸引無數並不擅長玩遊戲的人把遊戲當成愛好。

  當年,《太空入侵者》是一款街機遊戲,卷走了那一代玩家不少零花錢。後來Atari 2600家用遊戲機上市,《太空入侵者》成為拉到Atari硬件銷量的“殺手級應用”。

原版《太空入侵者》是一款街機遊戲

  在本教程中,我將教大家如何用Sprite Kit製作一款iOS版的《太空入侵者》。Sprite Kit是隨iOS 7一起推出的2D遊戲框架。

  為了學習本教程,你必須熟悉Sprite Kit的基礎。否則,你最好先學習一下Sprite Kit的入門教程。

  另外,為了實現本教程的大部分效果,你需要一部運行iOS7的iPhone或iPod Touch和一個Apple開發者帳號。因為你需要使用iOS模擬器不具有的加速計來使飛船移動,如果你沒有iOS7設備或開發者帳號,雖然你仍然可以完成本教程,但你的飛船將不能移動。

  言歸正傳,我們開始製作外星人吧!

  開始

  蘋果提供了叫作Sprite Kit Game的XCode 5模板。對於從無到有製作遊戲,Sprite Kit Game是非常實用的。然而,為了讓你更快上手,你要先下載本教程的初始項目。它是以Sprite Kit模板為基礎的,已經幫你做完一些單調乏味的工作。

  下載並解壓這個項目後,找到SKInvaders目錄,雙擊SKInvaders.xcodeproj,在Xcode中打開這個項目。

  點擊Xcode工具條(左上角的第一個按鈕)上的Run按鈕,創建並運行這個項目;或者使用鍵盤快捷鍵Command + R。你應該能看到以下屏幕出現在你的設備或模擬器上:

App Screenshot_FirstRun

  異形——這個外星入侵者正是看著你!然而,如果你看到以上屏幕,那麼你就可以看下麵的內容了。

  GameScene的作用

  為了完成你的《太空入侵者》,你要編寫幾個獨立的遊戲邏輯;本教程將是一個非常好的構建和精煉遊戲邏輯的練習。它還能幫助你鞏固理解如何將Sprite Kit元素組裝在一起以產生遊戲中的活動。

  我們來看看這款遊戲是如何設置的。打開GameViewController.m,向下滾動到viewDidLoad。這個方法是所有UIKit應用的關鍵,在GameViewController將它的視圖加載到內存後運行。它的作用是,作為運行時進一步自定義應用UI的地點。

  看看viewDidLoad的有趣的部分:

- (void)viewDidLoad
{
// … omitted …

SKView * skView = (SKView *)self.view;

// … omitted …

//1 Create and configure the scene.
SKScene * scene = [GameScene sceneWithSize:skView.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFill;

//2 Present the scene.
[skView presentScene:scene];
}

  以上代碼創建和以如下順序顯示場景(scene):

  1、製作具有相同尺寸的場景作為它的封閉視圖。scaleMode確保這個場景的大小足以充滿整個視圖(view)。

  2、呈現這個場景,把它繪製在屏幕上。

  當GameScene顯示在屏幕上後,它取代GameViewController並驅動遊戲的其他部分。

  打開GameScene.m,看看它的結構是怎麼樣的:

#import “GameScene.h”
#import “GameOverScene.h”
#import <CoreMotion/CoreMotion.h>

#pragma mark – Custom Type Definitions

#pragma mark – Private GameScene Properties

@interface GameScene ()
@property BOOL contentCreated;
@end

@implementation GameScene

#pragma mark Object Lifecycle Management

#pragma mark – Scene Setup and Content Creation

- (void)didMoveToView:(SKView *)view
{
if (!self.contentCreated) {
[self createContent];
self.contentCreated = YES;
}
}

- (void)createContent
{
// … omitted …
}

#pragma mark – Scene Update

- (void)update:(NSTimeInterval)currentTime
{
}

#pragma mark – Scene Update Helpers

#pragma mark – Invader Movement Helpers

#pragma mark – Bullet Helpers

#pragma mark – User Tap Helpers

#pragma mark – HUD Helpers

#pragma mark – Physics Contact Helpers

#pragma mark – Game End Helpers

@end

  你應該注意到這裏有許多#pragma mark-XXX型的語句。這些叫作“編譯程序指令”(compiler directives),因為它們控製編譯器。這些特殊的pragma(編譯器指令)的唯一作用就是讓源文件易更容易查找。

  你是不是想問,pragma如何使源文件更容易查找?注意GameScene.m旁邊寫的是“No Selection”,如下圖所示:

XCodeScreenshot_PragmaMenuDisplaying

  如果你點擊“No Selection”,會彈出一個小菜單,如下圖所示:

XCodeScreenshot_PragmaMenuLocation

  那就是你的所有pragma的列表!點擊任意pragma,就會跳到文件的那個部分。這個特征現在看起來還不太有用,但當你添加大量“消滅入侵者”的代碼後,你就會覺得這些pragma非常非常實用了!

  製作太空入侵者

  在你開始寫代碼前,先想一想GameScene類。它在什麼時候初始化和呈現在屏幕上?什麼時候最適合在屏幕上出現它的內容?

  你可能會想到場景的初始化程序initWithSize:正好滿足需要,但這個場景可能不能按初始化程序運行的時間完全配置。所以最好是當場景已經被視圖展示出來時再做一個場景的容器,因為在那時,場景運行的環境已經“準備就緒”。

  視圖觸發場景的didMoveToView:方法,使場景呈現在遊戲世界中。找到didMoveToView:,你會看到如下代碼:

- (void)didMoveToView:(SKView *)view
{
if (!self.contentCreated) {
[self createContent];
self.contentCreated = YES;
}
}

  這個方法用BOOL屬性contentCreated調用createContent,確保你不會二次創建場景的內容。

  這個屬性是在靠近文件開頭部分的Objective-C類拓展中定義的:

#pragma mark – Private GameScene Properties

@interface GameScene ()
@property BOOL contentCreated;
@end

  正如pragma指出的,這個類拓展允許你給GameScene類添加“專用”屬性,而不展現給其他外部類或代碼。你仍然得到使用Objective-C屬性的好處,但你的GameScene狀態是在內部保存的,不能被其他外部類虛位修改。同樣地,它不會混雜你的其他類看到的數據類型的命名空間。

  你可以在你的文件中定義重要的常量為專用定義。找到 #pragma mark – Custom Type Definitions後添加如下代碼:

//1
typedef enum InvaderType {
InvaderTypeA,
InvaderTypeB,
InvaderTypeC
} InvaderType;

//2
#define kInvaderSize CGSizeMake(24, 16)
#define kInvaderGridSpacing CGSizeMake(12, 12)
#define kInvaderRowCount 6
#define kInvaderColCount 6
//3
#define kInvaderName @”invader”

  以上類型定義和常量定義負責的任務如下:

  1、定義敵人的可能類型。你之後可以在switch聲明中使用這個,也就是當你需要做如展示各類敵人的不同sprite圖象時。typedef也使InvaderType成為正式的Objective-C類型,它是用於方法參數和變量的類型檢查。這保證你不會漏掉錯誤的方法參數或把它賦給錯誤的變量。

  2、定義敵人的大小,和確保它們按橫行豎列的布局出現在屏幕上。

  3、定義名稱,當在屏幕上搜索敵人時,你將使用它來確定敵人。

  像這樣定義常量比使用數字如6或字符串@“invader”來得好(容易拚寫錯誤)。想象一下當你想打@“invader”時卻誤輸入@“Invader”,然後花了數小時尋找一個簡單但卻把一切都搞砸了的錯字。使用像kInvaderRowCount和kInvaderName這樣的常量可以防止令人沮喪的bug,以及方便其他程序員理解這些常量的意思。

  現在可以做“入侵者”了!添加如下方法到GameScene.m,就接在createContent之後:

//1
SKColor* invaderColor;
switch (invaderType) {
case InvaderTypeA:
invaderColor = [SKColor redColor];
break;
case InvaderTypeB:
invaderColor = [SKColor greenColor];
break;
case InvaderTypeC:
default:
invaderColor = [SKColor blueColor];
break;
}

//2
SKSpriteNode* invader = [SKSpriteNode spriteNodeWithColor:invaderColor size:kInvaderSize];
invader.name = kInvaderName;

return invader;
}

  makeInvaderOfType:,顧名思義,創造給定類型的入侵者的sprite。以上代碼的作用是:

  1、使用invaderType參數確定入侵者的顏色。

  2、調用SKSpriteNode的spriteNodeWithColor:size:方法來分配和初始化sprite,sprite渲染為具有給定顏色invaderColor和大小kInvaderSize的矩形。

  好吧,所以有色方塊並不能代表我們所想象中的凶殘的敵人。你可能會有衝動先設計入侵者sprite圖像,然後就幻想著可以動畫化它們的所有好方法,但最好的辦法其實是專心做好遊戲邏輯先,再來關心美術方麵。

  添加makeInvaderOfType:還不足以在屏幕上表現入侵者。你需要一些東西來調用makeInvaderOfType:,然後把新創造的sprites放在場景裏。

  仍然是在GameScene.m,把以下方法直接添加到makeInvaderOfType:之後:

-(void)setupInvaders {
//1
CGPoint baseOrigin = CGPointMake(kInvaderSize.width / 2, 180);
for (NSUInteger row = 0; row < kInvaderRowCount; ++row) {
//2
InvaderType invaderType;
if (row % 3 == 0)      invaderType = InvaderTypeA;
else if (row % 3 == 1) invaderType = InvaderTypeB;
else                   invaderType = InvaderTypeC;

//3
CGPoint invaderPosition = CGPointMake(baseOrigin.x, row * (kInvaderGridSpacing.height + kInvaderSize.height) + baseOrigin.y);

//4
for (NSUInteger col = 0; col < kInvaderColCount; ++col) {
//5
SKNode* invader = [self makeInvaderOfType:invaderType];
invader.position = invaderPosition;
[self addChild:invader];
//6
invaderPosition.x += kInvaderSize.width + kInvaderGridSpacing.width;
}
}
}

  以上方法把入侵者按橫行豎列布局在屏幕上。每行隻有一種類型的入侵者。這個邏輯似乎很複雜,但如果你把它分解開來,就容易理解了:

  1、行循環。

  2、根據行數給這一行的所有入侵者選擇一個InvaderType。

  3、以同樣的方法算出這一行的第一個入侵者應該出現的位置。

  4、列循環。

  5、給當前行和列生成入侵者,並添加到場景中。

  6、更新invaderPosition,這樣下一個入侵者就不會錯位了。

  現在,你隻需要把這些入侵者展示在屏幕上。用以下代碼替換createContent中的當前代碼:

[self setupInvaders];

  創建並運行你的應用,你看到的入侵者布局應該如下圖所示:

AppScreenshot_SetupInvadersFirstRun

  製作飛船

  當這些邪惡的入侵者出現在屏幕上時,你的正義飛船不能離得太遠。與入侵者的做法一樣,首先必須定義一些常量。

  添加如下代碼到#define kInvaderName語句之後:

#define kShipSize CGSizeMake(30, 16)
#define kShipName @”ship”

  kShipSize表示飛船的大小,kShipName表示飛船的名稱。

  接著添加如下兩個方法到setupInvaders:後麵:

-(void)setupShip {
//1
SKNode* ship = [self makeShip];
//2
ship.position = CGPointMake(self.size.width / 2.0f, kShipSize.height/2.0f);
[self addChild:ship];
}

-(SKNode*)makeShip {
SKNode* ship = [SKSpriteNode spriteNodeWithColor:[SKColor greenColor] size:kShipSize];
ship.name = kShipName;
return ship;
}

  以下是上述兩個方法的作用:

  1、使用makeShip生成飛船。你可以簡單地重複使用makeShip,如果之後你需要製作另一個飛船的話(即如果當前飛船被外星人摧毀,且玩家還有剩餘“命數”)。

  2、把飛船放在屏幕上。在Sprite Kit中,源頭是在屏幕的左下角。anchorPoint是座標為(0,0)位於sprite區域左下方和座標為(1,1)、右上方的unit square。因為有默認的anchorPoint(0.5, 0.5),即它的中心就是飛船所在位置的中心。把飛船放在kShipSize.height/2.0f ,意味著飛船將一半露出來。如果你查看公式,你會看到飛船的底部與屏幕的底部完全一致。

  為了使飛船出現在屏幕上,添加如下語句到createContent的底部:

[self setupShip];

  創建和運行你的應用,你應該看到飛船在在屏幕上,如下圖所示:

AppScreenshot_AddedShip

  不要怕,地球人,你的飛船正在保護你們!

  添加HUD

  如果不能看到得分,玩這樣的《太空入侵者》也沒什麼意思,對吧?接下我們要給遊戲添加HUD。作為一個守護地球的太空戰士,你的表現會受到指揮官的監視。他們對你的“技術(得分)”和“戰備(命值)”都非常關心。

  添加如下常量到GameScene.m的開頭部分,也就是在#define kShipName的下方:

#define kScoreHudName @”scoreHud”
#define kHealthHudName @”healthHud”

  現在,通過插入以下方法到makeShip後麵,添加你的HUD:

-(void)setupHud {
SKLabelNode* scoreLabel = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
//1
scoreLabel.name = kScoreHudName;
scoreLabel.fontSize = 15;
//2
scoreLabel.fontColor = [SKColor greenColor];
scoreLabel.text = [NSString stringWithFormat:@"Score: %04u", 0];
//3
scoreLabel.position = CGPointMake(20 + scoreLabel.frame.size.width/2, self.size.height – (20 + scoreLabel.frame.size.height/2));
[self addChild:scoreLabel];

SKLabelNode* healthLabel = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
//4
healthLabel.name = kHealthHudName;
healthLabel.fontSize = 15;
//5
healthLabel.fontColor = [SKColor redColor];
healthLabel.text = [NSString stringWithFormat:@"Health: %.1f%%", 100.0f];
//6
healthLabel.position = CGPointMake(self.size.width – healthLabel.frame.size.width/2 – 20, self.size.height – (20 + healthLabel.frame.size.height/2));
[self addChild:healthLabel];
}

  這是在屏幕上創建和添加文本標簽的樣板代碼。相關的要點如下:

  1、給得分標簽一個名稱,這樣之後當你需要更新顯示的得分時,你就能找到它了。

  2、把得分標簽設為綠色。

  3、把得分標簽放在屏幕的左上方。

  4、給命值標簽一個名稱,這樣之後當你需要更新顯示的命值時,你就能找到它了。

  5、把命值標簽設為紅色;紅色和綠色是這類指示器的常用顏色,且容易與混亂的戰鬥元素區別開來。

  6、把命值標簽放在屏幕的右上方。

  添加如下語句到createContent底部,以調用HUD的設置方法:

[self setupHud];

  創建並運行你的遊戲,你應該如下圖所示的屏幕:

AppScreenshot_AddedHud

  入侵者?做好了。飛船?做好了。HUD?做好了。現在你所需要的是把它們都關聯起來的動態活動!

  添加入侵者的活動

  為了把你的遊戲渲染到屏幕上,Sprite Kit使用了game loop,它能夠不斷地搜索需要屏幕上的元素更新的狀態變化。這個game loop有幾個作用,但你你應該對更新你的場景的機製有興趣。你要重載update:方法,它是你的GameScene.m文件的存根。

  當你的遊戲順暢運行,渲染效率為60幀每秒(遊戲邦注:iOS設備能支持的最大值為60fps),update:會以這個速度調用。這時候可以修改場景的狀態,如改變得分、移除死亡的侵略者的圖形或移動飛船。

  你將使用update:讓侵略者在屏幕上橫向和縱向移動。第一次Sprite Kit調用update:,就像在問你“你的場景改變了嗎?”、“你的場景改變了嗎?”……回答這個問題是你的工作—-你通過寫代碼回答這個問題。

  在GameScene.m的開頭,即InvaderType枚舉的定義下插入以下代碼:

typedef enum InvaderMovementDirection {
InvaderMovementDirectionRight,
InvaderMovementDirectionLeft,
InvaderMovementDirectionDownThenRight,
InvaderMovementDirectionDownThenLeft,
InvaderMovementDirectionNone
} InvaderMovementDirection;

  入侵者有固定的移動模式:右,右,下,左,左,下,右,右……所以你要使用InvaderMovementDirection來追蹤侵略者的模式進度。例如,InvaderMovementDirectionRight意思是入侵者在右邊,移動模式的右半部分。、

  接著,在這個相同的文件中找到類拓展,並插入以下屬性到已有的contentCreated的屬性下方:

@property InvaderMovementDirection invaderMovementDirection;
@property NSTimeInterval timeOfLastMove;
@property NSTimeInterval timePerMove;

  添加以下代碼到createContent的開頭:

//1
self.invaderMovementDirection = InvaderMovementDirectionRight;
//2
self.timePerMove = 1.0;
//3
self.timeOfLastMove = 0.0;

  這個一次性設置代碼初始化入侵者的移動:

  1、一開始,入侵者向右移動。

  2、入侵者每秒移動一步。即向左、向右或向下移動都需要1秒鍾的時間。

  3、入侵者還沒移動,即設置時間為0.

  現在,你就要讓入侵者移動起來了。添加以下代碼到#pragma mark – Scene Update Helpers下麵:

// This method will get invoked by update:
-(void)moveInvadersForUpdate:(NSTimeInterval)currentTime {
//1
if (currentTime – self.timeOfLastMove < self.timePerMove) return;

//2
[self enumerateChildNodesWithName:kInvaderName usingBlock:^(SKNode *node, BOOL *stop) {
switch (self.invaderMovementDirection) {
case InvaderMovementDirectionRight:
node.position = CGPointMake(node.position.x + 10, node.position.y);
break;
case InvaderMovementDirectionLeft:
node.position = CGPointMake(node.position.x - 10, node.position.y);
break;
case InvaderMovementDirectionDownThenLeft:
case InvaderMovementDirectionDownThenRight:
node.position = CGPointMake(node.position.x, node.position.y - 10);
break;
InvaderMovementDirectionNone:
default:
break;
}
}];

//3
self.timeOfLastMove = currentTime;
}

  這是以上代碼的故障:

  1、如果還沒到移動的時間,就退出該方法。moveInvadersForUpdate:每秒調用60次,但你不希望入侵者移動得這麼頻繁,因為這個速度對正常人來說太快了。

  2、你的場景把所有入侵者保存為子節點;你使用setupInvaders的addChild:把它們添加到場景,用它的名稱屬性識別各個入侵者。調用kInvaderName;這使循環跳過你的飛船和HUD。塊移動的作用是向右、左或下移動入侵者10像素,取決於invaderMovementDirection的值。

  3、記錄入侵者的移動,這樣當這個方法下次調用時,侵略者就會再次移動,直到規定的一秒時間周期走完。

  為了讓入侵者移動,用以下代碼替換已存在的update:方法:

-(void)update:(NSTimeInterval)currentTime {
[self moveInvadersForUpdate:currentTime];
}

  創建並運行你的應用;你應該看到入侵者緩慢地通過屏幕。繼續觀察,你最終會看到以下屏幕:

AppScreenshot_InvadersMovedOffScreen

  什麼情況?為什麼入侵者消失了?也許這些侵略者沒有你想的那麼可怕!

  入侵者還不知道當觸到操作區域的邊緣時,自己必須向下移動並改變方向一次。看來你得幫助這些侵略者找到路了!

  控製入侵者的方向

  添加如下代碼到 #pragma mark – Invader Movement Helpers後麵:

-(void)determineInvaderMovementDirection {
//1
__block InvaderMovementDirection proposedMovementDirection = self.invaderMovementDirection;

//2
[self enumerateChildNodesWithName:kInvaderName usingBlock:^(SKNode *node, BOOL *stop) {
switch (self.invaderMovementDirection) {
case InvaderMovementDirectionRight:
//3
if (CGRectGetMaxX(node.frame) >= node.scene.size.width - 1.0f) {
proposedMovementDirection = InvaderMovementDirectionDownThenLeft;
*stop = YES;
}
break;
case InvaderMovementDirectionLeft:
//4
if (CGRectGetMinX(node.frame) <= 1.0f) {
proposedMovementDirection = InvaderMovementDirectionDownThenRight;
*stop = YES;
}
break;
case InvaderMovementDirectionDownThenLeft:
//5
proposedMovementDirection = InvaderMovementDirectionLeft;
*stop = YES;
break;
case InvaderMovementDirectionDownThenRight:
//6
proposedMovementDirection = InvaderMovementDirectionRight;
*stop = YES;
break;
default:
break;
}
}];

//7
if (proposedMovementDirection != self.invaderMovementDirection) {
self.invaderMovementDirection = proposedMovementDirection;
}
}

  以下是這段代碼的作用:

  1、因為塊獲取入侵者是按默認的const(常量,意味著它們是不可以修改的),你必須用 __block限製proposedMovementDirection,這樣你才可以在//2中修改它。

  2、循環屏幕上的所有入侵者,用作為參數的侵略者調用塊。

  3、如果入侵者的右邊在屏幕的右邊緣的1點內,這就要移出屏幕。設置proposedMovementDirection,這親入侵者就會先下移動再向左移動。你比較入侵者的幀(遊戲邦注:包含它在場景的座標係的內容的幀)和這個場景的寬度。因為這個場景有一個默認(0,0)座標的anchorPoint,被縮放以充滿它的父視圖,這個比較保證你測試入侵者是否觸到視圖的邊緣。

  4、如果入侵者的左邊緣在屏幕的左邊緣的1點內,它就要稱出屏幕。設置proposedMovementDirection迪樣侵略者就先向下再向右移動。

  5、如果入侵者正在向下然後向左移動,這時它們已經向下移動了,所以它們現在應該向左移動。當你把determineInvaderMovementDirection和moveInvadersForUpdate:結合起來時,這是如何運作的就顯得非常明顯了。

  6、如果入侵者正在向下然後向右移動,這時它們已經向下移動了,所以它們現在應該向右移動。

  7、如果這種計劃的入侵者移動方向不同於當前入侵者移動方向,那麼就用計劃的方向更新當前方向。

  添加以下代碼到moveInvadersForUpdate:中的determineInvaderMovementDirection,就是在self.timeOfLastMove的條件檢查之後:

[self determineInvaderMovementDirection];

  為什麼添加determineInvaderMovementDirection的調用到檢查self.timeOfLastMove之後很重要?那是因為你希望入侵者移動方向隻當侵略者確實正在移動時才改變。入侵者隻有當檢查self.timeOfLastMove——即這個條件表達式為真時,通過進才移動。

  如果你添加以上這一行新代碼到moveInvadersForUpdate:的第一行,會怎麼樣呢?如果你那麼做了,會出現兩個bug:

  1、更新移動方向太頻繁——60幀每秒,當你知道它可以每秒隻改變一次時。

  2、入侵者永遠不會向下移動,因為從InvaderMovementDirectionDownThenLeft到InvaderMovementDirectionLeft的狀態過渡在二者之間沒有入侵者移動時才發生。通過檢查self.timeOfLastMove的moveInvadersForUpdate:的下一次調用會有self.invaderMovementDirection == InvaderMovementDirectionLeft執行,並保持入侵者向左移動,跳過向下移動。類似的bug會因InvaderMovementDirectionDownThenRight和InvaderMovementDirectionRight而存在。

  創建和運行你的應用,你會看到入侵者正如期望的那樣橫向和縱向移動。如下圖所示:

moving right-left-left

  注:你可能已經注意到,入侵者的移動有些不順。那是因為你的代碼每秒移動入侵者一次——移動距離是適合的。但原版遊戲的移動是不順的,所以保持這個特征可以讓你的遊戲看起來更正宗。

  添加飛船的活動

  好消息:你的監督者現在可以看到入侵者的移動,決定你的飛船必須有一個推進係統!為了提高效率,任何優秀的推進係統都必須有良好的操作係統。換句話說,作為飛行員的你如何讓飛船的推進係統知道你想做什麼?

  務必記住,手機遊戲不是電腦/街機遊戲,所以電腦/街機操作方法不能照搬到手機上。

  在電腦/街機版的《太空入侵者》中,你有一個控製飛船移動的實體操作杆和射擊入侵者的發射鍵。而手機設備如iPhone或iPad就不是這樣了。

  有些遊戲企圖使用虛擬操作杆或虛擬D板,但我認為這些方法很少能管用。

  想一想你通常是如何使用你的iPhone的:用一隻手托著它,用另一隻手點擊/滑動屏幕。

  記住一手托iPhone的人體工學,思考幾種可能的飛船移動和開火的操作模式:

  觸擊一下就是移動飛船同,觸擊兩下就是發射加農炮:

  假設當你觸擊一下飛船的左邊,飛船就向左移,觸擊右邊就向右移,觸擊兩下飛船就開火。這個操作模式有幾個缺點。

  第一,用相同的方式識別單觸擊和雙觸擊,需要延遲識別單觸擊直到雙觸擊失敗或失效。當你正在瘋狂地觸擊屏幕時,這個延遲會使操作非常遲鈍。第二,單觸擊和雙觸擊有時候會很混亂。第三,當你的飛船接近屏幕的左邊緣或右邊緣時,就很難觸擊了。

  滑動移動飛船,單觸擊開火:

  這個方法稍好一些。單觸擊開火很合適,因為這個動作都是不連續的:一下觸擊相當於一次開火。很直觀。滑動適合用來移動飛船嗎?

  不可行是因為滑動是一種不連續的動作。換句話說,無論你是滑動了還是滑動不到能當作滑動的長度,控製飛船向左向右移動的量都會打破玩家關於滑動及其功能的心理模型。在所有其他應用中,滑動是不連續的,滑動的長度是沒有意義的。所以這種操作方式也不可行。

  設備向左/右傾斜移動飛船,單觸擊開火:

  單觸擊開火已經被確定是可行的。但傾斜設備來移動飛船呢?這是你的最佳選擇,因為你已經用手掌托著手機,通過傾斜來移動你的飛船隻需要手腕稍稍活動一下。

  既然確定了操作方式,現在你可以想法通過傾斜移動你的飛船了。

  用設備活動控製飛船移動

  你可能對UIAccelerometer很熟悉,自可檢測設備傾斜的iOS 2.0發布起,它就可以使用了。然而,iOS 5.0棄用UIAccelerometer了,所以iOS 7應用應該使用CMMotionManager,它是Apple的CoreMotion框架的一部分。

  CoreMotion庫已經被添加到初始項目中,所以你不必再添加一次了。

  你的代碼可以從CMMotionManager檢索到數據,方法能兩種:

  把加速計數據擠入代碼

  在這種情況下,你提供帶塊的CMMotionManager,這個塊可以經常調用加速計數據。這與你的場景的每秒60幀的update:方法不適合。你隻想在這些項目中抽樣加速計數據—-那些把戲可能不會與CMMotionManager決定把數據放進你的代碼的活動一致。

  從代碼中抽出加速計數據

  在這種情況下,你調用CMMotionManager且當需要時向它獲取數據。把這些調用放在你的場景的update:方法中,與你的係統的記號排列一致。你將以每秒60次的速度取樣加速計。所以不必擔心延遲。

  你的應用應該隻使用一個CMMotionManager實例來保證你的獲得最可靠的數據。為了達到那個效果,添加以下屬性到你的類拓展:

@property (strong) CMMotionManager* motionManager;

  現在,添加如下代碼到didMoveToView:,就在self.contentCreated = YES;語句下麵:

self.motionManager = [[CMMotionManager alloc] init];
[self.motionManager startAccelerometerUpdates];

  這個新代碼生成otion manager和accelerometer數據。在這時,你可以使用otion manager和它的accelerometer數據來控製飛船的移動。

  添加如下方法到moveInvadersForUpdate:後麵:

-(void)processUserMotionForUpdate:(NSTimeInterval)currentTime {
//1
SKSpriteNode* ship = (SKSpriteNode*)[self childNodeWithName:kShipName];
//2
CMAccelerometerData* data = self.motionManager.accelerometerData;
//3
if (fabs(data.acceleration.x) > 0.2) {
//4 How do you move the ship?
NSLog(@”How do you move the ship: %@”, ship);
}
}

  仔細檢查這個方法,你會發現:

  1、從屏幕上獲得飛船以便移動它。

  2、從motion manager中獲得accelerometer數據。

  3、如果你的設備是用屏幕朝上和按鍵的主頁鍵來調整方向的,那麼向右傾斜設備會產生data.acceleration.x > 0,而向左傾斜產生data.acceleration.x < 0。這個檢查結果0.2意味著設備被認為是完全平放的(data.acceleration.x == 0),隻要它接近於0(data.acceleration.x的值落在[-0.2, 0.2]內)。0.2沒有什麼特殊的,它隻是看起來對我管用。這種小技巧可以讓你的操作係統更可靠,更少讓玩家受挫。

  4、你到底如何使用data.acceleration.x來移動飛船?你希望小值能將飛船移動一小段距離,大值移動一大段距離。答案是——下一部分將介紹的物質物理學!

  通過物理學將動作控製變成移動

  Sprite Kit有一個基於Box 2D的強大的內置物理係統,Box 2D可以模擬大量物理現象,如力、轉化、旋轉、碰撞和接觸察覺。每一個SKNode和SKSpriteNode都有一個附屬於它的SKPhysicsBody。這個SKPhysicsBody表示物理模擬。

  添加如下代碼到makeShip中的最後的return ship;之前:

//1
ship.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:ship.frame.size];
//2
ship.physicsBody.dynamic = YES;
//3
ship.physicsBody.affectedByGravity = NO;
//4
ship.physicsBody.mass = 0.02;

  輪流看各個注釋,你會發現:

  1、創建與飛船同樣大小的矩形物理剛體。

  2、使形狀具有動態,這使它能承受碰撞和其他外力作用。

  3、你不希望飛船跌落到屏幕下方,所以你要指定它不受重力影響。

  4、給飛船任意質量,這樣它的移動會顯得更自然。

  現在用以下代碼替換processUserMotionForUpdate:中的NSLog聲明(接在注釋//4後麵):

[ship.physicsBody applyForce:CGVectorMake(40.0 * data.acceleration.x, 0)];

  新代碼把力在與data.acceleration.x相同的方向上賦給飛船的物理剛體。數字40.0是使飛船運行顯得自然的隨機值。

  最後,添加如下代碼到update:的開頭:

[self processUserMotionForUpdate:currentTime];

  你的新processUserMotionForUpdate:現在與場景更新一樣,每秒被調用60次。

  注:如果你已經在模擬器裏測試了你的代碼,現在可以切換到你的設備裏測試了。你要等到在真正的設備上運行遊戲才能測試傾斜代碼。

  創建並運行你的遊戲,試著向左或向右傾斜你的設備,你的飛船會響應加速計,如下圖所示:

No_Player_Ship

  你看到了什麼?你的飛船飛出屏幕的邊界,消失在黑暗的太空中。如果你向相反方向傾斜的時間夠久,你可能會看到你的飛船又回來了。但現在,這樣的操作太古怪,太敏感了。這樣你是殺不死任何入侵者的!

  在物理模擬階段時,防止飛船飛出屏幕的簡單更實用的方法是,給屏幕的邊緣構建一個叫作edge loop的東西。它是沒有體積和質量仍然能與你的飛船碰撞的物理剛體。你可以把它想象成圍繞著屏幕的無限薄的牆體。

  因為你的GameScene是一種SKNode,你可以給它的剛體創建一個edge loop。

  添加以下代碼到createContent,在[self setupInvaders];語句的前麵:

self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];

  這個新代碼物理剛體添加到你的場景。

  創建並運行你的遊戲,試試通過傾斜設備來控製飛船的移動:

Player_Ship_In_Bounds

  你看到了什麼?如果你傾斜設備夠久,你的飛船會與屏幕的邊緣碰撞。它不會再離開屏幕。問題解決了!

  取決於飛船的勢頭,你可能也會看到飛船從屏幕的邊緣反彈回來,而不是停在那裏。這是Sprite Kit的物理引擎的另一個優點——restitution屬性。反彈效果不僅看起來酷,而且可以讓玩家清楚地知道屏幕邊緣就是不可以越過的邊界。

  然後呢?

  到目前為止,你已經製作了入侵者、飛船、HUD並把它們都繪製在屏幕上了。你還寫好了讓入侵者自動移動和使飛船隨著設備傾斜移動的代碼邏輯。

  在本教程的第二部分,你將給飛船和入侵者添加開火動作,還有一些讓你知道飛船什麼時候擊中侵略者的碰撞檢測。你還要通過添加聲音效果和用圖像替換現在的有色矩形(作為點位符來表示入分侵者和飛船)來潤飾你的遊戲。
华体会hth体育网 賞析
  • 2101期學員李思庭作品

    2101期學員李思庭作品

  • 2104期學員林雪茹作品

    2104期學員林雪茹作品

  • 2107期學員趙淩作品

    2107期學員趙淩作品

  • 2107期學員趙燃作品

    2107期學員趙燃作品

  • 2106期學員徐正浩作品

    2106期學員徐正浩作品

  • 2106期學員弓莉作品

    2106期學員弓莉作品

  • 2105期學員白羽新作品

    2105期學員白羽新作品

  • 2107期學員王佳蕊作品

    2107期學員王佳蕊作品

專業問題谘詢

你擔心的問題,火星幫你解答

微信掃碼入群領福利

掃碼領福利最新AI資訊

點擊谘詢
添加老師微信,馬上領取免費課程資源

1. 打開微信掃一掃,掃描左側二維碼

2. 添加老師微信,馬上領取免費課程資源

×

同學您好!

您已成功報名0元試學活動,老師會在第一時間與您取得聯係,請保持電話暢通!
確定