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

Posted

技术标签:

【中文标题】如何防止 SKAction 序列在解码后重新启动?【英文标题】:How can I prevent SKAction sequence restarting after decoding? 【发布时间】:2016-03-29 19:50:47 【问题描述】:

我的应用是一款 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 应该显示与其他操作一致的解码行为是有道理的。但是,创建一个例外是有用的,这样任何已经完成的操作都不会再次运行。如何防止序列在解码后重新启动?

【问题讨论】:

除了在更多的模型视图控制器设计模式中处理这个之外,我不知道你是否能够实现你想要的。我怀疑 SKAction 实际上并不知道它有多远,因为场景决定了这一点。正如我所说的猜测。因此,保存时不会保存进度。如果您不使用 SKActions 而是在模型中保存/更新这些状态,您可以控制保存进度,但您必须在每个更新循环中更新精灵的状态。 你不能按照你的想法去做,但是你可以记录每个 SKAction 开始的时间,捕捉暂停时间,用它来确定你的动作还剩下多少时间,并且通过提高速度进行快进 谢谢@SkylerLauren 和@KnightOfDragon!根据进一步的实验,我编辑了问题以确认 所有 SKActions(不仅仅是序列)在解码时重新开始;显然,这是标准的。那么问题有两种解决方法:1)我们可以在解码时恢复部分完成的SKAction吗?或 2) 作为序列的特殊情况,我们能否让序列不重新运行任何已经完成的动作?您的 cmets 都可以处理 (1),这是一个更难的问题(可能太难了?),但我已将问题编辑为 (2)。我还有一些代码要展示。 【参考方案1】:

我能想到的唯一方法是完成您想要实现的目标。

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

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

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

编辑

要提供有关“理论上”如何解决此问题的更多信息,您是对的,这很麻烦。

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

在这个例子中,每个 SKSpriteNode 都保持并跟踪它自己的 SKAction。抱歉,我没有时间用 Objective-C 编写代码。我也绝不是声称或试图暗示这比您的答案更好或更差,而是解决如果我决心按照您的问题要求保存 SKActions 的状态,我将如何处理这个问题。 =)

【讨论】:

你说得对,我正在扩展 SKAction 的可能性。但这是我的理由:如果您在应用中构建自己的动画状态表示,那么您就是在重新实现 SKAction:SKAction 已经有效地表示了动画的状态。一旦你获得了代表动画状态的 SKAction,将其持久化是合理的:如果一个兽人在你为应用程序设置背景时处于死亡动画的中间,那么当你使用它时它应该处于死亡动画的中间恢复它。并且 SKAction 已经是可编码的。主要是:) 对您的具体建议的两个回应(也:谢谢!)。 1. 我正在尝试将问题重新集中在 SKAction sequence 上。在这种情况下,我们不需要每个 SKAction 中经过的时间;对于序列,我们只需要不重新运行已经运行的操作。 2. 我可以想象按照你所说的做,但是很难在解码时重新创建每个运行的 SKAction,并为每个跟踪(和编码!)一个单独的时间变量。如果我们可以将 SKAction 子类化来做到这一点,那将是可以的,但我们不能。 (可能这就是您建议不要使用 SKAction 的原因!) 好吧,我 100% 同意 SKAction 无论如何都应该提供有关其状态的更多信息。过去,我在自己的游戏中一直与 SKActions 作斗争,但在我想保存游戏时知道它们在哪里或当场景发生变化时做出反应时,我遇到了一些限制。我倾向于采取更多的方法,但是当有如此好的顺序和分组的动作时,这似乎是一种浪费。我喜欢 MVC 的想法,因为在任何给定时刻您都可以保存模型数据,但您必须管理从位置到动画帧的所有内容。 好的,是的,对你所说的关于 MVC 的看法大加赞赏:它不是“重新实现 SKAction”,而是将视图与模型分开。我承认,我在这里正在混合视图和模型,非常可怕。 (也就是说,如果我不接受你的回答,那是因为它的第一部分太难吃了——即使最后一段最终是最佳实践。)【参考方案2】:

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

代码

制作一个轻量级、可编码的对象,它可以管理序列,将其分解为子序列并记住(在编码时)已经运行的内容。我写了一个实现in a library on GitHub。这是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];

概念

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

[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,但会等待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 次? 好点,这个问题也适用于常规 SKAction,即使不是按顺序,因为moveBy 将在解码后重新启动。我想这意味着我们应该始终使用moveTo? 这至少可以保证它会在特定位置结束,即使它在解码时看起来很有趣(比正常速度更快或更慢)。我也很乐意进一步讨论这个问题,但对 SO 有点不满意。给我发一封电子邮件至 skyler@skymistdevelopment.com,我明天可以向您发送 SKA slack 的邀请,您可以与小组进行故障排除/辩论,或者只是与我来回发送电子邮件。不过我要睡觉了。祝你好运。

以上是关于如何防止 SKAction 序列在解码后重新启动?的主要内容,如果未能解决你的问题,请参考以下文章

在 SKAction 完成后重新定位精灵节点

即使在我重新加载页面后,如何防止已经在 J​​avascript 中启动的“onclick”功能?

如何向 SKAction 序列添加返回 void 的函数?

在 runBlock 发生后延迟 SKAction.sequence 中的下一个动作(Swift)?

如何防止系统在 Win32 服务中关闭或重新启动?

有没有办法保存 USB 设备,以便在重新插入后可以防止重新枚举?