My app is a SpriteKit game with application state preservation and restoration. When application state is preserved, most of the nodes in my current SKScene
are encoded.
When a node running an SKAction
is encoded and decoded, the action will restart from the beginning. This appears to be standard SpriteKit
behavior.
For me, this behavior is most noticeable for SKAction sequence
. On decoding, the sequence restarts, no matter how many of its component actions have already completed. For instance, say the code to run the sequence looks like this:
[self runAction:[SKAction sequence:@[ [SKAction fadeOutWithDuration:1.0],
[SKAction fadeInWithDuration:1.0],
[SKAction waitForDuration:10.0],
[SKAction removeFromParent] ]]];
If application state is preserved during the 10-second wait, and then restored, the SKAction
sequence will start again from the beginning, with a second visible fade-out-and-in.
It makes sense that SKAction sequence
should show decoding behavior consistent with other actions. It would be useful, though, to make an exception, so that any actions already completed are not run again. How can I prevent a sequence restarting after decoding?
The SKAction
sequence can be decomposed into a number of subsequences such that, once a particular subsequence has finished, it will be no longer running, and so won't be restarted on decode.
Make a lightweight, encodable object which can manage the sequence, breaking it into subsequences and remembering (on encode) what has already run. I've written an implementation in a library on GitHub. Here's the current state of the code in a gist.
And here's an example (using the same sequence as below):
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];
A first idea: Split the sequence into a few independent subsequences. As each subsequence completes, it will no longer be running, and so will not be encoded if the application is preserved. For instance, an original sequence like this:
[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] ]]];
No matter when the node is encoded, the methods doX
, doY
, and doZ
will only be run once.
Depending on the animation, though, the duration of the waits might seem weird. For example, say the application is preserved after doX
and doY
have run, during the 1-second delay before doZ
. Then, upon restoration, the application won't run doX
or doY
again, but it will wait 11 seconds before running doZ
.
To avoid the perhaps-strange delays, split the sequence into a chain of dependent subsequences, each of which triggers the next one. For the example, the split might look like this:
- (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]];
}
With this implementation, if the sequence is preserved after doX
and doY
have run, then, upon restoration, the delay before doZ
will be only 1 second. Sure, it's a full second (even if it was half elapsed before encoding), but the result is fairly understandable: Whatever action in the sequence was in progress at the time of encoding will restart, but once it completes, it is done.
Of course, making a bunch of methods like this is nasty. Instead, make a sequence-manager object, which, when triggered to do so, breaks the sequence into subsequences, and runs them in a stateful way.