在Swift中“暂停”游戏

时间:2016-12-08 16:59:23

标签: ios swift swift3 grand-central-dispatch

我在Swift中创建了一个涉及怪物出现的游戏。怪物出现并消失,基于计时器使用这样的东西:

func RunAfterDelay(_ delay: TimeInterval, block: @escaping ()->()) 
{
    let time = DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)

    DispatchQueue.main.asyncAfter(deadline: time, execute: block)
}

然后我就这样称呼它(例如在2秒后产生):

///Spawn Monster
RunAfterDelay(2) { 
                [unowned self] in
                self.spawnMonster()
 }

然后我做了类似的隐藏事情(在x秒后,我消灭了怪物)。

所以我在屏幕顶部创建了一个设置图标,当你点击它时,一个巨大的矩形窗口似乎会改变游戏设置,但自然问题是怪物仍然在后台产生。如果我将玩家带到另一个屏幕,我相信我将失去我所有的游戏状态,并且如果不重新开始就不能回到它(玩家可能正处于他们游戏的中间位置)。

有没有办法告诉我在上面创建的所有游戏计时器,即

DispatchQueue.main.asyncAfter(deadline: time, execute: block)

当我这样说时暂停和恢复?我想这对所有计时器都很好(如果没有办法标记和暂停某些计时器)。

谢谢!

2 个答案:

答案 0 :(得分:1)

我已经解决了这个问题,并希望在下面的结论中分享我的研究/编码时间。为了更简单地重述问题,我实际上想要实现这一点(不仅仅是使用SpriteKit场景暂停,这非常简单):

  1. 在Swift中启动一个或多个计时器
  2. 停止所有计时器(当用户按下暂停时)
  3. 当用户取消暂停时,所有计时器重新开始,他们停止的地方
  4. 有人曾向我提到,因为我正在使用DispatchQueue.main.asyncAfter无法以我想要的方式暂停/停止(你可以取消但我离题了)。这是有道理的,毕竟我做了asyncAfter。但要实际获得计时器,你需要使用NSTimer(现在在Swift3中它叫做Timer)。

    经过研究,我看到这实际上不可能暂停/取消暂停,所以你"作弊"当您想要重新启动暂停的计时器时,通过创建一个新的计时器(对于每个计时器)。我这样做的结论如下:

    1. 当每个计时器启动时,记录您需要的延迟(我们访问后者)并记录此计时器 " fire"的时间。因此,例如,如果它在3秒内启动并执行代码,则将时间记录为Date()+ 3秒。我使用以下代码实现了这一目标:
    2. //Take the delay you need (delay variable) and add this to the current time
      
      let calendar = Calendar.current        
      let YOUR_INITIAL_TIME_CAPTURED = calendar.date(byAdding: .nanosecond, value: Int(Int64(delay * Double(NSEC_PER_SEC))), to: Date())!
      
      1. 现在您已经记录了计时器开机的时间,您可以等待用户按停止。当它们执行时,您将使用.invalidate()使每个计时器无效并立即记录停止的时间。实际上,此时,您还可以完全计算用户启动时所需的剩余延迟:
      2. //Calculate the remaining delay when you start your timer back
        let elapsedTime = YOUR_INITIAL_TIME_CAPTURED.timeIntervalSince(Date)
        let remainingDelay = YOUR_INITIAL_TIMER_DELAY - elapsedTime
        
        1. 当用户点击开始时,您可以通过简单地创建新的计时器再次启动所有计时器,利用前面提到的余数(remainingDelay)和中提琴你有新的计时器。
        2. 现在因为我有多个计时器,所以我决定在AppDelegate中创建一个字典(通过服务类访问)以保留所有活动的计时器。每当计时器结束时,我都会从字典中删除它。我最终创建了一个特殊的类,它具有计时器,初始延迟和启动时间的属性。从技术上讲,我可以使用一个数组并将定时器键放在该类上,但我离题了..

          我创建了自己的addTimer方法,为每个计时器创建一个唯一的密钥,然后当计时器的代码完成时,它会自动删除,如下所示:

            let timerKey = UUID().uuidString
          
          let myTimer: Timer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) {
                      _ in
                         block()
                         self.timers.removeValue(forKey: timerKey)
                      }
          
                  }
          

          注意:block()只是调用你在计时器中包装的任何块。例如,我做了一些很酷的事情:

          addTimer(delay: 4, repeating: true)
                  { [unowned self] in
                      self.spawnMonster()
                  }
          

          因此addTimer将运行self.spawnMonster代码(如block()),然后在完成后自动从字典中删除。

          我后来变得越来越复杂了,并做了一些事情,比如继续重复计时器运行而不是自我删除,但它只是为我的目的提供了很多非常具体的代码,可能会消耗太多的回复:)

          无论如何,我真的希望这可以帮助某个人,并且愿意回答任何人的任何问题。我花了很多时间在这上面!

          谢谢!

答案 1 :(得分:1)

我将在这里为您展示一些内容,并为未来的读者展示更多内容,因此只需复制粘贴此代码,它们就会有一个可行的示例。接下来是这几件事:

1。使用SKAction

创建计时器

2。暂停操作

3. 暂停节点本身

4。正如我所说,还有一些事情:)

请注意,所有这些都可以通过不同的方式完成,甚至比这更简单(当暂停操作和节点时)但我会向您展示详细的方式,因此您可以选择最适合您的方式。

初始设置

我们有一个英雄节点和一个敌人节点。敌人节点将在屏幕顶部每隔5秒生成一次并向下朝向玩家毒害他。

正如我所说,我们将仅使用SKActions,不使用NSTimer,甚至不使用update:方法。纯粹的行动。所以,在这里,玩家将在屏幕底部静止(紫色方块),并且如前所述,敌人(红色方块)将向玩家行进并将毒害他。

让我们看一些代码。我们需要为所有这些工作定义通常的东西,比如设置物理类别,初始化和节点定位。此外,我们将设置像敌人产卵延迟(8秒)和毒药持续时间(3秒)这样的事情:

//Inside of a GameScene.swift

    let hero = SKSpriteNode(color: .purple , size: CGSize(width: 50, height: 50))
    let button = SKSpriteNode(color: .yellow, size: CGSize(width: 120, height:120))
    var isGamePaused = false
    let kPoisonDuration = 3.0

    override func didMove(to view: SKView) {
        super.didMove(to: view)

        self.physicsWorld.contactDelegate = self

        hero.position = CGPoint(x: frame.midX,  y:-frame.size.height / 2.0 + hero.size.height)
        hero.name = "hero"
        hero.physicsBody = SKPhysicsBody(rectangleOf: hero.frame.size)
        hero.physicsBody?.categoryBitMask = ColliderType.Hero.rawValue
        hero.physicsBody?.collisionBitMask = 0
        hero.physicsBody?.contactTestBitMask = ColliderType.Enemy.rawValue
        hero.physicsBody?.isDynamic = false

        button.position = CGPoint(x: frame.maxX - hero.size.width, y: -frame.size.height / 2.0 + hero.size.height)
        button.name = "button"

        addChild(button)
        addChild(hero)

        startSpawningEnemies()

    }

还有一个名为isGamePaused的变量,我稍后会对此进行评论,但正如您可以想象的那样,其目的是跟踪游戏是否暂停,当用户点击黄色方形按钮时其值会发生变化。

帮助方法

我已经为节点创建了一些辅助方法。我觉得这对你个人来说并不是必需的,因为你看起来对编程有很好的理解,但我会为完整性和未来的读者而努力。因此,您可以在此处设置节点名称或其物理类别......以下是代码:

 func getEnemy()->SKSpriteNode{

            let enemy = SKSpriteNode(color: .red , size: CGSize(width: 50, height: 50))
            enemy.physicsBody = SKPhysicsBody(rectangleOf: enemy.frame.size)
            enemy.physicsBody?.categoryBitMask = ColliderType.Enemy.rawValue
            enemy.physicsBody?.collisionBitMask = 0
            enemy.physicsBody?.contactTestBitMask = ColliderType.Hero.rawValue
            enemy.physicsBody?.isDynamic = true
            enemy.physicsBody?.affectedByGravity = false
            enemy.name = "enemy"

            return enemy
        }

另外,我用它的实际产卵分开了敌人的创造。因此,在此创建意味着创建,设置和返回稍后将添加到节点树的节点。产卵意味着使用先前创建的节点将其添加到场景中,并对其执行动作(移动动作),以便它可以向玩家移动:

func spawnEnemy(atPoint spawnPoint:CGPoint){

        let enemy = getEnemy()

        enemy.position = spawnPoint

        addChild(enemy)

        //moving action

        let move = SKAction.move(to: hero.position, duration: 5)

        enemy.run(move, withKey: "moving")
    }

我认为没有必要在这里讨论产卵方法,因为它非常简单。让我们进一步了解产卵部分:

SKAction计时器

这是一种每x秒产生一次敌人的方法。每当我们暂停与产生"产生相关的动作时,它将被暂停。键。

func startSpawningEnemies(){

        if action(forKey: "spawning") == nil {

            let spawnPoint = CGPoint(x: frame.midX, y: frame.size.height / 2.0 - hero.size.height)

            let wait = SKAction.wait(forDuration: 8)

            let spawn = SKAction.run({[unowned self] in

                self.spawnEnemy(atPoint: spawnPoint)
            })

            let sequence = SKAction.sequence([spawn,wait])

            run(SKAction.repeatForever(sequence), withKey: "spawning")
        }
    }

生成节点后,它最终会与英雄发生碰撞(更准确地说,它会发生联系)。这就是物理引擎发挥作用的地方......

检测联系人

当敌人在旅行时,它最终会到达玩家,我们将注册该联系人:

func didBegin(_ contact: SKPhysicsContact) {

        let contactMask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask

        switch contactMask {

        case ColliderType.Hero.rawValue | ColliderType.Enemy.rawValue :


            if let projectile = contact.bodyA.categoryBitMask == ColliderType.Enemy.rawValue ? contact.bodyA.node : contact.bodyB.node{

                projectile.removeAllActions()
                projectile.removeFromParent()
                addPoisionEffect(atPoint: hero.position)

            }

        // Handle more cases here

        default : break
            //Some other contact has occurred
        }
    }

联系检测代码来自here (from author Steve Ives).

我不会讨论SpriteKit中的联系处理是如何工作的,因为我会过多地讨论这个问题。因此,当注册英雄与射弹之间的接触时,我们做的事情很少:

1。停止对射弹的所有动作,使其停止移动。我们可以通过直接停止移动操作来实现这一点,稍后我会告诉你如何做到这一点。

2。从父母身上移除射弹,因为我们不再需要它了。

3. 通过添加发射器节点添加中毒效果(我使用Smoke模板在粒子编辑器中创建了该效果)。

以下是步骤3的相关方法:

func addPoisionEffect(atPoint point:CGPoint){

        if let poisonEmitter = SKEmitterNode(fileNamed: "poison"){

            let wait = SKAction.wait(forDuration: kPoisonDuration)

            let remove = SKAction.removeFromParent()

            let sequence = SKAction.sequence([wait, remove])

            poisonEmitter.run(sequence, withKey: "emitAndRemove")
            poisonEmitter.name = "emitter"
            poisonEmitter.position = point

            poisonEmitter.zPosition = hero.zPosition + 1

            addChild(poisonEmitter)

        }  
    }

正如我所说,我会提到一些对你的问题不重要的事情,但在SpriteKit中做这一切时至关重要。发射完成后不会移除SKEmitterNode。它停留在节点树中并占用资源(以百分之几为单位)。这就是为什么你必须自己删除它。您可以通过定义两个的操作顺序来完成此操作。第一个是等待给定时间的SKAction(直到发射完成),第二个项目将是一个动作,当时间到来时将从其父项中移除发射器。

最后 - 暂停:)

负责暂停的方法称为togglePaused(),当点按黄色按钮时,它会根据isGamePaused变量切换游戏的暂停状态:

func togglePaused(){

        let newSpeed:CGFloat = isGamePaused ? 1.0 : 0.0

        isGamePaused = !isGamePaused

        //pause spawning action
        if let spawningAction = action(forKey: "spawning"){

            spawningAction.speed = newSpeed
        }

        //pause moving enemy action
        enumerateChildNodes(withName: "enemy") {
            node, stop in
            if let movingAction = node.action(forKey: "moving"){

                movingAction.speed = newSpeed
            }

        }

        //pause emitters by pausing the emitter node itself
        enumerateChildNodes(withName: "emitter") {
            node, stop in

            node.isPaused = newSpeed > 0.0 ? false : true

        }
    }

这里发生的事情实际上很简单:我们通过使用先前定义的键(产卵)抓住它来停止产生动作,并且为了阻止它我们将动作的速度设置为零。要取消暂停,我们将执行相反的操作 - 将操作速度设置为1.0。这也适用于移动动作,但由于可以移动许多节点,我们将枚举场景中的所有节点。

为了向您显示差异,我直接暂停SKEmitterNode,因此您还可以通过另一种方式暂停SpriteKit中的内容。当节点暂停时,其子节点的所有动作和动作也会暂停。

剩下要提到的是我在touchesBegan中检测到按下了按钮,并且每次都运行togglePaused()方法,但我认为代码并不是真的需要。

视频示例

为了举一个更好的例子,我记录了一整件事。因此,当我点击黄色按钮时,所有操作都将停止。意味着产卵,移动和毒害效果(如果存在)将被冻结。通过再次点击,我将取消所有内容。所以这是结果:

video

在这里你可以(清楚地?)看到当一个敌人击中一名玩家时,我会暂停整个事情,比如命中发生后1-1.5秒。然后我等了5秒左右,我取消了所有的事情。您可以看到发射器继续发射一两秒,然后它就消失了。

请注意,当一个发射器未被启用时,它看起来并不像它真的是未经过停顿的:),而是看起来即使发射器暂停也会发射粒子(实际上是真的)。这是一个bug on iOS 9.1,我在这个设备上仍然在iOS 9.1上:)所以在iOS 10中,它是固定的。

结论

您在SpriteKit中不需要NSTimer来处理此类事情,因为SKActions就是为此而设的。正如您所看到的,当您暂停操作时,整个操作将停止。产卵停止,移动停止,就像你问的那样......我已经提到有一种更简单的方法可以做到这一切。也就是说,使用容器节点。因此,如果所有节点都在一个容器中,则只需暂停容器节点即可停止所有节点,操作和所有节点。就那么简单。但我只是想告诉你如何通过一个键来抓住一个动作,或者暂停一个节点,或者改变它的速度......希望这有帮助并且有意义!

相关问题