"Pausing" Swift 中的游戏

"Pausing" the Game in Swift

我在 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 秒后,我消灭了怪物)。

所以我在屏幕顶部创建了一个设置图标,当你点击它时,会出现一个巨大的矩形 window 来更改游戏设置,但问题是怪物仍然在后台生成.如果我将玩家快速转移到另一个屏幕,我相信我会失去所有的游戏状态,并且如果不从头开始就无法返回(玩家可能正在玩游戏)。

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

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

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

谢谢!

我已经解决了这个问题,并想在下面的结论中分享我的 research/coding 时间。为了更简单地重申问题,我实际上想实现这个(不是简单地使用 SpriteKit 场景暂停,这很容易):

  1. 在Swift
  2. 中启动一个或多个计时器
  3. 停止所有计时器(当用户按下暂停键时)
  4. 当用户取消暂停时,所有计时器都会重新开始,他们停止的地方

有人向我提到,因为我正在使用 DispatchQueue.main.asyncAfter,所以无法以我想要的方式 pause/stop(您可以取消,但我离题了)。这是有道理的,毕竟我正在做一个 asyncAfter。但是要真正让计时器运行,您需要使用 NSTimer(现在在 Swift3 中它被称为 Timer)。

经过研究,我发现 这实际上是不可能的 到 pause/unpause 所以你 "cheat" 通过在需要时创建一个新计时器(为每个计时器)重新启动暂停的计时器。我的结论如下:

  1. 当每个计时器启动时,记录您需要的延迟(我们访问后者)并记录该计时器"fire"的时间。因此,例如,如果它在 3 秒后开始并执行代码,则将时间记录为 Date() + 3 秒。我使用以下代码实现了这一点:
//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() 使每个计时器无效并立即记录停止的时间。其实至此,你也可以完全计算出用户启动时所需要的剩余延迟为:
//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) 和中提琴,您就有了新计时器。

现在因为我有多个计时器,所以我决定需要在我的 AppDelegate(通过服务 class 访问)中创建一个字典来保留我所有的活动计时器。每当计时器结束时,我都会将其从字典中删除。我最终制作了一个特殊的 class,它具有计时器、初始延迟和开始时间的属性。从技术上讲,我可以使用数组并将计时器键放在上面 class,但我离题了..

我创建了自己的 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())然后它会在完成后自动从字典中删除。

后来我变得更复杂了,做了一些事情,比如不断重复定时器 运行ning 而不是自我删除,但这只是很多非常具体的代码,用于我的目的,可能会消耗太多此回复:)

无论如何,我真的希望这对某人有所帮助,并且很乐意回答任何人提出的任何问题。我在这上面花了很多时间!

谢谢!

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

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
        }

此外,我将敌人的创建与其实际生成分开了。所以这里的创建意味着创建、设置和 return 一个节点,该节点稍后将被添加到节点树中。生成意味着使用先前创建的节点将其添加到场景中,并对其进行 运行 动作(移动动作),以便它可以向玩家移动:

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")
    }

我觉得这里没必要再讲产卵方法了,因为很简单。让我们更进一步到产卵部分:

SK动作计时器

这是一种每 x 秒生成一次敌人的方法。每次我们暂停与 "spawning" 键关联的操作时,它都会暂停。

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() 方法中检测,但我认为代码并不是真正需要的。

视频示例

为了做一个更好的例子,我记录了整个事情。所以当我点击黄色按钮时,所有的动作都会停止。意味着产卵、移动和中毒效果(如果存在)将被冻结。再次点击,我将取消暂停一切。所以这是结果:

在这里你可以(清楚地?)看到当敌人击中玩家时,我会暂停整个过程,比如击中发生后 1-1.5 秒。然后我等了大约 5 秒,然后取消暂停所有内容。您可以看到发射器继续发射一两秒,然后消失。

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

结论

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