Question

I have a number of SKActions running on various nodes. How can I know when they are all completed? I want to ignore touches while animations are running. If I could somehow run actions in parallel on a number of nodes, I could wait for a final action to run, but I don't see any way to coordinate actions across nodes.

I can fake this by running through all the scene's children and checking for hasActions on each child. Seems a little lame, but it does work.

Était-ce utile?

La solution 2

To my knowledge there is no way to do this via the default framework capabilities.

However, I think you could achieve something like this by creating a class with methods that act as a wrapper for calling SKAction runAction: on a node.

In that wrapper method, you could push the node into an array, and then append a performSelector action to each action/group/sequence. So whatever method you specify gets called after completion of the action/group/sequence. When that method is called, you can just remove that node from the array.

With this implementation you would always have an array of all nodes that currently have an action running on them. If the array is empty, none are running.

Autres conseils

The simplest way to do this is using a dispatch group. In Swift 3 this looks like

func moveAllNodes(withCompletionHandler onComplete:(()->())) {
    let group = DispatchGroup()
    for node in nodes {
        let moveAction = SKAction.move(to:target, duration: 0.3)
        group.enter()
        node.run(moveAction, completion: { 
            ...
            group.leave()
        }
    }
    group.notify(queue: .main) {
        onComplete()
    }
}

Before running each action we call group.enter(), adding that action to the group. Then inside each action completion handler we call group.leave(), taking that action out of the group.

The group.notify() block runs after all other blocks have left the dispatch group.

Each action you run has a duration. If you keep track of the longest running action's duration you know when it'll be finished. Use that to wait until the longest running action is finished.

Alternatively, keep a global counter of running actions. Each time you run an action that pauses input increase the counter. Each action you run needs a final execute block that then decreases the counter. If the counter is zero, none of the input-ignoring actions are running.

It looks like in the two years since this question was first posted, Apple has not extended the framework to deal with this case. I was hesitant to do a bunch of graph traversals to check for running actions, so I found a solution using an instance variable in my SKScene subclass (GameScene) combined with the atomic integer protection functions found in /usr/include/libkern/OSAtomic.h.

In my GameScene class, I have an int32_t variable called runningActionCount, initialized to zero in initWithSize().

I have two GameScene methods:

-(void) IncrementUILockCount
{
    OSAtomicIncrement32Barrier(&runningActionCount);
}

-(void) DecrementUILockCount
{
    OSAtomicDecrement32Barrier(&runningActionCount);
}

Then I declare a block type to pass to SKNode::runAction completion block:

void (^SignalActionEnd)(void);

In my method to launch the actions on the various SKSpriteNodes, set that completion block to point to the safe decrement method:

SignalActionEnd = ^
{
    [self DecrementUILockCount];
};

Then before I launch an action, run the safe increment block. When the action completes, DecrementUILockCount will be called to safely decrement the counter.

[self IncrementUILockCount];
[spriteToPerformActionOn runAction:theAction completion:SignalActionEnd];

In my update method, I simply check to see if that counter is zero before re-enabling the UI.

if (0 == runningActionCount)
{
    // Do the UI enabled stuff
}

The only other thing to note here is that if you happen to delete any of the nodes that have running actions before they complete, the completion block is also deleted (without being run) and your counter will never be decremented and your UI will never re-enable. The answer is to check for running actions on the node you are about to delete, and manually run the protected decrement method if there are any actions running:

if ([spriteToDelete hasActions])
{
    // Run the post-action completion block manually.
    [self DecrementUILockCount];
}

This is working fine for me - hope it helps!

I was dealing with this issue while fiddling around with a sliding-tile type game. I wanted to both prevent keyboard input and wait for as short a time as possible to perform another action, while the tiles were actually moving.

All the tiles that I was concerned about were instances of the same SKNode subclass, so I decided to give that class the resposibility for keeping track of animations in progress, and for responding to queries about whether animations were running.

The idea I had was to use a dispatch group to "count" activity: it has a built-in mechanism to be waited on, and it can be added to at any time, so that the waiting will continue as long as tasks are added to the group.*

This is a sketch of the solution. We have a node class, which creates and owns the dispatch group. A private class method allows instances to access the group so they can enter and leave it when they are animating. The class has two public methods that allow checking the group's status without exposing the actual mechanism: +waitOnAllNodeMovement and +anyNodeMovementInProgress. The former blocks until the group is empty; the latter simply returns a BOOL immediately indicating whether the group is busy or not.

@interface WSSNode : SKSpriteNode

/** The WSSNode class tracks whether any instances are running animations,
 *  in order to avoid overlapping other actions. 
 *  +waitOnAllNodeMovement blocks when called until all nodes have 
 *  completed their animations.
 */
+ (void)waitOnAllNodeMovement;

/** The WSSNode class tracks whether any instances are running animations,
 *  in order to avoid overlapping other actions. 
 *  +anyNodeMovementInProgress returns a BOOL immediately, indicating 
 *  whether any animations are currently running.
 */
+ (BOOL)anyNodeMovementInProgress;

/* Sample method: make the node do something that requires waiting on. */
- (void)moveToPosition:(CGPoint)destination;

@end

@interface WSSNode ()

+ (dispatch_group_t)movementDispatchGroup;

@end

@implementation WSSNode

+ (void)waitOnAllNodeMovement
{
    dispatch_group_wait([self movementDispatchGroup], 
                        DISPATCH_TIME_FOREVER);
}

+ (BOOL)anyNodeMovementInProgress
{
    // Return immediately regardless of state of group, but indicate 
    // whether group is empty or timeout occurred.
    return (0 != dispatch_group_wait([self movementDispatchGroup], 
                                     DISPATCH_TIME_NOW));
}

+ (dispatch_group_t)movementDispatchGroup
{
    static dispatch_group_t group;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        group = dispatch_group_create();
    });

    return group;
}

- (void)moveToPosition:(CGPoint)destination
{   
    // No need to actually enqueue anything; simply manually
    // tell the group that it's working.
    dispatch_group_enter([WSSNode movementDispatchGroup]);
    [self runAction:/* whatever */
         completion:^{ dispatch_group_leave([WSSNode movementDispatchGroup])}];
}

@end

A controller class that wants to prevent keyboard input during moves then can do something simple like this:

- (void)keyDown:(NSEvent *)theEvent
{
    // Don't accept input while movement is taking place.
    if( [WSSNode anyNodeMovementInProgress] ){
        return;
    }
    // ...
}

and you can do the same thing in a scene's update: as needed. Any other actions that must happen ASAP can wait on the animation:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
    ^{
        [WSSNode waitOnAllNodeMovement];
        dispatch_async(dispatch_get_main_queue(), ^{
          // Action that needs to wait for animation to finish
     });
});

This is the one tricky/messy part of this solution: because the wait... method is blocking, it obviously has to happen asynchronously to the main thread; then we come back to the main thread to do more work. But the same would be true with any other waiting procedure as well, so this seems reasonable.


*The other two possibilities that presented themselves were a queue with a barrier Block and a counting semaphore.

The barrier Block wouldn't work because I didn't know when I could actually enqueue it. At the point where I decided to enqueue the "after" task, no "before" tasks could be added.

The semaphore wouldn't work because it doesn't control ordering, just simultaneity. If the nodes incremented the semaphore when they were created, decremented when animating, and incremented again when done, the other task would only wait if all created nodes were animating, and wouldn't wait any longer than the first completion. If the nodes didn't increment the semaphore initially, then only one of them could function at a time.

The dispatch group is being used much like a semaphore, but with privileged access: the nodes themselves don't have to wait.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top