Swift中的“暂停”游戏

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

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秒后,我把这个怪物去掉了)。

所以我在屏幕上方创build了一个设置图标,当你点击它的时候,一个巨大的矩形窗口会改变游戏设置,但是问题自然就是在后台产生的怪物。 如果我把玩家甩到另一个屏幕上,我相信我会失去我所有的游戏状态,如果没有全部开始(玩家可能处于游戏中),就不能回到游戏状态。

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

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

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

谢谢!

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

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

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

经过研究,我发现实际上不可能暂停/取消暂停,所以当你想重新启动已暂停的定时器时,通过创build一个新的定时器(为每一个定时器)“欺骗”。 我的结论是这样的:

  1. 当每个定时器启动时,logging你需要的延迟(我们访问后者),并logging这个定时器 “触发”的时间。 例如,如果它在3秒内启动,并执行代码,则logging时间为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. 现在您已经logging了您的计时器将触发的时间,您可以等待用户按停止。 当他们这样做时,你将使用.invalidate()使每个定时器无效,并立即logging停止的时间。 事实上,在这一点上,您也可以完全计算用户启动时所需的剩余延迟:
 //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. 当用户点击开始时,您可以通过简单地创build新的定时器再次启动所有的定时器,利用上述剩余(remainingDelay)和中提琴您有你的新定时器。

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

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

  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.使用SKAction创build一个计时器

2.暂停一个行动

3.暂停节点本身

正如我所说的,还有更多的东西:)

请注意,所有这些都可以以不同的方式完成,甚至比这更简单(当涉及到暂停动作和节点时),但是我会告诉你详细的方式,所以你可以select最适合你的作品。

初始设置

我们有一个英雄节点和一个敌人节点。 敌方节点会在屏幕上方每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() } 

还有一个叫做isGamePausedvariables,我会在后面评论,但是你可以想象,它的目的是跟踪游戏是否暂停,当用户点击黄色的方形button时,它的值会改变。

帮手方法

我已经为创build节点做了一些辅助方法。 我有一种感觉,这不是你个人所需要的,因为你看起来对编程有很好的理解,但是我会为了完整性和未来的读者而做。 所以这是你设置节点名称或物理类别的地方…这里是代码:

  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 } 

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

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

联系人检测代码是从这里借用(来自作者Steve Ives)。

我不想深入了解SpriteKit中的联系人处理方式,因为我会以这种方式去讨论太多的问题。 所以当英雄和弹丸之间的联系被注册时,我们做的事情很less:

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() ,当点击黄色button时,它基于isGamePausedvariables来切换游戏的暂停状态:

 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中暂停。 当节点暂停时,其子节点的所有动作和动作也暂停。

剩下要提的是,如果按下button,我会在touchesBegan检测,并且每次都运行togglePaused()方法,但是我认为这个代码并不是真的需要。

video示例

举一个更好的例子,我logging了一件事情。 所以当我点击黄色button,所有的行动将被停止。 意味着产卵,移动和毒效如果存在的话将被冻结。 再次点击,我会解除所有的一切。 所以这里是结果:

视频

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

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

结论

你不需要NSTimer在SpriteKit的这种东西,因为SKActions是为了这个。 正如你所看到的,当你暂停动作的时候,整个事情就会停止。 产卵停止,移动停止,就像你问…我已经提到有一个更简单的方法来做这一切。 就是说,使用一个容器节点。 所以如果你所有的节点都在一个容器中,所有的节点,动作和所有的东西都只能通过暂停容器节点来停止。 就那么简单。 但是我只是想告诉你如何通过一个按键来抓取动作,或者暂停节点,或者改变它的速度……希望这有助于并且合理。