寫在前面:這個系列文章是轉載過來的锥腻,簡書里之前也有人轉載了嘱函,不過沒有進行重新編排蓉驹,圖文等格式并不適用于簡書曹宴,我參照原文樣式重新排版了一次搂橙。
- 原文地址:How To Build A SpriteKit Game In Swift 3 (Part 3)
- 原文作者:Marc Vandehey
- 譯文出自:掘金翻譯計劃
- 譯文地址:如何在 Swift 3 中用 SpriteKit 框架編寫游戲 (Part 3)
- 譯者:DeepMissea
- 校對者:Tina92,Tuccuay
你有沒有想過要如何開始創(chuàng)作一款基于 SpriteKit 的游戲浙炼?按鈕的開發(fā)是一個很龐大的任務嗎份氧?想過如何制作游戲的設置部分嗎?隨著 SpriteKit 的出現(xiàn)弯屈,在 iOS 上開發(fā)游戲已經(jīng)變得空前的簡單了蜗帜。在本系列的第三部分,我們將完成 RainCat 游戲的開發(fā)以及對 SpriteKit 框架的介紹资厉。
如果你錯過了上一課厅缺,你可以通過獲取 Github 上的代碼來趕上進度。請記住,本教程需要使用 Xcode 8 和 Swift 3湘捎。
這是我們 RainCat 之旅的第三課诀豁。在上節(jié)課里,我們用了很長一段時間來搞定了一些簡單動畫窥妇,貓的行為舷胜、音效和背景音樂。
今天活翩,我們將重點關注下面的內容:
- 用指示器(HUD)顯示得分烹骨;
- 主菜單 — 帶一些按鈕;
- 靜音選項材泄;
- 退出游戲選項沮焕。
更多的資源
最后一節(jié)課的資源都在 GitHub 上,再次把那些圖片拖進 Assets.xcassets
里拉宗,就像我們上節(jié)課做的那樣峦树。
第一步!
我們需要一種方式來顯示得分旦事。要做這個魁巩,我們就得創(chuàng)建一個指示器(HUD)。這個很簡單:指示器是一個 SKNode
族檬,它包含了分數(shù)和一個退出游戲的按鈕⊥嵊現(xiàn)在,我們先來搞定分數(shù)单料。我們用 Pixel Digivolve 字體來顯示分數(shù),你可以在 Dafont.com 找到它点楼。就像之前我們使用不是我們原創(chuàng)的圖片和音效一樣扫尖,使用字體前,一定要瀏覽它的使用協(xié)議掠廓。這個字體聲明换怖,個人使用是免費的,但如果你真的很喜歡蟀瞧,你可以去作者的頁面對他進行捐贈以表示支持沉颂。你不可能自己做所有的事,所以回饋那些一路幫助過你的人也是很愉快的悦污。
接著铸屉,我們就需要把自定義的字體添加到項目里了。如果是第一次添加切端,這可能是個棘手的過程彻坛。
下載字體并把它移動到項目文件夾的 “Fonts” 文件夾里。這個過程我們上節(jié)課已經(jīng)做過好幾次了,所以我們加快點兒速度昌屉。在項目里創(chuàng)建 Fonts
組钙蒙,然后把 Pixel digivolve.otf
文件加進去。
現(xiàn)在棘手的部分來了间驮。如果錯過了這部分躬厌,也許你就不能使用字體了。我們需要添加它到 Info.plist
文件竞帽。這個文件在 Xcode 的左邊扛施。打開它你會看到一堆屬性列表(或者叫 plist
文件)。右鍵點擊列表抢呆,然后點 “Add Row”煮嫌。
在新添加的一行里,輸入下面的內容:
Fonts provided by application
然后在 Item 0
下面抱虐,我們得添加字體的名字昌阿。plist
文件看起來應該像下面這樣:
字體已經(jīng)準備完畢啦!我們應該做個快速的測試恳邀,看看它能不能像預期那樣使用懦冰。打開 GameScene.swift
,把下面的代碼加在 sceneDidLoad
函數(shù)里的上方:
let label = SKLabelNode(fontNamed: "PixelDigivolve")
label.text = "Hello World!"
label.position = CGPoint(x: size.width /2, y: size.height /2)
label.zPosition = 1000
addChild(label)
一切 OK 嗎谣沸?
如果字體正常刷钢,那就說明你做的完全正確。如果不正常乳附,那就是什么地方出了問題内地。Code With Chris 有一篇更加深入的字體導入問題的文章,但要注意的是赋除,這是一篇老版本 Swift 的文章阱缓,你可能需要稍稍改動一些地方來過渡到 Swift 3 。
現(xiàn)在可以開始給我們的指示器加載自定義字體了举农。刪掉 “Hello World” 標簽荆针,因為這個只是測試字體是否正常用的。指示器是一個 SKNode
颁糟,作為我們 HUD 控件的容器航背。這和我們在第一節(jié)課創(chuàng)建背景節(jié)點的過程一樣。
老樣子棱貌,創(chuàng)建 HudNode.swift
文件玖媚,輸入下面的代碼:
import SpriteKit
class HudNode: SKNode {
private let scoreKey = "RAINCAT_HIGHSCORE"
private let scoreNode = SKLabelNode(fontNamed: "PixelDigivolve")
private(set) var score: Int = 0
private var highScore: Int = 0
private var showingHighScore = false
/// Set up HUD here.
public func setup(size: CGSize) {
let defaults = UserDefaults.standard
highScore = defaults.integer(forKey: scoreKey)
scoreNode.text = "\(score)"
scoreNode.fontSize = 70
scoreNode.position = CGPoint(x: size.width / 2, y: size.height - 100)
scoreNode.zPosition = 1
addChild(scoreNode)
}
/// Add point.
/// - Increments the score.
/// - Saves to user defaults.
/// - If a high score is achieved, then enlarge the scoreNode and update the color.
public func addPoint() {
score += 1
updateScoreboard()
if score > highScore {
let defaults = UserDefaults.standard
defaults.set(score, forKey: scoreKey)
if !showingHighScore {
showingHighScore = true
scoreNode.run(SKAction.scale(to: 1.5, duration: 0.25))
scoreNode.fontColor = SKColor(red: 0.99, green: 0.92, blue: 0.55, alpha: 1.0)
}
}
}
/// Reset points.
/// - Sets score to zero.
/// - Updates score label.
/// - Resets color and size to default values.
public func resetPoints() {
score = 0
updateScoreboard()
if showingHighScore {
showingHighScore = false
scoreNode.run(SKAction.scale(to: 1.0, duration: 0.25))
scoreNode.fontColor = SKColor.white
}
}
/// Updates the score label to show the current score.
private func updateScoreboard() {
scoreNode.text = "\(score)"
}
}
在我們做其他事之前,先在 Constants.swift
文件底部把下面的這行代碼加上 —— 我們用這個鍵來讀寫最高得分記錄:
let ScoreKey = "RAINCAT_HIGHSCORE"
代碼里键畴,有五個關于計分板的變量最盅,第一個實際上是個 SKLabelNode
突雪,用來表示標簽。接著是用來保存當前分數(shù)的變量涡贱;再接下來是記錄最高分的變量咏删,最后一個變量是布爾類型,用來判斷是否顯示我們當前獲得的分數(shù)(我們用這個變量來判斷是否需要運行一個 SKAction
來增加計分板的比例以及把地板弄成黃色)问词。
第一個函數(shù) setup(size:)
的功能是把一切都設置好督函。我們就像之前那樣來設置 SKLabelNode
。SKNode
類沒有任何默認尺寸激挪,所以我們要創(chuàng)建一種方式來設置一個尺寸用于固定 scoreNode
的大小辰狡。我們還要從 UserDefaults
里面得到當前最高分。這是一種簡單方便的存儲少量數(shù)據(jù)的方法垄分,不過不太安全宛篇。不過我們并不用擔心示例程序的安全性,所以使用 UserDefaults
也能讓很好地完成這個任務
在 addPoint()
函數(shù)里面薄湿,我們增加了 score
變量的值叫倍,接著檢查玩家是否得到一個更高的分數(shù)。如果是豺瘤,那么我們就把分數(shù)存到 UserDefaults
里吆倦,然后檢查當前是否顯示最高分。如果玩家達到了一個很高的分數(shù)坐求,我們就用動畫渲染 scoreNode
的顏色和大小蚕泽。
在 resetPoints()
函數(shù)中,我們把當前分數(shù)設為 0
桥嗤。然后须妻,我們就檢查是否需要顯示高的得分,如果需要的話泛领,重置默認值的顏色和大小璧南。
最后還有一個小函數(shù),叫 updateScoreboard
师逸。這個私有函數(shù)用來把分數(shù)設置到 scoreNode
的文本上。在 addPoint()
和 resetPoints()
里用到了這個函數(shù)豆混。
掛上指示器
我們得檢查一下指示器是不是正常工作篓像。到 GameScene.swift
文件,在文件的上方皿伺,foodNode
變量下邊添加一行代碼:
private let hudNode = HudNode()
在 sceneDidLoad()
函數(shù)內部的上方员辩,添加下面兩行代碼:
hudNode.setup(size: size)
addChild(hudNode)
接著,在 spawnCat()
函數(shù)鸵鸥,重置所有點防止貓從屏幕上掉下去奠滑。在把貓精靈加到場景的后面丹皱,加上這行代碼:
hudNode.resetPoints()
接下來,在 handleCatCollision(contact:)
函數(shù)中宋税,當貓被雨淋到時摊崭,我們也需要重置分數(shù)。在函數(shù)最后杰赛,switch
語句的 RainDropCategory
分支里呢簸,加上下面這行代碼:
hudNode.resetPoints()
最后,我們得告訴計分板乏屯,什么時候用戶得了分根时。在 handleFoodHit(contact:)
文件的最后,找到下面這幾行代碼:
//TODO increment points
print("fed cat")
換成這個:
hudNode.addPoint()
以上辰晕!
當來回收集食物時蛤迎,你就會看到指示器的效果了。第一次收集食物的時候含友,你應該會看到分數(shù)變黃然后比例變大替裆,如果你看到當貓淋到雨滴時,分數(shù)重置唱较,那么你就是正確的扎唾!
下一個場景
沒錯,我們要開始下一個場景了南缓!事實上胸遇,如果這個場景完成,它將會作為我們游戲的首屏展示汉形。在做其他事情之前纸镊,打開 Constants.swift
然后添加下面這行代碼到文件的底部 — 我們用它來檢索以及保持高分:
let ScoreKey = "RAINCAT_HIGHSCORE"
創(chuàng)建一個新場景,把它放到 “Scenes” 文件夾里概疆,然后命名為 MenuScene.swift
逗威。把下面的代碼加進去:
import SpriteKit
class MenuScene: SKScene {
let startButtonTexture = SKTexture(imageNamed: "button_start")
let startButtonPressedTexture = SKTexture(imageNamed: "button_start_pressed")
let soundButtonTexture = SKTexture(imageNamed: "speaker_on")
let soundButtonTextureOff = SKTexture(imageNamed: "speaker_off")
let logoSprite = SKSpriteNode(imageNamed: "logo")
var startButton: SKSpriteNode! = nil
var soundButton: SKSpriteNode! = nil
let highScoreNode = SKLabelNode(fontNamed: "PixelDigivolve")
var selectedButton: SKSpriteNode?
override func sceneDidLoad() {
backgroundColor = SKColor(red: 0.30, green: 0.81, blue: 0.89, alpha: 1.0)
//Set up logo - sprite initialized earlier
logoSprite.position = CGPoint(x: size.width/2, y: size.height/2 + 100)
addChild(logoSprite)
//Set up start button
startButton = SKSpriteNode(texture: startButtonTexture)
startButton.position = CGPoint(x: size.width/2, y: size.height/2 - startButton.size.height/2)
addChild(startButton)
let edgeMargin: CGFloat = 25
//Set up sound button
soundButton = SKSpriteNode(texture: soundButtonTexture)
soundButton.position = CGPoint(x: size.width - soundButton.size.width/2 - edgeMargin, y: soundButton.size.height/2 + edgeMargin)
addChild(soundButton)
//Set up high-score node
let defaults = UserDefaults.standard
let highScore = defaults.integer(forKey: ScoreKey)
highScoreNode.text = "\(highScore)"
highScoreNode.fontSize = 90
highScoreNode.verticalAlignmentMode = .top
highScoreNode.position = CGPoint(x: size.width /2, y: startButton.position.y - startButton.size.height/2 - 50)
highScoreNode.zPosition = 1
addChild(highScoreNode)
}
}
因為這個場景真的很簡單。所以我們不會創(chuàng)建任何特殊的類岔冀。我們的場景將只由兩個按鈕組成凯旭。這兩個按鈕可以(或者說應該)擁有自己的 SKSpriteNodes
類,但是因為他們都不一樣使套,所以我不會為他們創(chuàng)建新的類罐呼。在構建屬于你自己的游戲的時候,這是很重要的一點:在事情變得復雜時侦高,你需要有能力來判斷嫉柴,在哪里停下來并重構代碼。一旦你添加了三個或四個以上的按鈕到游戲里奉呛,那可能就是時候停下來把菜單按鈕放到他們自己的類里了计螺。
上面的代碼沒做什么特別的事兒夯尽;只是設置了四個精靈的坐標。當然我們也設置了場景的背景顏色登馒,所以整個背景的值也是正確的匙握。UI Color 是一個從十六進制串(HEX strings)生成 Xcode 顏色代碼的優(yōu)秀工具。上面的代碼還設置了按鈕狀態(tài)的紋理谊娇。開始按鈕有一個正常狀態(tài)和一個按下的狀態(tài)肺孤,而聲音按鈕則是一個開關。為了讓開關簡單點济欢,在玩家點擊時赠堵,我們改變聲音按鈕上的透明度。當然我們也設置了獲得高分的 SKLabelNode
法褥。
我們的 MenuScene
看起來不錯∶0龋現(xiàn)在,在游戲加載時需要展示場景半等。到 GameViewController.swift
文件揍愁,找到下面這行代碼:
let sceneNode = GameScene(size: view.frame.size)
把它換成這個:
let sceneNode = MenuScene(size: view.frame.size)
這個小改動會默認加載 MenuScene
場景,而不是 GameScene
杀饵。
按鈕的狀態(tài)
按鈕在 SpriteKit 中可能有些麻煩莽囤。有豐富的輪子可以用(我甚至還自己做了一個),但是理論上切距,你只需要理解這三個函數(shù):
touchesBegan(_ touches: with event:)
touchesMoved(_ touches: with event:)
touchesEnded(_ touches: with event:)
在更新傘的時候我們簡單提了幾句朽缎,但是現(xiàn)在我們需要知道接下來的幾點:哪個按鈕被觸摸,玩家是松開按鈕還是點擊按鈕谜悟,按鈕是不是一直被按著话肖。這個時候就需要 selectedButton
變量發(fā)揮它的作用了。在觸摸開始時葡幸,我們就可以通過這個變量來捕獲被按的按鈕最筒。如果他們拖拽按鈕,我們就可以處理并適當?shù)慕o它一些紋理蔚叨。在松開按鈕時床蜘,我們也可以知道他們是否還跟按鈕有接觸,如果有接觸蔑水,那就可以提供一些相關聯(lián)的動作悄泥。把下面這些代碼添加到 MenuScene.swift
的底部:
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
if let touch = touches.first {
if selectedButton != nil {
handleStartButtonHover(isHovering: false)
handleSoundButtonHover(isHovering: false)
}
// Check which button was clicked (if any)
if startButton.contains(touch.location(in: self)) {
selectedButton = startButton
handleStartButtonHover(isHovering: true)
} else if soundButton.contains(touch.location(in: self)) {
selectedButton = soundButton
handleSoundButtonHover(isHovering: true)
}
}
}
override func touchesMoved(_ touches: Set, with event: UIEvent?) {
if let touch = touches.first {
// Check which button was clicked (if any)
if selectedButton == startButton {
handleStartButtonHover(isHovering:(startButton.contains(touch.location(in: self))))
} else if selectedButton == soundButton {
handleSoundButtonHover(isHovering:(soundButton.contains(touch.location(in: self))))
}
}
}
override func touchesEnded(_ touches: Set, with event: UIEvent?) {
if let touch = touches.first {
if selectedButton == startButton {
// Start button clicked
handleStartButtonHover(isHovering: false)
if (startButton.contains(touch.location(in: self))) {
handleStartButtonClick()
}
} else if selectedButton == soundButton {
// Sound button clicked
handleSoundButtonHover(isHovering: false)
if (soundButton.contains(touch.location(in: self))) {
handleSoundButtonClick()
}
}
}
selectedButton = nil
}
/// Handles start button hover behavior
func handleStartButtonHover(isHovering: Bool) {
if isHovering {
startButton.texture = startButtonPressedTexture
} else {
startButton.texture = startButtonTexture
}
}
/// Handles sound button hover behavior
func handleSoundButtonHover(isHovering: Bool) {
if isHovering {
soundButton.alpha =0.5
}else{
soundButton.alpha =1.0
}
}
/// Stubbed out start button on click method
func handleStartButtonClick() {
print("start clicked")
}
/// Stubbed out sound button on click method
func handleSoundButtonClick() {
print("sound clicked")
}
這就是對我們兩個按鈕的簡單處理。在 touchesBegan(_ touches: with events:)
里肤粱,我們首先檢查當前是否有按鈕被選中。如果我們要做這個檢查厨相,我們就要得先重置按鈕到?jīng)]有被按下的狀態(tài)领曼,然后鸥鹉,檢查是否有哪個按鈕被按下。如果有被按下的按鈕庶骄,就顯示它的高亮狀態(tài)毁渗,接下來,我們就在其他兩個方法里設置按鈕的 selectedButton
屬性以供使用单刁。
在 touchesMoved(_ touches: with events:)
方法中,我們檢查最初觸摸的是哪個按鈕。接著啦粹,檢查當前觸摸是否還在 selectedButton
的邊界內挎袜,如果還在,就更新按鈕的狀態(tài)為高亮逻淌。startButton
的高亮狀態(tài)是改變按下的紋理么伯,而 soundButton
的高亮狀態(tài)是把精靈的透明度設置為 50%。
最后卡儒,在 touchesEnded(_ touches: with event:)
方法里田柔,我們再次檢查哪個按鈕被選中,如果有骨望,接著檢查這個觸摸時候還在按鈕的邊界內硬爆,如果前面的條件都滿足,那么我們根據(jù)不同的按鈕調用 handleStartButtonClick()
或者 handleSoundButtonClick()
擎鸠。
按鈕的動作
現(xiàn)在缀磕,我們已經(jīng)搞定了按鈕的基礎行為,在按鈕被點擊的時候糠亩,我們還需要一個觸發(fā)事件虐骑。對于 startButton
來說,這個實現(xiàn)很容易赎线。我們只需要在點擊時展示 GameScene
廷没。在 MenuScene.swift
文件里,更新 handleStartButtonClick()
方法里面的代碼:
func handleStartButtonClick() {
let transition = SKTransition.reveal(with: .down, duration: 0.75)
let gameScene = GameScene(size: size)
gameScene.scaleMode = scaleMode
view?.presentScene(gameScene, transition: transition)
}
如果你現(xiàn)在運行程序垂寥,然后點擊按鈕颠黎,游戲就開始了!
接著滞项,我們需要一個靜音的切換狭归。我們已經(jīng)有一個音樂管理器了,但是我們需要告訴它靜音是否開啟文判。我們需要在 Constants.swift
里添加一個 key 來持久化存儲靜音狀態(tài)过椎。添加下面這行代碼:
let MuteKey = "RAINCAT_MUTED"
用它把一個布爾類型的值保存到 UserDefaults
里。現(xiàn)在這里已經(jīng)設置完了戏仓,我們到 SoundManager.swift
文件中疚宇。我們在這里通過檢查和設置 UserDefaults
來確定靜音的開關亡鼠。在文件的頂部,trackPosition
變量的下面敷待,加上這行代碼:
private(set) var isMuted = false
這個變量用于主菜單(或者其他要播放聲音的地方)檢查是否允許播放聲音间涵。我們給他設置一個 false
的初始值,但首先我們需要檢查 UserDefaults
里榜揖,來看看玩家是怎樣設置的勾哩。把 init()
方法換成下面的代碼:
private override init() {
//This is private, so you can only have one Sound Manager ever.
trackPosition = Int(arc4random_uniform(UInt32(SoundManager.tracks.count)))
let defaults = UserDefaults.standard
isMuted = defaults.bool(forKey: MuteKey)
}
做完這些,我們的 isMuted
就有默認值了举哟,我們還需要它能夠切換思劳。在 SoundManager.swift
文件里的底部,加入這些代碼:
func toggleMute() -> Bool {
isMuted = !isMuted
let defaults = UserDefaults.standard
defaults.set(isMuted, forKey: MuteKey)
defaults.synchronize()
if isMuted {
audioPlayer?.stop()
} else {
startPlaying()
}
return isMuted
}
在 UserDefaults
更新時炎滞,這個方法會切換我們的靜音變量敢艰,如果新的值不是靜音,那音樂就會開始播放册赛;如果新的值是靜音钠导,那音樂就不會開始。此外森瘪,我們還會停止播放當前的音樂牡属。做完這些,我們還需要修改一下 startPlaying()
里的 if
語句扼睬。
找到下面的代碼:
if audioPlayer == nil || audioPlayer?.isPlaying == false {
換成這行:
if !isMuted && (audioPlayer == nil || audioPlayer?.isPlaying == false) {
現(xiàn)在逮栅,在靜音被關閉時,無論是播放器沒有設置窗宇,還是當前播放停止了措伐,我們都會繼續(xù)播放音樂。
從這開始军俊,我們就該完成 MenuScene.swift
的靜音按鈕了侥加。把 handleSoundbuttonClick()
方法換成下面的代碼:
func handleSoundButtonClick() {
if SoundManager.sharedInstance.toggleMute(){
//Is muted
soundButton.texture = soundButtonTextureOff
} else {
//Is not muted
soundButton.texture = soundButtonTexture
}
}
這里切換了在 SoundManager
的聲音,檢查結果粪躬,接著稍微改變了一下紋理担败,來告訴玩家音樂是否靜音。我們馬上就要完成了镰官!只剩下在游戲啟動時候提前,設置按鈕的初始紋理。在 sceneDidLoad()
泳唠,找到這行代碼:
soundButton = SKSpriteNode(texture: soundButtonTexture)
替換成下面的:
soundButton = SKSpriteNode(texture: SoundManager.sharedInstance.isMuted ?
soundButtonTextureOff : soundButtonTexture)
上面的例子使用了 ternary operator 來設置正確的紋理狈网。
音樂這部分處理已經(jīng)完成了,我們到 CatSprite.swift
文件,讓小貓在靜音的時候不能喵喵叫孙援。在 hitByRain()
方法害淤,刪除散步動作后,添加下面的這行 if
語句:
if SoundManager.sharedInstance.isMuted { return }
這條語句會判斷游戲是否靜音拓售,如果是就返回。這樣镶奉,我們就可以忽略 currentRainHits
础淤,maxRainHits
和喵喵聲的效果了。
所有的這些都弄完之后哨苛,是時候來試試靜音按鈕的效果了鸽凶。運行游戲,確定是否在播放音樂建峭。關閉音樂玻侥,然后重啟游戲。確定游戲還是靜音的亿蒸。需要注意的一點是凑兰,如果你只是開啟靜音并用 Xcode 重啟游戲,那可能沒有足夠的時間來向 UserDefaults
存儲靜音變量边锁。玩一下游戲姑食,確認在靜音的時候貓不會喵喵的叫。
退出游戲
現(xiàn)在為止茅坛,我們已經(jīng)弄完了主菜單的第一種按鈕音半,我們可以通過添加按鈕,來為場景處理一些棘手的業(yè)務了贡蓖。一些有趣的交互可以展示出我們游戲的風格曹鸠;現(xiàn)在,雨傘會隨著玩家的觸摸而移動到相應的位置斥铺。顯然彻桃,在玩家要退出游戲的時候,雨傘也會移動過去仅父,這肯定是個糟糕的用戶體驗叛薯,所以我們要阻止它發(fā)生。
我們會模仿前面添加的開始按鈕來實現(xiàn)退出按鈕笙纤,其中大部分過程都不會變耗溜。改變的地方在處理觸摸這部分。把你的 quit_button
和 quit_button_pressed
資源放進 Assets.xcassets
文件夾里省容,然后把下面的代碼添加到 HudNode.swift
文件中:
private var quitButton: SKSpriteNode!
private let quitButtonTexture = SKTexture(imageNamed: "quit_button")
private let quitButtonPressedTexture = SKTexture(imageNamed: "quit_button_pressed")
這些變量會處理我們的 quitButton
引用抖拴,并且會根據(jù)退出按鈕的不同狀態(tài)來設置紋理。為了確保不在退出游戲的時候,不小心更新雨傘對象阿宅,我們還需要一個變量來告訴指示器(和游戲場景)候衍,我們只是和退出按鈕交互,而不是雨傘洒放。把下面的代碼添加到 showingHighScore
變量后面:
private(set) var quitButtonPressed = false
同樣的蛉鹿,這是一個只有在 HudNode
中才能修改,而其他類只能查看的變量⊥現(xiàn)在變量已經(jīng)設置好了妖异,我們可以添加按鈕到指示器了。把下面的代碼添加到 setup(size:)
方法中:
quitButton = SKSpriteNode(texture: quitButtonTexture)
let margin: CGFloat = 15
quitButton.position = CGPoint(x: size.width - quitButton.size.width - margin, y: size.height - quitButton.size.height - margin)
quitButton.zPosition = 1000
addChild(quitButton)
上面的代碼會設置退出按鈕沒被按下狀態(tài)的紋理领追。我們也把它的位置設到了右上角他膳,并且把 zPosition
的值設置的很高,來讓它一直顯示在最前面绒窑。如果你現(xiàn)在運行游戲棕孙,他就會顯示在 GameScene
里,不過還不能點些膨。
現(xiàn)在按鈕已經(jīng)定位蟀俊,我們還要能夠和它交互。在 GameScene
中傀蓉,唯一有交互的地方就是和 umbrellaSprite
的交互欧漱。在我們的例子里,指示器的優(yōu)先級比傘高葬燎,所以玩家在退出時误甚,不用特意把傘移走。我們可以在 HudNode.swift
里創(chuàng)建一些相同的方法來模仿 GameScene.swift
里的觸摸功能谱净。在 HudNode.swift
文件加入下面的代碼:
func touchBeganAtPoint(point: CGPoint) {
let containsPoint = quitButton.contains(point)
if quitButtonPressed && !containsPoint {
//Cancel the last click
quitButtonPressed = false
quitButton.texture = quitButtonTexture
} else if containsPoint {
quitButton.texture = quitButtonPressedTexture
quitButtonPressed = true
}
}
func touchMovedToPoint(point: CGPoint) {
if quitButtonPressed {
if quitButton.contains(point) {
quitButton.texture = quitButtonPressedTexture
} else {
quitButton.texture = quitButtonTexture
}
}
}
func touchEndedAtPoint(point: CGPoint) {
if quitButton.contains(point) {
//TODO tell the gamescene to quit the game
}
quitButton.texture = quitButtonTexture
}
上面的代碼大部分和 MenuScene
創(chuàng)建的差不多窑邦。不同的地方是,只需要跟蹤一個按鈕的狀態(tài)壕探,所以我們可以在這些方法里處理所有的事情冈钦。而且,我們還知道 GameScene
里的觸摸點的位置李请,這樣就可以檢查我們的按鈕是否包含觸摸點瞧筛。
移動到 GameScene.swift
, 并用下面的代碼替換 touchesBegan(_ touches with event:)
和 touchesMoved(_ touches: with event:)
:
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
let touchPoint = touches.first?.location(in: self)
if let point = touchPoint {
hudNode.touchBeganAtPoint(point: point)
if !hudNode.quitButtonPressed {
umbrellaNode.setDestination(destination: point)
}
}
}
override func touchesMoved(_ touches: Set, with event: UIEvent?) {
let touchPoint = touches.first?.location(in: self)
if let point = touchPoint {
hudNode.touchMovedToPoint(point: point)
if !hudNode.quitButtonPressed {
umbrellaNode.setDestination(destination: point)
}
}
}
override func touchesEnded(_ touches: Set, with event: UIEvent?) {
let touchPoint = touches.first?.location(in: self)
if let point = touchPoint {
hudNode.touchEndedAtPoint(point: point)
}
}
這里导盅,每個方法以幾乎相同的方式處理一切较幌。我們告訴指示器玩家和場景交互。然后白翻,檢查退出按鈕當前是否在捕捉觸摸乍炉。如果它沒有捕捉觸摸绢片,那我們就移動傘。我們還在 touchesEnded(_ touches: with event:)
方法里添加了點擊退出按鈕結束的處理岛琼,但我們還是沒有使用到 umbrellaSprite
底循。
我們有個按鈕了,現(xiàn)在我們需要一種方式來作用于 GameScene
槐瑞。把下面這行代碼添加到 HudeNode.swift
的頂部:
var quitButtonAction: (()->())?
這是一個基本的閉包熙涤,沒有參數(shù)也沒返回值。我們會在 GameScene.swift
文件里設置它困檩,在點擊 HudNode.swift
里的按鈕時候調用灭袁。接著,我們就可以用下面的代碼窗看,來替換以前在 touchEndedAtPoint(point:)
里面創(chuàng)建的 TODO
部分:
if quitButton.contains(point)&& quitButtonAction != nil {
quitButtonAction!()
}
現(xiàn)在如果我們設置了 quitButtonAction
閉包,它就會在這被調用倦炒。
要設置 quitButtonAction
閉包显沈,我們就要到 GameScene.swift
文件里。在 sceneDidLoad()
函數(shù)逢唤,把設置指示器的代碼換成下面的:
hudNode.setup(size: size)
hudNode.quitButtonAction = {
let transition = SKTransition.reveal(with: .up, duration: 0.75)
let gameScene = MenuScene(size: self.size)
gameScene.scaleMode = self.scaleMode
self.view?.presentScene(gameScene, transition: transition)
self.hudNode.quitButtonAction = nil
}
addChild(hudNode)
運行程序拉讯,點擊開始游戲,然后點退出按鈕鳖藕。如果你回到了主菜單魔慷,那說明退出按鈕和預期的一樣。在閉包里著恩,我們創(chuàng)建并初始化了一個到 MenuScene
的過渡院尔。我們還把這個閉包設置為 HUD
的節(jié)點,當點擊退出按鈕時運行閉包喉誊。這里邀摆,另一行重要的代碼是我們把 quitButtonAction
設為 nil
。這么做的原因是有一個循環(huán)引用產(chǎn)生了伍茄。場景持有一個指示器的引用栋盹,而指示器也持有一個場景的引用。因為他們兩個互相引用敷矫,導致在垃圾回收的時候例获,他們都不會被處理。這種情形下曹仗,每次我們進入和離開 GameScene
的時候榨汤,都會有一個新的實例被創(chuàng)建,并且從來都不釋放整葡。這對性能有嚴重的影響件余,游戲最后一定會內存爆炸。有很多種方式來避免它,但在我們這里啼器,只是從指示器中移除對 GameScene
的引用旬渠,這樣在我們回到 MenuScene
的時候,場景和指示器都會被終止端壳。對于引用類型和如何避免循環(huán)引用告丢,Krakendev 有一些更深的見解 。
現(xiàn)在损谦,到 GameViewController.swift
文件岖免,把下面的這幾行代碼注掉或者刪除:
view.showsPhysics = true
view.showsFPS = true
view.showsNodeCount = true
把調試信息去掉以后,游戲看起來真的很不錯照捡!恭喜你:我們已經(jīng)現(xiàn)在進入 beta 版了颅湘!在 GitHub 上找到今天的最終代碼。
最后的思考
這是三遍教程的最后一篇栗精,如果你一直跟著到這闯参,那你已經(jīng)對你的游戲付出了很多工作。在本教程中悲立,你把一個一無所有的場景鹿寨,變成了一個完整的游戲。恭喜薪夕!在第一課里脚草,我們添加了地面,雨滴原献,背景和雨傘精靈馏慨。我們還通過物理引擎來確保雨滴沒有堆積在一起。我們用碰撞檢測來移除節(jié)點嚼贡,這樣就解決了內存溢出的問題熏纯。我們也添加了一些交互來允許傘向玩家觸摸屏幕的位置移動。
在第二課里粤策,我們添加了貓和食物樟澜,為他們定制了一些不同的生成方法。我們還更新了碰撞檢測叮盘,讓貓精靈和食物精靈產(chǎn)生一些作用秩贰。我們也在貓的移動上做了一些處理。小貓有一個目的:吃掉每一個食物柔吼。我們?yōu)樨執(zhí)砑恿撕唵蔚膭赢嬓Ч痉眩€增加了貓和雨滴之間的交互。最后愈魏,我們添加了音效和背景音樂觅玻,讓我們的程序看上去更像一個完整的游戲想际。
在這最后的一篇教程里,我們創(chuàng)建了一個指示器放我們的分數(shù)標簽和退出按鈕溪厘。我們處理節(jié)點上的操作胡本,并使用戶能夠從指示器節(jié)點的回調里退出。我們還添加了一個玩家啟動游戲的場景畸悬,并可以在點擊退出按鈕后返回侧甫。我們還處理了開始游戲和控制游戲中的聲音的過程。
接下來做什么
我們做到這一步用了很久蹋宦,但這個游戲還有許多工作需要繼續(xù)披粟。RainCat 也會繼續(xù)發(fā)展,而且它已經(jīng)可以在 App Store 下載了冷冗。下面的列表是一些想要加的和需要加的功能守屉。有一些已經(jīng)加上了,還有一些待定中:
- 添加 icon 圖標和啟動畫面蒿辙。
- 完成主菜單(教程的是簡化版)胸梆。
- 修復 bug,包括煩人的雨滴和多重食物的生成须板。
- 重構并優(yōu)化代碼。
- 根據(jù)得分更改游戲的調色板兢卵。
- 根據(jù)得分更新難度习瑰。
- 當食物在貓的正上方,讓貓有一些動作秽荤。
- 集成 Game Center甜奄。
- 標明出處(包括一些適當?shù)囊魳非浚?/li>
請持續(xù)關注 GitHub,因為在不久的將來這些都會被實現(xiàn)窃款。如果你對代碼有任何的問題课兄,隨時可以在 hello@thirteen23.com 給我們留言,我們可以一起討論它晨继。如果問題有足夠的關注烟阐,那也許我們會專門寫一篇文章來探討這些問題。
感謝紊扬!
我真的很感謝所有那些蜒茄,在制作游戲和寫文章的過程中,與之相伴的人餐屎。
提供了游戲最初的美術檀葛,設計和編輯,并且在 Garage 發(fā)布了文章腹缩。
提供了游戲最終菜單的設計和調色板(如果我實現(xiàn)了這些屿聋,效果肯定酷炫 — 敬請期待)空扎。
提供了文章中漂亮的標題和分割符,并且?guī)椭帉懳恼隆?/p>
提供了三篇文章里所有漂亮的 GIF 圖片润讥,還很友好的把小貓的 GIF 也發(fā)給了我转锈。
提供了編輯文章的幫助,如果沒有他象对,這個系列可能都不會出現(xiàn)黑忱。
提供了編輯文章的幫助,這的確是一項大工程勒魔。
提供了第三課的編輯工作和乒乓球甫煞,很多的乒乓球(譯者注:這里原文就是ping-pong,譯者的理解是冠绢,可能他們寫代碼有點累抚吠,所以打了會乒乓球。)
正因為這些幫助弟胀,教程才會像預計的那樣完成楷力。
認真的說,真的用了一大堆人來準備這篇文章孵户,并發(fā)布到商店萧朝。
也謝謝每一位讀到這句話的讀者,感謝夏哭。