如何防止 SKAction 序列在解码后重新启动?

How can I prevent SKAction sequence restarting after decoding?

我的应用程序是一款具有应用程序状态保存和恢复功能的 SpriteKit 游戏。当保留应用程序状态时,我当前 SKScene 中的大部分节点都已编码。

当一个节点运行宁一个SKAction被编码和解码时,动作将从头开始。这似乎是标准的 SpriteKit 行为。

对我来说,这种行为在 SKAction sequence 中最为明显。在解码时,序列重新开始,不管它的组件动作有多少已经完成。例如,假设 运行 序列的代码如下所示:

[self runAction:[SKAction sequence:@[ [SKAction fadeOutWithDuration:1.0],
                                      [SKAction fadeInWithDuration:1.0],
                                      [SKAction waitForDuration:10.0],
                                      [SKAction removeFromParent] ]]];

如果在 10 秒等待期间保留应用程序状态,然后恢复,SKAction 序列将从头开始,第二次可见淡出和淡入。

SKAction sequence 应该显示与其他操作一致的解码行为是有道理的。但是,创建一个例外是很有用的,这样任何已经完成的操作都不会再 运行 了。如何防止序列在解码后重新启动?

我能想到的实现您想要实现的目标的唯一方法如下。

  1. 当您启动动作时,将时间存储在一个变量中。请记住,您需要使用更新函数中传递的 "currentTime" 值。
  2. 当您需要编码时,计算从创建动作到编码经过了多少时间。

从那里您有两个选择,保存剩余时间以及何时重新创建动作,将其用于您的计算或根据剩余时间创建新动作并对其进行编码。

我不认为 SKActions 真的打算以这种方式使用,但这至少是一种解决方法。我认为开发人员更常见的做法是存储他们游戏的 "state" 以实现持久性,而不是尝试存储实际的精灵和动作。 UIKit 的东西也是一样的。您不会存储 UIViews 以实现持久性,而是会有一些其他对象包含根据用户进度重新创建的信息。希望其中一些至少有一点帮助。祝你好运。

编辑

要提供有关如何 "in theory" 我将如何处理的更多信息,你是对的,这很麻烦。

  1. 子类 SKSpriteNode
  2. 为该 Sprite 上的 运行 动作创建一个新方法(如 -(void)startAction:withKey:duration:),最终将调用 运行 带有键的动作。
  3. 当调用 startAction 时,您将其存储到某种 MutableArray 中,其中包含一个 Dictionary,该字典存储该操作、它的键、持续时间和 startTime(默认为 0)。您甚至可能不必实际存储该操作,只需存储密钥、持续时间和开始时间。
  4. 在此 SKSpriteNode 子类上添加更新:方法。您调用其更新的每个更新循环并检查 1 是否有任何操作没有开始时间,以及 2 是否这些操作仍在 运行ning。如果没有开始时间,则将当前时间添加为开始时间,如果不是 运行ning,则将其从数组中删除。
  5. 当您转到 encode/save 那个精灵时,您使用该数组中的信息来确定那些 SKAction 的状态。

在此示例中,重点在于每个 SKSpriteNode 都保持并跟踪其自己的 SKAction。抱歉,我没有时间浏览并编写 Objective-C 中的代码。同样,我绝不是在声称或试图暗示这比您的回答更好或更差,而是要解决如果我决定按照您的问题保存 SKActions 的状态,我将如何处理这个问题。 =)

SKAction 序列可以分解为多个子序列,这样,一旦特定的子序列完成,它将不再是 运行ning,因此不会重新启动解码。

代码

制作一个轻量级、可编码的对象,它可以管理序列,将其分解成子序列并记住(在编码时)已经 运行 的内容。我写了一个实现 in a library on GitHub. Here's the current state of the code in a gist.

这是一个示例(使用与下面相同的序列):

HLSequence *xyzSequence = [[HLSequence alloc] initWithNode:self actions:@[
                                      [SKAction waitForDuration:10.0],
                                      [SKAction performSelector:@selector(doY) onTarget:self],
                                      [SKAction waitForDuration:1.0],
                                      [SKAction performSelector:@selector(doZ) onTarget:self] ]];
[self runAction:xyzSequence.action];

概念

第一个想法:将序列拆分成几个独立的子序列。随着每个子序列的完成,它将不再是 运行ning,因此如果保留应用程序将不会被编码。例如,像这样的原始序列:

[self runAction:[SKAction sequence:@[ [SKAction performSelector:@selector(doX) onTarget:self],
                                      [SKAction waitForDuration:10.0],
                                      [SKAction performSelector:@selector(doY) onTarget:self],
                                      [SKAction waitForDuration:1.0],
                                      [SKAction performSelector:@selector(doZ) onTarget:self] ]]];

可以这样拆分:

[self runAction:[SKAction sequence:@[ [SKAction performSelector:@selector(doX) onTarget:self] ]]];

[self runAction:[SKAction sequence:@[ [SKAction waitForDuration:10.0],
                                      [SKAction performSelector:@selector(doY) onTarget:self] ]]];

[self runAction:[SKAction sequence:@[ [SKAction waitForDuration:11.0],
                                      [SKAction performSelector:@selector(doZ) onTarget:self] ]]];

无论节点何时编码,方法doXdoYdoZ只会运行一次

不过,根据动画的不同,等待的持续时间可能看起来很奇怪。例如,假设应用程序在 doXdoY 具有 运行 之后,在 doZ 之前的 1 秒延迟期间被保留。然后,在恢复时,应用程序不会再次 运行 doXdoY,但它会在 运行ning doZ.[=28 之前等待 11 秒=]

为了避免可能奇怪的延迟,将序列分成一系列相关的子序列,每个子序列都会触发下一个。例如,拆分可能如下所示:

- (void)doX
{
  // do X...
  [self runAction:[SKAction sequence:@[ [SKAction waitForDuration:10.0],
                                        [SKAction performSelector:@selector(doY) onTarget:self] ]]];
}

- (void)doY
{
  // do Y...
  [self runAction:[SKAction sequence:@[ [SKAction waitForDuration:1.0],
                                        [SKAction performSelector:@selector(doZ) onTarget:self] ]]];
}

- (void)doZ
{
  // do Z...
}

- (void)runAnimationSequence
{
  [self runAction:[SKAction performSelector:@selector(doX) onTarget:self]];
}

在这个实现中,如果在doXdoY有运行之后保留序列,那么在恢复时,doZ之前的延迟将只有1第二。当然,它是整整一秒(即使它在编码之前已经过去了一半),但结果是相当可以理解的:编码时序列中正在进行的任何操作都将重新开始,但一旦完成,它就完成了。

当然,弄一堆这样的方法是很讨厌的。相反,制作一个序列管理器对象,当被触发时,它会将序列分解成子序列,并 运行 以有状态的方式对它们进行分类。