고양이 게임에 배경음악과 효과음을 넣어볼 것이다. iOS
프로젝트에 오디오 기능을 구현하기 위한 프레임워크로 AVFoundation
을 제일 먼저 떠올리기 쉽지만, SpriteKit
만으로도 간단한 오디오 재생 구현이 가능하다. 바로 오디오 노드(SKAudioNode)
를 통해서다. SpriteKit
을 접해보거나 내 고양이 퍼즐 게임 시리즈를 읽어본 사람이라면 노드가 게임의 구성요소 단위 정도라는 건 알 것이다. 오디오 asset 또한 노드로써 추가할 수 있다.
출처 - https://developer.apple.com/documentation/spritekit/skaudionode/
SKAudioNode
는 씬에 오디오를 추가할 수 있게 하는 객체다. 이 객체는 AVFoundation
을 통해 자동으로 소리를 재생하며, 3D 공간 오디오 효과를 추가적으로 적용할 수도 있다. 표시 중인 씬 객체는 AVAudio3DMixing
프로토콜에 정의된 파라미터를 기반으로 씬에 있는 노드들과 오디오를 믹싱할 수 있다. 씬의 audioEngine
프로퍼티는 볼륨과 재생을 제어할 수 있다.
기본적으로 오디오 노드는 (isPositional
프로퍼티를 true
로 할 경우)위치 기반이다. 씬에 listener
가 설정된 상태에서 오디오 노드를 추가할 경우 SpriteKit
은 두 노드의 상대적인 위치를 기반으로 스테레오 밸런스와 볼륨을 자동으로 조정한다.
오디오 노드에 action
을 실행함으로써 스테레오 밸런스와 볼륨을 설정할 수 있다.
(뒤에 자세한 내용 생략 - 이 이상의 섬세한 동작이 필요할 때 다시 찾아볼 것)
중요한 결론은
SKAudioNode
를 사용하면 내장된AVFoundation
의 일부 기능을 통해 씬에서 오디오를 처리할 수 있는 것이다.
오디오 노드를 활용하기로 결정했으니, 오디오 소스를 준비하자.
배경음악 출처 - https://freetouse.com/music/moavii/foreign
효과음 출처 - https://pixabay.com/ko/users/slodkabonanza-43033281/?utm_source=link-attribution&utm_medium=referral&utm_campaign=music&utm_content=197846
저작권 걱정 없는 무료 음악 사이트에서 고양이 퍼즐에 어울리는 경쾌한 배경음악과 퍼즐 pop 효과음을 골라 Xcode 프로젝트에 넣었다.
class GameScene: SKScene {
// ... //
private let backgroundMusic: SKAudioNode = {
// 파일 이름으로 오디오 노드 생성
let node = SKAudioNode(fileNamed: Assets.backgroundMusic.rawValue)
node.isPositional = false // 위치 기반 사운드 필요 없음
node.autoplayLooped = true // 자동으로 재생 시작 + 반복 재생
return node
}()
// ... //
private func addAudioNodes() {
addChild(backgroundMusic)
}
override func didMove(to view: SKView) {
// ... //
addBackgroundMusic()
}
}
autoplayLooped
가 true
기 때문에, addChild
를 해주는 순간 오디오는 재생을 시작한다.
musicButton
의 토글에 따라 음악도 꺼졌다가 켜지도록 구현했다. 커스텀 버튼 노드 클래스에서 콜백할 클로저를 정의해주고 touchesEnded
에서 isMusicOn
값 전달과 함께 호출해주었다. 그 다음 씬에서 isMusicOn
값에 따라 음악의 재생과 일시정지가 토글되도록 구현했다.고양이 노드를 아래로 움직일 때 코드를 보면,
let downAction = SKAction.move(to: positionItem(for: item), duration: 0.3) item.run(downAction)
SKNode.run(SKAction)
형태로 노드의 액션을 제어하는 것을 볼 수 있다. 오디오 노드도 마찬가지로run
메소드를 통해 재생 액션을 추가하였다.
class MusicButton: SKSpriteNode {
private var isMusicOn: Bool = true {
didSet {
toggleImage()
}
}
// 클로저에 isMusicOn 값을 전달하기 위해 Bool -> Void 타입으로 지정
var buttonAction: ((Bool) -> Void)?
// ... //
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
setScale(1.0)
isMusicOn.toggle()
buttonAction?(isMusicOn) // buttonAction 클로저에 토글 값 전달
}
}
class GameScene: SKScene {
// ... //
private func addMusicButton() {
// ... //
musicButton.buttonAction = { [weak self] isMusicOn in
isMusicOn
? self?.backgroundMusic.run(SKAction.play())
: self?.backgroundMusic.run(SKAction.pause())
}
addChild(musicButton)
}
}
- 참고로, 오디오 노드 클래스에는
play/pause
와 같은 제어 메소드가 없다. 위에 언급했듯이autoplayLooped
가true
일 때addChild
를 하는 순간 재생이 시작되고removeFromParent
를 하면 멈추기 때문에 이걸 활용할 수도 있지만 이는정지
로 동작하며,일시정지
를 하고 싶다면SKAction.pause()
를 사용해야 한다.- 재생을 제어하는 방법은 이외에도 여러가지가 있지만, 이번 프로젝트에서는 섬세한 오디오 설정까진 필요없기 때문에 가장 간단한 방법인
SKAction
을 사용했다. (참고 키워드만 언급하자면AVFAudio
,AVAudioPlayerNode
,AVAudioPlayer
... )
배경음악은 일시정지가 필요하지만 효과음의 경우 고양이를 누른 순간 팝!
소리를 한번 재생하면 된다. autoplayLooped
가 false
인 노드를 추가해주고 play()
액션만 추가하면 끝이다.
class GameScene: SKScene {
private let popSound: SKAudioNode = {
let node = SKAudioNode(fileNamed: Assets.popSound.rawValue)
node.isPositional = false
node.autoplayLooped = false // 자동재생, 반복 off
return node
}()
// ... //
private func addAudioNodes() {
addChild(backgroundMusic)
addChild(popSound)
}
// ... //
func removeMatches() {
guard matchedItems.count > 1 else { return }
// ... //
popSound.run(SKAction.play()) // 2개 이상의 고양이들이 pop될 때 sound 재생
}
}
사실 효과음과 같은 단일 재생의 경우 노드 추가 없이 코드 한줄로도 구현이 가능하다.
func removeMatches() { // ... // let popSoundAction = SKAction.playSoundFileNamed("popSound.mp3", waitForCompletion: false) run(popSoundAction) }
써놓고 보니 두줄인데 하여튼 기분 상 한줄 수준인 코드다. 정말 간편하지 않은가? 이걸 보고 SKAction
의 공식문서에 들어가서 어떤 액션들이 있는지 구경하면 정말 흥미로울 거 같단 생각이 들었다.
원래 더 공부하기 귀찮아서 이번에는 볼륨을 다루고 싶지 않았는데, 막상 오디오 노드를 추가하고 나니 배경음악이 효과음에 비해 커서 조절하고 싶어졌다. 결국 어쩔 수 없이 좀더 찾아보게 되었다.
우선 맨 위에서 정리한 SKAudioNode
의 공식문서 내용 중에
씬의
audioEngine
프로퍼티는 볼륨과 재생을 제어할 수 있다.
는 내용이 있다. 그리고 그에 해당하는 코드를 사용해보았다.
scene?.audioEngine.mainMixerNode.outputVolume = 0.7
이 코드로 씬이 포함하고 있는 오디오 전체의 볼륨
을 조절할 수 있었다. (+ 참고로, AVAudioEngine
타입인 오디오 엔진은 SKScene
의 프로퍼티라고 해도 AVFAudio
프레임워크를 import
해야만 접근할 수 있다.)
다만 노드 개별적으로 조절하고 싶다면 다른 방법을 써야 한다. AVAudioPlayerNode
를 활용하는 방법이다. (이게 유일한 방법은 아니다. 제일 접근이 쉽고 편한 방법 하나만 소개하는 것이다)
SKAudioNode
에는 avAudioNode
프로퍼티가 있는데, 얘는 AVAudioNode
타입이다. 그런데 AVAudioNode
클래스의 속성으로 음원을 제어할 수 있는 건 아니고, AVAudioPlayerNode
로 타입 캐스팅 해줘야 한다. (왜 이런지는 모르겠다. 애초에 그냥 AVAudioPlayerNode
타입의 프로퍼티를 SKAudioNode
에 넣어줄 수는 없었을까? 깊게 공부하다보면 분명히 합리적인 이유가 있겠지만 아직 거기까지 파볼 건 아니다) 그렇게 생성된 플레이어 노드
의 프로퍼티인 volume
을 통해 노드의 볼륨을 제어할 수 있다.
if let playerNode = backgroundMusic.avAudioNode as? AVAudioPlayerNode {
playerNode.volume = 0.7
}
배경음악 노드의 플레이어 노드를 통해 볼륨을 70%로 줄여주었다. 그러고나니 효과음과 균형이 잘 맞았다.
그리고 한가지 더 실험을 해봤는데,
scene.audioEngine
으로 조절한 볼륨이playerNode
로 조절한 볼륨보다 우선순위가 높다. 오디오 엔진이라는 이름만 들어도 더 상위에서 제어할 거 같은 느낌이 들어 자연스럽게 추론이 되는 부분이긴 하다.
나는 코드에 대한 조언을 ChatGPT
보다는 Claude
를 통해 구하는 편인데, 얘네가 잘못 알려줄 때마다 어이가 없다.
SKAudioNode.volume
프로퍼티에 접근하라는 조언을 해줘서 어이가 없었다.AVAudioPlayerNode
에 volume
프로퍼티가 없단다. (있음. 내 어이는 여전히 없음)로드야 너가 날 가르쳐줘야지 내가 알려주는 게 맞냐.. 이래서 한번의 질문과 답으로 AI가 주는 정보를 그대로 받아들이면 안된다. 교차검증 필수.
다만 이렇게 옥신각신 하는 과정에서도 유익한 정보를 주워먹기도 한다.
노드마다 볼륨을 다르게 설정할 수 있냐는 첫번째 질문에 대한 여러가지 제안 중 하나로 나온 부분인데, SKAction
의 changeVolume
메소드를 통해 볼륨을 서서히 줄이거나 높이는 효과를 넣을 수 있다는 것을 알았다. 저 코드는 실험 결과 엉터리가 아니라 아주 잘 작동한다. 나중에 음원에 다양한 효과를 넣고 싶을 때 떠올려서 유용하게 쓸 수 있을 거 같다.
여기까지 오디오 효과를 넣으면서 배운 내용들을 정리해봤다. 기기로 시뮬레이팅한 화면 녹화 영상에서 소리를 들을 수 있다.