I am pretty new to SpriteKit. I have a set of nodes that need to move together to a different point for each node and after that animation completed for all of them I would like to do something else.
I was making this with UIView components before. A [UIView animateWithDuration:completion:] block was providing the thing that I needed. But in SpriteKit each node has its own action and animate by itself. I could not find any block animation for sprite nodes. So I cannot control the animation completion.
Hope, I am clear. Thanks in advance.
There are a couple of SKAction class methods that may be of interest. The first being runBlock: and the second being group: (this allows actions to be run in parallel).
You can run actions on nodes with a completion handler using runAction: completion:
The following code puts each 'animation action', along with completion block in a 'block action'. The block actions are then put into a 'group action'. Finally the group action is told to run. This causes each block action to start at the same time. The completion handler of each block action calls a method named checkCompletion. This method checks for nodes (with a specific name property value) still animating by calling hasActions on the node. Because the node that called the checkCompletion method will return YES, if only one node returns YES then all animations are done.
The following demonstrates this using two nodes, however it will work for more than two.
// assuming two SKNode subclasses named 'node1' and 'node2' AND 'self' is an SKScene subclass
// assign each animating node the same name (used in checkCompletion method)
node1.name = #"animating";
node2.name = #"animating";
// in this example both nodes will animate to the origin
CGPoint origin = CGPointMake(0.0, 0.0);
// make move actions for nodes
SKAction *shortMove = [SKAction moveTo:origin duration:1.2];
SKAction *longMove = [SKAction moveTo:origin duration:4.8];
// wrap nodes in their own separate block action
SKAction *moveNode1 = [SKAction runBlock:^{
[node1 runAction:shortMove completion:^{
NSLog(#"shortMove complete");
[self checkCompletion];
}];
}];
SKAction *moveNode2 = [SKAction runBlock:^{
[node2 runAction:longMove completion:^{
NSLog(#"longMove complete");
[self checkCompletion];
}];
}];
// put block actions in a group action
SKAction *groupAction = [SKAction group:#[moveNode1, moveNode2]];
// execute group action
[self runAction:groupAction];
checkCompletion method -
- (void)checkCompletion {
__block int currentlyHasActions = 0;
// check for all actions complete on animating nodes
[self enumerateChildNodesWithName:#"animating" usingBlock:^(SKNode *node, BOOL *stop){
if ([node hasActions]) {
currentlyHasActions++;
// prevent unnecessary enumeration
if (currentlyHasActions > 1) { *stop = YES; }
}
}];
if (currentlyHasActions == 1) {
NSLog(#"Actions Finished!!!");
// execute completion code here...
}
}
Unfortunately the following will not work as a block actions duration is instantaneous. Consequently the duration of the group action in this case is also instantaneous. This is the reason for the checkCompletion method.
[self runAction:groupAction completion:^{
// completion code here... :(
}];
Related
Problem
When the node hierarchy is encoded, as is common during application state preservation or a “game save”, nodes running SKAction actions with code blocks must be handled specially, since the code blocks cannot be encoded.
Example 1: Delayed Callback after Animation
Here, an orc has been killed. It is animated to fade out and then remove itself from the node hierarchy:
SKAction *fadeAction = [SKAction fadeOutWithDuration:3.0];
SKAction *removeAction = [SKAction removeFromParent];
[orcNode runAction:[SKAction sequence:#[ fadeAction, removeAction ]]];
If the orc node is encoded and then decoded, the animation will restore properly and complete as expected.
But now the example is modified to use a code block that runs after the fade. Perhaps the code cleans up some game state once the orc is (finally) dead.
SKAction *fadeAction = [SKAction fadeOutWithDuration:3.0];
SKAction *removeAction = [SKAction removeFromParent];
SKAction *cleanupAction = [SKAction runBlock:^{
[self orcDidFinishDying:orcNode];
}];
[orcNode runAction:[SKAction sequence:#[ fadeAction, removeAction, cleanupAction ]]];
Unfortunately, the code block will not encode. During application state preservation (or game save), if this sequence is running, a warning will be issued:
SKAction: Run block actions can not be properly encoded,
Objective-C blocks do not support NSCoding.
After decoding, the orc will fade and be removed from parent, but the cleanup method orcDidFinishDying: will not be called.
What is the best way to work around this limitation?
Example 2: Tweening
The SKAction customActionWithDuration:actionBlock: seems a beautiful fit for tweening. My boilerplate code for this kind of thing is this:
SKAction *slideInAction = [SKAction customActionWithDuration:2.0 actionBlock:^(SKNode *node, CGFloat elapsedTime){
CGFloat normalTime = (CGFloat)(elapsedTime / 2.0);
CGFloat normalValue = BackStandardEaseInOut(normalTime);
node.position = CGPointMake(node.position.x, slideStartPositionY * (1.0f - normalValue) + slideFinalPositionY * normalValue);
}];
Unfortunately, customActionWithDuration:actionBlock: cannot be encoded. If the game is saved during the animation, it will not restore properly on game load.
Again, what is the best way to work around this limitation?
Imperfect Solutions
Here are solutions I have considered but don’t like. (That said, I’d love to read answers that successfully champion one of these.)
Imperfect Solution: Use performSelector:onTarget: rather than runBlock: in the animation. This solution is imperfect because arguments cannot be passed to the invoked selector; context for the call can only be expressed by the target and the name of the selector. Not great.
Imperfect Solution: During encoding, remove the SKAction sequence from any relevant nodes and advance the program state as if the sequence had completed. In the first example, that would mean setting the node alpha immediately to 0.0, removing the orc node from parent, and calling orcDidFinishDying:. This is an unfortunate solution for at least two reasons: 1) It requires special handling code during encoding; 2) Visually, the node won’t get a chance to finish its animation.
Imperfect Solution: During encoding, remove the SKAction code blocks from any relevant nodes, and recreate them during decoding. This is non-trivial.
Imperfect Solution: Never use SKAction code blocks, especially after a delay. Never rely on the completion of an animation in order to restore good app state. (If you need to schedule a future event in an encodable way, build your own event queue not using code blocks.) This solution is imperfect because runBlock and customActionWithDuration:actionBlock: are just so damn useful, and it would be a shame (and a recurring trap for newbies) to consider them evil.
Encodable lightweight objects can model the kinds of SKAction code blocks that we want to use (but can’t).
Code for the below ideas is here.
Replacement for runBlock
The first encodable lightweight object replaces runBlock. It can make an arbitrary callback with one or two arguments.
The caller instantiates the lightweight object and sets its properties: target, selector, and arguments.
The lightweight object is triggered in a runAction animation by the standard no-argument [SKAction performSelector:onTarget:]. For this triggering action, the target is the lightweight object and the selector is a designated “execute” method.
The lightweight object conforms to NSCoding.
As a bonus, the triggering SKAction retains a strong reference to the lightweight object, and so both will be encoded along with the node running the actions.
A version of this lightweight object could be made that retains the target weakly, which might be nice and/or necessary.
Here is a draft of a possible interface:
#interface HLPerformSelector : NSObject <NSCoding>
- (instancetype)initWithTarget:(id)target selector:(SEL)selector argument:(id)argument;
#property (nonatomic, strong) id target;
#property (nonatomic, assign) SEL selector;
#property (nonatomic, strong) id argument;
- (void)execute;
#end
And an accompanying implementation:
#implementation HLPerformSelector
- (instancetype)initWithTarget:(id)target selector:(SEL)selector argument:(id)argument
{
self = [super init];
if (self) {
_target = target;
_selector = selector;
_argument = argument;
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (self) {
_target = [aDecoder decodeObjectForKey:#"target"];
_selector = NSSelectorFromString([aDecoder decodeObjectForKey:#"selector"]);
_argument = [aDecoder decodeObjectForKey:#"argument"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:_target forKey:#"target"];
[aCoder encodeObject:NSStringFromSelector(_selector) forKey:#"selector"];
[aCoder encodeObject:_argument forKey:#"argument"];
}
- (void)execute
{
if (!_target) {
return;
}
IMP imp = [_target methodForSelector:_selector];
void (*func)(id, SEL, id) = (void (*)(id, SEL, id))imp;
func(_target, _selector, _argument);
}
#end
And an example of using it:
SKAction *fadeAction = [SKAction fadeOutWithDuration:3.0];
SKAction *removeAction = [SKAction removeFromParent];
HLPerformSelector *cleanupCaller = [[HLPerformSelector alloc] initWithTarget:self selector:#selector(orcDidFinishDying:) argument:orcNode];
SKAction *cleanupAction = [SKAction performSelector:#selector(execute) onTarget:cleanupCaller];
[orcNode runAction:[SKAction sequence:#[ fadeAction, removeAction, cleanupAction ]]];
Replacement for customActionWithDuration:actionBlock:
A second encodable lightweight object replaces customActionWithDuration:actionBlock:. This one is not so simple, however.
Again, it is triggered by the no-argument [SKAction performSelector:onTarget:], invoking a designated execute method.
A customActionWithDuration:actionBlock: has a duration. But the triggering performSelector:onTarget: does not. The caller must insert a companion waitForDuration: action into her sequence if it depends on duration.
The lightweight object is initialized with a target, selector, node, and duration.
When it is triggered, the lightweight object tracks its own elapsed time and periodically calls the selector on the target, passing it the node and the elapsed time.
The lightweight object conforms to NSCoding. On decoding, if already triggered, it resumes calling the selector for the remainder of its configured duration.
Limitations
I have implemented a version of these proposed classes. Through light use I've already found an important limitation: Nodes encoded with a running SKAction sequence restart the sequence from the beginning upon decoding.
I've a player of Sprite * type in cocos2dx V3, I want it to run different animation on different time interval, I could not find method to pause and then resume a specific animation(Action). Although I can Pause and Resume all actions of Sprite Simultaneously using _player->pauseSchedulerAndActions().I'm using "CCRepeatForever" actions on sprite, so, I must have to pause one to resume other.Please help to Pause an action by tag or by any other method.
Thanks In Advance.
Oops
I made the assumption that this was Objective-C but #Droppy has informed me that it is not.
I didn't realise cocos2d-x was different. However, because this is a fairly high level framework the concept behind what I've done in the answer will still work. I'll keep the answer here for now.
The answer
It's been a while since I've done any Cocos2D stuff but I can give you the idea.
Instead of creating an action and repeating it forever you should have a method something like this...
- (void)twirlAround
{
// only create and perform the actions if variable set to YES
if (self.twirling) {
// this will do your action once.
CCAction *twirlAction = // create your twirl action (or whatever it is)
// this will run this function again
CCAction *repeatAction = [CCActionCallBlock actionWithBlock:^{
[self twirlAround];
}];
// put the action and method call in sequence.
CCActionSequence *sequence = [CCActionSequence actions:#[twirlAction, repeatAction]];
[self runAction:sequence];
}
}
This will run repeatedly as long as the twirling property is set to YES.
So, somewhere else in your code (probably where you are currently adding your repeating action) you can do this...
self.twirling = YES;
[self twirlAround];
This will start the repeated twirling.
To stop it you can then do...
self.twirling = NO;
This will stop the twirling.
Alternative method
- (void)twirlAround
{
// this will do your action once.
CCAction *twirlAction = // create your twirl action (or whatever it is)
// this will run this function again
CCAction *repeatAction = [CCActionCallBlock actionWithBlock:^{
if (self.twirling) {
[self twirlAround];
}
}];
// put the action and method call in sequence.
CCActionSequence *sequence = [CCActionSequence actions:#[twirlAction, repeatAction]];
[self runAction:sequence];
}
based on Fogmeister advice, this is cocos2d-x version of that
void MySprite::jumpForever(){
if (!twirling) return;
auto jump = JumpBy::create(0.5, Vec2(0, 0), 100, 1);
auto endCallback = CallFuncN::create(CC_CALLBACK_1(MySprite::jumpForever,this));
auto seq = Sequence::create(jump, endCallback, nullptr);
runAction(seq);
}
i don't completely understand the best choice in sprite kit animation;
1) Apple in "Adventure" example use this methodology, they store in memory animation as pictures in nsarray :
static NSArray *sSharedTouchAnimationFrames = nil;
- (NSArray *)touchAnimationFrames {
return sSharedTouchAnimationFrames;
}
- (void)runAnimation
{
if (self.isAnimated) {
[self resolveRequestedAnimation];
}
}
- (void)resolveRequestedAnimation
{
/* Determine the animation we want to play. */
NSString *animationKey = nil;
NSArray *animationFrames = nil;
VZAnimationState animationState = self.requestedAnimation;
switch (animationState) {
default:
case VZAnimationStateTouch:
animationKey = #"anim_touch";
animationFrames = [self touchAnimationFrames];
break;
case VZAnimationStateUntouch:
animationKey = #"anim_untouch";
animationFrames = [self untouchAnimationFrames];
break;
}
if (animationKey) {
[self fireAnimationForState:animationState usingTextures:animationFrames withKey:animationKey];
}
self.requestedAnimation = VZAnimationStateIdle;
}
- (void)fireAnimationForState:(VZAnimationState)animationState usingTextures:(NSArray *)frames withKey:(NSString *)key
{
SKAction *animAction = [self actionForKey:key];
if (animAction || [frames count] < 1) {
return; /* we already have a running animation or there aren't any frames to animate */
}
[self runAction:[SKAction sequence:#[
[SKAction animateWithTextures:frames timePerFrame:self.animationSpeed resize:YES restore:NO],
/* [SKAction runBlock:^{
[self animationHasCompleted:animationState];
}]*/]] withKey:key];
}
I appreciate this methodology, but i can't understand. Is storing SKAction in memory not better choice and use animation always like this ?
[self runAction:action];
without making always new SKAction;
[SKAction animateWithTextures:frames timePerFrame:self.animationSpeed resize:YES restore:NO]
Storing SKTextures in an NSArray is the recommended methodology to be used for animation. I can't say that I found SpriteKit's documentation to be lacking on the subject either, as SKAction has the method animateWithTextures.
You could indeed have a SKAction that has a given animation defined by a given NSArray, and maybe store those in a NSMutableDictionary with an animation name for a key. However this methodology I see above just has the animateTextures line of code once in their fireAnimationState method, and you can pass parameters to it such as the animationState and a key.
I think you would need to take more than a surface look at their methodology to determine why they chose to go this route. You can see that the animationState was being utilized upon completion of the animation and likely triggered something else.
[self runAction:action] is indeed simple, however it's also easy to see that it's not in any way managing an animationState or key, which I have to assume they decided their game needed to do.
Also keep in mind that their methodology likely has a higher level where in a given player class it's calling a flexible method for enacting an animation for a given game entity and doing other things besides just changing the animation. So in their high level coding they might be doing something more like this :
[self runAnimation:#"jump"];
or even
[self jump];
And because they have designed a low level system that manages animation states and adds keys for a given management design, they don't ever have to code the long line that you are pointing out except once in that method you see above.
A few reasons I have found it useful to store the NSArray of frames and not storing wrapped in a SKAction is because I sometimes want to manipulate the start frame of the animation or run the animation at a different speed. But you may or may not have the need to manipulate those aspects of your animation.
I stumbled upon a problem and I can't find the answer for it. I am working with the SK template from Xcode to create an iOS game. I am a beginner, so bear with me.
Basically I have this code:
SKAction *releaseBubbles = [SKAction sequence:#[
[SKAction performSelector:#selector(createBubbleNode)onTarget:self],
[SKAction waitForDuration:speed]]];
[self runAction: [SKAction repeatAction:releaseBubbles
count:300]];
which executes in
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
I change the level to my game in -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { and when I change the level it should also change that speed parameter. Of course, this doesn't work because I believe that my action is starting when the scene is initialised and I never get to switch the parameter.
What I need to do is populate the screen continuously with bubbles appearing at a certain pace (relative to the level).
I really have no clue how to fix this, because it seems to me like I need to stop and restart the action sequence somehow...
Looking forward to your valuable input.
To continuously populate the screen with bubbles you can use the update: method of your SKScene. Here is how to do it.
First, add a property that will store a date when you last added a bubble.
#property(nonatomic, strong) NSDate *lastBubbleCreationDate;
Then, change your update: method to:
-(void)update:(CFTimeInterval)currentTime
{
// Create new bubble every 5s.
if (ABS([_lastBubbleCreationDate timeIntervalSinceNow]) > 5)
{
[self createBubbleNode];
}
}
Finally, in your createBubbleNode method you have to store the time when you created last bubble:
-(void)createBubbleNode
{
// Your code here
// Set the date to now.
_lastBubbleCreationDate = [NSDate date];
}
You also need to call createBubbleNode to set the initial value of the _lastBubbleCreationDate. You can do this in didMoveToView: method. Just add this method to your scene implementation:
- (void)didMoveToView:(SKView *)view
{
// Creates first bubble and sets the initial value of the _lastBubbleCreationDate
[self createBubbleNode];
}
In next levels you can just change the 5s value to create bubbles more often which will make the game more difficult.
How do I check if a SKAction has finished its animation?
I need to check if my action has already finished or is still performing its action. After that I want to create a boolean to avoid multiple actions during the main action.
SKAction *lionJumpActionComplete = [lionNode actionForKey:#"lionIsJumping"];
lionJumpActionComplete = [SKAction sequence: #[lionJumpActionUp, lionJumpActionFly, lionJumpActionDown, lionJumpActionPause]];
if (lionJumpActionComplete) {
return;
}
[lionNode runAction:lionJumpActionComplete withKey:#"lionIsJumping"];
If this is the only action running on your node, you can check this using:
if (!lionNode.hasActions) { // check if no actions are running on this node
// action code here
}
Alternatively, you can set your boolean in a completion block that gets called after the action runs and completes:
[lionNode runAction:[SKAction sequence: #[lionJumpActionUp, lionJumpActionFly, lionJumpActionDown, lionJumpActionPause]] completion:^{
BOOL isActionCompleted = YES;
}];
Here's an example of me creating a walking animation on a node. Before I create it again I make sure the previous one has finished by looking for its key.
SKAction *animAction = [self actionForKey:#"WalkingZombie"];
if (animAction) {
return; // we already have a running animation
}
[self runAction:
[SKAction animateWithTextures:[self walkAnimationFrames]
timePerFrame:1.0f/15.0f
resize:YES
restore:NO]
withKey:#"WalkingZombie"];
}
You need to check to see if the node is running the action
so in this case
if (![self hasActions]) {
[self runAction:[self actionForKey:#"ZombieAction"]];
}
probably better might be
[self runAction:[SKAction repeatForever:[self actionForKey:#"zombieAction"]]];
which will keep doing the action forever.