Search code examples
objective-csprite-kitskaction

add SKAction to Sprite queue run one after another


I listen to touch and add SKAction to a sprite. If existing actions are not complete yet, I want the action to be added to a queue so it will execute one after another. Any experienced similar design?

I did using Array and Block. If there is any easier approach?

@interface GAPMyScene()
@property(strong,nonatomic)SKSpriteNode*ufo;
@property(strong,nonatomic)NSMutableArray*animationQueue;
@property(copy,nonatomic)void(^completeMove)();
@end

@implementation GAPMyScene

-(id)initWithSize:(CGSize)size {
    if (self = [super initWithSize:size]) {
        self.ufo = [SKSpriteNode spriteNodeWithImageNamed:@"Spaceship"];
        self.animationQueue = [[NSMutableArray alloc] init];
        __unsafe_unretained typeof(self) weakSelf = self;
        self.completeMove = ^(void){
            [weakSelf.ufo runAction:[SKAction sequence:[weakSelf.animationQueue copy]] completion:weakSelf.completeMove];
            NSLog(@"removeing %@", weakSelf.animationQueue);
            [weakSelf.animationQueue removeAllObjects];
        };
        [self addChild:self.ufo];
    }
    return self;
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    for (UITouch *touch in touches) {
        CGPoint location = [touch locationInNode:self];
        SKAction*moveAnimation = [SKAction moveTo:location duration:2];
        if (![self.ufo hasActions]) {
            [self.ufo runAction:moveAnimation completion:self.completeMove];

        } else {
            [self.animationQueue addObject:moveAnimation];
            NSLog(@"in queue %@", self.animationQueue);
        }
    }
}

@end

Solution

  • Generally, you can make SKActions run concurrently using the group method, and have them run sequentially using the sequence method.

    But if you need a queuing system, rather than building your own, use the native operation queue to do this for you. So you can create a serial operation queue and add operations to it. The issue is that you don't want an operation to complete until the SKAction does.

    So, you can wrap your SKAction in a concurrent NSOperation subclass that only completes when the SKAction does. Then you can add your operations to a serial NSOperationQueue, and then it will won't start the next one until it finishes the prior one.

    So, first, create an ActionOperation (subclassed from NSOperation) that looks like:

    // ActionOperation.h
    
    #import <Foundation/Foundation.h>
    
    @class SKNode;
    @class SKAction;
    
    @interface ActionOperation : NSOperation
    
    - (instancetype)initWithNode:(SKNode *)node action:(SKAction *)action;
    
    @end
    

    and

    // ActionOperation.m
    
    #import "ActionOperation.h"
    @import SpriteKit;
    
    @interface ActionOperation ()
    
    @property (nonatomic, readwrite, getter = isFinished)  BOOL finished;
    @property (nonatomic, readwrite, getter = isExecuting) BOOL executing;
    
    @property (nonatomic, strong) SKNode *node;
    @property (nonatomic, strong) SKAction *action;
    
    @end
    
    @implementation ActionOperation
    
    @synthesize finished  = _finished;
    @synthesize executing = _executing;
    
    - (instancetype)initWithNode:(SKNode *)node action:(SKAction *)action
    {
        self = [super init];
        if (self) {
            _node = node;
            _action = action;
        }
        return self;
    }
    
    - (void)start
    {
        if ([self isCancelled]) {
            self.finished = YES;
            return;
        }
    
        self.executing = YES;
    
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            [self.node runAction:self.action completion:^{
                self.executing = NO;
                self.finished = YES;
            }];
        }];
    }
    
    #pragma mark - NSOperation methods
    
    - (BOOL)isConcurrent
    {
        return YES;
    }
    
    - (void)setExecuting:(BOOL)executing
    {
        [self willChangeValueForKey:@"isExecuting"];
        _executing = executing;
        [self didChangeValueForKey:@"isExecuting"];
    }
    
    - (void)setFinished:(BOOL)finished
    {
        [self willChangeValueForKey:@"isFinished"];
        _finished = finished;
        [self didChangeValueForKey:@"isFinished"];
    }
    
    @end
    

    You could then, for example, create a serial queue during the initialization process:

    self.queue = [[NSOperationQueue alloc] init];
    self.queue.maxConcurrentOperationCount = 1;
    

    You can then add the operations to it:

    SKAction *move1 = [SKAction moveTo:point1 duration:2.0];
    [self.queue addOperation:[[ActionOperation alloc] initWithNode:nodeToMove action:move1]];
    

    and you can later add more actions:

    SKAction *move2 = [SKAction moveTo:point2 duration:2.0];
    [self.queue addOperation:[[ActionOperation alloc] initWithNode:nodeToMove action:move2]];
    

    And because the queue is serial, you know that move2 will not be started until move1 is done.