【问题标题】:"Pausing" the Game in Swift在 Swift 中“暂停”游戏
【发布时间】:2016-12-08 16:59:22
【问题描述】:

我用 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)

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

谢谢!

【问题讨论】:

  • 不使用 GCD,为什么不直接使用包含 2 个 SKAction 的 SKAction 序列:一个计时器和一个生成敌人的闭包?我会为暂停或不暂停设置一个枚举,并在暂停时清除 SKAction,并在未暂停时恢复 SKAction。

标签: ios swift swift3 grand-central-dispatch


【解决方案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
        }

另外,我将敌人的创建与实际生成分开。所以在这里创建意味着创建、设置和返回一个节点,该节点稍后将添加到节点树中。 Spawning 是指使用之前创建的节点将其添加到场景中,并对其运行动作(移动动作),使其可以向玩家移动:

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

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

SKA 动作计时器

这是一种每 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 模板制作了该效果)。

第三步的相关方法如下:

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 发射完成后不会被移除。它停留在节点树中并消耗资源(以一定百分比)。这就是为什么你必须自己删除它。您可以通过定义两个items 的动作序列来做到这一点。第一个是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 秒左右,然后我取消暂停所有内容。您可以看到发射器继续发射一两秒钟,然后消失。

请注意,当发射器未暂停时,它看起来并不是真的未暂停:),而是看起来即使发射器暂停了粒子也在发射(实际上是这样)。这是bug on iOS 9.1,我仍在此设备上使用 iOS 9.1 :) 所以在 iOS 10 中,它已修复。

结论

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

【讨论】:

  • 嘿,感谢您抽出宝贵时间来做这件事;我敢肯定这不是你在 5 分钟内搞定的,为此我非常感激。我会阅读这篇文章并尽可能多地吸收:) 不过有一个问题 - 假设你想毒死玩家,但没有任何来自 SpriteKit 的东西(玩家类有一个属性作为对他的一系列效果,每个效果上面有一个计时器,可以持续多长时间)。当您暂停游戏时,这种情况下毒药效果是否停止(根据您的代码)?我看到你添加了一个toxicEmitted,但这是它暂停的原因吗? b/c 它是一个 SpriteKit 对象吗?泰!!
  • 假设您在默认设置为false 的播放器类上有一个名为isPoisoned 的属性。要毒化播放器,您可以像我的代码中那样运行一个序列,但不是添加一个发射器,而是将此变量设置为 true(使用runBlock),而不是从其父级中删除一个发射器,而是设置@ 987654350@ 为假。要暂停效果,请暂停播放器或动作。我不知道您提到的数组的逻辑是什么(它是什么类型以及所有内容),但是,同样的逻辑适用。当玩家中毒时,你会适当地更新一个数组(使用动作)......
  • 如果您不清楚我在说什么,请随时提问,因为有时切换到SKActions 的思维方式需要时间,但是当您习惯它们时,这一切都将是对你来说很容易。
  • 好的,谢谢,我明天要试试这个:) 你认为按照我提到的原始方式进行操作可能会在实际设备上造成内存和内存崩溃问题吗?即计时器增加了开销
【解决方案2】:

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

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

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

经过研究,我发现这实际上不可能暂停/取消暂停,因此当您想重新启动暂停的计时器时,您通过创建一个新计时器(为每个计时器)“作弊”。我的结论如下:

  1. 当每个计时器启动时,记录您需要的延迟(我们会访问后者)并记录此计时器“触发”的时间。因此,例如,如果它在 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) 和 viola`,您就有了新计时器。

现在因为我有多个计时器,我决定我需要在我的 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()),然后它会在完成后自动从字典中删除。

我后来变得更加复杂,并且做了诸如保持重复计时器运行而不是自我删除之类的事情,但这只是我的目的的很多非常具体的代码,可能会消耗太多的回复:)

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

谢谢!

【讨论】:

  • 现在你宁愿做所有这些而不是使用默认实现所有这些的SKActions?我不明白。此外,无论您使用 NSTimer 做什么,SKActions 都能够以更优雅的方式执行相同的操作,并且适用于 SpriteKit 中与时间相关的操作 :)
  • 感谢您的回复! :) SKActions 不能在可以暂停的计时器上运行,对吧?我可能想让玩家中毒(他每秒受到 - 健康伤害)或让他无敌 10 秒。我希望这些效果暂停,以及怪物暂停。现在一切都在一个保护伞下,当我暂停时,所有这些计时器都会停止!
  • 其实我看不到你的意思。这当然可以通过行动来实现,这就是他们的工作方式。假设你想让玩家无敌一段时间。只需在播放器上设置一个 bool 属性,然后运行将在 10 秒后触发的操作来更改此值(并在需要时应用视觉效果)。所以这将是一个延迟 10 秒和一个(完成)块的动作序列?如果您在按钮上暂停游戏,则可以通过其键访问该特定操作并暂停它(或暂停播放器节点...或暂停容器节点...),一切都会停止。我在这里没有看到问题:)
  • 重点是,忘记 NSTimer。 SKActions 可以做到这一切。在这种情况下,SKAction 序列将充当计时器。该计时器将每 n 秒执行一次(其他操作或代码块)(并将根据您的游戏需要自动或手动暂停)。如果您需要任何这方面的示例,我可以在今天晚些时候使用计算机时给出答案。
  • 当然我很想看到你这样做。我不相信它会按照我想要的方式做,但我很想看到它。基本上发生了两件事:玩家随机中毒,每秒损失 1 点生命值,但如果游戏暂停,计时器会停止,但当他取消暂停时会恢复。并且怪物每隔几秒随机生成一次,并在 3 秒后消失。但是如果你在生成过程中暂停,怪物应该会在 1.5 秒内消失,而不是立即消失(这对我来说就是这样)。谢谢我真的很高兴看到这个:)
猜你喜欢
  • 2016-01-09
  • 1970-01-01
  • 2016-11-04
  • 2015-03-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-14
  • 1970-01-01
相关资源
最近更新 更多