How to loop a dynamically delayed sequence of SKActions - swift

I need to run an unknown amount (from an Array of Dictionaries) of SKAction sequences that are dynamically created and offset in a loop using a delay that increases as I go through the loop. printed output of the loop should appear in the following order
show line 5
hiding everything
show line 6
hiding everything
show line 2
hiding everything
to obtain this functionality I use this code
func display(winningLines: [Int : AnyObject]) {
var delay = 0.0
var actions = [SKAction]()
let hideAction = SKAction.run {
print("hiding everything")
}
for winningLine in winningLines {
let displayAction = SKAction.run {
print("line \(winningLine.key)")
}
let wait = SKAction.wait(forDuration: delay)
var action: SKAction!
//repeatForever = true
if !repeatForever {
action = SKAction.sequence([wait, displayAction, SKAction.wait(forDuration: 1.0), hideAction])
}
else {
let seq = SKAction.sequence([wait, displayAction, SKAction.wait(forDuration: 1.0), hideAction])
action = SKAction.repeatForever(seq)
}
self.run(action)
delay += 1.0
}
}
If the sequence only needs to occur once than it outputs as expected, however there are times that I need the sequence to just keep repeating forever (until the user stops the action). Putting a repeat forever around the action doesn't work because it repeats each individual sequence and messes up the order. The first sequence will repeat every 1 second, the second sequence will repeat every 2 seconds...etc.
Any suggestions on how to get this to loop properly forever?
line 5
hiding everything
line 5
line 6
hiding everything
line 5
hiding everything
line 2
hiding everything
line 5
line 6
hiding everything
hiding everything
line 5
hiding everything
hiding everything
line 5
line 6
line 2
hiding everything
line 5
hiding everything
hiding everything
I've also tried appending the actions to an Array of SKActions and looping through them at the end but it didn't work
var actions = [SKAction]()
...
actions.append(action)
for action in actions {
self.run(action)
}

If your only concern is to repeat forever, the unknown amount of sequences, you can do the following:
create all required sequences
put them in another sequence
loop that forever
So for example, this code will create four boxes, and will apply fade in/out (displayAction/hideAction from your code) sequentially to every box, and will repeat that forever:
import SpriteKit
class GameScene: SKScene {
var boxes:[SKSpriteNode] = []
override func didMove(to view: SKView) {
let distance:CGFloat = 10
for i in 0...3 {
let box = SKSpriteNode(color: .purple, size: CGSize(width: 50, height: 50))
box.name = String(i) //name == index
box.position.x = CGFloat(i) * (box.size.width + distance)
box.alpha = 0.0
addChild(box)
boxes.append(box)
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
var actions:[SKAction] = []
let action = SKAction.sequence([
SKAction.fadeIn(withDuration: TimeInterval(1)),
SKAction.fadeOut(withDuration: 0),
])
for index in 0...boxes.count {
let sequence = SKAction.sequence([
SKAction.run(action, onChildWithName: String(index)),
SKAction.wait(forDuration: 1)
])
actions.append(sequence)
}
self.run(SKAction.repeatForever(SKAction.sequence(actions)))
}
}

Related

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

How to run an actions on collection of sprites?

I try to run some actions on array of sprites, and need to run this actions in sequence for the sprites, the problem is when I use for loop its apply the action at once and there is not a period of time between them.
func setColors() {
for color in boyColors {
color.run(.wait(forDuration: 0.3))
color.run(.fadeIn(withDuration: 0.5))
}
}
When call this function its should fade in the colors
of boyColors array with some time period between them, but its fade in all the colors ate once
If you want all 4 to fade in concurrently after a delay:
func setColors() {
let seq = [SKAction.wait(forDuration: 0.3),SKAction.fadeIn(withDuration: 0.5)]
for color in boyColors {
color.run(SKAction.sequence(seq))
}
}
If you need them to fade in sequentially:
func setColors() {
var seq = [SKAction]()
let wait3 = SKAction.wait(forDuration: 0.3)
let wait5 = SKAction.wait(forDuration: 0.5)
let fadeIn = SKAction.fadeIn(withDuration: 0.5)
for color in boyColors {
let colorFadeIn = SKAction.run({color?.run(fadeIn)})
let group = [wait5,colorFadeIn]
seq.append(wait3)
seq.append(SKAction.group(group))
}
scene.run(SKAction.sequence(seq))
}
What this does is allow your scene (or any node you want to fire the actions) to control when a fade event starts. This assumes that all nodes are running at the same speed. If you need the nodes to run at individual speeds, you are going to need something a lot more complex like SomaMen proposes that chains your next colors action to the end of your current color. You will have to also check if color exists in this regards, because if you do a chain like RGBY, and you remove G, the only color that will fire is R.
There is a variety of SKActions available to you, including a run with completion, which you could use. One possible approach could be -
var colors = boyColors // so we can remove each item as we've faded it in
var waitAndFadeIn: (() -> ())?
waitAndFadeIn = {
guard let boyColor = colors.first else { return } // stops when we run out of items
let wait: SKAction = .wait(forDuration: 0.3)
let fadeIn: SKAction = .fadeIn(withDuration: 0.5)
let sequence: SKAction = .sequence([wait, fadeIn])
boyColor.run(sequence, completion: {
colors.remove(at: 0)
waitAndFadeIn?() // do it again with the next boyColor
})
}
waitAndFadeIn?()
This would fade in the objects, one after the other. I think the code will run, but it's back-of-the-envelope sort of stuff which I haven't been able to test.

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

Simple Action Loop

I am trying to repeat a set of actions by running them through a loop. My loop is fine when running something else, but it seems to have trouble running the actions. My code is as follows:
let pulse1 = SKAction.scaleTo(2.0, duration: 1.0)
let pulse2 = SKAction.scaleTo(0.5, duration: 1.0)
var i = 0
override func didMoveToView(view: SKView) {
for var i = 0; i <= 100; i++ {
self.sun.runAction(pulse1)
self.sun.runAction(pulse2)
}
This will cause the node to pulse1 and pulse2 each once but never again. If I add
println("")
to the loop, it runs whatever text properly, but for some reason doesn't run the actions like it runs the text. Or maybe it does and I don't understand how SKAction works? Either way, the loop is executing properly, I believe. I am not quite sure what's wrong with the SKAction call in the loop.
scaleTo simply changes the node's scale. Once pulse1 goes to 2.0 and pulse2 gets to 0.5, runAction runs repeatedly but you never change the scale for either pulse ever again.
That's why you're only seeing it do work the first time.
Instead of using a for loop, try something like this:
override func didMoveToView(view: SKView) {
if (i % 2 == 0) {
let pulse = SKAction.scaleTo(2.0, duration: 1.0)
} else {
let pulse = SKAction.scaleTo(0.5, duration: 1.0)
}
[self.sun runAction:pulse completion:^{
if( i < 100 )
{
didMoveToView(view);
}
}];
}
Maybe you can use
class func repeatAction(_ action: SKAction,
count count: Int) -> SKAction
Put as many single actions in a sequence an run repeatAction for x times.

Looping an A to B animation in Swift

I'm a swift newbie trying to loop an A to B positional animation. I'm not sure how to reset the position so the animation can loop. Any help appreciated.
import SpriteKit
class GameScene: SKScene {
let Cloud1 = SKSpriteNode(imageNamed:"Cloud_01.png")
override func didMoveToView(view: SKView) {
view.scene!.anchorPoint = CGPoint(x: 0.5,y: 0.5)
Cloud1.position = CGPoint(x: -800,y: 0)
Cloud1.xScale = 0.5
Cloud1.yScale = 0.5
self.addChild(Cloud1)
//DEFINING SPRITE ACTION & REPEAT
let animateCloud1 = SKAction.moveToX(800, duration: 1.4);
let repeatCloud1 = SKAction.repeatActionForever(animateCloud1)
let group = SKAction.group([ animateCloud1,repeatCloud1]);
//RUNNING ACTION
self.Cloud1.runAction(group);
}
override func update(currentTime: NSTimeInterval) {
if(Cloud1.position.x == 800){
Cloud1.position.x = -800
}
}
}
If I understand your question correctly, you want the Sprite to move back and forth between its current location and the new location you specified.
If so, a way to do this would be to create two animations and put them in a sequence. Then repeat the sequence forever.
let animateCloud = SKAction.moveToX(800, duration: 1.4)
let animateCloudBackwards = SKAction.moveToX(Cloud1.position.x, duration: 0)
// Sequences run each action one after another, whereas groups run
// each action in parallel
let sequence = SKAction.sequence([animateCloud, animateCloudBackwards])
let repeatedSequence = SKAction.repeatActionForever(sequence)
Cloud1.runAction(repeatedSequence)