如何在 Swift 3 中用 SpriteKit 框架編寫游戲 (Part 3)

寫在前面:這個系列文章是轉載過來的锥腻,簡書里之前也有人轉載了嘱函,不過沒有進行重新編排蓉驹,圖文等格式并不適用于簡書曹宴,我參照原文樣式重新排版了一次搂橙。

你有沒有想過要如何開始創(chuàng)作一款基于 SpriteKit 的游戲浙炼?按鈕的開發(fā)是一個很龐大的任務嗎份氧?想過如何制作游戲的設置部分嗎?隨著 SpriteKit 的出現(xiàn)弯屈,在 iOS 上開發(fā)游戲已經(jīng)變得空前的簡單了蜗帜。在本系列的第三部分,我們將完成 RainCat 游戲的開發(fā)以及對 SpriteKit 框架的介紹资厉。

如果你錯過了上一課厅缺,你可以通過獲取 Github 上的代碼來趕上進度。請記住,本教程需要使用 Xcode 8 和 Swift 3湘捎。

Raincat, 第三課

這是我們 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 文件看起來應該像下面這樣:

Pixel digivolve.otf

字體已經(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 嗎谣沸?

Hello world!

如果字體正常刷钢,那就說明你做的完全正確。如果不正常乳附,那就是什么地方出了問題内地。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:) 的功能是把一切都設置好督函。我們就像之前那樣來設置 SKLabelNodeSKNode 類沒有任何默認尺寸激挪,所以我們要創(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()

以上辰晕!

HUD unlocked!

當來回收集食物時蛤迎,你就會看到指示器的效果了。第一次收集食物的時候含友,你應該會看到分數(shù)變黃然后比例變大替裆,如果你看到當貓淋到雨滴時,分數(shù)重置唱较,那么你就是正確的扎唾!

High Score!

下一個場景

沒錯,我們要開始下一個場景了南缓!事實上胸遇,如果這個場景完成,它將會作為我們游戲的首屏展示汉形。在做其他事情之前纸镊,打開 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_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 引用抖拴,并且會根據(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 里,不過還不能點些膨。

Quit button

現(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ā)布到商店萧朝。

也謝謝每一位讀到這句話的讀者,感謝夏哭。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末检柬,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子竖配,更是在濱河造成了極大的恐慌何址,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件进胯,死亡現(xiàn)場離奇詭異用爪,居然都是意外死亡,警方通過查閱死者的電腦和手機胁镐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門偎血,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人盯漂,你說我怎么就攤上這事烁巫。” “怎么了宠能?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵亚隙,是天一觀的道長。 經(jīng)常有香客問我违崇,道長阿弃,這世上最難降的妖魔是什么诊霹? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮渣淳,結果婚禮上脾还,老公的妹妹穿的比我還像新娘。我一直安慰自己入愧,他們只是感情好鄙漏,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著棺蛛,像睡著了一般怔蚌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上旁赊,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天桦踊,我揣著相機與錄音,去河邊找鬼终畅。 笑死籍胯,一個胖子當著我的面吹牛,可吹牛的內容都是我干的离福。 我是一名探鬼主播杖狼,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼妖爷!你這毒婦竟也來了本刽?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤赠涮,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后暗挑,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體笋除,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年炸裆,在試婚紗的時候發(fā)現(xiàn)自己被綠了垃它。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡烹看,死狀恐怖国拇,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情惯殊,我是刑警寧澤酱吝,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站土思,受9級特大地震影響务热,放射性物質發(fā)生泄漏忆嗜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一崎岂、第九天 我趴在偏房一處隱蔽的房頂上張望捆毫。 院中可真熱鬧,春花似錦冲甘、人聲如沸绩卤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽濒憋。三九已至,卻和暖如春嫁审,著一層夾襖步出監(jiān)牢的瞬間跋炕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工律适, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留辐烂,地道東北人。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓捂贿,卻偏偏與公主長得像纠修,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子厂僧,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內容