Randomly run two SKActions at same time for two separate SKSpriteNodes - swift

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

Related

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.

Remove SKAction and restore node state

Desired behavior is: when an action is removed from a node (with removeAction(forKey:) for instance) it stops to animate and all the changes caused by action are discarded, so the node returns back to pervious state. In other words, I want to achieve behavior similar to CAAnimation.
But when a SKAction is removed, the node remains changed. It's not good, because to restore it's state I need to know exactly what action was removed. And if I then change the action, I also will need to update the node state restoration.
Update:
The particular purpose is to show possible move in a match-3 game. When I show a move, pieces start pulsating (scale action, repeating forever). And when the user moves I want to stop showing the move, so I remove the action. As the result, pieces may remain downscaled. Later I would like to add more fancy and complicated animations, so I want to be able to edit it easily.
Thanks to the helpful comment and answer I came to my own solution. I think the state machine would be bit too heavy here. Instead I created a wrapper node, which main purpose is run the animation. It also has a state: isAimating property. But, first of all, it allows to keep startAnimating() and stopAnimating() methods close to each other, incapsulated, so it's more difficult to mess up.
class ShowMoveAnimNode: SKNode {
let animKey = "showMove"
var isAnimating: Bool = false {
didSet {
guard oldValue != isAnimating else { return }
if isAnimating {
startAnimating()
} else {
stopAnimating()
}
}
}
private func startAnimating() {
let shortPeriod = 0.2
let scaleDown = SKAction.scale(by: 0.75, duration: shortPeriod)
let seq = SKAction.sequence([scaleDown,
scaleDown.reversed(),
scaleDown,
scaleDown.reversed(),
SKAction.wait(forDuration: shortPeriod * 6)])
let repeated = SKAction.repeatForever(seq)
run(repeated, withKey: animKey)
}
private func stopAnimating() {
removeAction(forKey: animKey)
xScale = 1
yScale = 1
}
}
Usage: just add everything that should be animated to this node. Works well with simple animations, like: fade, scale and move.
As #Knight0fDragon suggested, you would be better off using the GKStateMachine functionality, I will give you an example.
First declare the states of your player/character in your scene
lazy var playerState: GKStateMachine = GKStateMachine(states: [
Idle(scene: self),
Run(scene: self)
])
Then you need to create a class for each of these states, in this example I will show you only the Idle class
import SpriteKit
import GameplayKit
class Idle: GKState {
weak var scene: GameScene?
init(scene: SKScene) {
self.scene = scene as? GameScene
super.init()
}
override func didEnter(from previousState: GKState?) {
//Here you can make changes to your character when it enters this state, for example, change his texture.
}
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return stateClass is Run.Type //This is pretty obvious by the method name, which states can the character go to from this state.
}
override func update(deltaTime seconds: TimeInterval) {
//Here is the update method for this state, lets say you have a button which controls your character velocity, then you can check if the player go over a certain velocity you make it go to the Run state.
if playerVelocity > 500 { //playerVelocity is just an example of a variable to check the player velocity.
scene?.playerState.enter(Run.self)
}
}
}
Now of course in your scene you need to do two things, first is initialize the character to a certain state or else it will remain stateless, so you can to this in the didMove method.
override func didMove(to view: SKView) {
playerState.enter(Idle.self)
}
And last but no least is make sure the scene update method calls the state update method.
override func update(_ currentTime: TimeInterval) {
playerState.update(deltaTime: currentTime)
}

How to add and remove a SKErmitterNode within a sequence?

I'm adding a SKErmitterNode from the update() function. I want to remove the SKErmitterNode after 2 seconds, therefore I made a sequence but in the sequence, I can´t add Nodes. And if I add the Node outside of the sequence it gets added over and over again(because I´m doing all of this in the update function) Does someone know a better way to do this?
Here is my Code from the update function:
override func update(_ currentTime: CFTimeInterval) {
if player.position.y <= player.size.height / 2{
self.player.removeFromParent()
if let particles = SKEmitterNode(fileNamed: "MyParticle.sks") {
particles.position = player.position
let addParticle = addChild(particles)
let wait = SKAction.wait(forDuration: 2.0)
let removeParticle = SKAction.removeFromParent()
let particleSequence = SKAction.sequence([addParticle, wait, removeParticle]) //Error ->Cannot convert value of type 'Void' to expected element type 'SKAction'
self.run(SKAction.run(particleSequence))
}
}
So what I recommend for you to do is to create a function like the following
func myExplosion (explosionPosition: CGPoint){
let explosion = SKEmitterNode(fileNamed: "MyParticle")// borrowed this from you
explosion?.position = explosionPosition
explosion?.zPosition = 3
self.addChild(explosion!)
self.run(SKAction.wait(forDuration: 2)){//you can always change the duration to whatever you want
explosion?.removeFromParent()
}
}
then when it is time to use this function, use it like so
myExplosion(explosionPosition: player.position)
Hope this can help you out.

Swift/SpriteKit: Attempting to reach a function/method in a class from another class not working

I have a function in my ScoreSystem class named addScore. The function adds 1 point to the game, updates the SKLabelNode to the current score and in turn calls the function startNewLevel every 25 points.
func addScore(scene: SKScene) {
gameScore += 1
scoreLabel.text = "\(gameScore)"
if CGFloat(gameScore).truncatingRemainder(dividingBy: 25) == 0 {
NotificationCenter.default.post(name: Notification.Name.init("start_new_level"), object: nil)
GameScreen().displayLevel(scene: scene)
}
}
The function gets called every time a torpedo that has been fired hits the enemy. I now want to add a new level where meteors (SKSpriteNode) have to be avoided. I have several SKActions in a sequence to accomplish this. Essentially, the SKSpriteNode moves from the top of the screen, reaches below the screen and gets deleted. If the meteor reaches the bottom of the screen means that it has been avoided by the player.
I'm attempting to call the function addScore but it doesn't update.
Here is the function:
let scoreSystem = ScoreSystem()
func spawnMeteor() {
let randomXStart = CGFloat.random(min: gameArea.minX, max: gameArea.maxX)
let startPoint = CGPoint(x: randomXStart, y: scene.size.height * 1.2)
let endPoint = CGPoint(x: randomXStart, y: -scene.size.height * 0.2)
let meteor = SKSpriteNode(imageNamed: "meteor")
meteor.name = "Meteor"
meteor.zPosition = 2
meteor.position = startPoint
let moveMeteor = SKAction.move(to: endPoint, duration: 3)
let deleteEnemy = SKAction.removeFromParent()
let score = SKAction.run(addToScore)
let meteorSequence = SKAction.sequence([
moveMeteor,
score,
deleteEnemy])
scene.addChild(meteor)
meteor.run(meteorSequence)
}
I have tried a function addToScore like this:
func addToScore() {
scoreSystem.addScore(scene: scene!)
}
And also tried this
func addToScore() {
NotificationCenter.default.post(name: Notification.Name.init("add_to_score"), object: nil)
}
When trying this second alternative, I add the following to the GameScene
override func sceneDidLoad() {
super.sceneDidLoad()
NotificationCenter.default.addObserver(forName: Notification.Name.init("add_to_score"), object: nil, queue: OperationQueue.main) { [weak self] (notification) in
self?.scoreSystem.addScore(scene: self!)
}
}
I removed several lines from the spawnMeteor() function so not to clog the space with unnecessary lines of code. I have yet to figure out how to call that function using SKAction.run(). Can someone point me in the right direction?
You are passing along a lot of information to your functions, probably too much.
I would suggest either you implement Protocols or Notifications to handle your information, personally I prefer protocols so my example will be protocols.
I am making some assumptions about your code because not all of it is presented in your question...
protocol ScoreSystemDelegate: class {
func displayLevel()
}
class ScoreSystem: SKNode {
weak var scoreSystemDelegate: ScoreSystemDelegate!
//your init func and any other funcs in this class (unknown)
func addScore(scene: SKScene) {
gameScore += 1
scoreLabel.text = "\(gameScore)"
if CGFloat(gameScore).truncatingRemainder(dividingBy: 25) == 0 {
NotificationCenter.default.post(name: Notification.Name.init("start_new_level"), object: nil)
//GameScreen().displayLevel(scene: scene)
self.scoreSystemDelegate.displayLevel()
}
}
}
in your class that creates the scoreSystem (assuming GameScene)...
class GameScene: SKScene, ScoreSystemDelegate {
let scoreSystem = ScoreSystem()
//assign GameScene as the delegate of ScoreSystem that way the 2 can communicate
scoreSystem.scoreSystemDelegate = self
func displayLevel() {
//do whatever you do when you display the level
}
}
now your spawn meteor func should work as you have coded because addScore no longer takes a scene property, nice thing about this approach is that you can make any object a delegate of ScoreSystem it doesn't have to be a scene.

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