我SKActions
在各种节点上运行了许多。我怎么知道它们何时全部完成?我想在动画运行时忽略触摸。如果我能以某种方式在多个节点上并行运行动作,我可以等待最终动作运行,但我看不到任何方式来协调跨节点的动作。
我可以通过遍历所有场景的孩子并检查hasActions
每个孩子来伪造这一点。似乎有点蹩脚,但它确实有效。
我SKActions
在各种节点上运行了许多。我怎么知道它们何时全部完成?我想在动画运行时忽略触摸。如果我能以某种方式在多个节点上并行运行动作,我可以等待最终动作运行,但我看不到任何方式来协调跨节点的动作。
我可以通过遍历所有场景的孩子并检查hasActions
每个孩子来伪造这一点。似乎有点蹩脚,但它确实有效。
最简单的方法是使用调度组。在 Swift 3 中,这看起来像
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()
}
}
在运行我们调用的每个动作之前group.enter()
,将该动作添加到组中。然后在我们调用的每个动作完成处理程序中group.leave()
,将该动作从组中取出。
该group.notify()
块在所有其他块离开调度组之后运行。
据我所知,没有办法通过默认框架功能来做到这一点。
但是,我认为您可以通过创建一个类来实现类似的效果,该类的方法充当调用SKAction runAction:
节点的包装器。
在该包装方法中,您可以将节点推送到数组中,然后将performSelector
操作附加到每个操作/组/序列。因此,您指定的任何方法都会在操作/组/序列完成后调用。调用该方法时,您可以从数组中删除该节点。
使用此实现,您将始终拥有一个包含当前在其上运行操作的所有节点的数组。如果数组为空,则没有运行。
您运行的每个操作都有一个持续时间。如果您跟踪最长运行动作的持续时间,您就会知道它何时完成。使用它来等待最长运行的操作完成。
或者,保留一个运行动作的全局计数器。每次运行暂停输入的操作时,都会增加计数器。您运行的每个操作都需要一个最终执行块,然后减少计数器。如果计数器为零,则没有任何输入忽略操作正在运行。
自从这个问题首次发布以来的两年里,苹果似乎还没有扩展框架来处理这种情况。我犹豫要不要做一堆图遍历来检查正在运行的动作,所以我找到了一个解决方案,它使用了我的 SKScene 子类 (GameScene) 中的实例变量以及 /usr/include/libkern/OSAtomic 中的原子整数保护函数。 H。
在我的 GameScene 类中,我有一个名为 runningActionCount 的 int32_t 变量,在 initWithSize() 中初始化为零。
我有两个 GameScene 方法:
-(void) IncrementUILockCount
{
OSAtomicIncrement32Barrier(&runningActionCount);
}
-(void) DecrementUILockCount
{
OSAtomicDecrement32Barrier(&runningActionCount);
}
然后我声明一个块类型传递给 SKNode::runAction 完成块:
void (^SignalActionEnd)(void);
在我在各种 SKSpriteNodes 上启动操作的方法中,将该完成块设置为指向安全递减方法:
SignalActionEnd = ^
{
[self DecrementUILockCount];
};
然后在我启动一个动作之前,运行安全增量块。操作完成后,将调用 DecrementUILockCount 以安全地递减计数器。
[self IncrementUILockCount];
[spriteToPerformActionOn runAction:theAction completion:SignalActionEnd];
在我的更新方法中,我只是在重新启用 UI 之前检查该计数器是否为零。
if (0 == runningActionCount)
{
// Do the UI enabled stuff
}
这里唯一要注意的另一件事是,如果您碰巧在完成之前删除了正在运行的任何节点,则完成块也会被删除(不运行),并且您的计数器永远不会递减,您的 UI 也永远不会重新-使能够。答案是检查您要删除的节点上正在运行的操作,如果有任何操作正在运行,则手动运行受保护的递减方法:
if ([spriteToDelete hasActions])
{
// Run the post-action completion block manually.
[self DecrementUILockCount];
}
这对我来说很好 - 希望它有帮助!
我在摆弄滑动式游戏时正在处理这个问题。我既想阻止键盘输入,又想等待尽可能短的时间来执行另一个动作,而瓷砖实际上正在移动。
我关心的所有图块都是同一SKNode
个子类的实例,因此我决定让该类负责跟踪正在进行的动画,并响应有关动画是否正在运行的查询。
我的想法是使用调度组来“计数”活动:它有一个内置的等待机制,并且可以随时添加,这样只要添加了任务,等待就会继续到组。*
这是解决方案的草图。我们有一个节点类,它创建并拥有调度组。私有类方法允许实例访问组,以便他们在制作动画时可以进入和离开。该类有两个公共方法,允许在不暴露实际机制的情况下检查组的状态:+waitOnAllNodeMovement
和+anyNodeMovementInProgress
. 前者阻塞直到组为空;后者只是立即返回一个 BOOL 指示该组是否忙。
@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
想要在移动期间阻止键盘输入的控制器类可以执行以下简单操作:
- (void)keyDown:(NSEvent *)theEvent
{
// Don't accept input while movement is taking place.
if( [WSSNode anyNodeMovementInProgress] ){
return;
}
// ...
}
你可以根据需要在场景中做同样的事情update:
。任何其他必须尽快发生的动作都可以等待动画:
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
});
});
这是该解决方案的一个棘手/混乱的部分:因为该wait...
方法是阻塞的,它显然必须与主线程异步发生;然后我们回到主线程做更多的工作。但是任何其他等待程序也是如此,所以这似乎是合理的。
*出现的另外两种可能性是带有屏障块和计数信号量的队列。
屏障 Block 不起作用,因为我不知道什么时候可以真正入队。在我决定将“之后”任务排入队列时,无法添加“之前”任务。
信号量不起作用,因为它不控制顺序,只是同时性。如果节点在创建时增加信号量,在制作动画时减少,并在完成后再次增加,则其他任务只会在所有创建的节点都在制作动画时等待,并且不会等待比第一次完成更长的时间。如果节点最初没有增加信号量,那么一次只有一个节点可以运行。
调度组的使用很像信号量,但具有特权访问权限:节点本身不必等待。