3

我在这里和那里进行了一些搜索,但我没有找到(或理解)如何在 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
        }
    }

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

所以,如果你能帮助我理解为什么,那就太好了。我不挑剔,我不能用“for”循环来回答。

问候,

4

1 回答 1

8

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

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

因此,如果您在更新方法、事件处理程序或初始设置代码中有一个循环,则该循环中发生的所有事情都将在 SpriteKit 有机会绘制任何内容之前发生。也就是说,如果我们暂时忽略您问题的“延迟”部分,像这样的循环......

var i = 0
while i <= 3 {
    let oneSprite = allSprites[i]
    self.addChild(oneSprite)
}

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

解决问题

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

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

  1. 给你的每个精灵一个唯一的名字:

    let sprite1 = // ...
    sprite1.name = "sprite1"
    // etc
    
  2. 将所有节点添加到您的场景中,但将您不希望显示的节点保持隐藏:

    let allSprites : [SKSpriteNode] = [sprite1, sprite2, sprite3, sprite4]
    for sprite in allSprites {
        sprite.isHidden = true
        self.addChild(sprite)
    }
    
  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)
    

那应该完成你正在寻找的东西。(免责声明:在网络浏览器中编写的代码可能需要调整。)如果您想更好地了解情况,请继续阅读...

其他选择

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

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

关于DispatchQueue.asyncAfter...

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

一个简化的解释:还记得现代 CPU 有多个内核吗?当 CPU 0 正在执行你的while循环时,它会到达asyncAfter调用并将一条注释传递给 CPU 1,说等待一秒钟,然后执行附加到该asyncAfter调用的闭包主体。一旦它通过了该注释,CPU 0 就会愉快地继续前进——它不会等待 CPU 1 接收节点并完成该注释指定的工作。

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

于 2018-02-20T18:51:24.933 回答