如何在 Swift 3 中構建 SpriteKit 遊戲(第 3 部分)

已發表: 2022-03-10
快速總結↬你有沒有想過創建一個 SpriteKit 遊戲需要什麼? 按鈕看起來比它們應該做的任務更大嗎? 有沒有想過如何在遊戲中保留設置? 自從引入 SpriteKit 以來,iOS 上的遊戲製作從未如此簡單。 在這個由三部分組成的系列的第三部分中,我們將完成我們的 RainCat 遊戲並完成對 SpriteKit 的介紹。 如果您錯過了上一課,您可以通過在 GitHub 上獲取代碼來趕上進度。 請記住,本教程需要 Xcode 8 和 Swift 3。

你有沒有想過創建一個 SpriteKit 遊戲需要什麼? 按鈕看起來比它們應該做的任務更大嗎? 有沒有想過如何在遊戲中保留設置? 自從引入 SpriteKit 以來,iOS 上的遊戲製作從未如此簡單。 在這個由三部分組成的系列的第三部分中,我們將完成我們的 RainCat 遊戲並完成對 SpriteKit 的介紹。

如果您錯過了上一課,您可以通過在 GitHub 上獲取代碼來趕上進度。 請記住,本教程需要 Xcode 8 和 Swift 3。

SmashingMag 的進一步閱讀:鏈接

  • 遊戲化和用戶體驗:用戶贏或輸的地方
  • 有趣的用戶體驗設計:打造更好的遊戲
  • 結合用戶體驗設計和心理學來改變用戶行為
雨貓,第 3 課
雨貓,第 3 課
跳躍後更多! 繼續往下看↓

這是我們 RainCat 之旅的第三課。 在上一課中,我們度過了漫長的一天,學習了一些簡單的動畫、貓的行為、快速的音效和背景音樂。

今天我們將重點關注以下內容:

  • 用於評分的平視顯示器(HUD);
  • 主菜單——帶按鈕;
  • 靜音選項;
  • 遊戲退出選項。

更多資產

最後一課的資源可在 GitHub 上找到。 就像我們在之前的課程中所做的那樣,再次將圖像拖到Assets.xcassets中。

小心!

我們需要一種方法來保持得分。 為此,我們可以創建一個平視顯示器 (HUD)。 這將非常簡單; 它將是一個包含分數和退出遊戲按鈕的SKNode 。 現在,我們將只關注分數。 我們將使用的字體是 Pixel Digivolve,您可以從 Dafont.com 獲得。 與使用不屬於您的圖像或聲音一樣,請在使用之前閱讀字體的許可證。 這個聲明它是免費供個人使用的,但如果你真的喜歡這種字體,你可以從頁面捐贈給作者。 你不能總是自己做所有的事情,所以回饋那些一路幫助你的人是件好事。

接下來,我們需要將自定義字體添加到項目中。 這個過程第一次可能會很棘手。

下載字體並將其移動到項目文件夾中的“字體”文件夾下。 我們在之前的課程中已經做過幾次了,所以我們將更快地完成這個過程。 將一個名為Fonts的組添加到項目中,並添加Pixel digivolve.otf文件。

現在是棘手的部分。 如果您錯過了這部分,您可能無法使用該字體。 我們需要將它添加到我們的Info.plist文件中。 該文件位於 Xcode 的左側窗格中。 單擊它,您將看到屬性列表(或plist )。 右鍵單擊列表,然後單擊“添加行”。

添加行
plist添加一行。

當新行出現時,輸入以下內容:

 Fonts provided by application

然後,在Item 0下,我們需要添加我們的字體名稱。 plist應如下所示:

像素digivolve.otf
字體添加到plist成功!

字體應該可以使用了! 我們應該做一個快速測試以確保它按預期工作。 移動到GameScene.swift ,在sceneDidLoad函數的頂部添加以下代碼:

 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)

它有效嗎?

你好世界!
測試我們的SKLabelNode 。 不好了! “Hello World”標籤又回來了。

如果它有效,那麼你已經正確地完成了一切。 如果不是,那就有問題了。 Code With Chris 有更深入的故障排除指南,但請注意,它適用於舊版本的 Swift,因此您必須進行一些小的調整才能將其升級到 Swift 3。

現在我們可以加載自定義字體,我們可以從 HUD 開始。 刪除“Hello World”標籤,因為我們只使用它來確保我們的字體加載。 HUD 將是一個SKNode ,就像我們的 HUD 元素的容器一樣。 這與我們在第一課中創建背景節點時遵循的過程相同。

使用常用方法創建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 ,我們用它來顯示標籤。 接下來是我們保存當前分數的變量; 然後是保持最佳分數的變量。 最後一個變量是一個布爾值,它告訴我們當前是否呈現高分(我們使用它來確定是否需要運行SKAction來增加記分牌的比例並將其著色為地板的黃色)。

第一個函數setup(size:)只是用來設置所有內容。 我們以與之前相同的方式設置SKLabelNodeSKNode類默認沒有任何大小屬性,因此我們需要創建一種方法來設置大小來定位我們的scoreNode標籤。 我們還從UserDefaults獲取當前的高分。 這是一種保存小塊數據的快速簡便的方法,但它並不安全。 因為我們不擔心這個例子的安全性,所以UserDefaults非常好。

在我們的addPoint()中,我們正在遞增當前score變量並檢查用戶是否獲得了高分。 如果他們有高分,那麼我們將該分數保存到UserDefaults並檢查我們當前是否顯示最佳分數。 如果用戶獲得了高分,我們可以為scoreNode的大小和顏色設置動畫。

resetPoints()函數中,我們將當前分數設置為0 。 然後我們需要檢查我們是否顯示了高分,如果需要,將大小和顏色重置為默認值。

最後,我們有一個名為updateScoreboard的小函數。 這是一個將分數設置為scoreNode文本的內部函數。 這在addPoint()resetPoints()中都被調用。

連接 HUD

我們需要測試我們的 HUD 是否正常工作。 移動到GameScene.swift ,並在文件頂部的foodNode變量下方添加以下行:

 private let hudNode = HudNode()

在頂部附近的sceneDidLoad()函數中添加以下兩行:

 hudNode.setup(size: size) addChild(hudNode)

然後,在spawnCat()函數中,重置點以防貓從屏幕上掉下來。 在將貓精靈添加到場景後添加以下行:

 hudNode.resetPoints()

接下來,在handleCatCollision(contact:)函數中,當貓被雨擊中時,我們需要再次重置分數。 在函數末尾的switch語句中——當另一個主體是RainDropCategory ——添加以下行:

 hudNode.resetPoints()

最後,我們需要告訴記分牌用戶何時獲得積分。 在handleFoodHit(contact:)的文件末尾,找到以下幾行:

 //TODO increment points print("fed cat")

並將它們替換為:

 hudNode.addPoint()

瞧!

HUD解鎖!
HUD解鎖!

您應該看到 HUD 正在運行。 跑來跑去收集一些食物。 第一次收集食物時,您應該會看到分數變黃並擴大規模。 當你看到這種情況發生時,讓貓受到打擊。 如果分數重置,那麼你就會知道你在正確的軌道上!

高分!
有史以來的最高分(……在撰寫本文時)!

下一個場景

沒錯,我們正在轉移到另一個場景! 事實上,完成後,這將是我們應用程序的第一個屏幕。 在你做任何其他事情之前,打開Constants.swift並將以下行添加到文件的底部——我們將使用它來檢索和保存高分:

 let ScoreKey = "RAINCAT_HIGHSCORE"

創建新場景,將其放在“Scenes”文件夾下,並將其命名為MenuScene.swift 。 在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) } }

因為這個場景比較簡單,我們不會創建任何特殊的類。 我們的場景將包含兩個按鈕。 這些可能(並且可能應該是)他們自己的SKSpriteNodes類,但由於它們足夠不同,我們不需要為它們創建新類。 這是您構建自己的遊戲時的重要提示:當事情變得複雜時,您需要能夠確定在哪裡停止和重構代碼。 一旦您在遊戲中添加了三個或四個以上的按鈕,可能是時候停止並將菜單按鈕的代碼重構到它自己的類中了。

上面的代碼沒有做任何特別的事情; 它正在設置四個精靈的位置。 我們還設置了場景的背景顏色,以便整個背景都是正確的值。 從 Xcode 的 HEX 字符串生成顏色代碼的好工具是 UI Color。 上面的代碼還為我們的按鈕狀態設置紋理。 開始遊戲的按鈕有正常狀態和按下狀態,而聲音按鈕是切換按鈕。 為了簡化切換,我們將在用戶按下時更改聲音按鈕的 alpha 值。 我們也在拉取和設置高分SKLabelNode

我們的MenuScene看起來不錯。 現在我們需要在應用加載時顯示場景。 移動到GameViewController.swift並找到以下行:

 let sceneNode = GameScene(size: view.frame.size)

用這個替換它:

 let sceneNode = MenuScene(size: view.frame.size)

這個小改動將默認加載MenuScene ,而不是GameScene

我們的新場景!
我們的新場景! 注意每秒 1.0 幀:沒有任何東西在移動,所以不需要更新任何東西。

按鈕狀態

SpriteKit 中的按鈕可能很棘手。 有很多第三方選項可用(我什至自己做了一個),但理論上你只需要知道三種觸摸方法:

  • touchesBegan(_ touches: with event:)
  • touchesMoved(_ touches: with event:)
  • touchesEnded(_ touches: with event:)

我們在更新雨傘時簡要介紹了這一點,但現在我們需要知道以下內容:觸摸了哪個按鈕,用戶是否鬆開了點擊或單擊了該按鈕,以及用戶是否仍在觸摸它。 這就是我們的selectedButton變量發揮作用的地方。 當觸摸開始時,我們可以使用該變量捕獲用戶開始單擊的按鈕。 如果他們拖到按鈕外,我們可以處理它並為其賦予適當的紋理。 當他們釋放觸摸時,我們可以看到他們是否仍在按鈕內部進行觸摸。 如果是,那麼我們可以對它應用相關的操作。 將以下行添加到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:)中,我們首先檢查是否有任何當前選擇的按鈕。 如果這樣做,我們需要將按鈕的狀態重置為未按下。 然後,我們需要檢查是否按下了任何按鈕。 如果按下一個,它將顯示按鈕的突出顯示狀態。 然後,我們將selectedButton設置為用於其他兩個方法的按鈕。

touchesMoved(_ touches: with events:)中,我們檢查最初觸摸的是哪個按鈕。 然後,我們檢查當前觸摸是否仍在selectedButton的範圍內,並從那裡更新高亮狀態。 startButton的高亮狀態將紋理更改為按下狀態的紋理,其中soundButton的高亮狀態將精靈的 alpha 值設置為 50%。

最後,在touchesEnded(_ touches: with event:)中,我們再次檢查選擇了哪個按鈕(如果有),然後檢查觸摸是否仍在按鈕的範圍內。 如果所有情況都滿足,我們為正確的按鈕調用handleStartButtonClick()handleSoundButtonClick()

採取行動的時刻

現在我們已經完成了基本的按鈕行為,我們需要一個事件來在它們被點擊時觸發。 更容易實現的按鈕是startButton 。 點擊時,我們只需要呈現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) }

如果您現在運行應用程序並按下按鈕,遊戲將開始!

現在我們需要實現靜音切換。 我們已經有一個聲音管理器,但我們需要能夠告訴它靜音是打開還是關閉。 在Constants.swift中,我們需要添加一個鍵以在靜音打開時持久化。 添加以下行:

 let MuteKey = "RAINCAT_MUTED"

我們將使用它來將布爾值保存到UserDefaults 。 現在已經設置好了,我們可以進入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) {

現在,如果靜音關閉並且未設置音頻播放器或當前音頻播放器不再播放,我們將播放下一首曲目。

從這裡,我們可以回到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)

上面的示例使用三元運算符來設置正確的紋理。

現在音樂已經連接,我們可以移動到CatSprite.swift以在靜音打開時禁用貓喵喵叫。 在hitByRain()中,我們可以在刪除 walk 動作後添加以下if語句:

 if SoundManager.sharedInstance.isMuted { return }

此語句將返回用戶是否已將應用靜音。 因此,我們將完全忽略currentRainHitsmaxRainHits和喵喵聲效果。

畢竟,現在是時候試試我們的靜音按鈕了。 運行應用程序並驗證它是否正確播放和靜音。 靜音,關閉應用程序,然後重新打開。 確保靜音設置持續存在。 請注意,如果您只是從 Xcode 靜音並重新運行應用程序,您可能沒有給UserDefaults足夠的時間來保存。 玩遊戲,並確保在您靜音時貓永遠不會喵喵叫。

測試按鈕功能。

退出遊戲

現在我們有了第一種類型的主菜單按鈕,我們可以通過在遊戲場景中添加退出按鈕來處理一些棘手的事情。 一些有趣的互動可以想出我們的遊戲風格; 目前,雨傘將移動到用戶觸摸或移動觸摸的任何地方。 顯然,當用戶試圖退出遊戲時,傘移動到退出按鈕是一種非常糟糕的用戶體驗,因此我們將嘗試阻止這種情況發生。

我們正在實現的退出按鈕將模仿我們之前添加的開始遊戲按鈕,大部分過程保持不變。 變化將在於我們處理觸摸的方式。 將您的quit_buttonquit_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引用,以及我們將為按鈕狀態設置的紋理。 為了確保我們在嘗試退出時不會無意中更新傘,我們需要一個變量來告訴 HUD(和遊戲場景)我們正在與退出按鈕而不是傘交互。 在showingHighScore布爾變量下面添加以下代碼:

 private(set) var quitButtonPressed = false

同樣,這是一個只有HudNode可以設置但其他類可以檢查的變量。 現在我們的變量已經設置好了,我們可以將按鈕添加到 HUD 中。 將以下代碼添加到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)

上面的代碼將使用我們未按下狀態的紋理設置退出按鈕。 我們還將位置設置為右上角並將zPosition設置為較高的數字,以強制它始終繪製在頂部。 如果您現在運行遊戲,它將顯示在GameScene中,但還不能點擊。

退出按鈕
請注意我們 HUD 中的新退出按鈕。

現在按鈕已經定位,我們需要能夠與它進行交互。 現在,我們在GameScene中進行交互的唯一地方是我們與umbrellaSprite精靈交互的時候。 在我們的示例中,HUD 將優先於雨傘,因此用戶不必為了退出而將雨傘移開。 我們可以在HudNode.swift中創建相同的函數來模仿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創建的代碼。 不同之處在於只有一個按鈕可以跟踪,所以我們可以在這些觸摸方法中處理所有事情。 另外,因為我們會知道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) } }

在這裡,每種方法都以幾乎相同的方式處理所有內容。 我們告訴 HUD 用戶已經與場景進行了交互。 然後,我們檢查退出按鈕當前是否正在捕獲觸摸。 如果不是,那麼我們移動傘。 我們還添加了touchesEnded(_ touches: with event:)函數來處理退出按鈕的點擊結束,但我們仍然沒有將它用於umbrellaSprite

點擊退出按鈕不會移動傘,但點擊其他地方會。

現在我們有了一個按鈕,我們需要一種方法讓它影響GameScene 。 將以下行添加到HudeNode.swift的頂部:

 var quitButtonAction : (() -> ())?

這是一個沒有輸入也沒有輸出的通用閉包。 我們將使用GameScene.swift文件中的代碼設置它,並在我們單擊HudNode.swift中的按鈕時調用它。 然後,我們可以將之前在touchEndedAtPoint(point:)函數中創建的代碼中的TODO替換為:

 if quitButton.contains(point) && quitButtonAction != nil { quitButtonAction!() }

現在,如果我們設置了quitButtonAction閉包,它將從這一點開始調用。

要設置quitButtonAction閉包,我們需要轉到GameScene.swift 。 在sceneDidLoad()中,我們可以用以下代碼替換我們的 HUD 設置:

 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)

運行應用程序,按播放,然後按退出。 如果您回到主菜單,那麼您的退出按鈕正在按預期工作。 在我們創建的閉包中,我們初始化了到MenuScene的轉換。 我們將這個閉包設置為HUD節點,以便在單擊退出按鈕時運行。 另一個重要的行是當我們將quitButtonAction設置為nil時。 其原因是正在發生保留週期。 場景持有對 HUD 的引用,而 HUD 持有對場景的引用。 因為對這兩個對像都有引用,所以在垃圾收集時兩者都不會被丟棄。 在這種情況下,每次我們進入和離開GameScene時,都會創建另一個實例並且永遠不會釋放。 這對性能不利,應用程序最終會耗盡內存。 有很多方法可以避免這種情況,但在我們的例子中,我們可以從 HUD 中刪除對GameScene的引用,一旦我們返回MenuScene ,場景和 HUD 將被終止。 Krakendev 對引用類型以及如何避免這些循環有更深入的解釋。

現在,移動到GameViewController.swift ,刪除或註釋掉以下三行代碼:

 view.showsPhysics = true view.showsFPS = true view.showsNodeCount = true

排除了調試數據後,遊戲看起來非常棒! 恭喜:我們目前處於測試階段! 從今天開始在 GitHub 上查看最終代碼。

最後的想法

這是一個由三部分組成的教程的最後一課,如果你做到了這一步,你就在你的遊戲上做了很多工作。 在本教程中,您從一個完全沒有任何內容的場景變成了一個完整的遊戲。 恭喜! 在第一課中,我們添加了地板、雨滴、背景和雨傘精靈。 我們還玩弄了物理,並確保我們的雨滴不會堆積。 我們從碰撞檢測開始,並致力於剔除節點,這樣我們就不會耗盡內存。 我們還通過允許雨傘向用戶在屏幕上觸摸的位置移動來添加一些用戶交互。

在第二課中,我們添加了貓和食物,以及它們各自的自定義生成方法。 我們更新了碰撞檢測以允許貓和食物精靈。 我們還研究了貓的運動。 這隻貓獲得了一個目標:吃掉所有可用的食物。 我們為貓添加了簡單的動畫,並在貓和雨之間添加了自定義交互。 最後,我們添加了音效和音樂,讓它感覺像是一個完整的遊戲。

在最後一課中,我們創建了一個平視顯示器來保存我們的樂譜標籤,以及我們的退出按鈕。 我們處理了跨節點的操作,並允許用戶通過來自 H​​UD 節點的回調退出。 我們還添加了另一個場景,用戶可以啟動並在單擊退出按鈕後返回。 我們處理了啟動遊戲和控制遊戲聲音的過程。

從這往哪兒走

我們投入了很多時間來做到這一點,但是這款遊戲還有很多工作要做。 RainCat 仍在繼續開發,並在 App Store 中提供。 以下是需要和需要添加的列表。 一些項目已添加,而其他項目仍在等待中:

  • 添加圖標和啟動畫面。
  • 完成主菜單(為教程簡化)。
  • 修復錯誤,包括流氓雨滴和多個食物產卵。
  • 重構和優化代碼。
  • 根據分數更改遊戲的調色板。
  • 根據分數更新難度。
  • 當食物就在貓的正上方時,為貓設置動畫。
  • 集成遊戲中心。
  • 給予信用(包括對音樂曲目的適當信用)。

在 GitHub 上跟踪,因為這些更改將在未來進行。 如果您對代碼有任何疑問,請隨時通過 [email protected] 給我們留言,我們可以進行討論。 如果某些主題得到足夠的關注,也許我們可以寫另一篇討論該主題的文章。

謝謝!

我要感謝所有在創建遊戲和開發相關文章的過程中提供幫助的人。

  • Cathryn Rowe 用於最初的藝術、設計和編輯,以及在我們的車庫中發布文章。
  • Morgan Wheaton 對於最終的菜單設計和調色板(一旦我真正實現了這些功能,它們看起來會很棒——敬請期待)。
  • Nikki Clark 對於文章中令人敬畏的標題和分隔符以及編輯文章的幫助。
  • Laura Levisay 感謝文章中所有精彩的 GIF,以及向我發送可愛的貓咪 GIF 以獲得精神上的支持。
  • 湯姆·哈德森(Tom Hudson)幫助編輯文章,如果沒有他,這個系列根本就不會製作。
  • Lani DeGuire 尋求幫助編輯文章,這是一項繁重的工作。
  • Jeff Moon 幫助編輯第三課和乒乓球。 很多乒乓球。
  • Tom Nelson 幫助確保教程按預期工作。

說真的,花了很多人來為這篇文章準備好一切並將其發佈到商店。

也感謝所有讀到這句話的人。