【问题标题】:How to make a delay in a loop in SpriteKit?如何在 SpriteKit 中的循环中进行延迟?
【发布时间】:2018-07-31 01:28:00
【问题描述】:

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

问候,

【问题讨论】:

    标签: swift sprite-kit delay dispatch-queue


    【解决方案1】:

    这是初学者对事件驱动/基于运行循环/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。您想要制作动画的大部分内容都有动作,以及将其他动作组合成组和序列的动作。在您的情况下,您可能需要 waitunhiderun(_: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 调用完成等待并执行之后。

    【讨论】:

    • 非常感谢您的清晰和好的解释。我想我现在已经明白了很多东西(即使我的英语不完美)。我会尝试你的建议之一。问候,
    猜你喜欢
    • 2020-02-22
    • 2020-02-15
    • 2019-02-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多