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.