从零开始的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


<<:  DAY 23 『 客制化按钮 Custom Button 』

>>:  Day20 Let's ODOO: Scheduled Actions

口罩脸孔资料集的标注、资料前处理与资料扩增

我们已将资料集上传到 nilvana 的 Vision Studio 中, 也知道标注格式的种类与基...

[Day 19] 收集资料 — 你要对人家负责啊!

With data collection, ‘the sooner the better’ is ...

JavaScript 闭包(Closure) 上集

闭包 内部函数总是可以访问其所在的外部函数中声明的参数和变数,即使外部函式已经结束执行了。 看看这个...

{CMoney战斗营} 的第二周 #多型与介面

杂记   这是情绪十分起伏的一周,首先花了大约一天的时间在考试及检讨,经过上周密集的练习在包好多层的...

Android 学习笔记26

Android tv 上一篇我们使用了browsefragment,再来就把资料带到里面 首先先把J...