5

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
Elliot Yap
  • 1,076
  • 1
  • 12
  • 20
  • Just our of curiosity: Xcode shows me an ARC Retain Cycle warning for `__unsafe_unretained typeof(self) weakSelf = self;`. Is that code correct? –  Apr 04 '14 at 10:57
  • i think so. to prevent retain cycle in block i have to make it weak reference. – Elliot Yap Apr 04 '14 at 12:12
  • Okay, the warning disappeared after I used the code from [this answer](http://stackoverflow.com/a/17011096/867635). Regarding you question: I haven't found an easier approach for an animation-queue yet. –  Apr 04 '14 at 12:40
  • ok, the warning fixed. i have to put the weak self declaration outside of the block – Elliot Yap Apr 05 '14 at 10:49

1 Answers1

3

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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • hi, Thanks for your answer, but parameter to send to SKAction and the Node will need to be decided on runtime. E.g. I will only know which node to run on the SKAction after the end of the first SKAction – Elliot Yap Apr 14 '14 at 08:17
  • @ElliotYap That's fine. You haven't shown us how it knows which node you want perform that action on (so it's hard for me to show you the change necessary in the operation object to make that happen), but you can incorporate whatever logic you're alluding to in this `ActionOperation` (e.g. or use block/delegate pattern so caller can specify how to determine this). My point was simply that you shouldn't recreate queue mechanism when one already exists. – Rob Apr 14 '14 at 12:57
  • Understand, I'm reading on NSOperation now, Thanks for leading me to the right direction :) – Elliot Yap Apr 15 '14 at 01:50
  • [Rob](http://stackoverflow.com/users/1271826/rob), I found a limitation on the [Swift implementation](http://stackoverflow.com/q/28373490/540780) of this solution when `SKactions` transmitted during initialization contains `runAction:onChildWithName:`. In this case the duration for this `SKAction` is instantaneous. – Dominique Vial Jun 01 '15 at 17:31