3

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?

Karl Voskuil
  • 750
  • 6
  • 19
  • Beyond handling this in more of a Model View Controller design pattern I don't know if you are going to be able to achieve what you want. I suspect an SKAction doesn't actually know how far along it is because the scene determines that. As I said a guess. So when saved it doesn't save a progress. If you weren't using SKActions and rather saving/updating these states in a model you could have control over saving the progress, but you would have to update the state of your sprites every update loop. – Skyler Lauren Mar 29 '16 at 21:12
  • You can't do it the way you are thinking, but you could record the time each SKAction has started, capture the pause time,use that to determine how much time is left in your action, and do a fast forward by increasing the speed – Knight0fDragon Mar 29 '16 at 23:06
  • Thank you @SkylerLauren and @KnightOfDragon! Based on further experiments, I've edited the question to acknowledge that **all** SKActions (not just sequences) restart on decoding; that's standard, apparently. So then there's two ways for the question to go: 1) Can we, on decoding, resume an SKAction which has partially completed? or 2) Can we, as a special case for sequences, have sequences not re-run any actions which have already completed? Your comments both could handle (1), which is a harder problem (maybe too hard?), but I've edited the question to be (2). And I've got some code to show. – Karl Voskuil Apr 12 '16 at 00:26

2 Answers2

2

The only way I can think of accomplishing what you are looking to achieve would be the following.

  1. When you start the action store the time in a variable. Keep in mind you will want to use the "currentTime" value passed in update function.
  2. When you need to encode calculate how much time has passed from when you created the action to when you encoded.

From there you have two choices save how much time is left and when you go to recreate the action use that into your calculations or create new actions based on how much time is left and encode those.

I don't think SKActions were really intended to be used in this manner but that may be a work around at least. I think it is more common for developers to store the "state" of their game for persistence instead of trying to store the actual sprites and actions. It would be the same with UIKit stuff. You wouldn't store UIViews for persistence you instead would have some other object that would contain the info to recreate based on user progress. Hopefully some of that was at least a little helpful. Best of luck.

Edit

To give more info on how "in theory" I would go about this and you are right it is a hassle.

  1. Subclass SKSpriteNode
  2. Create a new method to run actions on that Sprite (like -(void)startAction:withKey:duration:) which would ultimately call run action with key.
  3. When startAction is called you store that into some sort of MutableArray with a Dictionary that stores that action, its key, duration, and startTime (defaulted to 0). You might not even have to actually store that action, just the key, duration, and start time.
  4. Add an update: method on to this SKSpriteNode subclass. Every update loop you call its update and check to see if 1 any actions don't have a start time and 2 if those actions are still running. If no start time you add current time as the start time and if not running you remove it form the array.
  5. When you go to encode/save that sprite you use the info in that array to determine state of those SKAction.

The big point of this is that each SKSpriteNode holds onto and tracks its own SKAction in this example. Sorry I don't have time to go through and write the code in Objective-C. Also by no means am I claiming or trying to imply this is better or worse than your answer, but rather addressing how I would handle this if I was determined to save the state of SKActions as your question asks. =)

Skyler Lauren
  • 3,792
  • 3
  • 18
  • 30
  • You're right that I'm stretching the possibilities of SKAction. But here's my justification: If you are building your own representation of **animation state** in your app, then you are reimplementing SKAction: SKAction already effectively represents the state of the animation. And once you've got SKAction representing the state of your animation, it's reasonable to persist it: If an orc is in the middle of a death animation when you background the app, then it should be in the middle of a death animation when you restore it. And SKAction is already encodable. Mostly :) – Karl Voskuil Apr 12 '16 at 02:59
  • Two responses to your specific suggestions (also: thank you!). 1. I'm trying to refocus the question on SKAction **sequence** in particular. In that case, we don't need the time elapsed in each SKAction; we just need, for sequences, to not re-run actions which have already run. 2. I can picture doing what you're saying, but it would be difficult to recreate each and every running SKAction on decode, and to track (and encode!) a separate time variable for each. It would be okay if we could subclass SKAction to do it, but we can't. (Probably this is why you are suggesting not using SKAction!) – Karl Voskuil Apr 12 '16 at 03:06
  • 1
    Well I 100% agree that SKAction should provide more info about its state regardless. I have struggled with SKActions in the past with my own games do to some of the limitations of knowing where they are when I want to save the game or reacting when something changes in the scene. I tend to take a more hands on approach but it seems like such a waste when there are actions that are so nicely sequenced and grouped. I do the like the idea of MVC though because at any given moment you can save model data but you have to manage everything from position to animation frame. – Skyler Lauren Apr 12 '16 at 03:15
  • Okay, yeah, big upvote for what you are saying about MVC: it's not "reimplementing SKAction", it's separating view from model. And I admit, I **am** mixing view and model most horribly here. (That said, if I don't accept your answer, it's because the first part of it is so unpalatable—even if the last paragraph is ultimately the best practice.) – Karl Voskuil Apr 12 '16 at 03:29
1

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.

The Code

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

The Concept

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.

Karl Voskuil
  • 750
  • 6
  • 19
  • What if one of the actions is do over time. Like move or animate? Wouldn't this mess you up of an Orc was to move 20 to the right over one second. Would the Orc move 30 overall or only 10 in this case if decoded .5 seconds in? – Skyler Lauren Apr 12 '16 at 03:09
  • Good point, and that question also applies to regular SKAction, even if not in a sequence, since the `moveBy` will restart after decode. I guess that means we should always use `moveTo`? – Karl Voskuil Apr 12 '16 at 03:12
  • That would at least guarantee it would end at a particular spot even if it would look funny (going faster or slower than normal) when decoded. Also I am more than happy to discuss this further but it is kind of frowned upon on SO. Shoot me an email at skyler@skymistdevelopment.com and I can send you an invite to the SKA slack tomorrow and you can troubleshoot/debate with the group or just email back and forth with me either works. I am off to bed for the night though. Best of luck. – Skyler Lauren Apr 12 '16 at 03:19