昨日はゲームでBGMやSEを流す方法について説明した。
ただ、最後に
実際には単に上のようにすると、シーンの遷移のときにちょっとした問題も出てくるんだけど・・・
と書いた通り、実はちょっとした問題が。
今日はそれについて。
問題の具体的な内容
さて、具体的にどんな問題が出てくるのか。
実は、昨日のようなコードを遷移前のシーンと遷移後のシーンの両方に書いて実行すると、
- シーンを遷移しようとする
- 再生していたBGMが先頭に巻き戻される
- 再生していたBGMが止まる
- シーンが遷移後のシーンになる
といった、不思議な挙動を示すことになる。
なんでこんなことが起きるのかというと、SKSceneのメソッドの呼ばれるタイミングに問題があるから。
SKSceneのメソッドの呼ばれるタイミング
SKSceneには、画面遷移のときに呼ばれるためのフックとして、次の2つのメソッドが用意されている:
- SKScene#didMoveToView(_: SKView)
- SKScnee#willMoveFromView(_: SKView)
前者は、シーンがビューに追加されたときに、後者はシーンがビューから取り除かれるときに呼ばれるんだけど、呼ばれるタイミングが、
- シーンの遷移が始まる
- 遷移先のシーンがビューに追加され、didMoveToView()が呼ばれる
- シーンの遷移が行われる(アニメーションなど)
- 遷移元のシーンがビューから取り除かれる前に、willMoveFromView()が呼ばれる
- シーンの遷移が終わる
となっているから。
このせいで、
- シーンの遷移が始まる
- 遷移先のシーンがビューに追加され、didMoveToView()が呼ばれる
- BGMがすでに流れているけど、再び先頭から流される
- シーンの遷移が行われる
- 遷移元のシーンがビューから取り除かれる前に、willMoveFromView()が呼ばれる
- BGMが止められる
- シーンの遷移が終わる
となって、最初に述べたような挙動になるみたい。
willMoveFromView()が先に呼ばれて、それから遷移が行われて、最後にdidMoveToView()が呼ばれれば、何も問題はないのだけど・・・
TransitionableScene
ところで、今回はBGMに関する問題だったけど、シーンの遷移が終わってから何かを行いたい、ということは普通にあること。
なので、シーンの遷移が終わった後に何かが行えるクラスとして、SKSceneを継承したTransitionableSceneというのを作ってみた。
(Transitionableなんて英単語ないけどw)
//============================== // SoloXmas //------------------------------ // TransitionableScene.swift //============================== import SpriteKit class TransitionableScene: SKScene { var willTransition: Bool var afterTransitionBlock: (() -> Void)? weak var destinationScene: TransitionableScene? override init(size: CGSize) { self.willTransition = false super.init(size: size) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func willMoveFromView(view: SKView) { self.destinationScene?.afterTransitionBlock?() self.destinationScene?.willTransition = false self.destinationScene = nil } }
遷移元、遷移先のシーンとも、このクラスを継承するようにして、
- 遷移先のシーンでは、didMoveToView()の中で、遷移が終わった後に行いたい処理をafterTransitionBlockプロパティに登録する
- 遷移元のシーンでは、wilMoveFromView()の最後で、スーパークラス(つまりTransitionableScene)のwillMoveFromView()を呼び出す
としておいて、遷移を行うときには、
- 遷移先のシーンのwillTransitionプロパティをtrueにする
- 遷移元のシーンのdestinationSceneとして遷移先のシーンを登録する
こうすることで、遷移が終わる直前、遷移元のシーンがビューから取り除かれるタイミングで、afterTransitionBlockプロパティに登録されていた処理が(あれば)実行されるようになる。
今回のBGMの場合、次のような感じ:
// GameScene.swift // SKSceneでなく、TransitionableSceneを継承する class GameScene: TransitionableScene { // 省略 override func didMoveToView(view: SKView) { // 省略 // ビューに追加されたら、設定に応じてBGMを再生するブロックを登録する // 遷移が行われる場合、遷移が終わってから再生されるようにする let soundManager = SoundManager.getInstance() if soundManager.config == .On { if self.willTransition { self.afterTransitionBlock = { soundManager.playSoundForKey("BGM", loop: true) } } else { soundManager.playSoundForKey("BGM", loop: true) } } // 省略 } override func willMoveFromView(view: SKView) { // 省略 // ビューから取り除かれるとき、 // 音を停止させて先頭に巻き戻す let soundManager = SoundManager.getInstance() soundManager.stopAllSounds() soundManager.rewindAllSounds() // 省略 // 最後にスーパークラス(TransitionableScene)のメソッドを呼び出す super.willMoveFromView(view) } // 省略 private func exitGame() { let startScene = StartScene(size: self.size) // 遷移先のシーンもTransitionableSceneを継承させておき、 // プロパティを適切に設定してから遷移を行う startScene.willTransition = true self.destinationScene = startScene let transition = SKTransition.fadeWithDuration(NSTimeInterval(1.0)) self.view?.presentScene(startScene, transition: transition) } // 省略 }
これで画面遷移のときのBGMの再生が期待通りに動くようになった。
今日はここまで!