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

3
我的应用是一个带有应用状态保存和恢复功能的SpriteKit游戏。当应用程序状态被保存时,当前SKScene中的大多数节点都会被编码。
当运行SKAction的节点被编码和解码时,该动作将从头开始重新启动。这似乎是标准的SpriteKit行为。
对我来说,这种行为在SKAction序列中最为明显。在解码时,序列将重新启动,无论其组成部分的动作已经完成了多少次。例如,假设运行序列的代码如下:
[self runAction:[SKAction sequence:@[ [SKAction fadeOutWithDuration:1.0],
                                      [SKAction fadeInWithDuration:1.0],
                                      [SKAction waitForDuration:10.0],
                                      [SKAction removeFromParent] ]]];

如果应用程序状态在10秒等待期间被保留并恢复,则SKAction序列将从头开始重新启动,出现第二个可见的淡出和淡入。 SKAction sequence应该表现出与其他动作一致的解码行为是有意义的。 但是,为了防止已经完成的任何操作再次运行,可以进行例外处理。 如何防止解码后序列重新启动?

除了更多地采用模型视图控制器设计模式,我不知道你是否能够实现你想要的。我猜测SKAction实际上并不知道它到底有多远,因为场景决定了这一点。就像我说的,这只是一个猜测。所以当保存时,它不会保存进度。如果你不使用SKActions而是在模型中保存/更新这些状态,你可以控制保存进度,但你必须在每个更新循环中更新你的精灵状态。 - Skyler Lauren
你不能按照你现在的想法去做,但是你可以记录每个SKAction开始的时间,捕获暂停时间,用它来确定你的动作还剩下多少时间,并通过增加速度来进行快进。 - Knight0fDragon
谢谢@SkylerLauren和@KnightOfDragon! 基于进一步的实验��我编辑了问题以承认所有SKActions(不仅是序列)在解码时重新启动; 显然这是标准的。 那么问题就有两种方式:1)我们能否在解码时恢复部分完成的SKAction?还是2)作为序列的特殊情况,我们是否可以使序列不重新运行已经完成的任何操作? 你们两个都可以处理(1),这是一个更难的问题(也许太难?),但我已经将问题编辑为(2)。 我有一些代码可以展示。 - Karl Voskuil
2个回答

2
我能想到的实现你想要达成目标的唯一方法如下:
1.开始操作时将时间存储在变量中。请记住,您将希望使用update函数传递的"currentTime"值。
2.当需要进行编码时,计算从创建操作到编码所经过的时间。
从这里开始,您有两个选择:保存剩余时间,并在重新创建操作时将其用于计算,或者根据剩余时间创建新的操作并对其进行编码。
我认为SKActions并不是真正旨在以这种方式使用的,但这可能至少是一个解决方法。我认为更常见的做法是将游戏的“状态”存储为持久性,而不是尝试存储实际的精灵和动作。这也适用于UIKit内容。您不会为持久性存储UIView,而是会有其他某个对象,该对象将包含信息以根据用户进度重新创建视图。希望其中一些内容至少有点有用。祝你好运。
编辑
为了提供更多关于我将如何“理论上”处理此问题的信息,你是正确的,这很麻烦。
1.子类化SKSpriteNode 2.创建一个可以在Sprite上运行动作的新方法(例如-(void)startAction:withKey:duration:),最终会调用run action with key。
3.当调用startAction时,您将其存储到某种可变数组中,该数组带有一个存储该操作、其键、持续时间和开始时间的字典(默认为0)。您甚至可能不必实际存储该操作,只需存储键、持续时间和开始时间即可。
4.在SKSpriteNode子类上添加update:方法。每个更新循环都会调用其更新并检查是否存在1.没有开始时间的操作和2.这些操作是否仍在运行。如果没有开始时间,则将当前时间作为开始时间添加,如果未运行,则从数组中删除。
5.当您要对该精灵进行编码/保存时,使用该数组中的信息确定这些SKAction的状态。
这里的重点是,每个SKSpriteNode在此示例中都保留并跟踪其自己的SKAction。抱歉我没有时间逐一写出Objective-C代码。我也不是在声称或试图暗示这比你的答案更好或更差,而是解决了你的问题,如果我决定保存SKActions的状态,我将如何处理它们。 =)

你说得对,我确实在拓展SKAction的可能性。但是这是我的理由:如果你正在构建自己的应用程序中的动画状态表示,那么你正在重新实现SKAction:SKAction已经有效地表示了动画的状态。一旦你有了SKAction来表示动画的状态,将其持久化是合理的:如果一个兽人在你将应用程序切换到后台时处于死亡动画的中间阶段,那么当你恢复它时,它应该仍然处于死亡动画的中间阶段。而SKAction已经可以编码。大部分情况下 :) - Karl Voskuil
针对你的具体建议,有两个回应(同时:谢谢!):
  1. 我试图将问题重新聚焦在 SKAction sequence 上。在这种情况下,我们不需要每个 SKAction 中经过的时间;我们只需要为序列指定不会重新运行已经运行过的操作。
  2. 我可以想象你所说的做法,但是重新创建每个正在运行的 SKAction 并对每个跟踪(和编码!)一个单独的时间变量将会很困难。如果我们能够对 SKAction 进行子类化来完成它,那就没事了,但我们不能这样做。(也许这就是你建议不使用 SKAction 的原因!)
- Karl Voskuil
1
我完全同意SKAction应该提供更多关于其状态的信息。过去在我的游戏中,由于无法知道它们的位置或在场景中发生变化时如何反应,我曾经与SKActions挣扎过。我倾向于采取更加亲手的方法,但当有如此好的动作序列和分组时,这似乎是一种浪费。不过,我喜欢MVC的想法,因为在任何给定的时刻,您都可以保存模型数据,但您必须管理从位置到动画帧的所有内容。 - Skyler Lauren
好的,是的,对于你关于MVC的说法我表示赞同:它不是“重新实现SKAction”,而是将视图与模型分离。我承认,在这里我混淆了视图和模型。尽管如此,如果我不接受你的答案,那是因为它的第一部分太难以接受了——即使最后一段是最佳实践。 - Karl Voskuil

1

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

代码

创建一个轻量级的、可编码的对象,可以管理该序列,将其分解为子序列,并记住(在编码时)已经运行的内容。我已经在GitHub上的库中编写了一个实现 这里是代码要点的当前状态。

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

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];

概念

一个初步的想法:将序列分成几个独立的子序列。每个子序列完成后,它将不再运行,因此如果应用程序被保留,则不会被编码。例如,一个原始序列可能是这样的:

[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] ]]];

could be split like this:

[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,但它将等待11秒后才运行doZ
为避免这种可能的奇怪延迟,将序列分成一系列依赖子序列,每个子序列触发下一个子序列。对于这个例子,拆分可能如下所示:
- (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秒。当然,它是整整一秒钟(即使在编码之前已经过了一半),但结果相当容易理解:在编码时正在进行的任何序列操作将重新启动,但一旦完成,它就完成了。
当然,创建大量像这样的方法很糟糕。相反,创建一个序列管理器对象,当触发时,将序列分成子序列,并以有状态的方式运行它们。

如果其中一个动作是随时间而进行的,比如移动或者动画,会怎样呢?如果一只兽人在一秒内向右移动20个单位,那么如果在0.5秒时解码,它会总共移动30个单位还是只有10个单位呢?这会不会让你感到困惑? - Skyler Lauren
好观点,这个问题也适用于常规的SKAction,即使不在序列中,因为移动操作(moveBy)会在解码后重新启动。我猜这意味着我们应该始终使用moveTo - Karl Voskuil
这至少可以保证它在特定位置结束,即使解码时看起来有些奇怪(比正常速度快或慢)。此外,我非常乐意进一步讨论,但在SO上这种做法有点不受欢迎。给我发电子邮件skyler@skymistdevelopment.com,明天我可以给您发送SKA Slack的邀请,您可以与团队进行故障排除/辩论,或者直接通过电子邮件来往。我今晚要睡觉了,祝你好运。 - Skyler Lauren

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接