我在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)
当我这样说时暂停和恢复?我想这对所有计时器都很好(如果没有办法标记和暂停某些计时器)。
谢谢!
答案 0 :(得分:1)
我已经解决了这个问题,并希望在下面的结论中分享我的研究/编码时间。为了更简单地重述问题,我实际上想要实现这一点(不仅仅是使用SpriteKit场景暂停,这非常简单):
有人曾向我提到,因为我正在使用DispatchQueue.main.asyncAfter无法以我想要的方式暂停/停止(你可以取消但我离题了)。这是有道理的,毕竟我做了asyncAfter。但要实际获得计时器,你需要使用NSTimer(现在在Swift3中它叫做Timer)。
经过研究,我看到这实际上不可能暂停/取消暂停,所以你"作弊"当您想要重新启动暂停的计时器时,通过创建一个新的计时器(对于每个计时器)。我这样做的结论如下:
//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())!
//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
现在因为我有多个计时器,所以我决定在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()
方法,但我认为代码并不是真的需要。
视频示例
为了举一个更好的例子,我记录了一整件事。因此,当我点击黄色按钮时,所有操作都将停止。意味着产卵,移动和毒害效果(如果存在)将被冻结。通过再次点击,我将取消所有内容。所以这是结果:
在这里你可以(清楚地?)看到当一个敌人击中一名玩家时,我会暂停整个事情,比如命中发生后1-1.5秒。然后我等了5秒左右,我取消了所有的事情。您可以看到发射器继续发射一两秒,然后它就消失了。
请注意,当一个发射器未被启用时,它看起来并不像它真的是未经过停顿的:),而是看起来即使发射器暂停也会发射粒子(实际上是真的)。这是一个bug on iOS 9.1,我在这个设备上仍然在iOS 9.1上:)所以在iOS 10中,它是固定的。
结论
您在SpriteKit中不需要NSTimer
来处理此类事情,因为SKActions
就是为此而设的。正如您所看到的,当您暂停操作时,整个操作将停止。产卵停止,移动停止,就像你问的那样......我已经提到有一种更简单的方法可以做到这一切。也就是说,使用容器节点。因此,如果所有节点都在一个容器中,则只需暂停容器节点即可停止所有节点,操作和所有节点。就那么简单。但我只是想告诉你如何通过一个键来抓住一个动作,或者暂停一个节点,或者改变它的速度......希望这有帮助并且有意义!