SKAction with duration: 0 doesn't work - SpriteKit (Language: Swift) - ios

I am trying to make a fairly simple running animation by changing 2 images every 0.15 seconds and moving the character slightly up, when it has the running image, and slightly down when it has the other. It would work properly if I have 0.5 seconds as duration, or even 0.1, but when I change it to 0, or even 0.0001 the character doesn't move at all. (the image changing works properly)
let movingUp = SKAction.moveTo(CGPoint(x: self.position.x, y: self.position.y + 10), duration: 0)
let waiting = SKAction.waitForDuration(0.15)
let movingDown = SKAction.moveTo(CGPoint(x: self.position.x, y: self.position.y - 10), duration: 0)
let movingSequence = SKAction.sequence([movingUp, waiting, movingDown])
self.runAction(SKAction.repeatActionForever(movingSequence))
let animateAction = SKAction.animateWithTextures(runningTexturesArray, timePerFrame: 0.15)
let repeatAction = SKAction.repeatActionForever(animateAction)
self.runAction(repeatAction)
I also tried moveBy, got the same result. I need to mention when I set the duration to 0.1 I got a strange behaviour having the character shivering up and down (quicker than it should). It has physics body applied because I want to use collide detection later, but I've set affectedByGravity to false. I was thinking that maybe the physicsWorld has some kind of air, which blocks the character from moving freely while in "vacuum" it could do that.
Thank you in advance for any answer. I'd really appreciate it!

Related

How to add a pause/wait in the middle of a nested SCNTransaction chain

This question must have a very simple answer, but I couldn't find it after searching for hours. Unless I just need to manually add some timer to wait.
Basically, I have an animation sequence to move a node to a certain place, here I want to wait 5 seconds and then I have another animation sequence to bring the node back to its original position.
Initially, I used SCNActions, grouping some actions and then sequencing them all. Here I just added an SCNAction.wait(duration: 5) and that did the trick.
However, one of the actions is to move the node 90 degrees around the X-axis and I need to simultaneously rotate around another axis as well. The result is incorrect and I have a feeling I've run into the gimbal lock issue.
So instead of using SCNAction.rotate(by: ) I decided to rotate using quaternions which don't have the gimbal lock problem, but then I needed to switch to using an SCNTransaction.
I'm nesting these transaction in the completionBlock of the previous SCNTransaction.
For the pause between the 2 animations, I don't have a wait action here, but I thought that just adding a transaction with a duration that does nothing will at least wait for as long. But it doesn't. The transaction immediately skips to the 3rd step.
So is there a simple command to tell the animation transaction to wait?
I did try to add DispatchQueue.main.asyncAfter(deadline: .now() + 5) { <3rd SCNTransaction here> } and that worked, but is that really the way to do it? I have a feeling there's something else that I just don't know that replaces SCNAction.wait().
Here's my current code which does the animations correctly, except it doesn't wait where I want it to (this is an excerpt of the relevant code only):
[...]
let moveSideQuat = simd_quatf(angle: -currentYRotation, axis: simd_float3(0, 0, 1))
let moveAwaySimd = simd_float3(-20 * sinf(currentYRotation), 0, -20 * cosf(currentYRotation))
let moveUpQuat = simd_quatf(angle: .pi/2, axis: simd_float3(1, 0, 0))
let moveDownQuat = simd_quatf(angle: -.pi/2, axis: simd_float3(1, 0, 0))
let moveTowardsQuat = simd_float3(0, 0, pivotZLocation)
let moveBackSideQuat = simd_quatf(angle: currentYRotation, axis: simd_float3(0, 0, 1))
SCNTransaction.begin()
SCNTransaction.animationDuration = 1
baseNode?.simdOrientation = moveSideQuat * baseNode!.simdOrientation
baseNode?.simdPosition = moveAwaySimd
baseNode?.simdOrientation = moveUpQuat * baseNode!.simdOrientation
SCNTransaction.completionBlock = {
SCNTransaction.begin()
SCNTransaction.animationDuration = 5 // <<<--- I WANT TO WAIT HERE
SCNTransaction.completionBlock = {
SCNTransaction.begin()
SCNTransaction.animationDuration = 1
self.baseNode?.simdOrientation = moveDownQuat * self.baseNode!.simdOrientation
self.baseNode?.simdPosition = moveTowardsQuat
self.baseNode?.simdOrientation = moveBackSideQuat * self.baseNode!.simdOrientation
SCNTransaction.commit()
}
SCNTransaction.commit()
}
SCNTransaction.commit()
[...]
In the 2nd completion block, after the animationDuration = 5 I even tried to add a simple assignment for the node's simdPosition, basically leaving it in the same position in the hope to trick the sequence to take 5 seconds to basically do nothing, but it still skipped to the next completionBlock.
So is the asyncAfter actually the way to go or am I missing something else?
Thanks!
Using asyncAfter to wait is fine. That is the purpose of this method, after all.
Creating an empty transaction with the duration of 5 seconds doesn't work because SceneKit can easily see that you haven't changed anything, and knows that the animation can complete immediately. You can hide a node somewhere in the scene and animate that node for 5 seconds, but that feels way more like a hack.
If you want, you can still use SCNActions for the rest of your animations, and just use SCNTransaction for the ones that need the quaternions. For example:
let step1 = SCNAction.group([
SCNAction.run {
SCNTransaction.begin()
SCNTransaction.animationDuration = 1
// do rotations here with quaternions...
SCNTransaction.commit()
},
SCNAction.move(to: /* destination */, duration: 1)
])
let step2 = SCNAction.wait(duration: 5)
let step3 = SCNAction.group([
SCNAction.run {
SCNTransaction.begin()
SCNTransaction.animationDuration = 1
// rotate back...
SCNTransaction.commit()
},
SCNAction.move(to: /* origin */, duration: 1)
])
let entireAction = SCNAction.sequence([
step1, step2, step3
])

Animation to Remove Sprite

I am currently making a Sprite and I want it to animate before it disappears.
For example: I want it to animate it in the sense that it disappears from the top of the block until the bottom. To put it another way, I want to the size to decrease slowly until there is nothing left. But I want to give it the appearance that it is disappearing rather than scaling to nothing.
let hand = SKSpriteNode(imageNamed: "hand")
hand.size = CGSize(width: size.width/10, height: size.height/30)
hand.position = CGPoint(x: CGFloat(posX-2)*size.width/10+offsetX, y:CGFloat(posY)*size.height/30+offsetY)
addChild(hand)
tl;dr is it possible to make this sort of effect using SpriteKit in Swift.
Ideal Animation: https://ibb.co/sPsffmK
SKAction is an animation that is executed by a node in the scene. Actions are used to change a node in some way (like move its position over time), but you can also use actions to change the scene, like doing a fadeout.
In your case, you can apply multiple animations:
let move_action = SKAction.moveBy(x: -100, y: 0, duration: 1.0)
let scale_action = SKAction.scale(to: 0.0, duration: 1.0)
let fade_action = SKAction.fadeAlpha(to: 0.0, duration: 1.0)
hand.run(move_action)
hand.run(scale_action)
//hand.run(fade_action)
In the previous example, the hand runs the move and scale animation at the same time. But you can also make the hand to move to the position, and after it reaches, to scale it.
let sequence = SKAction.sequence([move_animation,scale_animation])
hand.run(sequence)
There are lots of animations that SKAction has, here you can find the complete list.

SpriteKit camera actions being very jerky/laggy

I'm using Swift 3.0 and Xcode 8.2.1, testing on an iPhone 6s running iOS 10.2.
I'm simply attempting to rotate an SKCameraNode at a rate that is slowing down (I.E. it has both an angular velocity and acceleration).
This is my current solution, and yes I know it's weird:
cam.run(
SKAction.repeatForever(
SKAction.sequence([
SKAction.run
{
self.cam.run(SKAction.rotate(toAngle: 0.0, duration: 0.7, shortestUnitArc: true))
},
SKAction.wait(forDuration: 0.1)])))
The camera's zRotation starts at some non-zero place, and then this is called. It then calls the SKAction.rotate part every 0.1 seconds. It's not shown here, but I have it terminate after 3 seconds.
This provides the exact effect that I want, because every time the SKAction is called, the zRotation is closer to its 0.0, but the time it needs to take to get there stays at 0.7, so the rate at which it approaches 0.0 slows down.
HOWEVER, 1 of 3 things happen from this:
It works perfectly as intended
It feels like it stops and continues every 0.1 seconds
The camera just immediately and completely stops functioning as a camera as soon as the SKAction is called, and the player is just stuck looking at the same spot till the cameras are switched.
I tried reducing the waitForDuration from 0.1 to 0.01 but then it always does option 3.
Is there some sort of "rules of execution" that I'm not following when it comes to using cameras?
Thanks!
You can use a single rotate(toAngle:) action that decelerates when the camera's zRotation is close to the ending angle and setting the timingMode property to .easeOut. For example,
cam.zRotation = CGFloat.pi
let action = SKAction.rotate(toAngle: 0, duration: duration, shortestUnitArc: true)
// Slows when zRotation is near 0
action.timingMode = .easeOut
cam.run(action)
Alternatively, you can define your own timing function to customize the ease-out behavior by replacing timingMode statement with
action.timingFunction = {
time in
// Custom ease-out, modify this as needed
return 1-pow(1-time, 5)
}
Moreover, you can use the following to calculate the action's duration so the angular velocity and acceleration are consistent regardless of the starting angle (i.e., the amount of rotation)
let duration = TimeInterval(normalizedArcFromZero(angle:cam.zRotation)) * 3
func normalizedArcFromZero(angle:CGFloat) -> CGFloat {
let diff = angle.mod(dividingBy:CGFloat.pi*2)
let arc = CGFloat.pi - (diff + CGFloat.pi).mod(dividingBy:2 * CGFloat.pi)
return abs(arc)/CGFloat.pi
}
Lastly, since the above requires a modulo function that performs a "floored" division (instead of Swift's truncatingRemainder), you'll need to add this to your project
extension CGFloat {
func mod(dividingBy x:CGFloat) -> CGFloat {
return self - floor(self/x) * x
}
}

Slow down SKSpriteNode with Impulse

I have a simple game, where after my score the ball gets more speed with
ball.physicsBody?.applyImpulse(CGVector(dx: 1, dy: -1))
So now i want to slow down after he loses. I tried:
ball.pause = 0
and many other codes, that i found on the web. Can you help me?
// Sorry if this is not a advanced Question, I'm not that long in swift.
If you want to stop the ball completely, you can run this line:
ball.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
This sets the velocity to nothing, effectively stopping the ball, without pausing it.
If you only want to slow it down, you can try doing something like this:
ball.physicsBody?.velocity = CGVector(dx: ball.physicsBody.velocity.dx - 5, dy: ball.physicsBody.velocity.dy + 5)
This brings the velocity closer to nothing (designed for your example), rather than completely stopping it.
Alternatively, you could apply an impulse in the opposite direction, that would slow down its movement in the original direction:
ball.physicsBody?.applyImpulse(CGVector(dx: -1, dy: 1))
Note that if you're trying to completely pause the ball (not just slow down or stop it, but pause it's actions) you would run this: ball.isPaused = true

Cannot get SKActionmove to work reliably

I'm trying to build a small spriteKitGame. I have been trying to use the function to move a couple of tank sprite nodes to a specific point.
Attached below is the code snippet.
let tankSpawn = CGPoint(x: self.size.width , y: 70);
tank.position = tankSpawn;
tank.zPosition = 3.0;
let targetPoint = CGPoint(x: -tank.size.width/2, y: tank.position.y);
let actionMove = SKAction.move(to: targetPoint, duration: TimeInterval(tankMoveDuration))
This is my result. They are spawning at the correct point ( 70units high ), but are going down as shown.
I want them to go in a straight line. I set the target points y as a constant. I have no idea why they are going to wards some bottom source.
I have similar code for planes that spawn above( which are working perfectly ).
let plane = SKSpriteNode(imageNamed: "SpaceShip");
let planeMoveDuration = 3.0
let planeSpawn = CGPoint(x: self.size.width , y: self.size.height/2);
plane.position = planeSpawn;
plane.zPosition = 3.0;
let actionMove = SKAction.move(to: CGPoint(x: -plane.size.width/2, y: plane.position.y), duration: TimeInterval(planeMoveDuration))
I have no idea what my mistake here is.
I have tried changing the y co ordinate of the target to tank.position.y, but it doesn't work.
Are they falling under gravity? A sprite-kit scene with physics bodies will have gravity and things will fall unless you take specific action to avoid this.
It's easily done - you're developing your game, you have basic elements on screen and movement etc is all working well, then you want to add some collision detection so you add physicsBodies and then whoosh - where'd my sprites go?

Resources