Know when all SKActions are completed or if there are no starts.

I have a number of SKActions running on different nodes. How can I find out when everything is completed? I want to ignore touches during animation. If I could somehow start actions in parallel on several nodes, I could wait for the final action to complete, but I see no way to coordinate actions between nodes.

I can fake this by skipping all the children in the scene and checking for hasActions for each child. It seems a bit lame, but it really works.

+6
source share
5 answers

As far as I know, there is no way to do this with the default platform features.

However, I think you could achieve something similar by creating a class using methods that act as a wrapper to call SKAction runAction: on node.

In this wrapper method, you can push node into an array and then add a performSelector action for each action / group / sequence. Therefore, any method you specify is called upon completion of an action / group / sequence. When this method is called, you can simply remove this node from the array.

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

+2
source

The easiest way to do this is to use the dispatch group . In Swift 3, it 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 starting each action, we call group.enter() , adding this action to the group. Then, inside each completion handler, we call group.leave() , calling this action from the group.

The group.notify() block is started after all other blocks have left the sending group.

+2
source

Every action you take has a duration. If you keep track of the longest duration of an action you know when it will be completed. Use this to wait for the completion of the longest action.

Alternatively, keep a global current activity counter. Each time you run an action that pauses input, increase the counter. Each action you take requires a final execution block, which then decreases the counter. If the counter is zero, none of the actions to ignore input work.

+1
source

It seems that in the two years since the publication of this issue, Apple has not expanded the scope for solving this case. I hesitated to do a bunch of graph traces to verify the execution of actions, so I found a solution that uses the instance variable in my SKScene (GameScene) subclass in combination with the integer atom protection functions found in / usr / include / libkern / OSAtomic. h

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

I have two GameScene methods:

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

Then I declare the type of block to go to the SKNode :: runAction completion block:

 void (^SignalActionEnd)(void); 

In my method of triggering actions on various SKSpriteNodes, set this completion block to point to the safe decrement method:

 SignalActionEnd = ^ { [self DecrementUILockCount]; }; 

Then, before starting the action, run the safe increment block. When the action completes, DecrementUILockCount will be called to safely decrease the counter.

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

In my update method, I just check to see if this counter is zero before re-enabling the user interface.

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

The only thing to note here is that if you manage to delete any of the nodes that have started actions before they are completed, the completion block will also be deleted (without starting), and your counter will never be reduced, and your user interface will never be reactivated. The answer is to verify that you are performing actions on the node that you are about to delete, and manually run the protected decrement method if there are any actions:

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

This works great for me - hope this helps!

+1
source

I dealt with this problem by playing a game with unfolding tiles. I wanted to both prevent keyboard input and wait as short as possible to perform another action, while the tiles really moved.

All the tiles that I was worried about were instances of the same SKNode subclass, so I decided to give this class the ability to keep track of animations in the process and respond to requests 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 wait on, and it can be added at any time, so the wait will continue until the tasks are added to the group. *

This is a sketch of the solution. We have a node class that creates and owns a dispatch group. A private class method allows instances to access a group so that they can enter and leave it when animating. The class has two public methods that allow you to check the status of a group without exposing the actual mechanism: +waitOnAllNodeMovement and +anyNodeMovementInProgress . The former block until the group is empty; the latter simply returns 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 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 can do something simple:

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

and you can do the same in the update: scene update: as needed. Any other actions that should happen as soon as possible can wait for 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 one hard / dirty part of this solution: since the wait... method is blocked, it obviously should happen asynchronously with the main thread; then we return to the main topic to do more work. But the same would be true with any other waiting procedure, so this seems reasonable.


* Two other possibilities that seemed to represent a queue with a barrier block and a counting semaphore.

The barrier unit will not work, because I did not know when I could put it in the queue. At that moment, when I decided to queue the task β€œafter”, no tasks β€œbefore” could be added.

The semaphore will not work, because it does not control the order, just simultaneity. If the nodes increased the semaphore when they were created, decreased during the animation, and increased again when it was done, another task could only wait if all the created nodes were animated, and would not wait longer than the first completion. If the nodes at first did not increase the semaphore, then only one of them could function at a time.

The send group is used as a semaphore, but with privileged access: the nodes themselves should not wait.

0
source

Source: https://habr.com/ru/post/958815/


All Articles