SpriteKit 游戏什么时候第一次循环 运行?

When does the SpriteKit game loop run for the first time?

我试图了解 SpriteKit 场景的 frame cycle 运行 何时在主 iOS 运行 循环中。具体来说,我担心 AppDelegate 的 applicationDidBecomeActive(_:) 方法。我一直认为该方法是在应用程序激活之后调用的,但在您呈现的场景的帧周期 运行s.

之前

这对我正在构建的项目很重要,因为我使用 applicationDidBecomeActive(_:) 方法来执行一些 time-sensitive 任务,例如检查时间戳、设置标志、启动计时器等。所以我需要可靠地预测在帧周期中何时调用此方法(我们将其称为“游戏循环”)。

我做了一些测试,表明游戏循环 运行s 在与 applicationDidBecomeActive(_:) 方法相关的不同时间,具体取决于 iOS 应用程序的版本 运行继续。这是一个问题,因为这意味着我不能依赖此方法的单一实现来在正确的时间执行我需要的任务。

我想确切地知道什么时候调用 applicationDidBecomeActive(_:) 与 SpriteKit 游戏循环有关。这似乎是任何编写 SpriteKit 游戏的人都需要了解的基本知识。我很震惊地看到它似乎因 OS 版本而异。有可能我在测试和假设中犯了错误。但我会报告我在这里发现的内容,看看是否有其他人注意到这一点,以及是否有人可以解释这种奇怪的行为。

在我当前的项目中,我一直在测试物理 iPhone 运行ning iOS 12.4,有时使用模拟器进行 iPhone 运行ning iOS 13. 通过使用 print 语句,我观察到 AppDelegateapplicationDidBecomeActive(_:) 方法和 SKSceneupdate(_:)方法的调用顺序不同,具体取决于使用的 iOS 版本。

请注意,我的项目使用UIViewControllerviewDidLoad()方法来呈现场景。我尝试使用 viewWillLayoutSubviews() 来代替,希望这样可以更可靠地工作。但事实证明那更不可靠,所以我不会在这里讨论。

方法调用顺序(iOS 12.4):

didFinishLaunchingWithOptions
viewDidLoad
didMove
update
applicationDidBecomeActive
update
...

方法调用顺序(iOS 13):

didFinishLaunchingWithOptions
viewDidLoad
didMove
?
applicationDidBecomeActive
update
...

您可以看到 OS 的两个版本首先调用 AppDelegateapplication(_:didFinishLaunchingWithOptions:) 方法,然后加载视图。在 viewDidLoad(),我打电话让视图显示我的 SKScene。正如所料,场景的 didMove(to:) 方法在视图呈现场景后被调用。但接下来发生的是奇怪的部分。

在iOS 12.4中,调用了场景的update(_:)方法,表示场景执行了一个运行的游戏循环。 然后 AppDelegate 调用它的 applicationDidBecomeActive(_:) 方法。接下来,update(_:) 方法再次 运行s。然后 update(_:) 不断被调用,正如预期的那样,场景的游戏循环每秒触发 60 次。

在 iOS 13 中,update(_:) 方法不会在 didMove(to:) 被调用后立即被调用。相反,applicationDidBecomeActive(_:)didMove(to:) 之后立即被调用。只有这样 update(_:) 方法才会 运行(然后继续 运行ning,正如预期的那样)。

所以基本上,这里的问题是在 iOS 12.4 中,游戏循环在 applicationDidBecomeActive(_:) 被调用之前立即出现 运行 一次。但是在 iOS 13 中不会发生这种情况。

在调用 applicationDidBecomeActive(_:) 之前,游戏循环 iOS 12.4 运行 秒是一个问题。这使得 OS 不同版本的游戏生命周期不一致,这意味着我将不得不编写不同的代码来处理不同 OS 版本的情况。要么,要么我必须重新设计依赖 applicationDidBecomeActive(_:) 的应用程序部分,以找到更一致的处理方式。这也让我想知道游戏循环的额外 运行 是否是 iOS 12.

中的错误

我一直假设应用程序的生命周期在 OS 版本之间是一致的(至少关于 AppDelegateSKScene 的方法调用顺序)。但这一发现使所有这些都受到质疑。我还没有用其他版本的 iOS 进行测试,因为即使这是所有 OS 版本之间唯一的差异,它仍然意味着您的代码必须根据 [=116] 以不同的方式处理事情=] 版本.

要对此分析再添加一个皱纹...

我也新建了一个SpriteKit模板工程,进行了同样的测试。我发现了相同的差异,但增加了一个特点:在 iOS 12.4 中,update(_:) 方法被调用 两次 紧随 didMove(to:),在 applicationDidBecomeActive(_:) 被调用。在 iOS 13 中,行为与上述相同。

我不确定为什么 update(_:) 会触发两次而不是像在我的其他项目中那样触发一次。这似乎很奇怪。但是这个在“干净”模板项目中的测试表明这是一个真正的问题,而不是我自己的代码中的一些错误。

重申我的问题...
我想知道是否有其他人注意到这一点。也许我的结论是错误的。如果这是一个真正的问题,我想知道是否有y “修复”可以使所有 OS 版本的游戏循环以一致的方式工作。如果没有,任何人都可以提出一个好的解决方法,以便您在 applicationDidBecomeActive(_:) 中的代码在游戏循环首次触发之前始终如一地 运行s 吗?我已经有了一些想法。但首先,我想确认这是 iOS 的实际问题还是我自己的代码中的错误。

这不是直接相关的,但可能会为您提供前进的方向...

SpriteKit 尝试在游戏进入后台并返回前台时自动暂停和取消暂停。我有一种情况,我想要更多的控制权,尤其是不希望游戏总是在恢复时重新开始。我的 hack(在 iOS 12 和 13 上工作,虽然我不记得版本)是在我派生的游戏场景中覆盖 isPaused,例如

override var isPaused: Bool {
  get { super.isPaused }
  set {
    if forcePause && !newValue {
      os_log("holding isPaused at true because forcePause is true", log: .app, type: .debug)
    }
    super.isPaused = newValue || forcePause
  }
}

然后我会有额外的变量forcePause来控制游戏。

也许你可以做类似的事情,当你进入后台时设置 forcePause 然后让 applicationDidBecomeActive 清除它。也许这足以阻止 iOS 12 向你开枪。

我使用作为 Apple Developer Program 的一部分获得的代码级支持事件之一向他们的一位支持工程师提出了这个问题。他告诉我的是,无法保证 update(_:) 总是在 applicationDidBecomeActive(_:) 之后调用,因此您的应用不应依赖此类行为。我已经实施了一个解决方法,他告诉我我应该继续使用该方法,或者探索不依赖于活动状态的不同设计。

我的解决方法是:

  • 设置一个名为applicationDidBecomeActiveRan的全局布尔变量,初始化为false.

  • applicationDidBecomeActive(_:)的最后,设置全局变量为true

  • applicationWillResignActive(_:)开头,设置全局变量为false.

  • SKScene子类的update(_:)方法中,把这个guard语句放在方法的开头:

    guard applicationDidBecomeActiveRan else { return }
    

这样,我的 update(_:) 方法中的其余代码将不会 运行,除非 applicationDidBecomeActive(_:) 已经 运行。这确保了 applicationDidBecomeActive(_:)update(_:) 的代码执行顺序是一致的,无论游戏 运行 是哪个 iOS 版本。

听到这两个方法调用的顺序在默认情况下并没有得到保证,我感到很惊讶,因为在到目前为止我遇到的每种情况下,它的行为都是一致的(除非我只是没有注意到不一致)。不可否认,我从未见过任何 Apple 文档声明它应该保持一致。但是当你每次看到某件事以某种方式发生时,你自然会期望它总是以这种方式发生,而且确实应该以这种方式发生。了解在这种罕见的边缘情况下,它可能无法以通常的方式工作,这很有帮助。