0

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

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

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

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

在我当前的项目中,我一直在运行 iOS 12.4 的物理 iPhone 上进行测试,有时还使用运行 iOS 13 的 iPhone 的模拟器。通过 usingprint语句,我观察到AppDelegate'applicationDidBecomeActive(_:)方法和SKScene'update(_:)方法在不同的顺序,具体取决于使用的 iOS 版本。

请注意,我的项目使用UIViewController'viewDidLoad()方法来呈现场景。我尝试viewWillLayoutSubviews()改用,希望事情可以以这种方式更可靠地工作。但事实证明这更不可靠,所以我不会在这里讨论。

方法调用顺序(iOS 12.4):

didFinishLaunchingWithOptions
viewDidLoad
didMove
update
applicationDidBecomeActive
update
...

方法调用顺序(iOS 13):

didFinishLaunchingWithOptions
viewDidLoad
didMove
?
applicationDidBecomeActive
update
...

您可以看到两个版本的操作系统都先调用了AppDelegate方法application(_:didFinishLaunchingWithOptions:),然后才加载视图。在viewDidLoad(),我打电话让视图呈现我的SKScene。正如预期的那样,didMove(to:)在视图呈现场景之后调用场景的方法。但接下来发生的是奇怪的部分。

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

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

所以基本上,这里的问题是,在 iOS 12.4 中,游戏循环似乎在它出现后立即运行一次,beforeapplicationDidBecomeActive(_:)被调用。但在 iOS 13 中,这不会发生。

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

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

为了给这个分析增加一个皱纹......

我还制作了一个新的 SpriteKit 模板项目并执行了相同的测试。我发现了同样的差异,但有一个额外的特点:在 iOS 12.4 中,该update(_:)方法在被调用之后立即被调用两次didMove(to:),在applicationDidBecomeActive(_:)被调用之前。在 iOS 13 中,行为与上述相同。

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

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

4

2 回答 2

1

这不是直接相关的,但可能会给你一个前进的方向......

当游戏进入后台并回到前台时,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 对你开枪。

于 2021-12-21T00:00:27.053 回答
0

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

我的解决方法是:

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

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

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

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

    guard applicationDidBecomeActiveRan else { return }
    

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

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

于 2022-01-16T08:09:08.060 回答