基于SpriteKit+Swift開發(fā)打竹塊游戲
譯文一、 簡介
SpriteKit是蘋果公司推出的跑在iOS和OS X上的游戲開發(fā)框架。這個工具不僅提供了強有力的圖形功能,而且還包括一個易于使用的物理引擎。最重要的是,你可以使用你熟悉的工具 ——Swift,Xcode和Interface Builder完成所有的工作!你可以用SpriteKit做很多的事情;但是,想了解它是如何工作的最佳方法就是使用它開發(fā)一個簡單的游戲。
在本系列教程(兩部分)中,你將要學習如何使用SpriteKit來開發(fā)一款Breakout游戲。其中,加入了完整的碰撞檢測技術,使用物理效果控制小球彈跳,通過觸摸來拖動擋板,游戲狀態(tài)控制等。
二、 開始
作為初始準備,建議你一定要先下載本教程對應的初始項目。此項目是使用標準的Xcode游戲模板創(chuàng)建的。所有的資源和狀態(tài)類都已經(jīng)被導入到項目中,這樣可以節(jié)省您的一點時間。隨著進一步閱讀,你會了解更多的游戲狀態(tài)。
你不妨先花點時間來熟悉一下整個項目。為此運行命令“Build”和”Run“,你會看到一個橫向模式的灰色屏幕。請參考下圖。
三、 Sprite Kit Visual Editor簡介
讓我們從配置場景文件開始工作吧。為此,請打開GameScene.sks文件。這是一個已鏈接到你的Sprite Kit場景的可視化編輯器,你可以從游戲的 GameScene.swift文件中訪問其中已有的每一個元素。
首先,你將調(diào)整場景的大小,使其適合您在本教程中介紹的目標屏幕:一個iPhone 6屏幕。為此,你可以在位于Xcode窗口右上角的Attributes inspector的Scene部分完成這一操作。如果你看不到Attributes inspector,您可以通過 View\Utilities\Show Attributes inspector來訪問它。請將場景的大小設置為 568 × 320,如下面的屏幕快照所示。
[注意]如果您的資源庫中包含了為適應多屏幕縮放因子(即 1 x,2x,3 x等)準備的圖片等資源的話,Sprite Kit將自動在當前運行的設備使用正確的資源文件。
現(xiàn)在,我們來考慮游戲的背景。如下面的屏幕快照所示,從Xcode窗口的右下角的對象庫面板上拖出一個Color Sprite。如果你看不到對象庫面板,那么可以從菜單欄選擇View\Utilities\Show Object Library。
請使用屬性檢查器將位置更改為284,160,把它的紋理設置為bg。
現(xiàn)在,你可以生成并運行一下游戲工程,欣賞一下你的游戲的背景顯示情況。
一旦你建立了擁有背景的橫屏場景,那么接下來就可以往其中添加小球了!仍然在GameScene.sks文件中,將一個新的Color Sprite拖動到場景中。然后,把它的名稱改為ball,紋理設置為ball,位置修改為284,220。然后,再把它的Z位置屬性值也設置為2,以確保小球出現(xiàn)在背景上。
現(xiàn)在,生成和運行你的項目,你會看到小球已經(jīng)出現(xiàn)在屏幕上,如圖所示。
然而,到目前為止,我們的游戲還不能動起來,這是因為我們還沒有添加物理部分呢。
四、 物理引擎
在Sprite Kit中,你需要工作在兩種環(huán)境中:你在屏幕上看到的圖形世界和物理世界,這決定了對象的移動和交互的方式。
在使用Sprite Kit物理引擎時,你需要做的第一件事是根據(jù)你的游戲的需要改變世界。世界對象是在使用Sprite Kit時管理所有的對象和物理模擬的主要對象。它還設置了物理機構(gòu)加入到世界對象中需要的重力屬性。默認的重力值是-9.81,因此類似于地球的實際重力值。所以,只要你在世界中添加一個物體,它就會往下落。
一旦配置了世界對象,你就可以往此世界對象中添加根據(jù)物理原則與它進行交互的東西。為此,最通常的方法是創(chuàng)建一個精靈 (圖形) 并設置它的物理body。物體的body屬性與世界決定了物體的移動方式。
Body可以是受物理力量影響的動態(tài)對象(如球,星星,小鳥......),或者是不受物理力量影響的靜態(tài)對象(平臺、墻......)。當創(chuàng)建一個Body時,你可以設置各種屬性,如形狀、密度、摩擦力,等等。這些屬性將嚴重影響B(tài)ody在世界范圍內(nèi)的行為。
當定義一個Body時,你可能會擔心其大小和密度的單位。在內(nèi)部,Sprite Kit使用公制(SI單位)。但是,在你的游戲中你通常不需要擔心實際的力量和質(zhì)量,只要你使用一致的值就行。
一旦你在世界中添加了所有的Body,Sprite Kit就會接管過控制權,并進行仿真。
為了設置第一個物理Body,你需要選擇剛剛添加的小球節(jié)點并從屬性檢查器的Physics Definition一節(jié)中選擇Body Type下的Bounding Circle并設置以下屬性值︰
- 取消勾選“Allows Rotation”
- 設置Friction為0
- 設置Restitution為1
- 將linear Damping(線性阻尼)設置為0
- 設置角阻尼(Angular Damping)為0
給小球添加物理特性
在這里,你創(chuàng)建了一個基于體積的物理Body,其形式為圓圈,具有與小球精靈完全相同的尺寸。這個物理Body受外力或沖動的影響,并能夠與其他物體發(fā)生碰撞。
下面具體介紹一下它的屬性。
- Allows Rotation:指定是否允許旋轉(zhuǎn)。在本例中,你不希望小球旋轉(zhuǎn)。
- Friction:這個屬性也很簡單,在我們的例子中要除去所有的摩擦。
- Restitution:是指對象的彈力。其值設置為1意味著,當小球與物體碰撞時將保持原來完整的彈性。簡言之,這意味著:小球會以與最初同等的作用力彈回來。
- Linear Damping(線性阻尼):通過減少物體的線性速度來模擬流體或空氣摩擦。在本例游戲中,小球移動時不應該減速。所以,在上面你需要設置阻尼為0。
- Angular Damping(角阻尼):除了角速度外,它與線性阻尼是相同的。當你不允許球旋轉(zhuǎn)時將此值設置為可選的。
[注意]通常情況下,最好是讓物理Body與玩家看到的極其相似。對于小球來說,我們已經(jīng)做到完美的匹配。然而,當你需要使用更復雜的形狀時,要格外小心,因為很復雜的Body意味著高性能的系統(tǒng)資源消耗。自從IOS 8和Xcode 6以來,Sprite Kit支持alpha蒙版Body類型(alpha masks body types),這將自動地把精靈的形狀用作其物理Body的形狀,但仍然要小心使用,因為這也可以降低系統(tǒng)性能。
現(xiàn)在,我們再一遍生成并運行工程。如果你反應足夠迅速,你應該看到小球從場景中落下,最后消失在屏幕的底部,如圖所示。
這種現(xiàn)象的出現(xiàn)存在兩個原因︰首先,場景的默認重力模擬了地球重力——沿x軸方向值為0而沿y軸方向是-9.8。第二,你的場景的物理世界是沒有界限的,尚無法用作封閉小球的籠子?,F(xiàn)在,讓我們著手解決這個問題!
五、 把小球關起來
把小球關起來的效果
現(xiàn)在,請打開文件GameScene.swift并把以下代碼行添加到didMoveToView(_:)方法的最后,用來創(chuàng)建一種圍繞屏幕的無形的障礙︰
- // 1
- let borderBody = SKPhysicsBody(edgeLoopFromRect: self.frame)
- // 2
- borderBody.friction = 0
- // 3
- self.physicsBody = borderBody
讓我們分析一下這行代碼︰
(1)創(chuàng)建一個基于邊緣(edge-based)的Body。與你添加到小球上的基于體積(volume-based)的Body相比,基于邊緣的Body并沒有質(zhì)量或體積,并且不受外力或沖量的影響。
(2)我們把摩擦力設置為0,這樣,小球與邊界障礙發(fā)生碰撞時其運動就不會減慢。相反,你想產(chǎn)生一種完美的效果,此時小球沿著它撞擊的屏障以相同的角度離開。
(3)你可以設置為每個節(jié)點設置一個物理Body。然后,將它添加到場景中。注︰SKPhysicsBody的坐標是相對于節(jié)點位置的。
再次運行你的項目,你現(xiàn)在應該看到像以前一樣的小球下落情景,但是現(xiàn)在當它降落到籠子的底部邊緣時它會回彈回來。因為你從與籠子和環(huán)境的接觸(Contact)中去除了摩擦力,并且設置小球的變形為完全彈性形變,因此,小球會永遠地那樣反彈運動下去。
為了讓小球運動效果更為圓滿,讓我們刪除重力并施加單脈沖,這樣它就會沿著屏幕永遠彈起彈落并運動下去。
六、 永久性彈性運動
現(xiàn)在,我們讓小球滾動起來(實際上還是彈起)。在文件GameScene.swift中在緊鄰上面添加的代碼行的后面添加以下代碼:
- physicsWorld.gravity = CGVector(dx: 0.0, dy: 0.0)
- let ball = childNodeWithName(BallCategoryName) as! SKSpriteNode
- ball.physicsBody!.applyImpulse(CGVector(dx: 2.0, dy: -2.0))
這段新代碼首先從場景中刪除所有的重力,然后從場景的子節(jié)點上檢索到小球并應用脈沖效果。脈沖能夠把一種立即生效的力施加到物理Body上,從而讓它朝著一個特定的方向(在本例中,沿對角線方向向右)運動。一旦小球設置為運動,它會簡單地在屏幕上反彈起來,這是因為您剛添加了屏障部分!
現(xiàn)在,是時候再去試試了!當你編譯并運行該項目時,您應該看到一個小球不斷跳躍在屏幕上——酷極了!
七、 添加擋板
如果沒有擋板,則不能稱其為一款打竹塊游戲,是不是?
現(xiàn)在,打開GameScene.sks文件來使用Visual Editor生成擋板(還有它的同伴物理Body),方式差不多就像你在場景的底部中間位置放置一個Color Sprite一樣,然后設置下列屬性值:
- Name = paddle
- Texture = paddle.png
- Position = 284,30
- Z Position = 3
- Body Type > Bounding rectangle
- 取消勾選Dynamic
- Friction: 0
- Restitution: 1
顯然,這里大部分的選項與前面創(chuàng)建小球時使用的選項是類似的。然而,這一次你使用了Bounding rectangle來形成物理Body,因而它將更好地匹配矩形的擋板。
這里通過關閉Dynamic選項,設置擋板是靜態(tài)的。這將確保擋板不會受外力和沖量的影響。你很快會看到為什么這很重要。
如果現(xiàn)在你生成和運行一下項目,你會看到擋板出現(xiàn)在場景中,而小球在碰到擋板時會彈起(如果你能等待一段足夠長時間的話)。請參考下圖。
到目前為止,一切比較順利!接下來,我們要使玩家能夠移動擋板。
八、 移動擋板
移動擋板需要檢測接觸相關信息。為此,我們在GameScene類中執(zhí)行下面的觸摸處理方法︰
- override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?)
- override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)
- override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?)
但是,在此之前,你還需要添加一個屬性。轉(zhuǎn)到GameScene.swift文件,然后向類中添加以下屬性︰
- var isFingerOnPaddle = false
這個屬性負責存儲是否玩家點按了擋板這一信息。你會需要它來執(zhí)行拖動擋板相關操作。
現(xiàn)在,請在GameScene.swift文件的touchesBegan(_:withEvent:)方法中添加如下代碼︰
- override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
- let touch = touches.first
- let touchtouchLocation = touch!.locationInNode(self)
- if let body = physicsWorld.bodyAtPoint(touchLocation) {
- if body.node!.name == PaddleCategoryName {
- print("Began touch on paddle")
- isFingerOnPaddle = true
- }
- }
- }
上面的代碼獲取觸摸信息并使用它來查找場景中觸摸位置。下一步,使用 bodyAtPoint(_:)方法查找在該位置與節(jié)點(如果有的話)相關聯(lián)的物理Body。
最后,檢查觸摸位置是否存在一個節(jié)點;如果存在的話,判斷該節(jié)點是否是擋板。這正是較早時創(chuàng)建對象名稱發(fā)揮作用的時候——你可以通過檢查名稱來檢查特定對象。如果觸摸位置處的對象是擋板,那么會有一條日志消息發(fā)送到控制臺,同時isFingerOnPaddle被設置為true。
現(xiàn)在,你可以建立并重新運行該項目。當你點擊擋板時,您應該看到在控制臺中的日志消息。
接下來,添加如下所示代碼:
- override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
- // 1
- if isFingerOnPaddle {
- // 2
- let touch = touches.first
- let touchtouchLocation = touch!.locationInNode(self)
- let previousLocation = touch!.previousLocationInNode(self)
- // 3
- let paddle = childNodeWithName(PaddleCategoryName) as! SKSpriteNode
- // 4
- var paddlepaddleX = paddle.position.x + (touchLocation.x - previousLocation.x)
- // 5
- paddleX = max(paddleX, paddle.size.width/2)
- paddleX = min(paddleX, size.width - paddle.size.width/2)
- // 6
- paddle.position = CGPoint(x: paddleX, y: paddle.position.y)
- }
- }
這是擋板運動主要的邏輯所在。
(1)檢查是否有玩家在觸摸擋板。
(2)如果是,那么你需要更新?lián)醢宓奈恢?,當然具體方式要取決于玩家移動手指的方式。要做到這一點,你需要得到當前觸摸位置和上一次觸摸的位置。
(3)獲取擋板的SKSpriteNode。
(4)使用當前位置加上新位置和上一次觸摸位置的差來計算擋板x坐標值。
(5)在重新定位擋板前,限定一下其x坐標位置,這樣擋板就不會走出屏幕的左右側(cè)。
(6)將擋板的位置設置為你剛剛計算的位置。
有關觸摸處理剩下的唯一事情是要做一些清理工作,這是在方法 touchesEnded(_:withEvent:)中實現(xiàn)的,如下所示︰
- override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
- isFingerOnPaddle = false
- }
在這里,你將isFingerOnPaddle屬性設置為false。這可以確保,當玩家把他們的手指離開屏幕然后再點按擋板時,擋板不會跳到以前的觸摸位置。
完美!現(xiàn)在再次生成和運行項目時,你會發(fā)現(xiàn)小球彈跳在屏幕周圍,而且你可以使用擋板來影響它的運動了。
現(xiàn)在,已經(jīng)感覺不錯了吧!
九、 接觸
到目前為止,你已經(jīng)實現(xiàn)了基本的小球彈跳和擋板移動來控制小球。雖然這已經(jīng)是很有趣的,但是,要想使其真正成為一款游戲,你還需要一種方法來控制玩家贏取與輸?shù)粢淮斡螒颉.斝∏蛴|及屏幕的底部而不是擋板時玩家應該失敗。但你如何使用Sprite Kit來檢測這種情況呢?
Sprite Kit可以檢測到兩個物理物體之間的接觸。然而,為了使其正常工作,您需要按照幾個步驟來設置某種方式。在此僅給出簡短的描述。稍后會解釋每個步驟的更多細節(jié)。描述如下:
- 設置物理Body位掩碼:在你的游戲中,你可能有幾種不同類型的物理Body——例如,你可能擁有玩家、敵人、子彈、獎勵項等。為了唯一地標識這些不同類型的物理Body,你需要使用幾位掩碼來配置每個物理Body。這些掩碼包括:
- categoryBitMask:這位掩碼標識一個Body隸屬的類別。你可以使用類別來定義一個Body與其他Body的互動。CategoryBitMask是一個32位整數(shù),其中每一位代表一個類別。所以,你的游戲中可以使用多達32個自定義類別。這應該足夠應對大多數(shù)游戲中為每個對象類型設置一個單獨類別的要求了。對于更復雜的游戲,請記住,每個Body可以隸屬多個類別這一技巧。所以,通過精心設計類別,你甚至可以克服32個類別的局限性。
- contactTestBitMask:在這個掩碼中設置位能夠?qū)崿F(xiàn)當一個Body接觸到分配給該特定類別的另一個Body時Sprite Kit通知代理。默認情況下,所有的位都被清除掉——任何對象之間的接觸都不會通知你。為了獲得最佳性能,你應該只設置你真正感興趣的用于相互作用的接觸掩碼。
- collisionBitMask:借助這個掩碼,您可以定義哪些Body可以與當前物理Body碰撞。例如,你可以使用此技術來避免當一個非常沉重的Body遇到一個比它輕得多的物體時的碰撞計算,因為這只會給沉重的Body的速度帶來微不足道的變化影響。但是,你也可以使用它讓兩個Body穿透對方。
- 設置并實現(xiàn)接觸委托(delegate,也有的翻譯為“代理”):接觸委托實際上是SKPhysicsWorld的一個屬性。當兩個使用了contactTestBitMasks的Body開始和結(jié)束碰撞會通知這個委托。
十、 3,2,1接觸算法
首先,我們來創(chuàng)建描述不同類別的常數(shù)。為此,只需在GameScene.swift文件中添加下列常數(shù)定義:
- let BallCategory : UInt32 = 0x1 << 0
- let BottomCategory : UInt32 = 0x1 << 1
- let BlockCategory : UInt32 = 0x1 << 2
- let PaddleCategory : UInt32 = 0x1 << 3
- let BorderCategory : UInt32 = 0x1 << 4
上面定義了五個類別。這里使用的辦法是:將最后一位設置為1,所有其他位設置為零。然后使用<<運算符向左移動這個位。因此,每個類別常數(shù)只有一位設置為 1 而且在二進制數(shù)中的這個1的位置對于上面四個類別來說都是唯一的。
現(xiàn)在,你只需要上述類別來描述屏幕和小球;但是,你還應當使用其他一些辦法來解釋游戲運行邏輯。
一旦建立了上面這些常數(shù),現(xiàn)在就可以創(chuàng)建橫跨屏幕底部的物理Body了。
[建議]各位讀者根據(jù)本文前面介紹的原則試著使用自己的方法來解決創(chuàng)建圍繞屏幕邊緣障礙有關的問題。
現(xiàn)在,我們來討論創(chuàng)建接觸有關編程的核心問題。首先,通過將下面的代碼添加到 didMoveToView(_:)方法中為游戲?qū)ο笤O置categoryBitMasks掩碼:
- let paddle = childNodeWithName(PaddleCategoryName) as! SKSpriteNode
- bottom.physicsBody!.categoryBitMask = BottomCategory
- ball.physicsBody!.categoryBitMask = BallCategory
- paddle.physicsBody!.categoryBitMask = PaddleCategory
- borderBody.categoryBitMask = BorderCategory
此代碼簡單地把較早前創(chuàng)建的常數(shù)賦值給相應的物理Body的categoryBitMask掩碼。
現(xiàn)在,通過添加下面一行代碼到didMoveToView(_:)方法中來設置contactTestBitMask掩碼:
ball.physicsBody!.contactTestBitMask = BottomCategory
現(xiàn)在,你只想要在小球接觸屏幕底部時被通知。
下一步,我們來為所有的物理接觸創(chuàng)建GameScene類中的委托。
為此,僅需將下面這一行:
- class GameScene: SKScene {
修改成如下形式:
- class GameScene: SKScene, SKPhysicsContactDelegate {
這就可以了:GameScene的身份現(xiàn)在是SKPhysicsContactDelegate(因為它遵守SKPhysicsContactDelegate協(xié)議),它將會接收到所有配置的物理Body的碰撞通知。
現(xiàn)在,您需要將GameScene設置為physicsWorld中的委派。所以,將下面一行代碼添加到方法didMoveToView(_:)中,正好位于語句physicsWorld.gravity = CGVector (dx: 0.0,dy: 0.0)的下面:
- physicsWorld.contactDelegate = self
最后,您需要執(zhí)行didBeginContact(_:)來處理碰撞問題。為此,只需要將以下方法添加到GameScene.swift中:
- func didBeginContact(contact: SKPhysicsContact) {
- // 1
- var firstBody: SKPhysicsBody
- var secondBody: SKPhysicsBody
- // 2
- if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
- firstBody = contact.bodyA
- secondBody = contact.bodyB
- } else {
- firstBody = contact.bodyB
- secondBody = contact.bodyA
- }
- // 3
- if firstBody.categoryBitMask == BallCategory && secondBody.categoryBitMask == BottomCategory {
- print("Hit bottom. First contact has been made.")
- }
- }
讓我們分析一下上面的方法:
(1)創(chuàng)建兩個本地變量來存放參與碰撞的兩個物理Body。
(2)檢查這兩個碰撞的物理Body來看一下其中哪一個使用了較低的 categoryBitmask掩碼。然后,將它們存儲到本地變量;這樣,對應于較低類別的Body總是存儲在firstBody變量中。當分析具體類別之間的接觸時這將節(jié)省你不少的努力。
(3)得益于以前實現(xiàn)的排序操作,現(xiàn)在你只需要檢查是否firstBody屬于BallCategory類別以及是否secondBody屬于BottomCategory類別,以便弄明白小球已碰到了屏幕底部——正如你已經(jīng)知道的,如果firstBody屬于類別BottomCategory ,則secondBody不可能屬于BallCategory類別(因為 BottomCategory 比 BallCategory 有更高的位掩碼)。在本示例中,我們僅僅輸出一條簡單的日志消息。
現(xiàn)在,請再次建立和運行你的游戲。如果一切正常,每當小球錯過擋板并點擊屏幕底部時你應該看到在控制臺中的日志消息。就你下圖一樣:
還好吧!現(xiàn)在最艱難的部分已經(jīng)完成了。最后,剩下的就是添加竹塊和游戲邏輯,你會在本系列的下篇中了解到這些。