Таймеры в комплекте Sprite

У меня нет опыта работы с Sprite Kit. Мне было интересно, есть ли что-то похожее на Cocos2D планировщики в Sprite Kit? Если нет, то что следует использовать NSTimer - единственный вариант? Я полагаю, что если единственный вариант использует NSTimer, нам нужно вручную обрабатывать случай, когда приложение находится в фоновом режиме. Спасибо.

Ответ 1

Для достижения функциональности, аналогичной планировщику кокосов, вы можете использовать SKAction.

Например, для достижения чего-то подобного

[self schedule:@selector(fireMethod:) interval:0.5];

используя SKAction. Вы должны написать это

SKAction *wait = [SKAction waitForDuration:0.5];
SKAction *performSelector = [SKAction performSelector:@selector(fireMethod:) onTarget:self];
SKAction *sequence = [SKAction sequence:@[performSelector, wait]];
SKAction *repeat   = [SKAction repeatActionForever:sequence];
[self runAction:repeat]; 

Это не лучший взгляд и не обладает некоторой гибкостью CCScheduler, но он остановится на фоне, приостановке сцены/просмотра и т.д. + это похоже на игру с LEGO:)

Ответ 2

Я сделал demo для простого планировщика для использования с Sprite Kit в Swift.

Невозможно управлять NSTimers из-за цикла фонового цикла приложения, и SKActions может быть не очень подходит для этого (для одного - создание запланированных событий, так как SKAction - это боль и не очень читаемая в конечном итоге + она делает не заботятся о приостановленном состоянии SKScene).

Подход, который я принял, состоял в том, чтобы развернуть пользовательский планировщик, который позволяет писать код наподобие:

Расписание повторяющегося события

scheduler
  .every(1.0) // every one second
  .perform( self=>GameScene.updateElapsedTimeLabel ) // update the elapsed time label
  .end()

Запланировать событие в течение определенного времени

scheduler
  .at(10.0) // ten seconds after game starts
  .perform( self=>GameScene.createRandomSprite ) // randomly place a sprite on the scene
  .end()

Запланировать событие позже и повторить 5 раз

scheduler
  .after(10.0) // ten seconds from now
  .perform( self=>GameScene.createRandomSprite ) // randomly place a sprite on the scene
  .repeat(5) // repeat 5 times
  .end()

Как это работает?

Вкратце, планировщик - это класс, который содержит приоритетную очередь событий планировщика. Планировщик поддерживает переменную, которая представляет прошедшее время в игре. При каждом обновлении фрейма:

  • Планировщик обновляет свою истекшую временную привязку
  • Проверяет наличие очереди в очереди приоритетов, если необходимо запустить любые элементы в очереди; если он находит их, выталкивает их из очереди и запускает соответствующее действие. Если это было повторяющееся событие, его следующее время запуска будет обновляться и возвращаться в очередь
  • Планировщик работает вечно, если вы не остановите его явно

Поскольку планировщик работает, поддерживая счетчик прошедшего времени, он использует еще один рулон вашего собственного компонента Timer. Значение синхронизации по умолчанию в методе Sprite Kit update не полезно при создании фонового рисунка/переднего плана приложения, поэтому нам нужно также развернуть компонент Timer - это позволит нам вычислить правильный временной шаг для нашего игрового цикла.

Я довольно подробно объясняю gotchas с поиском вашего timestep далее в статье в блоге.

Резюме

  • Асинхронные методы отправки на основе NSTimer/GCD не соблюдают ваше игровое понятие истекшего времени и не интегрируются с комплектом Sprite Kit. Он не будет работать корректно во всех случаях (на основе вашей логики игры) и приведет к жестким определениям ошибок времени.

  • Sprite Kit SKAction является отличным для выполнения заранее определенных действий, таких как применение преобразований на узлах, поскольку он встроен и учитывает состояние приостановленной сцены. Но для планирования блоков/закрытий и поддержания контроля над его исполнением это сложный вызов. Выразить свои намерения сложно. SKAction.runBlock остановит ваш рабочий блок, когда состояние сцены paused

  • Сканируйте свою собственную/используйте библиотеку. Этот подход дает вам максимальный контроль и позволяет интегрироваться со сценой, приостановленным состоянием и игровым понятием прошедшего времени. Сначала это может показаться сложным, но если у вас уже есть механизм для расчета временного шага вашей игры, сделать планировщик поверх этого просто. Демонстрационный проект, который я поделил, должен предоставить некоторую информацию о том, как достичь этого, если вы будете использовать свои собственные/использовать эти компоненты, если вы находитесь в Swift.

Ответ 3

Вдохновленный из разных подходов, я применил расширение для Swift 3:

// © timer
// SCHEDULETIMERWITHINTERVAL maked with SKAction
class func scheduledTimerWith(timeInterval:TimeInterval, selector: Selector,withObject: AnyObject = SKNode(), repeats:Bool)->SKAction {
    // instead of NSTimer use skactions
    // now starting to search the selector: is in node, node parent or node childs?
    let call = SKAction.customAction(withDuration: 0.0) { node, _ in
        if node.responds(to: selector) {
            node.performSelector(onMainThread: selector, with: withObject, waitUntilDone: false)
        } else // check for the direct parent
            if let p = node.parent, p.responds(to: selector) {
                p.performSelector(onMainThread: selector, with: withObject, waitUntilDone: false)
            } else { // check for childs
                let nodes = node.children.filter { $0.responds(to: selector)}
                if nodes.count>0 {
                    let child = nodes[0]
                    child.performSelector(onMainThread: selector, with: withObject, waitUntilDone: false)
                } else {
                    assertionFailure("node parent or childs don't are valid or don't have the selector \(selector)")
                }
            }
    }
    let wait = SKAction.wait(forDuration: timeInterval)
    let seq = SKAction.sequence([wait,call])
    let callSelector = repeats ? SKAction.repeatForever(seq) : seq
    return callSelector
}

Использование

let generateIdleTimer = SKAction.scheduleTimerWith(timeInterval:20, selector: #selector(PlayerNode.makeRandomIdle), repeats: true)
self.run(generateIdleTimer,withKey: "generateIdleTimer")

Таймер запускается из родителя:

if parent = self.parent {
     let dic = ["hello":"timer"]
     let generateIdleTimer = SKAction.scheduleTimerWith(timeInterval:20, selector: #selector(PlayerNode.makeRandomIdle),withObject:dict, repeats: true)
     parent.run(generateIdleTimer,withKey: "generateIdleTimer")
}

Почему я должен использовать этот метод?

Это только альтернатива, но она также имеет входной параметр withObject, если вам нужно вызвать метод, который имеет свойство ввода.

С помощью этого метода вы также можете запустить таймер из родителя node, и он работает (потому что поиск метода для родителя и дочерних элементов для поиска селектора..), то же самое, если вы хотите запустить таймер из дочерний элемент, который не имеет этого селектора, поэтому метод всегда ищет его родительский или дочерний (это полезно, если вы хотите часто запускать removeAllActions без потери таймера..)

Ответ 4

Самый простой метод:

var _: Timer = Timer.scheduledTimer(timeInterval: 20, target: self, selector: #selector(objcFunc), userInfo: nil, repeats: false)