如何在SpriteKit中循环延迟?

Vin*_*ent 1 delay sprite-kit swift dispatch-queue

我在这里和那里进行了一些搜索,但我没有找到(或理解)如何在SpriteKit中循环延迟.

这是个想法:我有一些SKSpriteNodes按数组排序,我想在屏幕上显示它们,每秒一个.问题是......我根本无法做到这一点(对不起,我对此很新).

       let sprite1 = SKSpriteNode(color: .red, size: CGSize(width: 20, height: 20))
    sprite1.position = CGPoint(x: 100, y: 100)

    let sprite2 = SKSpriteNode(color: .red, size: CGSize(width: 20, height: 20))
    sprite2.position = CGPoint(x: 100, y: 300)

    let sprite3 = SKSpriteNode(color: .red, size: CGSize(width: 20, height: 20))
    sprite3.position = CGPoint(x: 300, y: 100)

    let sprite4 = SKSpriteNode(color: .red, size: CGSize(width: 20, height: 20))
    sprite4.position = CGPoint(x: 300, y: 300)

    let allSprites : [SKSpriteNode] = [sprite1, sprite2, sprite3, sprite4]
    let delay = 1.0
    var i = 0
    while i <= 3
    {
        let oneSprite = allSprites[i]
        self.addChild(oneSprite)


       DispatchQueue.main.asyncAfter(deadline : .now() + delay) {
            i = i + 1
        }
    }
Run Code Online (Sandbox Code Playgroud)

如果你问:这根本不起作用.似乎没有读取DispatchQueue.main.asyncAfter中的内容.

所以,如果你能帮我理解原因,那就太好了.我不挑剔,我不能用"for"循环来回答.

问候,

ric*_*ter 6

这是对事件驱动/基于运行循环的/ GUI编程系统的常见初学者误解.一些关键点可帮助您走上正轨:

  • SpriteKit试图每秒渲染场景60(左右).
  • 这意味着SpriteKit内部有一个循环,每帧一次,它调用你的代码询问什么是新的(更新),然后运行自己的代码来绘制更改(渲染).
  • 设置代码或响应事件而发生的事情(点击/点击/按钮)在此循环外部,但是输入:事件改变状态,渲染循环作出反应.

因此,如果你在更新方法,事件处理程序或初始设置代码中有一个循环,那么在循环中发生的一切都将在SpriteKit有机会绘制任何一个之前发生.也就是说,如果我们暂时忽略你问题的"延迟"部分,就像这样的循环......

var i = 0
while i <= 3 {
    let oneSprite = allSprites[i]
    self.addChild(oneSprite)
}
Run Code Online (Sandbox Code Playgroud)

...将导致在循环开始之前没有任何精灵可见,并且所有精灵在完成后都可见.循环中没有引入延迟的数量会改变这一点,因为SpriteKit在循环结束之后才第一次获得绘制机会.

解决问题

在像SpriteKit这样的系统中进行动画和延迟的最佳方法是利用它为您提供的声明处理动画的工具.也就是说,你告诉SpriteKit你想要在接下来的几百个帧中发生什么,而SpriteKit就会发生这种情况 - 每次通过更新/渲染循环时,SpriteKit都会确定需要在场景中进行哪些更改.完成你的动画.例如,如果你以60 fps的速度运行,并且你要求某个精灵的淡出动画持续一秒,那么每个帧它会将精灵的不透明度降低1/60.

SpriteKit的声明性动画工具是SKAction.对于您想要制作动画的大部分内容,以及将其他动作组合成组和序列的动作都有动作.在您的情况下,您可能想要waitunhiderun(_:onChildWithName)动作的某种组合:

  1. 给每个精灵一个独特的名字:

    let sprite1 = // ...
    sprite1.name = "sprite1"
    // etc
    
    Run Code Online (Sandbox Code Playgroud)
  2. 将所有节点添加到场景中,但保留您不希望隐藏的节点:

    let allSprites : [SKSpriteNode] = [sprite1, sprite2, sprite3, sprite4]
    for sprite in allSprites {
        sprite.isHidden = true
        self.addChild(sprite)
    }
    
    Run Code Online (Sandbox Code Playgroud)
  3. 创建一个序列操作,通过告知每个节点取消隐藏来交替延迟,并在场景上运行该操作:

    let action = SKAction.sequence([
        run(.unhide(), onChildNodeWithName: "sprite1"),
        wait(forDuration: 1.0),
        run(.unhide(), onChildNodeWithName: "sprite2"),
        wait(forDuration: 1.0),
        // etc
    ])
    self.run(action)
    
    Run Code Online (Sandbox Code Playgroud)

那应该可以实现你想要的.(免责声明:使用网络浏览器编写的代码,可能需要调整.)如果您想更好地了解情况,请继续阅读...

其他选择

如果您真的想了解更新/渲染循环的工作原理,您可以直接参与该循环.我不会推荐它,因为它有更多的代码和簿记,但知道它很有用.

  1. 保留要添加或取消隐藏的节点的队列(阵列).
  2. 在场景的update方法中,记录已经过了多少时间.
  3. 如果自上次显示节点以来至少已经过了1.0秒(或者你所追求的任何延迟),请在数组中添加第一个节点,然后将其从数组中删除.
  4. 当数组为空时,你就完成了.

关于DispatchQueue.asyncAfter......

这对于完成任务不是必不可少的,但有助于理解,因此您以后不会遇到类似的问题:您用来尝试延迟事务的调用中的"async"一词是"异步"的缩写.异步意味着您正在编写的代码不按您编写的顺序执行.

简化说明:请记住,现代CPU有多个内核?当CPU 0正在执行while循环时,它会进入asyncAfter调用并将注释传递给CPU 1,说等待一秒钟,然后执行附加到该asyncAfter调用的闭包主体.一旦它通过该注释,CPU 0就会继续快速继续 - 它不会等待CPU 1接收节点并完成注释指定的工作.

仅仅是阅读代码,我不是100%清楚这种方法如何失败,但它确实失败了.你有一个无限循环,因为while条件永远不会改变,因为要更新的工作i发生在其他地方而不是传播回本地范围.或者i要做的更改会传播回去,但只有在循环旋转不确定的次数后才等待四次asyncAfter调用完成等待和执行.