寫在前面:這個(gè)系列文章是轉(zhuǎn)載過來的,簡書里之前也有人轉(zhuǎn)載了,不過沒有進(jìn)行重新編排,圖文等格式并不適用于簡書,我參照原文樣式重新排版了一次。
- 原文地址: How To Build A SpriteKit Game In Swift 3 (Part 2)
- 原文作者: Marc Vandehey
- 譯文出自:掘金翻譯計(jì)劃
- 譯文地址:如何在 Swift 3 中用 SpriteKit 框架編寫游戲 (Part 2)
- 譯者:ZiXYu
- 校對者:DeepMissea, Tuccuay
你是否想過如何來開發(fā)一款 SpriteKit 游戲?實(shí)現(xiàn)碰撞檢測會是個(gè)令人生畏的任務(wù)嗎?你想知道如何正確的處理音效和背景音樂嗎?隨著 SpriteKit 的發(fā)布,在 iOS 上的游戲開發(fā)已經(jīng)變得空前簡單了。在本系列三部中的第二部分中,我們將繼續(xù)探索 SpriteKit 的基礎(chǔ)知識。
如果你錯(cuò)過了 之前的課程,你可以通過獲取 GitHub 上的代碼 來趕上進(jìn)度。請記住,本教程需要使用 Xcode 8 和 Swift 3。

RainCat, 第二課
在 上一課 中,我們創(chuàng)建了地板和背景,隨機(jī)生成了雨滴并添加了雨傘。這把雨傘的精靈(譯者注:sprite,中文譯名精靈,在游戲開發(fā)中,精靈指的是以圖像方式呈現(xiàn)在屏幕上的一個(gè)圖像)中存在一個(gè)自定義的 SKPhysicsBody,是通過 CGPath 來生成的,同時(shí)我們啟用了觸摸檢測,因此我們可以在屏幕范圍內(nèi)移動它。而且我們通過 categoryBitMask 和 contactTestBitMask 來實(shí)現(xiàn)了碰撞檢測。我們在雨滴落到任何物體上時(shí)消除了碰撞,因此它們不會堆積起來,而是會在一次彈跳后穿過地板。最后,我們設(shè)置了一個(gè)世界邊框來移除所有和它接觸的 SKNode。
本文中,我們將重點(diǎn)實(shí)現(xiàn)以下幾點(diǎn):
- 生成貓
- 實(shí)現(xiàn)貓的碰撞
- 生成食物
- 實(shí)現(xiàn)食物的碰撞
- 使貓向食物移動
- 創(chuàng)建貓的動畫
- 當(dāng)貓接觸雨滴時(shí),使貓受到傷害
- 添加音效和背景音樂
獲取資源
你可以從 GitHub (ZIP) 上獲取本課所需要的資源。下載圖片后,通過一次性拖拽所有圖片將它們添加到你的 Assets.xcassets 文件中。你現(xiàn)在應(yīng)該有了包含貓動畫和寵物碗的資源文件。我們之后將會添加音效和背景音樂文件。

貓貓時(shí)間!
我們從添加游戲主角開始本期課程。我們首先在 “Sprites” 組下創(chuàng)建一個(gè)新文件,命名為 CatSprite。
將如下代碼添加到 CatSprite.swift 文件中:
import SpriteKit
public class CatSprite: SKSpriteNode {
public static func newInstance() -> CatSprite {
let catSprite = CatSprite(imageNamed: "cat_one")
catSprite.zPosition = 5
catSprite.physicsBody = SKPhysicsBody(circleOfRadius: catSprite.size.width / 2)
return catSprite
}
public func update(deltaTime : TimeInterval) {
}
}
在這個(gè)文件中,我們用了一個(gè)會返回貓精靈的靜態(tài)初始化函數(shù)。在另一個(gè) update 函數(shù)中,我們也使用了同樣的方法。如果我們需要生成更多的精靈,我們應(yīng)該嘗試把這個(gè)函數(shù)變成一個(gè) 協(xié)議 的一部分來生成合適的精靈。這里需要注意一點(diǎn),對于貓精靈,我們使用的是一個(gè)圓形的 SKPhysicsBody。就像我們創(chuàng)建雨滴一樣,我們當(dāng)然可以使用紋理來創(chuàng)建貓的物理實(shí)體,但是這是一個(gè)有“美感”的決定。當(dāng)貓被雨滴或雨傘碰到時(shí), 與其讓貓始終坐著,讓貓?jiān)诘厣洗驖L顯然更有趣一些。
當(dāng)貓接觸雨滴或貓掉出該世界時(shí),我們將需要回調(diào)函數(shù)來處理這些事件。我們可以打開 Constants.swift 文件,將下列代碼加入該文件,使它作為一個(gè) CatCategory:
let CatCategory: UInt32 = 0x1 << 4
上面代碼中定義的變量將決定貓的身體是哪個(gè) SKPhysicsBody。讓我們重新打開 CatSprite.swift 來更新貓精靈的狀態(tài),使它包含 categoryBitMask 和 contactTestBitMask 這兩個(gè)屬性。 在 newInstance() 返回 catSprite 之前,我們需要添加如下代碼:
catSprite.physicsBody?.categoryBitMask = CatCategory
catSprite.physicsBody?.contactTestBitMask = RainDropCategory | WorldCategory
現(xiàn)在,當(dāng)貓被雨滴擊中或者當(dāng)貓跌出世界時(shí),我們將會得到一個(gè)回調(diào)。在添加了如上代碼后,我們需要將貓?zhí)砑拥綀鼍爸小?/p>
在 GameScene.swift 文件的頂部, 初始化了 umbrellaSprite 之后, 我們需要添加如下代碼:
private var catNode: CatSprite!
我們可以立刻在 sceneDidLoad() 里創(chuàng)建一只貓,但是我們更想要從一個(gè)單獨(dú)的函數(shù)中來創(chuàng)建貓對象,以便于代碼重用。! 告訴編譯器,它并不需要在 init 語句中立即初始化,而且它應(yīng)該不會是 nil。我們這么做有兩個(gè)理由。首先,我們不想單獨(dú)為了一個(gè)變量創(chuàng)建 init() 語句。其次,我們并不想立刻初始化貓精靈,只要在我們第一次運(yùn)行 spawnCat() 時(shí)重新初始化和定位它就可以了。我們也可以用 ? 來定義該變量,但是當(dāng)我們第一次運(yùn)行了 spawnCat() 函數(shù)后,我們的貓精靈就再也不會變成 nil 了。為了解決初始化問題和讓我們頭疼的拆包,我們會說使用感嘆號來進(jìn)行自動拆包是安全的操作。如果我們在初始化我們的貓對象前就使用了它,我們的應(yīng)用就會閃退,因?yàn)槲覀兏嬖V應(yīng)用對貓對象進(jìn)行拆包是安全的,然而它還沒有初始化。在我們使用它之前,需要先在合適的函數(shù)中將它初始化
接下來,我們將要在 GameScene.swift 文件中新建一個(gè) spawnCat() 函數(shù)來初始化我們的貓精靈。我們會把這個(gè)初始化的部分拆分到一個(gè)單獨(dú)的函數(shù)中,使這部分代碼具有重用性,同時(shí)保證在場景里每次只有一只貓。
在這個(gè)文件中接近底部的地方,spawnRaindrop() 函數(shù)后面添加如下代碼:
func spawnCat() {
if let currentCat = catNode, children.contains(currentCat) {
catNode.removeFromParent()
catNode.removeAllActions()
catNode.physicsBody = nil
}
catNode = CatSprite.newInstance()
catNode.position = CGPoint(x: umbrellaNode.position.x, y: umbrellaNode.position.y - 30)
addChild(catNode)
}
縱觀這段函數(shù),我們首先檢查了貓對象是否為空。然后,我們檢查了這個(gè)場景中是否已經(jīng)存在了一個(gè)貓對象。如果這個(gè)場景內(nèi)已經(jīng)存在了一只小貓,我們就要從父類中移除它,移除它現(xiàn)在正在進(jìn)行的所有操作,并清除這個(gè)貓對象的 SKPhysicsBody。而這些操作僅僅會在貓掉出該世界時(shí)被觸發(fā)。在這之后,我們會重新初始化一個(gè)新的貓對象,同時(shí)設(shè)定它的初始位置為傘下 30 像素的地方。其實(shí)我們可以在任何位置初始化我們的貓對象,但是我想這個(gè)位置總比直接從天空中把貓丟下來好一些。
最后,在 sceneDidLoad() 函數(shù)中,在我們定位并添加了雨傘之后,調(diào)用 spawnCat() 函數(shù):
umbrellaNode.zPosition = 4
addChild(umbrellaNode)
spawnCat()
現(xiàn)在我們可以運(yùn)行我們的應(yīng)用啦!

如果現(xiàn)在貓碰到雨滴或是雨傘,它將會在地上打滾。這時(shí)候,貓可能會滾出屏幕然后在接觸世界邊框的一瞬間被刪除掉,那么,我們就需要重新生成貓對象了。因?yàn)楝F(xiàn)在回調(diào)函數(shù)會在當(dāng)貓接觸到雨滴時(shí)或貓掉出世界時(shí)被觸發(fā),所以我們可以在 didBegin(_ contact:) 函數(shù)中來處理這個(gè)碰撞事件。
我們想要在貓觸碰到雨滴后和觸碰世界邊框后觸發(fā)不同的事件,所以我們把這些邏輯拆分到了一個(gè)新的函數(shù)中。在 GameScene.swift 文件的底部, didBegin(_ contact:) 函數(shù)的后面,加上如下代碼:
func handleCatCollision(contact: SKPhysicsContact) {
var otherBody: SKPhysicsBody
if contact.bodyA.categoryBitMask == CatCategory {
otherBody = contact.bodyB
} else {
otherBody = contact.bodyA
}
switch otherBody.categoryBitMask {
case RainDropCategory:
print("rain hit the cat")
case WorldCategory:
spawnCat()
default:
print("Something hit the cat")
}
}
在這段代碼中,我們在尋找除了貓以外的物理實(shí)體(physics body)。在我們發(fā)現(xiàn)其他實(shí)體對象時(shí),我們就需要判斷是什么觸碰了貓?,F(xiàn)在,如果是雨滴在貓身上,我們只在控制臺中輸出這個(gè)碰撞發(fā)生了,而如果是貓觸碰了這個(gè)游戲世界的邊緣,我們就會重新生成一個(gè)貓對象。
如果(什么東西)與貓對象發(fā)生接觸,我們就調(diào)用這個(gè)函數(shù)。時(shí)那么,讓我們用如下代碼來更新 didBegin(_ contact:) 函數(shù):
func didBegin(_ contact: SKPhysicsContact) {
if (contact.bodyA.categoryBitMask == RainDropCategory) {
contact.bodyA.node?.physicsBody?.collisionBitMask = 0
} else if (contact.bodyB.categoryBitMask == RainDropCategory) {
contact.bodyB.node?.physicsBody?.collisionBitMask = 0
}
if contact.bodyA.categoryBitMask == CatCategory || contact.bodyB.categoryBitMask == CatCategory {
handleCatCollision(contact: contact)
return
}
if contact.bodyA.categoryBitMask == WorldCategory {
contact.bodyB.node?.removeFromParent()
contact.bodyB.node?.physicsBody = nil
contact.bodyB.node?.removeAllActions()
} else if contact.bodyB.categoryBitMask == WorldCategory {
contact.bodyA.node?.removeFromParent()
contact.bodyA.node?.physicsBody = nil
contact.bodyA.node?.removeAllActions()
}
}
我們在移除雨滴碰撞和移除離屏節(jié)點(diǎn)中間插入了一個(gè)條件判斷。這個(gè) if 語句判斷了碰撞物體是不是貓,然后我們在 handleCatCollision(contact:) 函數(shù)中處理貓的行為。
我們現(xiàn)在可以用雨傘把貓推出屏幕來測試貓的重生函數(shù)了。我們會看到,貓將在傘下重新被定義出來。請注意,如果雨傘的底部低于地板,那么貓就會一直從屏幕中掉出去。到現(xiàn)在為止這并不是什么大問題,但是我們之后會提供一個(gè)方法來解決它。
生成食物
現(xiàn)在看來,是時(shí)候生成一些食物來喂我們的小貓了。當(dāng)然了,現(xiàn)在貓并不能自己移動,不過我們一會可以修復(fù)這個(gè)問題。在創(chuàng)建食物精靈之前,我們可以先在 Constants.swift 文件中為食物新建一個(gè)類。讓我們在 CatCategory 中添加如下代碼:
let FoodCategory: UInt32 = 0x1 << 5
上面代碼中定義的變量將決定食物的物理對象是哪個(gè) SKPhysicsBody。在“Sprites”組中,我們用創(chuàng)建 CatSprite.swift 文件同樣的方法新建一個(gè)名為 FoodSprite.swift 的文件,并在該文件中添加如下代碼:
import SpriteKit
public class FoodSprite: SKSpriteNode {
public static func newInstance() -> FoodSprite {
let foodDish = FoodSprite(imageNamed: "food_dish")
foodDish.physicsBody = SKPhysicsBody(rectangleOf: foodDish.size)
foodDish.physicsBody?.categoryBitMask = FoodCategory
foodDish.physicsBody?.contactTestBitMask = WorldCategory | RainDropCategory | CatCategory
foodDish.zPosition = 5
return foodDish
}
}
這是一個(gè)靜態(tài)的函數(shù),當(dāng)它被調(diào)用時(shí),將會初始化一個(gè) FoodSprite 并且返回它。我們把食物的物理實(shí)體設(shè)置為一個(gè)和食物精靈同樣大小的矩形。因?yàn)槭澄锞`本身就是一個(gè)矩形。接下來,我們把物理對象的種類設(shè)置為我們剛剛創(chuàng)建的 FoodCategory ,然后把它添加到它可能會碰撞的對象(世界邊框,雨滴和貓)中。我們把食物和貓的 zPosition 設(shè)置成相同的,這樣它們將永遠(yuǎn)不會重疊,因?yàn)楫?dāng)它們相遇時(shí),食物就會被刪除然后玩家將會得到一分。
重新打開 GameScene.swift 文件,我們需要添加一些功能來生成和移除食物。在這個(gè)文件的頂部,rainDropSpawnRate 變量的下面,我們添加如下代碼:
private let foodEdgeMargin: CGFloat = 75.0
這個(gè)變量將會作為生成食物時(shí)的外邊距。我們不想將食物生成在離屏幕兩側(cè)特別近的位置。我們把這個(gè)值定義在文件的頂部,這樣如果我們之后要改變這個(gè)值的時(shí)候就不用搜索整個(gè)文檔了。接下來,在我們的 spawnCat() 函數(shù)下面,我們可以新增我們的 spawnFood 函數(shù)了。
func spawnFood() {
let food = FoodSprite.newInstance()
var randomPosition: CGFloat = CGFloat(arc4random())
randomPosition = randomPosition.truncatingRemainder(dividingBy: size.width - foodEdgeMargin * 2)
randomPosition += foodEdgeMargin
food.position = CGPoint(x: randomPosition, y: size.height)
addChild(food)
}
這個(gè)函數(shù)和我們的 spawnRaindrop() 函數(shù)幾乎一模一樣。我們新建了一個(gè) FoodSprite,然后把它放在了屏幕上一個(gè)隨機(jī)的位置 x。這里我們用了之前設(shè)定的外邊距(margin)變量來限制了能夠生成食物精靈的屏幕范圍。首先,我們設(shè)置了隨機(jī)位置的范圍為屏幕的寬度減去 2 乘以外邊距。然后,我們用外邊距來偏移起始位置。這使得食物不會生成在任意距屏幕邊界 0 到 75 的位置里。
在 sceneDidLoad() 文件接近頂部的位置,讓我們在 spawnCat() 函數(shù)的初始化調(diào)用下面加上如下代碼:
spawnCat()
spawnFood()
現(xiàn)在當(dāng)場景加載時(shí),我們會生成一把雨傘,雨傘下面有一只貓,還有一些從天上掉下來的雨滴和食物?,F(xiàn)在雨滴可以和貓(譯者注:原文寫的是 food,百分百是寫錯(cuò)了)互動,讓它來回滾動了。對食物來說,它跟雨滴碰到雨傘和地板一樣,反彈一次然后失去所有的碰撞屬性,直到觸碰到世界邊界后被刪除。我們也同樣需要添加一些食物和貓的互動。
在 GameScene.swift 文件的底部,我們將添加所有有關(guān)于食物碰撞的代碼。讓我們在 handleCatCollision() 函數(shù)后添加如下代碼:
func handleFoodHit(contact: SKPhysicsContact) {
var otherBody: SKPhysicsBody
var foodBody: SKPhysicsBody
if (contact.bodyA.categoryBitMask == FoodCategory) {
otherBody = contact.bodyB
foodBody = contact.bodyA
} else {
otherBody = contact.bodyA
foodBody = contact.bodyB
}
switch otherBody.categoryBitMask {
case CatCategory:
//TODO increment points
print("fed cat")
fallthrough
case WorldCategory:
foodBody.node?.removeFromParent()
foodBody.node?.physicsBody = nil
spawnFood()
default:
print("something else touched the food")
}
}
在這個(gè)函數(shù)中,我們將用和處理貓碰撞同樣的方式來處理食物碰撞。首先,我們定義了食物的物理實(shí)體,然后我們用了一個(gè) switch 語句來判斷除食物之外的物理實(shí)體。接著,我們添加了一個(gè) CatCategory 條件分支 - 這是個(gè)預(yù)留的接口,我們之后可以添加代碼來更新游戲分?jǐn)?shù)。接下來我們 fallthrough 到 WorldFrameCategory 分支語句,這里我們需要從場景里移除食物精靈和它的物理實(shí)體。最后,我們需要重新生成食物??偠灾?,當(dāng)食物觸碰到了世界邊界,我們只需要移除食物精靈和它的物理實(shí)體。如果食物觸碰到了其它物理實(shí)體,那么 default 分支語句就會被觸發(fā)然后在控制臺打印一個(gè)通用語句。現(xiàn)在,唯一能觸發(fā)這個(gè)語句的物理實(shí)體就是 RainDropCategory。而到現(xiàn)在為止,我們并不關(guān)心當(dāng)雨擊中食物時(shí)會發(fā)生什么。我們只希望雨滴和食物在擊中地板或雨傘時(shí)有同樣的表現(xiàn)。
為了讓所有部分連接起來,我們將在 didBegin(_ contact) 函數(shù)中添加幾行代碼。在判斷 CatCategory 之前添加如下代碼:
if contact.bodyA.categoryBitMask == FoodCategory || contact.bodyB.categoryBitMask == FoodCategory {
handleFoodHit(contact: contact)
return
}
didBegin(_ contact) 最后應(yīng)該看起來像這樣:
func didBegin(_ contact: SKPhysicsContact) {
if (contact.bodyA.categoryBitMask == RainDropCategory) {
contact.bodyA.node?.physicsBody?.collisionBitMask = 0
} else if (contact.bodyB.categoryBitMask == RainDropCategory) {
contact.bodyB.node?.physicsBody?.collisionBitMask = 0
}
if contact.bodyA.categoryBitMask == FoodCategory || contact.bodyB.categoryBitMask == FoodCategory {
handleFoodHit(contact: contact)
return
}
if contact.bodyA.categoryBitMask == CatCategory || contact.bodyB.categoryBitMask == CatCategory {
handleCatCollision(contact: contact)
return
}
if contact.bodyA.categoryBitMask == WorldCategory {
contact.bodyB.node?.removeFromParent()
contact.bodyB.node?.physicsBody = nil
contact.bodyB.node?.removeAllActions()
} else if contact.bodyB.categoryBitMask == WorldCategory {
contact.bodyA.node?.removeFromParent()
contact.bodyA.node?.physicsBody = nil
contact.bodyA.node?.removeAllActions()
}
}
我們再次運(yùn)行我們的應(yīng)用。貓現(xiàn)在還不會自己跑來跑去,但是我們可以通過把食物推出屏幕邊界或把貓移動到食物上來測試我們的函數(shù)。兩個(gè)情況都會刪除食物節(jié)點(diǎn),而其中一個(gè)情況則會從屏幕外重新生成食物。
讓物理實(shí)體動起來吧
現(xiàn)在是時(shí)候讓我們的小貓動起來了。是什么驅(qū)使了小貓移動呢?當(dāng)然是食物啦!我們剛剛生成了食物,那么現(xiàn)在我們就需要讓小貓向著食物移動啦?,F(xiàn)在我們的食物精靈被添加到了場景中,然后就被遺忘了。我們需要修正這個(gè)問題。如果我們能夠保留食物的引用(reference),我們就可以知道它在任何時(shí)候的位置,這樣我們就可以告訴小貓食物在場景的哪個(gè)位置了。小貓可以通過檢查自己的坐標(biāo)來了解自己在場景中的哪個(gè)位置。有了這些位置信息,我們就可以讓小貓向著食物移動了。
重新打開 GameScene.swift 文件,讓我們在文件的頂部,貓變量的下面添加一個(gè)變量:
private var foodNode: FoodSprite!
現(xiàn)在我們可以更新 spawnFood() 函數(shù),使每次食物生成時(shí)都會刷新這個(gè)變量的值。
用如下代碼更新 spawnFood() 函數(shù):
func spawnFood() {
if let currentFood = foodNode, children.contains(currentFood) {
foodNode.removeFromParent()
foodNode.removeAllActions()
foodNode.physicsBody = nil
}
foodNode = FoodSprite.newInstance()
var randomPosition: CGFloat = CGFloat(arc4random())
randomPosition = randomPosition.truncatingRemainder(dividingBy: size.width - foodEdgeMargin * 2)
randomPosition += foodEdgeMargin
foodNode.position = CGPoint(x: randomPosition, y: size.height)
addChild(foodNode)
}
這個(gè)函數(shù)將把食物變量的作用域從 spawnFood() 函數(shù)變?yōu)檎麄€(gè) GameScene.swift 文件。在我們的代碼中,同一時(shí)間我們只會生成一個(gè) FoodSprite,同時(shí)我們需要保持對它的引用。因?yàn)橛羞@個(gè)引用,我們就可以檢測到在任何時(shí)間食物的位置了。同樣的,在任何時(shí)間場景內(nèi)也只會有一只貓,同樣我們也需要保持對它的引用。
我們知道小貓想要獲得食物,我們只需要提供一個(gè)方法讓小貓能夠移動。我們需要編輯 CatSprite.swift 文件以便我們知道小貓需要往哪個(gè)方向前進(jìn)來獲取食物。為了讓小貓獲得食物,我們還需要知道小貓的移動速度。在 CatSprite.swift 文件的頂部,我們可以在 newInstance() 函數(shù)前添加如下代碼:
private let movementSpeed: CGFloat = 100
這一行代碼定義了貓的移動速度,這是對一個(gè)復(fù)雜問題的簡單解法。我們用了一個(gè)簡單的線性方程,不考慮任何摩擦和加速。
現(xiàn)在我們需要在我們的 update(deltaTime:) 方法中做點(diǎn)什么了。因?yàn)槲覀円呀?jīng)知道了食物的位置,我們需要讓小貓朝著這個(gè)位置移動了。用如下代碼更新 CatSprite.swift 文件中的 update 函數(shù):
public func update(deltaTime: TimeInterval, foodLocation: CGPoint) {
if foodLocation.x < position.x {
//Food is left
position.x -= movementSpeed * CGFloat(deltaTime)
xScale = -1
} else {
//Food is right
position.x += movementSpeed * CGFloat(deltaTime)
xScale = 1
}
}
我們更新了這個(gè)函數(shù)的函數(shù)簽名(signature)。因?yàn)槲覀冃枰嬖V小貓食物的位置,所以在傳參時(shí),我們不僅傳遞了 delta 時(shí)間,也傳遞了食物的位置信息。因?yàn)楹芏嗍虑榭梢杂绊懯澄锏奈恢?,所以我們需要不停地更新食物的位置信息,以保證小貓一直在正確的方向上前進(jìn)。接下來,讓我們來看一下函數(shù)的功能。在這個(gè)更新過的函數(shù)中,我們?nèi)〉?delta 時(shí)間是一個(gè)非常短的時(shí)間,大約只有 0.166 秒左右。我們也取了食物的位置,是 CGPoint 類型的參數(shù)。如果食物的 x 位置比小貓的 x 位置更小,那么我們就知道食物在小貓的左邊,反之,食物就在小貓的上邊或右邊。如果小貓朝左邊移動,那么我們?nèi)⌒∝埖?x 位置減去小貓的移動速度乘以 delta 時(shí)間。我們需要把 delta 時(shí)間的類型從 TimeInterval 轉(zhuǎn)換到 CGFloat,因?yàn)槲覀兊奈恢煤退俣茸兞坑玫氖沁@個(gè)單位,而 Swift 恰恰是一種強(qiáng)類型語言。
這個(gè)效果實(shí)際上是以一個(gè)恒定的速率將小貓往左邊推,讓它看起來像是在移動。在這里,每隔 0.166 秒,我們就將貓精靈放在上一位置左邊 16.6 單位的位置上。這是因?yàn)槲覀兊?movementSpeed 變量是 100,而 0.166 × 100 = 16.6。小貓往右邊移動時(shí)進(jìn)行一樣的處理,除了我們是將貓精靈放在上一位置右邊 16.6 單位的位置上。接下來,我們設(shè)定了我們貓的 xScale 屬性。這個(gè)值決定了貓精靈的寬度。默認(rèn)值是 1.0,如果我們把 xScale 設(shè)置成 0.5,貓的寬度就會變成之前的一半。如果我們把這個(gè)值翻倍到 2.0,那么貓的寬度就會變成之前的一倍,以此類推。因?yàn)樵嫉呢埦`是面朝右邊的,當(dāng)貓朝著右邊移動時(shí),xScale 值會被設(shè)定為默認(rèn)的 1。如果我們想要“翻轉(zhuǎn)”貓精靈,我們就把 xScale 設(shè)置成 -1,這會把貓的 frame 值置為負(fù)數(shù)并且反向渲染。我們把這個(gè)值保持在 -1 來保證貓精靈的比例一致。現(xiàn)在,當(dāng)貓朝左邊移動時(shí),它會面朝左邊,當(dāng)貓朝右邊移動時(shí),它會面朝右邊。
現(xiàn)在小貓會以一個(gè)恒定的速率朝著食物的位置移動了。首先,我們確定了小貓需要移動的方向,之后讓小貓?jiān)?x 軸上朝著那個(gè)方向移動。我們同樣也需要更新貓的 xScale 參數(shù),因?yàn)槲覀兿M∝埧梢栽谝苿訒r(shí)面朝正確的方向。除非我們希望小貓?jiān)谟锰詹揭苿樱∽詈?,我們需要告訴小貓來更新我們的游戲場景。
打開 GameScene.swift 文件,找到我們的 update(_ currentTime:) 函數(shù),在更新雨傘的調(diào)用下面,新增如下代碼:
catNode.update(deltaTime: dt, foodLocation: foodNode.position)
運(yùn)行我們的應(yīng)用,然后成功!最起碼是在絕大多數(shù)情況下。到現(xiàn)在為止,小貓會朝著食物移動了,但是卻可能會陷入一些有意思的情況里。
只是一只小貓做著小貓?jiān)撟龅氖?/p>
接下來,我們就要來添加移動動畫啦!在這之后,我們會繞回來解決貓被打中后的滾動效果。你可能已經(jīng)注意到了一個(gè)名為 cat_two 的未使用資源。我們需要添加這個(gè)紋理,并且穿插使用它,使小貓看起來像在行走。為了實(shí)現(xiàn)這個(gè),我們需要添加我們第一個(gè) SKAction!
行走樣式
在 CatSprite.swift 文件的頂部,我們將要添加一個(gè)字符串常量,以便我們添加一個(gè)與該鍵值相關(guān)聯(lián)的步行動作。這樣做使得我們可以單獨(dú)停止貓的步行動作,而不是移除之后可能會添加的所有動作。在 movementSpeed 變量前添加如下代碼:
private let walkingActionKey = "action_walking"
這個(gè)字符串本身并不是那么重要,但是它是步行動畫的標(biāo)志位。我也很喜歡在給鍵值命名時(shí)添加一些有意義的字段,以方便調(diào)試。例如,當(dāng)我看到這個(gè)鍵值時(shí),我會知道這是個(gè) SKAction,具體來說,是個(gè)步行動作。
在 walkingActionKey 的下面,我們將會添加圖像幀。因?yàn)槲覀冎粫褂脙蓚€(gè)不同的圖象幀,我們可以把它放在文件的頂部:
private let walkFrames = [
SKTexture(imageNamed: "cat_one"),
SKTexture(imageNamed: "cat_two")
]
這只是個(gè)包含了兩個(gè)紋理的數(shù)組,而這兩個(gè)紋理是在貓行走時(shí)需要交替使用的。為了完成這個(gè)功能,我們需要用如下代碼更新我們的 update(deltaTime: foodLocation:) 函數(shù):
public func update(deltaTime: TimeInterval, foodLocation: CGPoint) {
if action(forKey: walkingActionKey) == nil {
let walkingAction = SKAction.repeatForever(
SKAction.animate(with: walkFrames,
timePerFrame: 0.1,
resize: false,
restore: true))
run(walkingAction, withKey:walkingActionKey)
}
if foodLocation.x < position.x {
//Food is left
position.x -= movementSpeed * CGFloat(deltaTime)
xScale = -1
} else {
//Food is right
position.x += movementSpeed * CGFloat(deltaTime)
xScale = 1
}
}
通過此更新,我們檢查了我們的貓精靈是否已經(jīng)在運(yùn)行步行動畫序列了。如果沒有,那么我們就會將步行動畫添加到貓精靈上。這是個(gè)嵌套的 SKAction。首先,我們新建了一個(gè)會一直重復(fù)的動作。然后,在那個(gè)動作里,我們新建了步行的動畫序列。 SKAction.animate(with: …) 函數(shù)會接收動畫幀數(shù)組,以及每幀持續(xù)的時(shí)間。 函數(shù)中接收的下一個(gè)變量確定了其中的紋理是否具有不一樣的大小,同時(shí)當(dāng)該紋理在動畫幀上生效時(shí)是否需要調(diào)整 SKSpriteNode 的大小。 Restore 確定了當(dāng)動畫結(jié)束時(shí),精靈是否需要重置到它的初始狀態(tài)。我們把這兩個(gè)值都設(shè)置成了 false,這樣就不會有什么出人意料的事情發(fā)生了。在我們設(shè)定好了步行動畫之后,我們就可以通過運(yùn)行 run() 函數(shù)來讓貓精靈開始行走了。
再次運(yùn)行我們的應(yīng)用,我們將看到我們的小貓專心致志地朝著食物移動啦!
Yeah, on the catwalk, on the catwalk, yeah I do my little turn on the catwalk(譯者注:這是 “I am Too Sexy” 的歌詞).
如果在這個(gè)過程中,小貓被擊中,它會打滾,但是仍舊朝著食物移動。我們需要顯示小貓的受損狀態(tài),以便用戶知道他們做了什么不好的事。同樣的,我們需要修正小貓?jiān)谝苿舆^程中的打滾動作,以保證小貓不會在亂七八糟的方向上移動。
讓我們來看一下我們的計(jì)劃。我們希望能夠顯示小貓被擊中了,而不是僅僅更新游戲得分。有些游戲會使該受損單位閃爍并且進(jìn)入無敵狀態(tài)。如果我們有紋理的話,我們也可以做一個(gè)受損動畫。對這個(gè)游戲而言,我想保持它的簡單性,所以我只添加了一些“搖動”功能。當(dāng)小貓被雨滴擊中時(shí),它會被暈眩然后不可置信地翻倒;它會被震驚,因?yàn)橥婕揖尤蛔屵@種事發(fā)生了。為了實(shí)現(xiàn)這個(gè)功能,我們會定義一些變量。我們需要知道小貓會被暈眩多長時(shí)間和它已經(jīng)被暈眩了多長時(shí)間。在這個(gè)文件的頂部, movementSpeed 變量的下面添加如下代碼:
private var timeSinceLastHit: TimeInterval = 2
private let maxFlailTime: TimeInterval = 2
第一個(gè)變量, timeSinceLastHit 保存了自小貓上次被打中后過了多長時(shí)間。因?yàn)橄乱粋€(gè)變量 maxFlailTime,我們把這個(gè)值設(shè)置成 2。maxFlailTime 變量是個(gè)常數(shù),表示小貓每次會被暈眩 2 秒鐘。我們把這兩個(gè)值都被設(shè)置成 2,這樣小貓就不會在生成的一瞬間就被暈眩了。你可以嘗試著重新設(shè)定這兩個(gè)值,來確定最好的暈眩時(shí)間。
現(xiàn)在,我們需要添加一個(gè)函數(shù),讓小貓知道它被打中了,它需要通過停止移動來對此做出反應(yīng)。在我們的 update(deltaTime: foodLocation:) 函數(shù)下添加如下代碼:
public func hitByRain() {
timeSinceLastHit = 0
removeAction(forKey: walkingActionKey)
}
這段代碼只是把 timeSinceLastHit 變量設(shè)置成了 0,同時(shí)移除了小貓的步行動畫?,F(xiàn)在我們需要重寫 update(deltaTime: foodLocation:) 函數(shù),以保證小貓就不會在它被暈眩的時(shí)候移動。讓我們用如下代碼更新該函數(shù):
public func update(deltaTime: TimeInterval, foodLocation: CGPoint) {
timeSinceLastHit += deltaTime
if timeSinceLastHit >= maxFlailTime {
if action(forKey: walkingActionKey) == nil {
let walkingAction = SKAction.repeatForever(
SKAction.animate(with: walkFrames,
timePerFrame: 0.1,
resize: false,
restore: true))
run(walkingAction, withKey:walkingActionKey)
}
if foodLocation.x < position.x {
//Food is left
position.x -= movementSpeed * CGFloat(deltaTime)
xScale = -1
} else {
//Food is right
position.x += movementSpeed * CGFloat(deltaTime)
xScale = 1
}
}
}
現(xiàn)在,我們的 timeSinceLastHit 變量會不停更新,而且如果小貓?jiān)谶^去的 2 秒鐘沒有被打中,那么它就會繼續(xù)朝著食物移動。如果我們并沒有設(shè)置步行動畫,那么必須要正確地設(shè)置它。步行動畫是個(gè)基于幀的動畫,而它只是每 0.1 秒交換兩個(gè)紋理使得小貓看起來像在行走。不過它看起來的確很像小貓真的在行走,對吧?
我們需要重新打開 GameScene.swift 文件來告訴小貓它被擊中了。在 handleCatCollision(contact:) 函數(shù)中,我們需要調(diào)用 hitByRain 函數(shù)。在 switch 語句里,找到 RainDropCategory 然后把其中的這個(gè)語句:
print("rain hit the cat")
換成這個(gè):
catNode.hitByRain()
如果我們現(xiàn)在運(yùn)行我們的應(yīng)用,當(dāng)小貓被雨滴擊中時(shí),它就會被暈眩 2 秒啦!
這個(gè)功能成功實(shí)現(xiàn)了,只是現(xiàn)在小貓會進(jìn)入一個(gè)顛倒的狀態(tài),看起來很滑稽。同樣的,這也會讓雨滴看起來真的很痛——可能我們需要做點(diǎn)什么了。
對于雨滴的問題,我們可以對它的 physicsBody 做點(diǎn)細(xì)微的調(diào)整。在 spawnRaindrop 函數(shù)中,初始化 physicsBody 語句的下面,我們可以添加如下代碼:
raindrop.physicsBody?.density = 0.5
這會使雨滴的密度從它的初始值 1.0 減半。這會使得小貓沒這么容易被擊中了。
打開 CatSprite.swift 文件,我們可以修改 SKAction 來修正小貓的旋轉(zhuǎn)。在 update(deltaTime: foodLocation:) 函數(shù)中添加如下代碼。確保它在 if 語句的里面判斷貓是否在抖動。
找到這一行:
if timeSinceLastHit >= maxFlailTime {
并且添加如下代碼來修正小貓的旋轉(zhuǎn)角度:
if zRotation != 0 && action(forKey: "action_rotate") == nil {
run(SKAction.rotate(toAngle: 0, duration: 0.25), withKey: "action_rotate")
}
這個(gè)代碼塊會判斷是否小貓已經(jīng)被旋轉(zhuǎn)了,哪怕只是一點(diǎn)點(diǎn)。然后,我們要判斷當(dāng)前正在運(yùn)行的這些 SKAction 來確定我們是否已經(jīng)運(yùn)行貓的重置動畫。如果小貓被旋轉(zhuǎn)了,而又沒有運(yùn)行動畫,那么我們就需要運(yùn)行一個(gè)動畫來讓小貓回歸到初始狀態(tài)。需要注意的是,我們這里采用了硬編碼,因?yàn)槲覀儠簳r(shí)不需要在任何別的部分使用這個(gè)值。以后如果我們需要在別的函數(shù)或類中判斷旋轉(zhuǎn)動畫,我們就需要在文件的頂部設(shè)置一個(gè)常量了,就像 walkingActionKey 一樣。
運(yùn)行我們的應(yīng)用,現(xiàn)在你能看到奇跡發(fā)生了:小貓被擊中了,小貓旋轉(zhuǎn)了,小貓又轉(zhuǎn)回來了,它很開心可以繼續(xù)去吃掉更多的食物了??墒沁@里仍舊有兩個(gè)小問題。因?yàn)槲覀儼沿埖?physicsBody 設(shè)置成了一個(gè)圓,在小貓第一次修正自己時(shí),你可能會發(fā)現(xiàn)小貓的狀態(tài)變得不太穩(wěn)定了。它會不停的旋轉(zhuǎn)然后修正自己。為了解決這個(gè)問題,我們需要重設(shè) angularVelocity。本質(zhì)上,小貓?jiān)诒粨糁袝r(shí)會旋轉(zhuǎn),然而我們并沒有修正我們?yōu)樾∝執(zhí)砑拥囊苿铀俣?。而小貓也在被擊中后沒有更新自己的速度。如果小貓被擊中了然后嘗試著向相反方向移動,你可能會發(fā)現(xiàn)它比正常的速度慢了。另外一個(gè)問題是,食物可能會在小貓的正上方。當(dāng)食物在小貓正上方時(shí),小貓會迅速地轉(zhuǎn)身。我們可以通過用如下代碼更新我們的 update(deltaTime :, foodLocation:) 函數(shù)來解決這個(gè)問題:
public func update(deltaTime: TimeInterval, foodLocation: CGPoint) {
timeSinceLastHit += deltaTime
if timeSinceLastHit >= maxFlailTime {
if action(forKey: walkingActionKey) == nil {
let walkingAction = SKAction.repeatForever(
SKAction.animate(with: walkFrames,
timePerFrame: 0.1,
resize: false,
restore: true))
run(walkingAction, withKey:walkingActionKey)
}
if zRotation != 0 && action(forKey: "action_rotate") == nil {
run(SKAction.rotate(toAngle: 0, duration: 0.25), withKey: "action_rotate")
}
//Stand still if the food is above the cat.
if foodLocation.y > position.y && abs(foodLocation.x - position.x) < 2 {
physicsBody?.velocity.dx = 0
removeAction(forKey: walkingActionKey)
texture = walkFrames[1]
} else if foodLocation.x < position.x {
//Food is left
physicsBody?.velocity.dx = -movementSpeed
xScale = -1
} else {
//Food is right
physicsBody?.velocity.dx = movementSpeed
xScale = 1
}
physicsBody?.angularVelocity = 0
}
}
現(xiàn)在讓我們再來重新運(yùn)行應(yīng)用,大部分的不穩(wěn)定動作已經(jīng)被修正了。不僅僅是這樣,當(dāng)食物在小貓正上方時(shí),小貓也會穩(wěn)穩(wěn)地站著了。
現(xiàn)在來添加音樂吧
在我們開始寫代碼前,我們應(yīng)該先要找點(diǎn)音效。一般來說,在尋找音效時(shí),我只會搜索一些類似于 “cat meow royalty free” 的關(guān)鍵詞。第一個(gè)匹配的通常是 SoundBible.com,它會提供一些免費(fèi)的音效。請務(wù)必閱讀使用許可證。如果你不打算發(fā)布你的應(yīng)用,那么就不需要關(guān)心許可證,因?yàn)檫@只是個(gè)個(gè)人應(yīng)用??墒?,如果你想要在 App store 中發(fā)售它,或者通過別的方式發(fā)布它,那么就請確保附上了 Creative Commons Attribution 3.0 或者是類似的許可證。這里有許多種許可證,所以當(dāng)你使用別人的作品前,請確定你找到了相對應(yīng)的許可證。
在該應(yīng)用中使用的音效都是通過 Creative Commons-licensed 授權(quán)并且免費(fèi)使用的。為了之后的操作,我們需要將之前下載的 SFX 文件夾移動到 RainCat 文件夾中。

在你把這些文件拷貝到項(xiàng)目中之后,你需要用 Xcode 來把它們添加到你的項(xiàng)目中。在 “Support” 文件夾下新建一個(gè)名為 “SFX” 的 group。右鍵點(diǎn)擊這個(gè)group 然后點(diǎn)擊 “Add Files to RainCat…” 選項(xiàng)。

找到你的 “SFX” 文件夾,選中你的所有音效文件,然后點(diǎn)擊 “Add” 按鈕?,F(xiàn)在項(xiàng)目中就有了你所有需要使用的音效文件了。打開 CatSprite.swift 文件,我們可以添加一個(gè)包含了所有音效文件名的數(shù)組,這樣我們就可以在雨滴擊中物體時(shí)播放它們了。在該文件的頂部, walkFrames 變量下,添加如下數(shù)組:
private let meowSFX = [
"cat_meow_1.mp3",
"cat_meow_2.mp3",
"cat_meow_3.mp3",
"cat_meow_4.mp3",
"cat_meow_5.wav",
"cat_meow_6.wav"
]
我們在 hitByRain 函數(shù)中添加兩行代碼,來讓小貓發(fā)出聲音了:
let selectedSFX = Int(arc4random_uniform(UInt32(meowSFX.count)))
run(SKAction.playSoundFileNamed(meowSFX[selectedSFX], waitForCompletion: true))
上面的代碼會在 0 到 meowSFX 數(shù)組大小的范圍內(nèi)隨機(jī)選擇一個(gè)值。然后,我們從字符串?dāng)?shù)組中選擇相對應(yīng)的音效名并且播放它。我們將得到一個(gè) 1 bit 的 waitForCompletion 變量. 同樣的,我們將使用 SKAction.playSoundFileNamed 來播放我們可愛的音效。
那么現(xiàn)在我們的應(yīng)用就有聲音啦!那么多聲音!可是有些聲音會重疊起來?,F(xiàn)在,每當(dāng)小貓被雨滴擊中時(shí),我們就會播放一個(gè)音效。很快我們就會覺得煩了。我們需要在播放音效時(shí)添加更多的邏輯判斷,而且我們也不應(yīng)該同時(shí)播放兩個(gè)音效。
在 CatSprite.swift 文件的頂部,maxFlailTime 變量的下面,添加如下兩個(gè)變量:
private var currentRainHits = 4
private let maxRainHits = 4
第一個(gè)變量,currentRainHits,是一個(gè)計(jì)數(shù)器,會統(tǒng)計(jì)小貓總共被雨滴打中了多少次,而 maxRainHits 表示了在小貓喵喵叫前能被擊中幾次。
現(xiàn)在我們將要更新 hitByRain 函數(shù)了。我們需要應(yīng)用 currentRainHits 和 maxRainHits 兩個(gè)變量來制定規(guī)則了。讓我們用如下代碼來更新 hitByRain 函數(shù):
public func hitByRain() {
timeSinceLastHit = 0
removeAction(forKey: walkingActionKey)
//Determine if we should meow or not
if (currentRainHits < maxRainHits) {
currentRainHits += 1
return
}
if action(forKey: "action_sound_effect") == nil {
currentRainHits = 0
let selectedSFX = Int(arc4random_uniform(UInt32(meowSFX.count)))
run(SKAction.playSoundFileNamed(meowSFX[selectedSFX], waitForCompletion: true),
withKey: "action_sound_effect")
}
}
現(xiàn)在,如果 currentRainHits 的值比設(shè)定的最大值小,那么我們只增加 currentRainHits 的值而不播放音效。然后,我們需要通過我們提供的鍵值: action_sound_effect 來判斷我們現(xiàn)在是否已經(jīng)在播放音效了。如果我們沒在播放音效,那么我們可以隨機(jī)播放一個(gè)音效。我們把 waitForCompletion 參數(shù)設(shè)置成 true, 因?yàn)檫@個(gè)操作在音效結(jié)束前并不會完成。如果我們把該參數(shù)設(shè)置成 false,那么它會在音效剛開始時(shí)就把它當(dāng)做播放結(jié)束來計(jì)數(shù)了。
添加音樂
在我們新建一個(gè)方法在我們的應(yīng)用中播放音樂之前,我們需要找到能播放的東西。類似于搜索音效的過程,我們可以在 Google 中搜索 “royalty free music” 來找到需要播放的音樂。此外,你可以去 SoundCloud 網(wǎng)站,并與里面的藝術(shù)家交談。你需要查看你是否可以找到音樂相對應(yīng)的許可證以保證你可以在你的游戲中使用它。 對這個(gè)應(yīng)用而言,我碰巧發(fā)現(xiàn)了 Bensound,根據(jù) Creative Commons license,有一些我們可以使用的音樂。你必須遵從 licensing agreement 來使用它。操作其實(shí)很簡單:credit Bensound 或者付費(fèi)購買許可。
下載我們的四個(gè)音軌 (1, 2, 3, 4),或者把它們從之前下載的 “Music” 文件夾里拖出來。我們將在四個(gè)音軌循環(huán)播放,來保證玩家不會感到厭煩。另外一件需要考慮的事是,這些音軌可能并不能正確循環(huán),這樣你就需要知道每個(gè)音軌的開始和結(jié)束時(shí)間。好的背景音樂可以很好的在不同的音軌間循環(huán)或切換。
在你下載了這些音軌之后,你需要在 “RainCat” 文件夾下新建一個(gè)名叫 “Music” 的文件夾,和你之前創(chuàng)建 “SFX” 文件夾的操作一樣。然后把下載的音軌移動到這個(gè)文件夾中。

然后,在我們的項(xiàng)目結(jié)構(gòu)里的 “Support” 中創(chuàng)建一個(gè)組,命名為 “Music”。 右鍵點(diǎn)擊 “Music” 組,點(diǎn)擊 “Add Files to RainCat”,把我們的音樂添加到項(xiàng)目里。這和我們添加音效的操作一樣。
然后,我們需要創(chuàng)建一個(gè)名為 SoundManager.swift 新文件,正如你在上面圖片中看到的那樣。這將用來作為播放音樂的單例,對音效而言,我們并不介意兩個(gè)音效重疊,但是如果有兩個(gè)背景音樂同時(shí)播放那將是一件很恐怖的事。所以我們需要實(shí)現(xiàn) SoundManager:
import AVFoundation
class SoundManager: NSObject, AVAudioPlayerDelegate {
static let sharedInstance = SoundManager()
var audioPlayer: AVAudioPlayer?
var trackPosition = 0
//Music: http://www.bensound.com/royalty-free-music
static private let tracks = [
"bensound-clearday",
"bensound-jazzcomedy",
"bensound-jazzyfrenchy",
"bensound-littleidea"
]
private override init() {
//This is private, so you can have only one Sound Manager ever.
trackPosition = Int(arc4random_uniform(UInt32(SoundManager.tracks.count)))
}
public func startPlaying() {
if audioPlayer == nil || audioPlayer?.isPlaying == false {
let soundURL = Bundle.main.url(forResource: SoundManager.tracks[trackPosition], withExtension: "mp3")
do {
audioPlayer = try AVAudioPlayer(contentsOf: soundURL!)
audioPlayer?.delegate = self
} catch {
print("audio player failed to load")
startPlaying()
}
audioPlayer?.prepareToPlay()
audioPlayer?.play()
trackPosition = (trackPosition + 1) % SoundManager.tracks.count
} else {
print("Audio player is already playing!")
}
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
//Just play the next track.
startPlaying()
}
}
在 SoundManager 類中,我們需要使用 單例 來創(chuàng)建 SoundManager,來處理巨大的音軌文件并且按順序連續(xù)播放它們。為了處理更長時(shí)間的音頻文件,我們需要使用 AVFoundation。它是專門為此構(gòu)建的,而 SKAction 并不能邊加載邊播放一個(gè)大音頻文件,這和它在加載小的 SFX 文件時(shí)不一樣。因?yàn)檫@個(gè)庫一直都存在, delegate 是依賴于 NSObjects。我們需要使用 AVAudioPlayerDelegate 來檢測音頻何時(shí)播放完畢。
我們需要持有現(xiàn)在正在播放的 audioPlayer 變量,以用來實(shí)現(xiàn)靜音操作。
現(xiàn)在我們有當(dāng)前音軌的位置,我們可以按照文件名數(shù)組來播放下一個(gè)音軌。當(dāng)然我們也應(yīng)該遵守 Bensound 協(xié)議許可。
我們需要實(shí)現(xiàn)默認(rèn)的 init 函數(shù),在這里,我們將隨機(jī)選擇起始音樂,這樣我們不用總是在游戲開始時(shí)聽同樣的音樂。在這之后,我們需要等待程序告訴我們開始播放操作。在 startPlaying 函數(shù)中,我們需要檢查當(dāng)前播放器是否正在播放,如果沒有,我們開始嘗試播放被選中的音樂。我們需要啟動音樂播放器,因?yàn)樵摬僮饔锌赡苁?,所以我們需要將該操作放?try/catch block 中。然后,我們準(zhǔn)備開始播放選中的音軌,同時(shí)設(shè)置索引給下一個(gè)需要播放的音樂。因此,下面這行代碼非常重要:
trackPosition = (trackPosition + 1) % SoundManager.tracks.count
這行代碼會通過增加索引值來設(shè)置音軌的下個(gè)位置,然后會執(zhí)行 modulo 操作,以保持索引值不會越界。最后,在 audioPlayerDidFinishPlaying(_ player:successfully flag:) 函數(shù)中,我們實(shí)現(xiàn)了 delegate 方法,這可以讓我們知道音樂播放完畢?,F(xiàn)在,我們并不需要關(guān)心這個(gè)方法是否成功——只要在這個(gè)方法被調(diào)用時(shí)播放下一個(gè)音樂就好了。
按下 Play 鍵
現(xiàn)在我們已經(jīng)實(shí)現(xiàn)了 SoundManager,我們就需要告訴它什么時(shí)候開始運(yùn)行,這樣我們就有無限循環(huán)播放的背景音樂了。讓我們重新打開 GameViewController.swift 文件,然后將下面這行代碼放到初始化場景的地方:
SoundManager.sharedInstance.startPlaying()
我們在 GameViewController 里執(zhí)行這個(gè)操作,是因?yàn)槲覀冃枰魳藩?dú)立于場景。如果我們在這個(gè)時(shí)候運(yùn)行 app,而且所有的東西都已經(jīng)被正確地添加到了項(xiàng)目中,我們就可以聽到背景音樂了!
在本課中,我們主要實(shí)現(xiàn)了兩個(gè)部分:精靈動畫和聲音。我們使用了一個(gè)基于幀的動畫來使精靈可以動起來,用了 SKAction 來實(shí)現(xiàn),并使用了一些方法來重設(shè)我們被雨滴擊中的小貓。我們使用了 SKAction 來添加了音效,并指定了當(dāng)小貓被雨擊中時(shí)來播放音效。 最后,我們?yōu)槲覀兊挠螒蛱砑恿顺跏急尘耙魳贰?/p>
到這里,恭喜!我們的游戲即將完成!如果你有什么不明白的地方,請仔細(xì)檢查我們在 在Github 上的代碼。
你做的怎么樣了?你的代碼和我的差不多嗎?如果你做了一些修改,或者有更好的更新,可以通過評論讓我知道。
第三節(jié)課即將到來!