SKScene暂停恢复时SKAction与SKAudioNode不同步问题求助
概述
在我的SKScene中,我使用SKAudioNode播放音频文件,声明如下:
var backgroundMusic: SKAudioNode?
初始化代码如下:
guard let musicURL = Bundle.main.url(forResource: "MY_AUDIO_FILENAME", withExtension: "mp3") else { fatalError() } backgroundMusic = SKAudioNode(url: musicURL) backgroundMusic?.isPositional = false
为便于理解,假设播放的音频是一段从1数到1000的人声:
"one, break, two, break, three, break, -- [cut]"
我的目标是在音频播放到特定位置(比如人声数到45时)执行代码逻辑。为实现此需求,我使用SKAction设置了计时器:
let wait = SKAction.wait(forDuration: 48) let playAlarmSound = SKAction.run { // 执行逻辑 } let timer = SKAction.sequence([wait, playAlarmSound])
我将计时器和音频节点同时添加到场景中,理论上音频到达指定位置时计时器会触发:
run(timer, withKey: "Timer") addChild(backgroundMusic!)
问题
- 每次
SKScene暂停后恢复,SKAudioNode会跳过一小段音频,例如示例中会跳过一个数字。 - 每次暂停恢复后,计时器的触发时间会逐渐延迟。在下方示例项目中,若暂停恢复约10次,计时器会在数到50左右才触发,而非预期的45。
已尝试的解决方案
- 每次
SKScene暂停时单独暂停SKAudioNode,执行backgroundMusic?.run(SKAction.pause())。此方法解决了问题1,大幅减少了问题2的延迟,但仍非最优解,计时器与音频仍不同步,会比预期提前约1秒触发。 - 使用
Timer。此方案不可行,原因有二:1)它会忽略节点、场景或视图的paused状态;2)实际测试会给游戏引入延迟,SpriteKit开发中不建议混用此类方案。
我不清楚延迟产生的原因,希望有人能解惑。此外,我需要一个稳定的最优解决方案,能在音频播放到特定时间点(非结束时)精准执行代码逻辑。
示例项目
这是一个极简示例项目,可复现上述问题,无需.sks文件,直接复制代码即可运行。
GameScene加载后会自动启动音频和计时器,点击红色屏幕可暂停/恢复场景,复现问题。使用指定的数字计数音频文件时,计时器应在数到45时触发。
每次暂停/恢复场景及计时器触发时,控制台都会打印信息。
注:我使用了另一个问题的解决方案来确保应用进入后台时paused状态一致。
final class GameViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let skView = SKView(frame: view.frame) view = skView skView.ignoresSiblingOrder = true let gameScene = GameScene() skView.presentScene(gameScene) } } import SpriteKit final class GameScene: SKScene { var backgroundMusic: SKAudioNode? var isGamePaused: Bool = false { didSet { self.isPaused = isGamePaused // 取消注释以下代码可测试我的非最优解决方案,多次暂停恢复后计时器仍与音频不同步,会提前约1秒触发 //if isGamePaused { // self.backgroundMusic?.run(SKAction.pause()) //} else { // self.backgroundMusic?.run(SKAction.play()) //} } } override var isPaused: Bool { didSet { if (self.isPaused == false && self.isGamePaused == true) { self.isPaused = true // 取消注释以下代码可测试我的非最优解决方案,多次暂停恢复后计时器仍与音频不同步,会提前约1秒触发 //self.backgroundMusic?.run(SKAction.pause()) } } } override func didMove(to view: SKView) { super.didMove(to: view) view.isMultipleTouchEnabled = false backgroundColor = .red startAudioAndTimer() } func startAudioAndTimer() { // 问题:暂停恢复后,尤其是多次操作后,计时器与音频不同步,触发时间大幅延迟 guard let musicURL = Bundle.main.url(forResource: "Test_Song", withExtension: "mp3") else { fatalError("Error: add a mp3 audio file to the project") } backgroundMusic = SKAudioNode(url: musicURL) backgroundMusic?.isPositional = false let wait = SKAction.wait(forDuration: 48) // 使用此时长,搭配指定音频,警报应在数到45结束时触发 let playAlarmSound = SKAction.run { print("\n--- The alarm rings ---") } let timer = SKAction.sequence([wait, playAlarmSound]) run(timer, withKey: "Timer") addChild(backgroundMusic!) } override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { isGamePaused = !isGamePaused let statusString = isGamePaused ? "paused" : "resumed" print("\nScene \(statusString)\n") } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { } override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) { } }
内容的提问来源于stack exchange,提问作者oneshot
相关产品推荐
相关产品推荐

