Exponentially shrink an SKNode - ios

In a game I'm developing with SpriteKit, I want certain objects to appear and shrink. I already know how to scale them down, and I'm achieving this using the following code:
myNode.run(SKAction.scale(to: 0, duration: 3))
However, the shrinking happens 'linearly'. Is there a way to make it shrink exponentially faster? Or at least that it starts slowly and at the last second it shrinks twice as fast?

Sorry I could not test this out, I do not have a compiler. It is properly not the best way to do it, but I gave it a shot:
func delay(_ delay:Double, closure:#escaping ()->()) {
let when = DispatchTime.now() + delay
DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
let totalRunTime = 1
var add = 0.0
var scale = 1
var done = false
while !done{
delay(add, closure: {
add += 0.1
scale = 1 - (add ^2)
myNode.run(SKAction.scale(to: scale, duration: totalRunTime / 10))
})
if add == 0{
done = true
}
}
Edit: When I look at my code I may see a bug: maybe you need to switch the exponential formula from scale to duration to make it work, I can not test it now :(

Have a look at the Sprite Kit Utils from Ray Wenderlich. It's quite a useful library and also provides easing functions for movement, scale, and rotate actions.
(For reference, have a look at the different easing functions demonstrated on easings.net)
Hope that helps!

Related

CAAnimation on multiple SceneKit nodes simultaneously

I am creating an application wherein I am using SceneKit contents in AR app. I have multiple nodes which are being placed at different places in my scene. They may or may not be necessarily be inside one parent node. The user has to choose a correct node, as per challenge set by the application. If the user chooses correct node, the correct node goes through one kind of animation and incorrect ones (may be several) undergo another set of animation. I am accomplishing animations using CAAnimation directly, which is all good. Basically to accomplish this, I am creating an array of all nodes and using them for animation.
DispatchQueue.global(qos: .userInteractive).async { [weak self] in
for node in (self?.nodesAddedInScene.keys)! {
for index in 1...node.childNodes.count - 1 {
if node.childNodes[index].childNodes.first?.name == "target" {
self?.riseUpSpinAndFadeAnimation(on: node.childNodes[index])
} else {
self?.fadeAnimation(on: node.childNodes[index])
}
}
}
}
When user chooses "target" node, that node goes through one set of animation and others go through another set of animations.
RiseUpSpinAndFadeAnimation:
private func riseUpSpinAndFadeAnimation(on shape: SCNNode) {
let riseUpAnimation = CABasicAnimation(keyPath: "position")
riseUpAnimation.fromValue = SCNVector3(shape.position.x, shape.position.y, shape.position.z)
riseUpAnimation.toValue = SCNVector3(shape.position.x, shape.position.y + 0.5, shape.position.z)
let spinAnimation = CABasicAnimation(keyPath: "eulerAngles.y")
spinAnimation.toValue = shape.eulerAngles.y + 180.0
spinAnimation.autoreverses = true
let fadeAnimation = CABasicAnimation(keyPath: "opacity")
fadeAnimation.toValue = 0.0
let riseUpSpinAndFadeAnimation = CAAnimationGroup()
riseUpSpinAndFadeAnimation.animations = [riseUpAnimation, fadeAnimation, spinAnimation]
riseUpSpinAndFadeAnimation.duration = 1.0
riseUpSpinAndFadeAnimation.fillMode = kCAFillModeForwards
riseUpSpinAndFadeAnimation.isRemovedOnCompletion = false
shape.addAnimation(riseUpSpinAndFadeAnimation, forKey: "riseUpSpinAndFade")
}
FadeAnimation:
private func fadeAnimation(on shape: SCNNode) {
let fadeAnimation = CABasicAnimation(keyPath: "opacity")
fadeAnimation.toValue = 0.0
fadeAnimation.duration = 0.5
fadeAnimation.fillMode = kCAFillModeForwards
fadeAnimation.isRemovedOnCompletion = false
shape.addAnimation(fadeAnimation, forKey: "fade")
}
I expect animations to work out, which they are actually. However, the issue is since the nodes are in an array animation is not being done at the same time for all nodes. There are minute differences in start of animation which actually is leading to not so good UI.
What I am looking for is a logic wherein I can attach animations on all nodes and call these animations together later when let's say the user taps correct node. Arrays don't seem to be a wise choice to me. However, I am afraid if I make all of these nodes child nodes of an empty node and the run animation on that empty node, possibly it would be difficult to manage placement of these child nodes in the first place since they supposed to be kept at random distances and not necessarily close together. Given that this ultimately drives AR experience, it more so becomes a bummer.
Requesting some inputs whether there are methods to attach animation to multiple (selected out of those) object (even if sequentially) but RUN them together. I used shape.addAnimation(fadeAnimation, forKey: "fade") "forKey", can that be made of use in such use case? Any pointers appreciated.
I've had up to fifty SCNNodes animating in perfect harmony by using CAKeyframe animations that are paused (.speed = 0) and setting the animation's time offset (.timeOffset) inside a SCNSceneRendererDelegate "renderer(updateAtTime:)" function.
It's pretty amazing how you can add a paused animation with an time offset every 1/60th of a second for a large number of nodes. Hats off to the SceneKit developers for having so little overhead on adding and removing CAAnimations.
I tried many different CAAnimation/SCNAction techniques before settling on this. In other methods the animations would drift out of sync over time.
Manganese,
I am just taking a guess here or could spark an idea for you :-)
I am focusing on this part of your question:
"What I am looking for is a logic wherein I can attach animations on all nodes and call these animations together later when let's say the user taps correct node."
I wonder if SCNTransaction:
[https://developer.apple.com/documentation/scenekit/scntransaction][1]
might do the trick
or maybe dispatch.sync or async (totally guessing...but could help)
[https://developer.apple.com/documentation/dispatch][1]
or I am way off the mark :-)
just trying to help out....
We all learn by sharing what we know
RAD

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

dismiss view controller custom animation

I am trying to replicate this animation to dismiss a view controller (15 second video): https://www.youtube.com/watch?v=u87thAbT0CQ
This is what my animation looks like so far: https://www.youtube.com/watch?v=A2XmXTVxLdw
This is my code for the pan gesture recognizer:
#IBAction func recognizerDragged(sender: AnyObject) {
let displacement = recognizer.translationInView(view)
view.center = CGPointMake(view.center.x + displacement.x, view.center.y + displacement.y)
recognizer.setTranslation(CGPoint.zero, inView: view)
switch recognizer.state {
case .Ended:
UIView.animateWithDuration(0.4, animations: { () -> Void in
self.view.layer.position = CGPoint(x: self.view.frame.width / 2, y: self.view.frame.height / 2)
})
default:
print("default")
}
let velocity = recognizer.velocityInView(self.titleView)
print(velocity)
if velocity.y < -1500 {
up = true
self.dismissViewControllerAnimated(true, completion: nil)
}
if velocity.x > 1500 {
right = true
self.dismissViewControllerAnimated(true, completion: nil)
}
}
It may be a little hard to notice in my video, but there is a small disconnect in how fast the user flicks up, and how fast the animation completes. That is to say, the user may flip up very fast but the animation is set to a hardcoded 0.3 seconds. So if the user flicks the view fast, then as the animation completes, as soon as their finger lifts off the view, the animation actually slows down.
I think what I need is a way to take the velocity recorded in the recognizerDragged IBAction, and pass that to the animation controller, and based on that, calculate how long the animation should take, so that the velocity is consistent throughout, and it looks smooth. How can I do that?
Additioanlly, I'm slightly confused because the Apple Documentation says that the velocityInView function returns a velocity in points, not pixels. Yet different iOS devices have different points per pixels, so that would further complicate how I would translate the velocity before passing it to the animation class.
Any idea how to pass the velocity back to the animation controller, so that the animation duration changes based on that, and make it work for different iPhones ?
thanks
What you are likely looking at in the video you are trying to replicate is a UIDynamics style interaction, not a CoreAnimation animation. The velocity returned from velocityInView can be used directly in UIDynamics like this:
[self.behavior addLinearVelocity:
[pan velocityInView:pan.view.superview] forItem:pan.view];
I wrote a tutorial for doing this style of view interaction here: https://www.smashingmagazine.com/2014/03/ios7-new-dynamic-app-interactions/
To stick with UIView animations you just need to look at the frame's bottom (which is also in points) and calculate the new time. This assume that you want frame's bottom to be at 0 at the end of the animation:
animationTime = CGRectGetMaxY(frame) / velocity
You aren't showing how you created the animation controller, but just keep a reference to it and pass the time before calling dismiss. This is also assuming you are using a linear curve. With any other kind of curve, you will have to estimate what the starting velocity would have to be to be based on time and adjust.

accelerometer data not correct, delayed for few seconds

I am creating a very simple game using Swift and SpriteKit and I am moving a ball on the screen using the accelerometer data (acceleration x,y).
I would say the code works fine but I have noticed that sometimes (often right when I open the app) the accelerometer data is not correct and delayed for few seconds.
Why is that happening?
I am using the following code to read the accelerometer data:
if motionManager.accelerometerAvailable == true {
motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue.currentQueue(), withHandler:{
data, error in
self.accX = CGFloat(data.acceleration.x)
self.accY = CGFloat(data.acceleration.y)
})
}
And the function update to apply some impulse to the ball:
override func update(currentTime: CFTimeInterval) {
var impulse = CGVectorMake(accX, accY)
var obj = childNodeWithName("ball") as SKSpriteNode
obj.physicsBody?.applyImpulse(impulse)
}
Am i missing something?
Thank you
With any accelerometer data, it is a good idea to run it through a filter to smooth out any irregular spikes. Here is my favorite:
double filteredAcceleration[3];
memset(filteredAcceleration, 0, sizeof(filteredAcceleration));
CMAccelerometerData *newestAccel = motionManager.accelerometerData;
filteredAcceleration[0] = (filteredAcceleration[0]*(1.0-alpha)) + (newestAccel.acceleration.x*alpha);
filteredAcceleration[1] = (filteredAcceleration[1]*(1.0-alpha)) + (newestAccel.acceleration.y*alpha);
filteredAcceleration[2] = (filteredAcceleration[2]*(1.0-alpha)) + (newestAccel.acceleration.z*alpha);
alpha can be any value from 0 to 1. The closer to 1 the more responsive it will be, the closer to zero the more smooth it will be. My favorite value on the iPhone is 0.2 It is a good compromise for smooth yet responsive for a game like doodle jump, or possibly moving a ball around.
I don't know why the accelerometer data is incorrect/delayed on startup, my guess would be that the hardware has to wake up and calibrate itself, but regardless of the why, if you implement a filter, it will smooth out these irregularities, and they won't be nearly as noticeable.
I have given priority to both functions and the issue seems fixed.
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
// do some task
dispatch_async(dispatch_get_main_queue()) {
// code with priority
}
}

Staggered animations with CAKeyframeAnimation?

I want to animate 3 different images at specific point in time such that it behaves this way.
1) 1st image moves from (Xx, Yx) to (Xz,Yz)
2) Wait 10 seconds
3) 2nd image appears in place at Xa,Yb
4) Wait half as long as in step 2
5) Fade out 2nd image
6) 3rd image appears at the same place as 2nd image
If each of these image's animations are on their own CALayers, can I use CAKeyframeAnimation with multiple layers? If not, what's another way to go about doing staggered animations?
I'm trying to animate a playing card move from offscreen to a particular spot and then few other tricks to appear on screen several seconds later.
Edited
When I wrote this, I thought you could not use a CAAnimationGroup to animate multiple layers. Matt just posted an answer demonstrating that you can do that. I hereby eat my words.
I've taking the code in Matt's answer and adapted it to a project which I've uploaded to Github (link.)
The effect Matt's animation creates is of a pair of feet walking up the screen. I found some open source feet and installed them in the project, and made some changes, but the basic approach is Matt's. Props to him.
Here is what the effect looks like:
(The statement below is incorrect)
No, you can't use a keyframe animation to animate multiple layers. A given CAAnimation can only act on a single layer. This includes group layers, by the way.
If all you're doing is things like moving images on a straight line, fading out, and fading in, why don't you use UIView animation? Take a look at the methods who's names start with animateWithDuration:animations: Those will let you create multiple animations at the same time, and the completion block can then trigger additional animations.
If you need to use layer animation for some reason, you can use the beginTime property (which CAAnimation objects have because they conform to the CAMediaTiming protocol.) For CAAnimations that are not part of an animation group, you use
animation.beginTime = CACurrentMediaTime() + delay;
Where delay is a double which expresses the delay in seconds.
If the delay is 0, the animation would begin.
A third option would be to set your view controller up as the delegate of the animation and use the animationDidStop:finished: method to chain your animations. This ends up being the messiest approach to implement, in my opinion.
The claim that a single animation group cannot animate properties of different layers is not true. It can. The technique is to attach the animation group to the superlayer and refer to the properties of the sublayers in the individual animations' key paths.
Here is a complete example just for demonstration purposes. When launched, this project displays two "footprints" that proceed to step in alternation, walking off the top of the screen.
class ViewController: UIViewController, CAAnimationDelegate {
let leftfoot = CALayer()
let rightfoot = CALayer()
override func viewDidLoad() {
super.viewDidLoad()
self.leftfoot.name = "left"
self.leftfoot.contents = UIImage(named:"leftfoot")!.cgImage
self.leftfoot.frame = CGRect(x: 100, y: 300, width: 50, height: 80)
self.view.layer.addSublayer(self.leftfoot)
self.rightfoot.name = "right"
self.rightfoot.contents = UIImage(named:"rightfoot")!.cgImage
self.rightfoot.frame = CGRect(x: 170, y: 300, width: 50, height: 80)
self.view.layer.addSublayer(self.rightfoot)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.start()
}
}
func start() {
let firstLeftStep = CABasicAnimation(keyPath: "sublayers.left.position.y")
firstLeftStep.byValue = -80
firstLeftStep.duration = 1
firstLeftStep.fillMode = .forwards
func rightStepAfter(_ t: Double) -> CABasicAnimation {
let rightStep = CABasicAnimation(keyPath: "sublayers.right.position.y")
rightStep.byValue = -160
rightStep.beginTime = t
rightStep.duration = 2
rightStep.fillMode = .forwards
return rightStep
}
func leftStepAfter(_ t: Double) -> CABasicAnimation {
let leftStep = CABasicAnimation(keyPath: "sublayers.left.position.y")
leftStep.byValue = -160
leftStep.beginTime = t
leftStep.duration = 2
leftStep.fillMode = .forwards
return leftStep
}
let group = CAAnimationGroup()
group.duration = 11
group.animations = [firstLeftStep]
for i in stride(from: 1, through: 9, by: 4) {
group.animations?.append(rightStepAfter(Double(i)))
group.animations?.append(leftStepAfter(Double(i+2)))
}
group.delegate = self
self.view.layer.add(group, forKey: nil)
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
print("done")
self.rightfoot.removeFromSuperlayer()
self.leftfoot.removeFromSuperlayer()
}
}
Having said all that, I should add that if you are animating a core property like the position of something, it might be simpler to make it a view and use a UIView keyframe animation to coordinate animations on different views. Still, the point is that to say that this cannot be done with CAAnimationGroup is just wrong.

Resources