【问题标题】:Know when all SKActions are complete or there aren't any running知道所有 SKAction 何时完成或没有任何运行
【发布时间】:2013-12-11 00:23:13
【问题描述】:

我有许多SKActions 在各个节点上运行。我怎么知道它们什么时候都完成了?我想在动画运行时忽略触摸。如果我能以某种方式在多个节点上并行运行动作,我可以等待最终动作运行,但我看不到任何方式来协调跨节点的动作。

我可以通过遍历所有场景的孩子并检查每个孩子的hasActions 来伪造这一点。看起来有点蹩脚,但确实有效。

【问题讨论】:

    标签: ios cocoa-touch animation sprite-kit skaction


    【解决方案1】:

    据我所知,没有办法通过默认框架功能来做到这一点。

    但是,我认为您可以通过创建一个类来实现类似的效果,该类的方法充当在节点上调用 SKAction runAction: 的包装器。

    在该包装方法中,您可以将节点推送到数组中,然后将performSelector 操作附加到每个操作/组/序列。因此,您指定的任何方法都会在操作/组/序列完成后调用。调用该方法时,您可以从数组中删除该节点。

    使用此实现,您将始终拥有一个包含当前在其上运行操​​作的所有节点的数组。如果数组为空,则没有运行。

    【讨论】:

    • 仍然不是一个理想或简单的解决方案。它可能比运行显示列表的完整层次结构更有效。但是,如果您只是在游戏关卡结束时停止动作等情况下使用它,我可能会坚持使用您已经拥有的东西!
    • 如果你创建 int 变量而不是数组,它包含许多正在运行的动作,那不是更有效吗? :)
    • 我想到了后者,但又担心不同步。这将被重复使用,如开始操作 - 等到完成 - 开始更多操作 - 等待等。似乎我依赖 SK 内部未知的实现细节来处理这个问题。
    • 很奇怪,Apple 选择忽略这个问题。
    • @ahwulf 我不知道我是否会使用“忽略”这个词,但他们肯定有一个优先级列表,这在 SpriteKit 的第一个版本中并没有促进这一点。我个人对他们将这个 SpriteKit 与第一次迭代中包含的最重要的方面放在一起印象深刻。对于这个特定问题,它是可管理的,您可以开发自己的解决方案来处理。这可能导致它的优先级较低。
    【解决方案2】:

    您运行的每个动作都有一个持续时间。如果您跟踪最长运行动作的持续时间,您就会知道它何时完成。使用它来等待最长运行的操作完成。

    或者,保留一个全局计数器来记录正在运行的操作。每次您运行暂停输入的操作时,都会增加计数器。您运行的每个操作都需要一个最终执行块,然后减少计数器。如果计数器为零,则没有任何输入忽略操作正在运行。

    【讨论】:

    • 我想同时做这两件事,但我担心会不同步。
    【解决方案3】:

    自从这个问题首次发布以来的两年里,Apple 似乎还没有扩展框架来处理这个案例。我犹豫要不要做一堆图遍历来检查正在运行的动作,所以我找到了一个解决方案,它使用了我的 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);
    

    在我在各种 SKSpriteNode 上启动操作的方法中,将完成块设置为指向安全递减方法:

    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];
    }
    

    这对我来说很好 - 希望它有所帮助!

    【讨论】:

    • 实际上,我最终自己在 Swift 中创建了一些在我正在开发的游戏中运行的东西……主要是为了学习 Swift。 thecodist.com/article/…
    【解决方案4】:

    我在玩一个滑动式游戏时正在处理这个问题。我既想阻止键盘输入,又想等待尽可能短的时间执行另一个操作,同时瓷砖实际上正在移动。

    我关心的所有图块都是同一 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 不起作用,因为我不知道什么时候可以真正将它排入队列。在我决定将“之后”任务排入队列时,无法添加“之前”任务。

    信号量不起作用,因为它不控制顺序,只是同时性。如果节点在创建时增加信号量,在制作动画时减少,并在完成后再次增加,则其他任务只会在 所有 创建的节点正在制作动画时等待,并且不会等待比第一次完成。如果节点最初没有增加信号量,那么它们一次只能运行一个。

    调度组的使用很像信号量,但具有特权访问权限:节点本身不必等待。

    【讨论】:

    • 如果您使用dispatch_group_notify 而不是dispatch_group_wait,则waitOnAllNodeMovement 函数不需要阻塞
    • 不幸的是,这与我提到的屏障 Block 选项存在相同的问题:无法知道所有“之前”任务何时已提交,因此您无法知道何时调用它。
    【解决方案5】:

    最简单的方法是使用dispatch group。在 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() 块在所有其他块离开调度组之后运行。

    【讨论】:

    • 很好的答案!这个解决方案完全符合我的预期,即在许多对象上等待一堆 SCNAction。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-25
    相关资源
    最近更新 更多