How to know when an SKAction.repeat(action, count) is finished - swift

I'm making a video games with Swift & SpriteKit. I'm trying to make the level system of my game. Each levels has his own specifications (but it's not in the code right now).
However, I would like that when my SKaction.repeat is done, to move to an other scene (such as "Level completed" scene).
Do you know how can I do it ?
Here's my code :
func parametersLevel(){
let spawn = SKAction.run(asteroids)
let waitSpawn = SKAction.wait(forDuration: 0.8)
let sequence = SKAction.sequence([waitSpawn,spawn])
let spawnCount = SKAction.repeat(sequence, count: 750)
self.run(spawnCount)
}
Thanks for you help.

From run(_:completion:) instead of self.run(spawnCount) try with:
self.run(spawnCount, completion: {() -> Void in
println("completed")
})

If you need a key with your action, you can also do:
func parametersLevel(){
let spawn = SKAction.run(asteroids)
let waitSpawn = SKAction.wait(forDuration: 0.8)
let sequence1 = SKAction.sequence([waitSpawn,spawn])
let spawnCount = SKAction.repeat(sequence, count: 750)
let endAction = SKAction.run{} //whatever you need your ending to be
let sequence2 = SKAction.sequence([spawnCount ,endAction])
self.run(sequence2,withKey:”spawn” )
}

Related

How to transport SKAction between devices?

I'm trying to send a SKAction between devices but I've got some problem... could you help me please? This is my code to send it:
let movment1 = SKAction.moveTo(x: 1600, duration: 0.25)
car.run(movment1)
var positionToSend = movment1
let dataSend = Data(bytes: &positionToSend, count: MemoryLayout.size(ofValue: positionToSend))
try match?.sendData(toAllPlayers: dataSend, with: GKMatchSendDataMode.reliable)
and this is how I receive it:
func receiveDataP(position: Data, player: GKPlayer) {
let _ : SKAction = position.withUnsafeBytes { $0.pointee }
}
This is not working, in fact I was just trying...can someone help me rewriting the correct code? I'm getting mad...
Instead of sending the whole SKAction, you can just send the necessary information needed to recreate the SKAction in the other device.
Here, I suppose an SKAction is described an x value and a duration. If duration is always 0.25 then you don't need to send the duration.
Create something like this:
struct SKActionDescriptor : Codable {
let x: CGFloat
let duration: Double
}
To create the data to send, use a JSONEncoder:
let encoder = JSONEncoder()
let data = try! encoder.encode(SKActionDescriptor(x: 1600, duration: 0.25))
And you can use a JSONDecoder at the other end:
let decoder = JSONDecoder()
let descriptor = try! decoder.decode(SKActionDescriptor.self, from: receivedData)
And then create an SKAction:
let action = SKAction.moveTo(x: descriptor.x, duration: descriptor.duration)
if you want it to hit some thing or make it more movable you could make it physically
like that and try to use
Player.physicsBody?.velocity = CGVector(dx: 1600, dy: 0)
Player.physicsBody?.angularVelocity = 0.25
enter image description here
am not sure but I hope it work;

Dispatch queue to animate SCNNode in ARKit

I'm facing an issue when trying to periodically animate my nodes on an ARSession. I'm fetching data from Internet every 5 seconds and then with that data I update this nodes (shrink or enlarge).
My code looks something like this:
Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { timer in
fetchDataFromServer() {
let fetchedData = $0
DispatchQueue.main.async {
node1.update(fetchedData)
node2.update(fetchedData)
node3.update(fetchedData)
}
if stopCondition { timer.invalidate() }
}
}
Problem is that when calling the updates I'm seeing a glitch in which the camera seems to freeze for a fraction of second and I see the following message in the console: [Technique] World tracking performance is being affected by resource constraints [1]
Update happens correctly, but the UX is really clumpsy if every 5 seconds I get these "short freezes"
I've tried creating a concurrent queue too:
let animationQueue = DispatchQueue(label: "animationQueue", attributes: DispatchQueue.Attributes.concurrent)
and call animationQueue.async instead of main queue but problem persists.
I'd appreciate any suggestions.
EDIT: Each of the subnodes on it's update method looks like this
private func growingGeometryAnimation(newHeight height: Float) -> CAAnimation{
// Change height
let grow = CABasicAnimation(keyPath: "geometry.height")
grow.toValue = height
grow.fromValue = prevValue
// .... and the position
let move = CABasicAnimation(keyPath: "position.y")
let newPosition = getNewPosition(height: height)
move.toValue = newPosition.y + (yOffset ?? 0)
let growGroup = CAAnimationGroup()
growGroup.animations = [grow, move]
growGroup.duration = 0.5
growGroup.beginTime = CACurrentMediaTime()
growGroup.timingFunction = CAMediaTimingFunction(
name: kCAMediaTimingFunctionEaseInEaseOut)
growGroup.fillMode = kCAFillModeForwards
growGroup.isRemovedOnCompletion = false
growGroup.delegate = self
return growGroup
}
self.addAnimation(growingGeometryAnimation(newHeight: self.value), forKey: "bar_grow_animation")
To make any updates to the scene use SCNTransaction, it makes sure all of the changes are made on the appropriate thread.
Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { timer in
fetchDataFromServer() {
let fetchedData = $0
SCNTransaction.begin()
node1.update(fetchedData)
node2.update(fetchedData)
node3.update(fetchedData)
SCNTransaction.commit()
if stopCondition { timer.invalidate() }
}
}

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

Swift SpriteKit unwrapping and wrapping

func bombTowerTurnShoot() {
let zombieGreen = self.childNode(withName: "zombie") as! SKSpriteNode
self.enumerateChildNodes(withName: "bomb tower") {
node, stop in
if let bombTower = node as? SKSpriteNode {
let angle = atan2((zombieGreen.position.x) - bombTower.position.x, (zombieGreen.position.y) - bombTower.position.y)
let actionTurn = SKAction.rotate(toAngle: -(angle - CGFloat(Double.pi/2)), duration: 0.2)
bombTower.run(actionTurn)
}
}
}
My issue is on the let angle line. When I call the function when there is no zombieGreens on the scene I get a Thread 1 problem. How can I change the code to take into account when the zombie is not present?
if there is no ZombiGreens in the scene the error should happen already at the second line:
let zombieGreen = self.childNode(withName: "zombie") as! SKSpriteNode
I think the simplest solution without changing to much of your code would be to use an if let just as you did for the bomb tower. and it would look something like this:
func bombTowerTurnShoot() {
if let zombieGreen = self.childNode(withName: "zombie") as? SKSpriteNode{
self.enumerateChildNodes(withName: "bomb tower") {
node, stop in
if let bombTower = node as? SKSpriteNode {
let angle = atan2((zombieGreen.position.x) - bombTower.position.x, (zombieGreen.position.y) - bombTower.position.y)
let actionTurn = SKAction.rotate(toAngle: -(angle - CGFloat(Double.pi/2)), duration: 0.2)
bombTower.run(actionTurn)
}
}
}
}
But it could be a good idea to review your code when you have more logic to handle. it might be a better way to do things but this should work :)

Properly detect completion of simultaneous SKActions

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