Properly detect completion of simultaneous SKActions - swift

I'm pretty new to native programming on iOS. I have a function in my Game Scene which moves an array of SKSpriteNodes. After each move is completed, its node should be removed from the scene. The function also has a completion function which should be called AFTER removing all the SKNodes. My question is, if there is a cleaner way to just add a little extra time to the duration of the the moveAction until the completion function is called (see below). I also played around with action sequences, but couldn't come up with a working solution...
Every help will be appreciated!
func animateNodes(nodes: Array<SKSpriteNode>, completion:() -> ()) {
let duration = NSTimeInterval(0.5)
for (_, node) in nodes.enumerate() {
let point = CGPointMake(node.position.x - 80, node.position.y + 80)
let moveAction = SKAction.moveTo(point, duration: duration)
let fadeOutAction = SKAction.fadeAlphaTo(0, duration: duration)
node.runAction(SKAction.group([moveAction, fadeOutAction]), completion: {
node.removeFromParent()
})
}
runAction(SKAction.waitForDuration(duration + 0.1), completion:completion)
}

Add a variable count that keeps track of how many nodes are currently alive. In each node's completion, decrement that count and check to see if it was the last one (count == 0) and run the completion action if it is:
func animateNodes(nodes: Array<SKSpriteNode>, completion:() -> ()) {
var count = nodes.count
let duration = NSTimeInterval(0.5)
for (_, node) in nodes.enumerate() {
let point = CGPointMake(node.position.x - 80, node.position.y + 80)
let moveAction = SKAction.moveTo(point, duration: duration)
let fadeOutAction = SKAction.fadeAlphaTo(0, duration: duration)
node.runAction(SKAction.group([moveAction, fadeOutAction]), completion: {
node.removeFromParent()
count--
if count == 0 {
completion()
}
})
}
}
I haven't compiled or run this, so there may be errors, but this should work

Related

Swift - SCNAnimationPlayer setting duration cancels out timeOffset

I have an animation that I'm trying to start & end at specific places. I can set the start by setting the animationPlayer.animation.timeOffset, I'm also trying to set the animation to end about 20s after the timeOffset & I can do that by setting animationPlayer.animation.duration.
The problem that I'm facing is that setting the duration cancels out the timeOffset. If I use just .timeOffset I can get the animation to start from any position but as soon as duration is set the animation will play from the beginning.
The intended result would be this: The animation starts at 25s (timeOffset) runs for 20s (duration) & then loops back to the timeOffset.
let rootNode = sceneView.rootNode
rootNode.enumerateChildNodes { child, _ in
guard let animationPlayer = child.animationPlayer(forKey: key) else { return }
animationPlayer.animation.timeOffset = 25
animationPlayer.animation.duration = 20
animationPlayer.animation.autoreverses = true
animationPlayer.animation.isRemovedOnCompletion = false
}
The best solution I have found is something like this:
let player = model.animationPlayer(forKey: "all")
let animation = player?.animation
func restartAnimation(atTimeOffset timeOffset: TimeInterval, duration: TimeInterval) {
animation?.timeOffset = timeOffset
if isWalking {
player?.play()
let uuid = isWalkingUUID
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
guard uuid == self.isWalkingUUID else { return }
player?.stop(withBlendOutDuration: 0.2)
restartAnimation(atTimeOffset: timeOffset, duration: duration)
}
} else {
player?.stop(withBlendOutDuration: 0.2)
}
}
restartAnimation(atTimeOffset: 33, duration: 0.6)

Randomly run two SKActions at same time for two separate SKSpriteNodes

Title says it all. I've got two SKSpriteNodes [ leftTrap, rightTrap] with two separate SKActions [ rotateSequenceLeft, rotateSequence] that need to run at the same time but need to do it randomly.
SKSpriteNodes with attached SKActions
Need to run these two in parallel at same random intervals.
leftTrap.run(SKAction.repeatForever(rotateSequenceLeft))
rightTrap.run(SKAction.repeatForever(rotateSequence))
What I have tried
I have tried to group the leftTrap node and action rotateSequenceLeft with a wait duration range action. But it seems that the created group never even runs the wait duration action.
let randomPivotInterval = SKAction.wait(forDuration: 1.0, withRange: 5.0)
let leftGroup = SKAction.group([rotateSequenceLeft, randomPivotInterval])
let rightGroup = SKAction.group([rotateSequence, randomPivotInterval])
leftTrap.run(SKAction.repeatForever(leftGroup))
rightTrap.run(SKAction.repeatForever(rightGroup))
If both flippers need to be in sync at all times, just apply the action to the left flipper, and do this for the right:
func didEvaluateActions(){
rightTrap.angle = -leftTrap.angle
}
This will ensure both flippers are always at the opposite angle
You can try something like this
class GameScene: SKScene {
var leftTrap: SKSpriteNode! // TODO: populate this
var rightTrap: SKSpriteNode! // TODO: populate this
func start() {
let rotateTraps = SKAction.run { [weak self] in
guard let self = self else { return }
self.rotate(trap: self.leftTrap, clockwise: .random())
self.rotate(trap: self.rightTrap, clockwise: .random())
}
let wait = SKAction.wait(forDuration: 5)
let sequence = SKAction.sequence([rotateTraps, wait])
run(.repeatForever(sequence))
}
private func rotate(trap: SKSpriteNode, clockwise: Bool) {
// TODO: put rotation code here
}
}
How does it work?
The start() method creates a rotateTraps action which, each time it is executed, call self?.rotate on the left trap passing a random bool for the clockwise param and then does the same for the right trap.
Then the rotateTraps action is wrapped into a sequence and repeated forever every 5 seconds.
If for whatever reason you don't want to add a post-evaluate phase like in Knight0fDragon's answer, then you can probably do something like this:
func operateTrap() {
run(.wait(forDuration: .random(in: 1.0 ... 6.0)) { // On completion...
// I assume both sequences take the same time.
leftTrap.run(rotateSequenceLeft)
rightTrap.run(rotateSequenceRight) { self.operateTrap() }
}
}

swift call function multiple times inside update

I create a game via SpriteKit. in the game every few second a ball spawn to the screen (via function) and the player has to blow them up. now, I want to check the player level via his score so if the score is bigger than 10 the spawnBall function will be executed twice (so 2 ball will spawn on the screen) an so on. I tried to to it via the update fun (that will "read" the player score and depends on the score will call the spawnBall function). Unfortunately when I do it the screen is spawn with million (or so) balls in few seconds (as I said I want it to call the function every few seconds and increase the call while the score is X). I really don't have any idea how to do it.
here is my code:
override func update(_ currentTime: CFTimeInterval) {
if (self.score <= 10){
spawnBalls()
}
if (self.score > 10 && self.score <= 20){
spawnBalls()
spawnBalls()
}
if (self.score > 20){
spawnBalls()
spawnBalls()
spawnBalls()
}
if (self.subscore == 3) {
_ = randomBallColorToBlow()
self.subscore = 0
}
}
func spawnBalls() {
let wait = SKAction.wait(forDuration: 1)
let action = SKAction.run {
self.createBall()
}
run(SKAction.repeatForever((SKAction.sequence([wait, action]))))
}
how can I do it without using the update function??
you are calling spawn balls 60 times a second by calling it in your update func.
try just checking if a certain requirement is met to upgrade to a higher spawn rate in your update but keep the calls out of the update func.
private var upgradedToLevel2 = false
private var upgradedToLevel3 = false
//called somewhere probably in a start game func
spawnBalls(duration: 1.0)
override func update(_ currentTime: CFTimeInterval) {
if (self.score > 10 && self.score <= 20) && !upgradedToLevel2 {
//this prevents the if loop from running more than once
upgradedToLevel2 = true
self.removeAction(forKey: "spawn")
spawnBalls(duration: 0.5)
}
if (self.score > 20) && !upgradedToLevel3 {
//this prevents the if loop from running more than once
upgradedToLevel3 = true
spawnBalls(duration: 0.33)
}
}
func spawnBalls(duration: Double) {
let wait = SKAction.wait(forDuration: duration)
let action = SKAction.run { self.createBall() }
let repeater = SKAction.repeatForever(SKAction.sequence([wait, action]))
run(repeater, withKey: "spawn")
}
As stated, you are spawning your balls multiple times and need to break it up. I would recommend keeping track of level using an Int instead of a bool to be able to handle an "infinite" amount of level ups without making an "infinite" amount of boolean variables
private var nextLevel = 0 //Level 0 allows us to spawn a ball on startup, so no need to call spawnBalls anywhere else
override func update(_ currentTime: CFTimeInterval) {
if (self.score > 10 * nextLevel){
self.removeAction(forKey: "spawn") //this prevents the if loop from running more than once
nextLevel += 1
spawnBalls(count:nextLevel,forDuration:1) //You can change the 1 here if you want to spawn balls at a faster speed, I would recommend a variable that uses nextLevel in a forumula
}
}
func spawnBalls(count:Int, forDuration duration:TimeInterval) {
let range = 0..<count
let wait = SKAction.wait(forDuration: duration)
let action = SKAction.run {range.forEach{self.createBall()}}
let repeater = SKAction.repeatForever(SKAction.sequence([wait, action]))
removeAction(forKey:"spawn")
run(repeater, withKey: "spawn")
}

SpriteKit multiple SKActions for multiple SKSpriteNode - wait for all to complete

I'm working on a iOs game and I have a problem.
I need to display x sprites (for each one I have a scale SKAction).
I need to be able to wait until all SKAction sprites and then do something else.
Each SKAction run in a separate thread.
How I can wait ?
Here is a piece of code:
for tile in tiles {
let randomNum:UInt32 = arc4random_uniform(20) // range is 0 to 99
let randomTime:TimeInterval = TimeInterval(randomNum/10)
let scale = SKAction.scale(by: 1, duration: 2, delay:randomTime , usingSpringWithDamping: 0.1, initialSpringVelocity: 5)
tile.sprite = SKSpriteNode(imageNamed: tile.type.spriteName)
tile.sprite?.size = CGSize(width: TileWidth, height: TileHeight)
tile.sprite?.position = tile.position!
tile.sprite?.scale(to: CGSize(width: 0, height: 0))
cookiesLayer.addChild(tile.sprite!)
tile.sprite?.run(scale)
}
//TODO code to add to be executed after all SKActions
How can I do my TODO code to be executer after all SKActions ?
I would like to run the SKAction in parallel or one after another.
Thanks.
You could do this very easily using a completion block with your run method.
Just for the sake of this example, say you have a SKSpriteNode named someSpriteNode and want to know when two actions (applyImpulse in that case) have finished running:
// 1) Create your actions:
let action1 = SKAction.applyImpulse(CGVector(dx: 1.0, dy: 0.0), duration: 2.0)
let action2 = SKAction.applyImpulse(CGVector(dx: 6.0, dy: 2.0), duration: 1.0)
// 2) Add them to a sequence:
let actionSequence = SKAction.sequence([action1, action2])
// 3) Run the sequence using a completion block:
someSpriteNode?.run(actionSequence, completion: {
// All your actions are now finished
// Do whatever you want here :)
})
UPDATE: Get notified when a group of actions got executed, where all the actions run on the same node
You might be looking for action groups then:
// Declare an empty array that will store all your actions:
var actions = [SKAction]()
// Iterate through your nodes:
for _ in 0..<6 {
// ...
// Generate your random scale, delay, or whatever you need:
let randomScale = CGFloat(GKRandomDistribution(lowestValue: 0, highestValue: 10).nextInt())
// Create your custom action
let scaleAction = SKAction.scale(by: randomScale, duration: 2.0)
// Append your action to the actions array:
actions.append(scaleAction)
}
// Create an action group using the actions array:
let actionGroup = SKAction.group(actions)
// Run your action group, and do whatever you need inside the completion block:
self.run(actionGroup, completion: {
// All your actions are now finished, no matter what node they were ran on.
})
Also, I would recommend you using GameplayKit to generate random numbers in your game, it will definitely make your life easier :)
UPDATE 2: Get notified when all actions got executed, in the case where all the actions run on different nodes
Using DispatchGroup :
// Create a DispatchGroup:
let dispatchGroup = DispatchGroup()
for _ in 0..<6 {
// ...
let randomWait = Double(GKRandomDistribution(lowestValue: 1, highestValue: 12).nextInt())
let waitAction = SKAction.wait(forDuration: randomWait)
let fadeOutAction = SKAction.fadeOut(withDuration: 2.0)
let fadeInAction = SKAction.fadeIn(withDuration: 2.0)
let sequenceAction = SKAction.sequence([waitAction, fadeOutAction, fadeInAction])
// Enter the DispatchGroup
dispatchGroup.enter()
colorSquares[i].run(sequenceAction, completion: {
// Leave the DispatchGroup
dispatchGroup.leave()
})
}
// Get notified when all your actions left the DispatchGroup:
dispatchGroup.notify(queue: DispatchQueue.main, execute: {
// When this block is executed, all your actions are now finished
})
I think this solution is way more elegant than a counter :)
First of all you should always create a Minimum Verifiable Example. Remove the unneeded things from your question and make sure to include the everything we needed to test your code.
Premise
I assume you have a Tile class similar to this
class Tile {
var sprite: SKSpriteNode?
}
and an array like this
let tiles:[Tile] = ...
Your objective
You want to run an action of random duration on the sprite element of each tile in tiles.
You want the actions to start at the same time
You want to be able to run some code when all the actions are completed
Solution
// 0. create a maxDuration variable
var maxDuration:TimeInterval = 0
// 1. create all the actions
let actions = tiles.map { tile in
return SKAction.run {
let randomNum = arc4random_uniform(100)
let randomTime = TimeInterval(randomNum / 10)
let wait = SKAction.wait(forDuration: randomTime)
let scale = SKAction.scale(by: 1, duration: 2)
tile.sprite?.run(scale)
maxDuration = max(maxDuration, randomTime + 2)
}
}
// 2. create a wait action for the max duration
let wait = SKAction.wait(forDuration: maxDuration)
// 3. write inside this action the code to be executed after all the actions
let completion = SKAction.run {
print("now all the actions are completed")
}
// 4. create a sequence of wait + completion
let sequence = SKAction.sequence([wait, completion])
// 5. create a group to run in parallel actions + sequence
let group = SKAction.group(actions + [sequence])
// 6. run the group on the node you prefer (it doesn't really matter which node since every inner action is tied to a specific node)
self.run(group)
Update (as suggested by #Alex)
var maxDuration:TimeInterval = 0
tiles.forEach { tile in
let randomNum = arc4random_uniform(100)
let randomTime = TimeInterval(randomNum / 10)
let wait = SKAction.wait(forDuration: randomTime)
let scale = SKAction.scale(by: 1, duration: 2)
tile.sprite?.run(scale)
maxDuration = max(maxDuration, randomTime + 2)
}
run(.wait(forDuration: maxDuration)) {
print("now all the actions are completed")
}

Smooth animation with timer and loop in iOS app

I have ViewController with stars rating that looks like this (except that there are 10 stars)
When user opens ViewController for some object that have no rating I want to point user's attention to this stars with very simple way: animate stars highlighting (you could see such behaviour on some ads in real world when each letter is highlighted one after another).
One star highlighted
Two stars highlighted
Three stars highlighted
......
Turn off all of them
So this is the way how I am doing it
func delayWithSeconds(_ seconds: Double, completion: #escaping () -> ()) {
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
completion()
}
}
func ratingStarsAnimation() {
for i in 1...11 {
var timer : Double = 0.6 + Double(i)*0.12
delayWithSeconds(timer) {
ratingStars.rating = (i < 10) ? Double(i) : 0
}
}
}
What is going on here? I have function called delayWithSeconds that delays action and I use this function to delay each star highlighting. And 0.6 is initial delay before animation begins. After all stars are highlighted - last step is to turn off highlighting of all stars.
This code works but I can't say that it is smooth.
My questions are:
How can I change 0.6 + Double(i)*0.12 to get smooth animation feel?
I think that my solution with delays is not good - how can I solve smooth stars highlighting task better?
Have a look at the CADisplaylink class. Its a specialized timer that is linked to the refresh rate of the screen, on iOS this is 60fps.
It's the backbone of many 3rd party animation libraries.
Usage example:
var displayLink: CADisplayLink?
let start: Double = 0
let end: Double = 10
let duration: CFTimeInterval = 5 // seconds
var startTime: CFTimeInterval = 0
let ratingStars = RatingView()
func create() {
displayLink = CADisplayLink(target: self, selector: #selector(tick))
displayLink?.add(to: .main, forMode: .defaultRunLoopMode)
}
func tick() {
guard let link = displayLink else {
cleanup()
return
}
if startTime == 0 { // first tick
startTime = link.timestamp
return
}
let maxTime = startTime + duration
let currentTime = link.timestamp
guard currentTime < maxTime else {
finish()
return
}
// Add math here to ease the animation
let progress = (currentTime - startTime) / duration
let progressInterval = (end - start) * Double(progress)
// get value =~ 0...10
let normalizedProgress = start + progressInterval
ratingStars.rating = normalizedProgress
}
func finish() {
ratingStars.rating = 0
cleanup()
}
func cleanup() {
displayLink?.remove(from: .main, forMode: .defaultRunLoopMode)
displayLink = nil
startTime = 0
}
As a start this will allow your animation to be smoother. You will still need to add some trigonometry if you want to add easing but that shouldn't be too difficult.
CADisplaylink:
https://developer.apple.com/reference/quartzcore/cadisplaylink
Easing curves: http://gizma.com/easing/