You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 22:17:08