iT邦幫忙

2021 iThome 鐵人賽

DAY 27
0
Mobile Development

從零開始的8-bit迷宮探險!Swift SpriteKit 遊戲開發實戰系列 第 27

從零開始的8-bit迷宮探險【Level 27】神助攻-老弟幫我配個音效

奄奄一息的山姆躺在地上,腦海中浮現了人生跑馬燈。
「我為什麼會在這裡?我的夢想,終究只是夢想吧...。」
『要嘛等死,要嘛寫歌』出自《反正我很閒》的一段話冒了出來。
「對!這是發生山難的生存法則,我怎麼給忘了呢!」山姆跳了起來。
「寫歌嗎?找我就對了」一個名為伊索雷托的森林小精靈望著山姆,手裡抱著一把三味線。

今日目標

  • 與編曲師 (我老弟) 確認遊戲配樂需求
  • 幫遊戲加上背景音樂及音效

PS. 這裡是開發 iOS 手機遊戲的系列文,如果還沒看過之前 劇情 文章的朋友,歡迎先點這邊回顧唷!


音樂及音效

我想一個遊戲的靈魂,最重要的就在於背景音樂跟音效了。雖然老姐我可以自己畫圖跟寫程式碼,但是音樂創作還是很講求天份的!這個就交給專業的老弟來吧!

配樂需求

  • 風格:8-bit 復古遊戲風格的音樂

  • 音樂:

    • 起始音樂 (約5秒)
      • 遊戲開始前,所有角色都尚未開始移動時的前奏音樂
    • 背景音樂1 (循環播放)
      • 怪物可攻擊主角時的主要遊戲背景音樂
    • 背景音樂2 (循環播放)
      • 主角收集到魔幻水晶後,可以反擊怪物時的遊戲背景音樂
    • 遊戲結束音樂 (約3秒)
      • 切換至遊戲結束場景時的音樂
    • 遊戲破關音樂 (約6秒)
      • 主角將所有水晶都收集完畢時的破關音樂
  • 音效:

    • 怪物攻擊主角時的音效
    • 主角反擊怪物時的音效
    • 主角收集水晶、魔幻水晶時的音效
    • 主角吃到香菇時的音效
    • 點擊方向鍵的音效

熱騰騰的 mp3 檔案出爐

  • 音樂:
    • 起始音樂:StartMusic.mp3
    • 背景音樂1:BaseMusic.mp3
    • 背景音樂2:FastMusic.mp3
    • 遊戲結束音樂:GameOverMusic.mp3
    • 遊戲破關音樂:FinishMusic.mp3
  • 音效:
    • 怪物攻擊主角:FallSound.mp3
    • 主角反擊怪物:HitSound.mp3
    • 主角收集水晶、魔幻水晶:GotSound.mp3
    • 主角吃到香菇:EatSound.mp3
    • 點擊方向鍵:ClickSound.mp3

音源創作:【Isoletto】Magical Crystal - Main Theme

將檔案放進專案中

請直接將所有 mp3 檔案拖移到專案左側的檔案導覽器中,會跳出以下的訊息,請點擊 Finish

https://imgur.com/cJtAamC.png


來配音樂吧

import

請先在遊戲場景中 import AVFoundation,它是一個框架,可以用來處理視聽媒體

  • GameScene.swift
import AVFoundation

播放器

在遊戲場景中新增 4 個播放器,可以將它想像成四個音軌,分別播放背景音樂、主角與收集物碰觸的音效、主角與怪物碰觸的音效、點擊方向鍵的音效,彼此間可以互相疊加。

  • musicPlayer
  • soundCollectionPlayer
  • soundWeatherPlayer
  • soundClickPlayer

分別新增四個播放器播音樂的方法,讓呼叫方法的時候可以帶入檔名 musicName ,其中音樂類型的再加上是否循環播放的參數 loop,並且設定預設為 false

  • playMusicByName(musicName: String, loop: Bool = false)
  • playCollectionSoundByName(soundName: String)
  • playWeatherSoundByName(soundName: String)
  • playClickSound()

方法內容:

  • 使用 Bundle.main.url 取得指定的檔案位置,並新增一個播放器來播放音樂檔,使用 play() 可以開始播放音樂
  • numberOfLoops 可以設定重複播放的次數,預設為 0,音樂只會播一次,不會重複。設定 -1 則會不斷循環播放,直到呼叫 stop() 才會停止
  • GameScene.swift
class GameScene: SKScene {
    ...
    var musicPlayer: AVAudioPlayer?
    var soundCollectionPlayer: AVAudioPlayer?
    var soundWeatherPlayer: AVAudioPlayer?
    var soundClickPlayer: AVAudioPlayer?
    
    func playMusicByName(musicName: String, loop: Bool = false) {
        guard let url = Bundle.main.url(forResource: musicName, withExtension: "mp3") else {
            return
        }
        do {
            self.musicPlayer = try AVAudioPlayer(contentsOf: url)
            self.musicPlayer!.play()
            if loop {
                self.musicPlayer?.numberOfLoops = -1
            }
        }
        catch {
            print(error)
        }
    }
    
    func playCollectionSoundByName(soundName: String) {
        guard let url = Bundle.main.url(forResource: soundName, withExtension: "mp3") else {
            return
        }
        do {
            self.soundCollectionPlayer = try AVAudioPlayer(contentsOf: url)
            self.soundCollectionPlayer!.play()
        }
        catch {
            print(error)
        }
    }
    
    func playWeatherSoundByName(soundName: String) {
        guard let url = Bundle.main.url(forResource: soundName, withExtension: "mp3") else {
            return
        }
        do {
            self.soundWeatherPlayer = try AVAudioPlayer(contentsOf: url)
            self.soundWeatherPlayer!.play()
        }
        catch {
            print(error)
        }
    }
    
    func playClickSound() {
        guard let url = Bundle.main.url(forResource: "ClickSound", withExtension: "mp3") else {
            return
        }
        do {
            self.soundClickPlayer = try AVAudioPlayer(contentsOf: url)
            self.soundClickPlayer!.play()
        }
        catch {
            print(error)
        }
    }
}

起始音樂

  • 音樂檔名:StartMusic
  • 在一進入遊戲時 (gameStart) 呼叫 playMusicByName 方法,musicName 帶入音樂檔名
  • 這邊讓它播放一次就好,所以不帶入 loop 的值,預設為 false
  • GameScene.swift
class GameScene: SKScene {
    ...
    func gameStart() {
        ...
        self.playMusicByName(musicName: "StartMusic")
    }
}

背景音樂1

  • 音樂檔名:BaseMusic
  • 在遊戲開始動作時 (gameStartAction),呼叫 playMusicByName 方法,musicName 帶入音樂檔名
  • 這邊讓它循環播放,loop 帶入 true
  • 在怪物的模式回到攻擊時 (eacapeToAttackModeAction),也呼叫 playMusicByName 方法播放背景音樂1
  • GameScene.swift
class GameScene: SKScene {
    ...
    @objc func gameStartAction() {
        ...
        self.playMusicByName(musicName: "BaseMusic", loop: true)
    }
}
  • GameScene.swift
class GameScene: SKScene {
    ...
    // 逃跑->攻擊
    @objc func eacapeToAttackModeAction() {
        ...
        self.playMusicByName(musicName: "BaseMusic", loop: true)
    }
}

背景音樂2

  • 音樂檔名:FastMusic
  • 在主角收集到魔幻水晶時,呼叫 playMusicByName 方法,musicName 帶入音樂檔名
  • 這邊讓它循環播放,loop 帶入 true
  • GameScene.swift
class GameScene: SKScene {
    ...
    override func update(_ currentTime: TimeInterval) {
        ...
        for magicCrystal in self.magicCrystals where !magicCrystal.isGotten && magicCrystal.gridX == sam.gridX && magicCrystal.gridY == sam.gridY {
            ...
            self.playMusicByName(musicName: "FastMusic", loop: true)
        }
    }
}

遊戲結束音樂

  • 音樂檔名:GameOverMusic
  • 新增 playMusicGameOver 方法,找到音樂檔案,並且播放
  • 在切換至遊戲結束場景時,呼叫 playMusicGameOver 方法
  • 在點擊重新開始遊戲按鈕時,將音樂停止 self.musicGameOver?.stop()
  • GameOverScene.swift
import AVFoundation
class GameOverScene: SKScene {
    ...
    var musicGameOver: AVAudioPlayer?
    override func didMove(to view: SKView) {
        ...
        self.playMusicGameOver()
    }
    func playMusicGameOver() {
        if let url = Bundle.main.url(forResource: "GameOverMusic", withExtension: "mp3") {
            self.musicGameOver = try? AVAudioPlayer(contentsOf: url)
            self.musicGameOver?.play()
        }
    }
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in (touches) {
            let location = touch.location(in: self)
            if self.atPoint(location) == self.restartNode || self.atPoint(location) == self.labelRestart {
                if let gameScene = GameScene(fileNamed: "GameScene") {
                    ...
                    self.musicGameOver?.stop()
                }
            }
        }
    }
}

怪物攻擊主角音效

  • 音效檔名:FallSound
  • 先透過 stop() 將背景音樂關閉
  • 在怪物攻擊到主角時 (gameStop),呼叫 playWeatherSoundByName 方法,soundName 帶入音效檔名
  • GameScene.swift
class GameScene: SKScene {
    ...
    func gameStop() {
        ...
        self.musicPlayer?.stop()
        self.playWeatherSoundByName(soundName: "FallSound")
    }
}

主角反擊怪物音效

  • 音效檔名:HitSound
  • 在主角反擊怪物時,呼叫 playWeatherSoundByName 方法,soundName 帶入音效檔名
  • GameScene.swift
class GameScene: SKScene {
    ...
    override func update(_ currentTime: TimeInterval) {
        ...
        for weather in self.weathers where (weather.gridX == sam.gridX && abs(weather.node.position.y - sam.node.position.y) <= CGFloat(self.gridWH + 6) || weather.gridY == sam.gridY && abs(weather.node.position.x - sam.node.position.x) <= CGFloat(self.gridWH + 6)) && (gridMapping.purpleTree.x != sam.gridX && gridMapping.purpleTree.y != sam.gridY)
        {
            if weather.mode == .ATTACK || weather.mode == .PLAY {
                ...
            } else if weather.mode == .ESCAPE {
                ...
                self.playWeatherSoundByName(soundName: "HitSound")
            }
        }
    }
}

主角收集水晶、魔幻水晶

  • 音效檔名:GotSound
  • 在主角收集到水晶和魔幻水晶時,分別呼叫 playCollectionSoundByName 方法,soundName 帶入音效檔名
  • GameScene.swift
class GameScene: SKScene {
    ...
    override func update(_ currentTime: TimeInterval) {
        ...
        for crystal in self.crystals where !crystal.isGotten && crystal.gridX == sam.gridX && crystal.gridY == sam.gridY {
            ...
            self.playCollectionSoundByName(soundName: "GotSound")
        }
        for magicCrystal in self.magicCrystals where !magicCrystal.isGotten && magicCrystal.gridX == sam.gridX && magicCrystal.gridY == sam.gridY {
            ...
            self.playCollectionSoundByName(soundName: "GotSound")
        }
    }
}

主角吃到香菇

  • 音效檔名:EatSound
  • 在主角吃到香菇時,呼叫 playCollectionSoundByName 方法,soundName 帶入音效檔名
  • GameScene.swift
class GameScene: SKScene {
    ...
    override func update(_ currentTime: TimeInterval) {
        ...
        for mushroom in self.mushrooms where !mushroom.isGotten && mushroom.gridX == sam.gridX && mushroom.gridY == sam.gridY {
            ...
            self.playCollectionSoundByName(soundName: "EatSound")
        }
    }
}

點擊方向鍵

  • 音效檔名:ClickSound
  • 在點擊方向鍵時,呼叫 playClickSound 方法
  • GameScene.swift
class GameScene: SKScene {
    ...
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        ...
        for touch in (touches) {
            let location = touch.location(in: self)
            if self.atPoint(location) == btnLeft {
                ...
                self.playClickSound()
            }
            if self.atPoint(location) == btnRight {
                ...
                self.playClickSound()
            }
            if self.atPoint(location) == btnUp {
                ...
                self.playClickSound()
            }
            if self.atPoint(location) == btnDown {
                ...
                self.playClickSound()
            }
        }
    }
}

來看看套用音樂及音效後的效果吧


今日小結

目前遊戲已經套用音樂及音效了,還差一個遊戲破關音樂還沒套用。
明日會帶大家實作遊戲破關時的動作,到時候再將音樂也套用上去吧!


參考來源:
AVFoundation
AVAudioPlayer


上一篇
從零開始的8-bit迷宮探險【Level 26】這遊戲沒有華佗,不能補血啊!Game Over 場景切換
下一篇
從零開始的8-bit迷宮探險【Level 28】看我把關卡難度提升-在場景加上聚光燈效果
系列文
從零開始的8-bit迷宮探險!Swift SpriteKit 遊戲開發實戰30

尚未有邦友留言

立即登入留言