Multiple serial animations in SceneKit - ios

I want to be able to run multiple animations, one after another, in SceneKit. I've implemented a function which runs one animation like so:
fileprivate func animateMove(_ move: Move) {
print("Animate move started " + move.identifier)
// I am creating rotateNode
let rotateNode = SCNNode()
rotateNode.eulerAngles.x = CGFloat.pi
scene.rootNode.addChildNode(rotateNode)
// Then I am selecting nodes which I want to rotate
nodesToRotate = ...
// Then I am adding the nodes to rotate node
_ = nodesToRotate.map { rotateNode.addChildNode($0) }
SCNTransaction.begin()
SCNTransaction.animationDuration = move.animationDuration
SCNTransaction.completionBlock = {
rotateNode.enumerateChildNodes { node, _ in
node.transform = node.worldTransform
node.removeFromParentNode()
scene.rootNode.addChildNode(node)
}
rotateNode.removeFromParentNode()
print("Animate move finished " + move.identifier)
}
SCNTransaction.commit()
}
And then I've tried to run multiple serial animations like so:
func animateMoves(_ moves: [Move]) {
for (index, move) in moves.enumerated() {
perform(#selector(animateMove(_:)),
with: move,
afterDelay: TimeInterval(Double(index) * move.duration)
}
}
Everything is animating but the animations don't run in a serial manner. Animations start and finish in unpredictable time.
Sample logs from debugger:
Animate move started 1
Animate move started 2
Animate move finished 1
Animate move finished 2
Animate move started 3
Animate move finished 3
I realise that my approach isn't the best, but only in that way I was able to achieve almost working animations.
I know that there is a SCNAction class available. Maybe I should make many actions within one transaction? If so, could someone explain to me how exactly SCNTransactions work and why the completion SCNTransaction's completion block fires in unpredictable time?

Try to use SCNAction.sequence():
class func sequence([SCNAction])
Creates an action that runs a collection of actions sequentially
let sequence = SCNAction.sequence([action1, action2, action3]) // will be executed one by one
let node = SCNNode()
node.runAction(sequence, completionHandler:nil)

Following #Oleh Zayats' answer I've tried to implement my case using SCNAction.sequence(_:) method, but the problem was that I needed completion handler to be fired after every completed subaction in order to be able to remove the nodes from rotationNode.
After a couple of hours of struggling I've ended up with quite a nice solution and it worked like a charm.
Namely:
I've made a function rotateAction which looks something like this:
func rotateAction(with move: Move, from rotateNode: SCNNode) -> SCNAction {
let preAction = SCNAction.run { (rotateNode) in
// all the pre action setup like choosing nodes to rotate
}
let action = SCNAction.rotate(by: -move.angle, around: vector, duration: move.animationDuration)
let postAction = SCNAction.run { (rotateNode) in
// completion handler for each action
}
return SCNAction.sequence([preAction, action, postAction])
}
Then I was able to write a function to be able to run multiple animations one after another:
func animateRotateMoves(_ moves: [Move]) {
let rotateNode = SCNNode()
scene.rootNode.addChildNode(rotateNode)
var actions: [SCNAction] = []
for move in moves {
let action = rotateAction(with: move, from: rotateNode)
actions.append(action)
}
actions.append(SCNAction.removeFromParentNode())
let sequence = SCNAction.sequence(actions)
rotateNode.runAction(sequence)
}

Related

Swift: What is a better way to implement a game loop for dice roll game (without using wait delay)

I am building a simple dice roll game.
The game cycle is like this:
1) Roll dice and move piece
2) Check if there is extra bonus moves, if Yes then move the piece
3) Check if piece has reached its destination, if Yes then Game ends
4) End turn
I built my Game loop using SKAction.run statements and append them into an array and then run them.
The actual code is very long, so I am providing only the basic flow here.
func runDiceRoll() {
// some SKActions
}
func checkExtraMoves() {
// run some SKActions depending on runDiceRoll() outcome
}
func checkStatus() {
// code to check if Game ends, depends on checkExtraMoves() outcome
}
func endTurn() {
// code to end the player's turn and give it to the other player
}
My game loop looks something like this
func runGameCycle() {
var actions = [SKAction]()
let actionRun = SKAction.run {runDiceRoll()}
let actionCheckExtraMoves = SKAction.run {checkExtraMoves()}
let actionCheckStatus = SKAction.run {checkStatus()}
let actionEndTurn = SKAction.run {endTurn()}
let actionWait = SKAction.wait(forDuration:2.0)
actions.append(actionRun)
actions.append(actionWait)
actions.append(actionCheckExtraMoves)
actions.append(actionCheckStatus)
actions.append(actionEndTurn)
piece.run(SKAction.sequence(actions))
}
If I remove the Wait action, then the game play is not correct.
I would prefer not to use a Wait duration because it is just an estimate that the preceeding actions will be completed in 2 seconds or less.
I think it would be better if all the actions waits for the preceeding action to complete before firing. I am not sure how to use a completion handler or DispatchQueue for such purposes.
Is there a better way, more foolproof way to write the game loop?

Spawning Sprite problems in swift3 xCode

Im having some issues, I have sprites spawn on the right of the screen and work there way left then when they go off the screen they are removed from the scene and the process restarts. I want every spawn to get quicker by like 0.1seconds.
Problem is i am calling my spawn function when the game starts and its on a constant loop so i cant then update the delay.
Spawn Code:
func spawnBirdRL() {
let spawn = SKAction.run({ () -> Void in
self.createEnemyBird()
})
let delay = SKAction.wait(forDuration: (TimeInterval(spawnDuration)))
let sequence = SKAction.sequence([spawn, delay])
self.run(SKAction.repeatForever(sequence), withKey: "Spawn")
}
Which is being called in the TouchesBegan function.
It creates the enemy bird and runs the movement loop over and over
I have changed the question slightly with more information...

How do I only allow one instance of an SKSpriteNode on screen at one time?

This is my first ever post - I have searched for a long time and could not find the answer.
I am making a game with SpriteKit and want the player to be able to only launch one bomb at a time- i.e they can't fire again until the previous bomb has exploded or gone off screen. Currently when the player taps the screen, they can launch as many bombs as they want.
Any help would be greatly appreciated!
Thanks,
Iain
Steve's idea works out well and is better than mine, but here is a more novice-friendly explanation IMO... Put this in your gamescene :)
var canFireMissile = true
func fireMissile() {
guard canFireMissile else { return }
canFireMissile = false // So you can't fire anymore missiles until 0.5secs later
let wait = SKAction.wait(forDuration: 0.5) // the duration of the missile animation (example)
let reset = SKAction.run { canFireMissile = true } // lets us refire the missile
let sequence = SKAction.sequence([wait, reset])
run(sequence)
}
override func mouseDown(with event: NSEvent) { // touchesBegan on iOS
fireMissile()
}
Create a SKSpriteNode property for your misssile.
Create an SKAction for the movement of the missile and give the action a key so you can refer to it by name).
When the fire button is pressed, check to see if the named action is already running; if it is, do nothing, otherwise run the ‘fireMissile’ action.

Swift app development (game w/ SpriteKit) I want some visual changes to appear on the screen and then disappear after a given period of time

I am making a game using Swift and SpriteKit. I have written something of this sort:
node.alpha = 0
sleep(1)
node.alpha = 1
where node is an SKSpriteNode. Instead of making node invisible, freezing the running of the program for one second and making node visible again, what this code does is it simply freezes the running of the program for one second. I figured that all visual changes take place periodically, maybe after each update. How can resolve this and make the node disappear for one second, having the program frozen?
Thank you!
This answer is going to assume you want to just pause the scene, and not the entire program.
What you want to use is the SKActions sequence and waitForDuration,
and the nodes pause variable.
You essentially want to move the entire scene into a separate node, and let another node control the pausing of it.
Set your scene nodes like this:
let sceneWorld = SKNode() //make this available to the entire class
let timerNode = SKNode()
...
override func didMoveToView()
{
....
scene.addNode(timerNode)
scene.addNode(sceneWorld) //sceneWorld is where you will be adding all gfx sprites now
}
func pauseWorld()
{
let wait1sec = SKAction.waitForDuration(1)
let unpause = SKAction.runBlock({sceneWorld.pause = false;showNodes(true)})
timerNode.runAction(SKAction.sequence[wait1sec,unpause])
showNodes(false)
sceneWorld.pause = true;
}
func showNodes(show : Boolean)
{
let alpha = (show) : 1.0 ? 0.0
//set all nodes that need to hide here with alpha
...
}
The solution of luk2303 has worked perfectly for me in my apps.
you can create a timer like this:
node.alpha = 0
NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "makeNodeVisible", userInfo: nil, repeats: false)
func makeNodeVisible(){
node.alpha = 1
}
For some reason using the sleep function won't work in SpriteKit because it is meant for the ViewController or something like that.

How to change wait for duration SKAction ran by an SKScene?

In my SpriteKit project, I have a spawnEnemy method. I want this method to be called over and over again so that enemies continue to spawn throughout the game.
What I did to achieve this is to put the spawnEnemy method in an SKAction runBlock and have my SKScene run that action forever with a delay in between calls (to prevent overloading the app).
Below is a snippet of the relevant code:
var _spawnSpeedSeconds: Double = 2.0
func startSpawning()
{
let waitForXSeconds = SKAction.waitForDuration(self._spawnSpeedSeconds)
let spawn = SKAction.runBlock({ () -> Void in
self.spawnEnemy()
})
self.runAction(SKAction.repeatActionForever(SKAction.sequence([spawn, waitForXSeconds])), withKey: "spawnEnemy")
}
func spawnEnemy()
{
...
}
Now after a certain time (for example when the player gets 5 points or something), I decrease the _spawnSpeedSeconds to make the enemies spawn more in a shorter amount of time to increase difficulty.
The problem is, even if I decrease my _spawnSpeedSeconds variable, the spawn action's delay being ran by the SKScene is still the same.
The only way I can think of resolving this issue is to remove the action, and then re-add the action with a new delay/spawn rate. Is there a better way to approach this issue?
Thanks
don't use an action in this case. They don't really work in a dynamic way. Once you set them, they're stuck. just use your update method.
I'll show you how I'm periodically launching missles:
first set two timers in your class
var missleTimer:NSTimeInterval = NSTimeInterval(2)
var missleInterval:NSTimeInterval = NSTimeInterval(2)
now in our update method we count down the time and spawn missles
// subtract time from our timer
self.missleTimer -= self.delta
// when our timer reaches zero
if self.missleTimer <= 0 {
// run your spawning code
self.launchMissle()
// reset timer
self.missleTimer = self.missleInterval
}
this is better than using an action in this case because I can set missleInterval anywhere in my code and the change will always be reflected.
-(void)recursiveMethod
{
if(shouldSpawnEnemy)
{
_spawnSpeedSeconds -= 0.01;
SKAction *wait = [SKAction waitForDuration: 0.5];
SKAction *action = [SKAction performSelector:#selector(recursiveMethod) onTarget:self];
SKAction *sequence = [SKAction sequence:#[wait,action]];
[self repeatActionForever:sequence];
}
}
Remember to call the recursiveMethod when you want to start spawning enemies.
Hope that helps.
Providing swift solution without the need for timers, using recursion with a circuit breaker flag:
class YourScene: SKScene {
var gameOver: Bool = false
func spawnEnemies() {
let waitAction = SKAction.wait(forDuration: yourDynamicDurationHere)
self.run(SKAction.sequence([SKAction.run(self.spawnEnemy), waitAction])) {
if !gameOver {
spawnEnemies()
}
}
}
}
This solution leverages the completion handler of run method to recursively schedule another execution of the sequence. Since each individual SKSequence is created just before it is executed, we can pass in a different wait time for each iteration.
To stop the recursive loop, just set gameOver = true.

Resources