How do SKAction durations work in relation to frame rates? - ios

I am trying to understand how SKActions work. Specifically with SKAction.runAction(), one of the parameters is a duration for the animation. However it seems that whatever I put in as the duration, the animation will render every frame. I have tried to put the SKAction.runAction() line in other places, but it seems to only work in the update() override method in SKScene. I have tried the SKAction.repeatActionForever, but this does not seem to be able to run parallel to other processes.
My question is, if there is a required parameter for the duration of the animation, where would one place the SKAction.runAction() method to have the animation run based on the given duration rather then on the frame rate?
This is my code:
GameScene.swift (The Player class is a subclass of the Character class. Refer to the code below.)
let player = Player()
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
//Code here to recognize user inputs
scene?.addChild(player.sprite)
}
override func update(currentTime: CFTimeInterval) {
player.sprite.runAction(SKAction.runBlock(player.move))
}
}
CharachterClass.swift move() function
func move(){
switch(direction){
case "up":
sprite.position = CGPoint(x: sprite.position.x, y: sprite.position.y + speed)
case "down":
sprite.position = CGPoint(x: sprite.position.x, y: sprite.position.y - speed)
case "right":
sprite.position = CGPoint(x: sprite.position.x + speed, y: sprite.position.y)
case "left":
sprite.position = CGPoint(x: sprite.position.x - speed, y: sprite.position.y)
default:
break
}
SKAction.moveTo(sprite.position, duration: 1)
}
Note: the direction variable is the direction that the sprite is moving in and speed variable is the amount of pixels the sprite should move each time the SKAction is run.

I don't know what you intend to do with this:
override func update(currentTime: CFTimeInterval) {
player.sprite.runAction(SKAction.runBlock(player.move))
}
But in effect you can quite simply do the following instead:
override func update(currentTime: CFTimeInterval) {
player.move()
}
The difference between the two methods is that runAction will schedule the block execution and depending on when the scheduling occurs, it may not run in the current frame (update cycle) but in the next one. So quite possibly the run block solution introduces a 1-frame delay.
Next, this will run a move action for the duration of 1 second (not: 1 frame):
SKAction.moveTo(sprite.position, duration: 1)
Since you run this in every update method, you effectively null and void the SKAction's primary purpose: to run a task over a given time (duration).
Worse, you run a new move action every frame so over the duration of 1 second you may accumulate about 60 move actions that run simultaneously, with undefined behavior.
If you were to call runAction with the "key" parameter you could at least prevent the actions from stacking, but it would still be rather pointless because replacing an action every frame that is supposed to run over the time of 1 second means it will have little effect, if any.
Coincidentally, with the way the code is set up right now, you could as well just assign to position directly ... but ... wait, you already do that:
sprite.position = CGPoint(x: sprite.position.x - speed, y: sprite.position.y)
But immediately afterwards you run this move action:
SKAction.moveTo(sprite.position, duration: 1)
So all in all, you try to run an action over the course of 1 second that ends up being replaced every frame (60 times per second) - and that action does ... nothing. It will not do anything because you've already set the sprite to be at the position that you then pass into the move action as the desired target position. But the sprite is already at that position, leaving the action nothing to do. For one second - or perhaps it may quit early, that depends on SK implementation details we don't know about.
No matter how you look at it, this move action is over-engineered and has no effect. I suggest you (re-)read the actions article in the SK Programming Guide to better understand the timing and other action behaviors.

Related

Swift Game Scene alter vertically moving background with time?

I have a moving background which is 1500 x 600 pixels and constantly moves vertically down the screen using this code:
let bgTexture = SKTexture(imageNamed: "bg.png")
let moveBGanimation = SKAction.move(by: CGVector(dx: 0, dy: -bgTexture.size().height), duration: 4)
let shiftBGAnimation = SKAction.move(by: CGVector(dx: 0, dy: bgTexture.size().height), duration: 0)
let moveBGForever = SKAction.repeatForever(SKAction.sequence([moveBGanimation, shiftBGAnimation]))
var i: CGFloat = 0
while i < 3 {
bg = SKSpriteNode(texture: bgTexture)
bg.position = CGPoint(x: self.frame.midX, y: bgTexture.size().height * i)
bg.size.width = self.frame.width
bg.zPosition = -2
bg.run(moveBGForever)
self.addChild(bg)
i += 1
}
I now want a new background to come onto the screen after x amount of time to give the feel the player is moving into a different part of the game.
Could I put this code into a function and trigger it with NSTimer after say 20 seconds but change the start position of the new bg to be off screen?
The trouble with repeatForever actions is you don't know where they are at a certain moment. NSTimers are not as precise as you'd like, so using a timer may miss the right time or jump in too early depending on rendering speeds and frame rate.
I would suggest replacing your moveBGForever with a bgAnimation as a sequence of your move & shift actions. Then, when you run bgAnimation action, you run it with a completion block of { self.cycleComplete = true }. cycleComplete would be a boolean variable that indicates whether the action sequence is done or not. In your scene update method you can check if this variable is true and if it is, you can run the sequence action once again. Don't forget to reset the cycleComplete var to false.
Perhaps it sounds more complex but gives you control of whether you want to run one more cycle or not. If not, then you can change the texture and run the cycle again.
Alternatively you may leave it as it is and only change the texture(s) after making sure the sprite is outside the visible area, e.g. its Y position is > view size height.
In SpriteKit you can use wait actions with completion blocks. This is more straightforward than using a NSTimer.
So, to answer your question - when using actions for moving the sprites on-screen, you should not change the sprite's position at any time - this is what the actions do. You only need to make sure that you update the texture when the position is off-screen. When the time comes, obviously some of the sprites will be displayed, so you can't change the texture of all 3 at the same time. For that you may need a helper variable to check in your update cycle (as I suggested above) and replace the textures when the time is right (sprite Y pos is off-screen).

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

SpriteKit: how to smoothly animate SKCameraNode while tracking node but only after node moves Y pixels?

This question and others discuss how to track a node in SpriteKit using a SKCameraNode.
However, our needs vary.
Other solutions, such as updating the camera's position in update(_ currentTime: CFTimeInterval) of the SKScene, do not work because we only want to adjust the camera position after the node has moved Y pixels down the screen.
In other words, if the node moves 10 pixels up, the camera should remain still. If the node moves left or right, the camera should remain still.
We tried animating the camera's position over time instead of instantly, but running a SKAction against the camera inside of update(_ currentTime: CFTimeInterval) fails to do anything.
I just quickly made this. I believe this is what you are looking for?
(the actual animation is smooth, just i had to compress the GIF)
This is update Code:
-(void)update:(CFTimeInterval)currentTime {
/* Called before each frame is rendered */
SKShapeNode *ball = (SKShapeNode*)[self childNodeWithName:#"ball"];
if (ball.position.y>100) camera.position = ball.position;
if (fabs(ball.position.x-newLoc.x)>10) {
// move x
ball.position = CGPointMake(ball.position.x+stepX, ball.position.y);
}
if (fabs(ball.position.y-newLoc.y)>10) {
// move y
ball.position = CGPointMake(ball.position.x, ball.position.y+stepY);
}
}
I would not put this in the update code, try to keep your update section clutter free, remember you only have 16ms to work with.
Instead create a sub class for your character node, and override the position property. What we are basically saying is if your camera is 10 pixels away from your character, move towards your character. We use a key on our action so that we do not get multiple actions stacking up and a timing mode to allow for the camera to smoothly move to your point, instead of being instant.
class MyCharacter : SKSpriteNode
{
override var position : CGPoint
{
didSet
{
if let scene = self.scene, let camera = scene.camera,(abs(position.y - camera.position.y) > 10)
{
let move = SKAction.move(to: position, duration:0.1)
move.timingMode = .easeInEaseOut
camera.run(move,withKey:"moving")
}
}
}
}
Edit: #Epsilon reminded me that SKActions and SKPhysics access the variable directly instead of going through the stored property, so this will not work. In this case, do it at the didFinishUpdate method:
override func didFinishUpdate()
{
//character should be a known property to the class, calling find everytime is too slow
if let character = self.character, let camera = self.camera,(abs(character.position.y - camera.position.y) > 10)
{
let move = SKAction.move(to: character.position, duration:0.1)
move.timingMode = .easeInEaseOut
camera.run(move,withKey:"moving")
}
}

How do I use SpriteKit's SKAction to create a time interval for an SKSpriteNode?

I have a function, basketball. I want to be able to spawn my basketBall node at CGPoint(515,700), the top of the screen, in a regular time interval. The only method I knew that would accomplish waiting a few seconds was sleep(); however, sleep() apparently doesn't allow SpriteNodes to remain on screen, so I need an alternative.
I discovered NSTimeInterval, but I would prefer to refrain from importing Foundation. I think that SKAction allows time to pass through waitForDuration(), but I am very confused as to how that works. Can someone shed some light on SpriteKit's SKAction?
override func didMoveToView(view: SKView) {
let basketBall = SKSpriteNode(imageNamed: "basketball")
let waitForObjects = SKAction.waitForDuration(3)
let basketBallFalls = SKAction.runBlock({
self.Basketball()
})
let action = SKAction.sequence([waitForObjects, basketBallFalls])
SKAction.runAction(action, onChildWithName: "basketball")
SKAction.repeatActionForever(action)
//Basketball()
}
func Basketball(){
basketBall.position = CGPointMake(515, 700)
basketBall.size = CGSizeMake(50, 50)
basketBall.size = CGSize(width: 50.0, height: 50.0)
basketBall.zPosition = 10
basketBall.physicsBody = SKPhysicsBody(circleOfRadius: 25.0)
basketBall.physicsBody?.mass = 0.8
basketBall.physicsBody?.restitution = 0.6
basketBall.physicsBody?.dynamic = true
self.addChild(basketBall)
}
Try to use SKAction.waitForDuration(_:).
Ex. something like this:
let waitDuration = NSTimeInterval(arc4random_uniform(20))
let waitAction = SKAction.waitForDuration(waitDuration)
let ballAction = SKAction.runBlock(self.Basketball) //call your function
runAction(SKAction.sequence([waitAction, ballAction]))
There are two straightforward ways to spawn a new node at a regular interval. They both involve your scene.
The scene's update(_:) method is called every frame, and the argument is the current time. Add an NSTimeInterval property to your scene class which will store the last time you created a basketball. In update(_:), subtract the current time from the last spawn time. If the difference is greater than the interval that you want, spawn a new node and keep the new current time.
Your scene can also run actions, like any other node. SKAction has a waitForDuration(_:) method which creates an action that does, well, exactly what its name says. You can create another action using SKAction.runBlock(_:) which will perform the spawning. Then create a sequence action that runs the delay action followed by the spawn action. Wrap that in a repeatActionForever(_:), and finally tell your scene to run the repeater action.

swift: SKSpriteNode not rotating in update function

I'm trying to rotate a bone image which is a SKSpriteNode. I created it and on update, I tried running a SKAction which would rotate it. This doesn't do anything. I even tried to decrease the zRotation each time on update by 10 degrees and still nothing happened. I'm sure that the application is not frozen as when I do a println on update, text keeps rolling. Also, other parts of the app are responding. Its just the bone thats not rotating.
Code:
Declaration on start of class
let bone = SKSpriteNode(imageNamed: "bone")
let rotateBone = SKAction.rotateByAngle(30, duration: 1)
Setting up at didMoveToView
bone.anchorPoint = CGPointMake(0.5, 0.5)
bone.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
bone.zRotation = CGFloat(arc4random())
self.addChild(bone)
Attempting to rotate
override func update(currentTime: NSTimeInterval)
{
bone.runAction(rotateBone)
}
You are re-running the action on every update (every frame):
override func update(currentTime: NSTimeInterval)
{
bone.runAction(rotateBone)
}
Actions run over time. Run it once and let it do its job. Restarting it repeatedly will only replace the previous action and essentially reset it.

Resources