Is there a way to chain SceneKit Animations? - swift

I’m looking for a way of chaining SceneKit animations.
I’m adding x number of nodes to a scene, I’d like to add the first node, make it fade in and once it’s visible then we go to the next node until all nodes are visible. So I need the animation on the first node to end before we start on the second.
Obviously I tried a for loop with SCNActions but the animation is batched together so all nodes appear at the same time.
I don’t know how many nodes will be added, so I can’t make a sequence.
What would be the best way to handle this?
Edit:
Okay, I've figured that if I add a sequence to the node before I add it to the paused scene (that includes an incremented wait interval)
let sequence = SCNAction.sequence([.wait(duration: delay), .fadeIn(duration: 0.5)])
node.runAction(sequence)
then I un-pause the scene once all the nodes are added it achieves the effect that I'm looking for. But it seems hacky.
Is there a better way?

Pause/unpause - hmm, yeah it works, but just feels like that might cause problems down the road if you start doing more things.
I like the completionHandler route per (James P). You could set up multiple (and different) animations or movements with this method. Like move to (5,0,0), rotate, animate, and when it gets there, call it again and move to (10,0,0), etc.
You could do it with a timer if they all work the same way and that would give you some consistency if that's what you are looking for. If you go this route, please ensure to put timers in the main thread.
You can also create some pre-defined sequences, depending on your needs:
let heartBeat = SCNAction.sequence([
SCNAction.move(to: SCNVector3(-0.5, 0.0, 0.80), duration: 0.4),
SCNAction.unhide(),
SCNAction.fadeIn(duration: 1.0),
SCNAction.move(to: SCNVector3(-0.3, 0.0, 0.80), duration: 0.4),
SCNAction.move(to: SCNVector3(-0.2, 0.2, 0.80), duration: 0.4),
SCNAction.move(to: SCNVector3(-0.1, -0.2, 0.80), duration: 0.4),
SCNAction.move(to: SCNVector3( 0.0, 0.0, 0.80), duration: 0.4),
SCNAction.move(to: SCNVector3( 0.1, 0.5, 0.80), duration: 0.4),
SCNAction.move(to: SCNVector3( 0.3, -0.5, 0.80), duration: 0.4),
SCNAction.move(to: SCNVector3( 0.5, 0.0, 0.80), duration: 0.4),
SCNAction.fadeOut(duration: 0.1),
SCNAction.hide()
])
node.runAction(SCNAction.repeatForever(heartBeat))

I ended up using a Timer, I did think about this but couldn't get it to work, probably because I wasn't doing it on the main thread.
Here's the code I'm using so far in case anyone else is in the same boat.
func animateNodesInTo(scene: SCNScene, withDuration: TimeInterval) {
DispatchQueue.main.async {
let nodes = scene.rootNode.childNodes
let acion = SCNAction.fadeIn(duration: withDuration)
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
let opaqueNode = nodes.first(where: {$0.opacity == 0})
opaqueNode?.runAction(acion)
if opaqueNode == nil {
timer.invalidate()
}
}
}
}

Related

Problem with stored properties in extensions in project from GitHub

I'm learning how to implement great CollectionViewPagingLayout templates in my project.
This one: https://github.com/amirdew/CollectionViewPagingLayout
First I specify which template to use (now I use "invertedCylinder") - and this part works well:
extension MovieCollectionViewCell: ScaleTransformView {
var scaleOptions: ScaleTransformViewOptions {
.layout(.invertedCylinder)
}
}
The problem appears when I try to modify the template. There is an extension, written by the creator of the Layout:
extension YourCell: ScaleTransformView {
var scaleOptions = ScaleTransformViewOptions(
minScale: 0.6,
scaleRatio: 0.4,
translationRatio: CGPoint(x: 0.66, y: 0.2),
maxTranslationRatio: CGPoint(x: 2, y: 0
)
}
I have tried to get rid of stored properties error and modify the code:
extension MovieCollectionViewCell: ScaleTransformView {
var scaleOptionsDetailed: ScaleTransformViewOptions {
minScale: 0.6,
scaleRatio: 0.4,
translationRatio: CGPoint(x: 0.66, y: 0.2),
maxTranslationRatio: CGPoint(x: 2, y: 0)
}
}
But this gives me more errors:
Redundant conformance of 'MovieCollectionViewCell' to protocol 'ScaleTransformView'
Cannot find 'minScale' in scope
Consecutive statements on a line must be separated by ';'
I understand that this is a question about basics. But it is already the second day im trying to solve the issue and would be very grateful for a guidance.
I don’t know why that repo includes stored properties on an extension. That is illegal and will likely always be illegal.
You could convert your variable scaleOptions to a computed property. To do that get rid of the equals sign. Then every time you reference that property it will run the code in the closure and generate a new value.
It is also possible to fake stored properties for extensions using associated values from the Objective-C runtime, but that is considered a hack and probably not a great idea.

Is there a NULL SKAction method for SKSpriteNode?

I have multiple SKSpriteNodes that I keep in an array, I also keep their SKActions in an array. This code is just an example I trimmed down, there are many more.
I wrote it this way so that when they have to be repositioned, resized, etc. I go through the array of grouped SKActions.
moves.append(SKAction.follow(iconPath[0], asOffset: false, orientToPath: false, duration: 1))
moves.append(SKAction.follow(iconPath[1], asOffset: false, orientToPath: false, duration: 1))
moves.append(SKAction.follow(iconPath[2], asOffset: false, orientToPath: false, duration: 1))
...
sizing.append(SKAction.resize(byWidth: -iSize.width/2, height: -iSize.width/2, duration: 1))
sizing.append(SKAction.wait(forDuration: 0))
sizing.append(SKAction.resize(byWidth: +iSize.width/2, height: +iSize.width/2, duration: 1))
...
groups.append(SKAction.group([moves[0], sizing[0], blurs[0]]))
groups.append(SKAction.group([moves[1], sizing[1], blurs[1]]))
groups.append(SKAction.group([moves[2], sizing[2], blurs[2]]))
However, depending on a nodes position, it doesn't get every SKAction, as seen in the sizing array. So am using SKAction.wait set to zero. Is this kludgy? Is there another/proper way to do this?

How to interact with 3D objects in RealityKit

Problems with interaction of 3D objects. I've found some beta-functions of RealityKit such as PhysicsBodyComponent, applyImpulse, addForce, applyAngularImpulse and etc.
I was trying to add physics characteristics to object 'vase' and making an impulse to object on event a tap or something like that.
It's really strange, after execution the commands, the physics characteristics are added normal and at same time impulses and force aren't added into object(see below at debugging output).
Output of debugging print:
Something 1 Optional(RealityKit.PhysicsBodyComponent(mode: RealityKit.PhysicsBodyMode.dynamic, massProperties: RealityKit.PhysicsMassProperties(mass: 0.2, inertia: SIMD3(0.1, 0.1, 0.1), centerOfMass: (position: SIMD3(0.0, 0.0, 0.0), orientation: simd_quatf(real: 1.0, imag: SIMD3(0.0, 0.0, 0.0)))), material: RealityKit.PhysicsMaterialResource, isTranslationLocked: (x: false, y: false, z: false), isRotationLocked: (x: false, y: false, z: false), isContinuousCollisionDetectionEnabled: false, teleport: false, userForce: SIMD3(0.0, 0.0, 0.0), userTorque: SIMD3(0.0, 0.0, 0.0), userLinearImpulse: SIMD3(0.0, 0.0, 0.0), userAngularImpulse: SIMD3(0.0, 0.0, 0.0)))
Something 5 Optional(RealityKit.PhysicsBodyComponent(mode: RealityKit.PhysicsBodyMode.dynamic, massProperties: RealityKit.PhysicsMassProperties(mass: 0.2, inertia: SIMD3(0.1, 0.1, 0.1), centerOfMass: (position: SIMD3(0.0, 0.0, 0.0), orientation: simd_quatf(real: 1.0, imag: SIMD3(0.0, 0.0, >0.0)))), material: RealityKit.PhysicsMaterialResource, isTranslationLocked: (x: false, y: false, z: false), isRotationLocked: (x: false, y: false, z: false), isContinuousCollisionDetectionEnabled: false, teleport: false, userForce: SIMD3(0.0, 0.0, 0.0), userTorque: SIMD3(0.0, 0.0, 0.0), userLinearImpulse: SIMD3(0.0, 0.0, 0.0), userAngularImpulse: SIMD3(0.0, 0.0, 0.0)))
As we can see, functions doesn't add impulses and force to the object 'vase'. Maybe I make something wrong.
I don't think you're supposed to create instances of ModelEntity directly. I think that is an internal component of the Entity class representing just the mesh. You augment the interactions/animations directly with components in RealityKit. These apply to the entire entity object. I think it's similar to how if you create a Metal view, you need it to be backed by a Core Animation layer in order to get access to user interactions.
If you look at the SwiftStrike sample application they do not directly instantiate ModelEntity which leads me to believe that it's not best practice to create those outside of an Entity object.
I believe you would add your vase to the project through Reality Composer. You can apply materials and collisions to it there. You can also add behaviors like responding to touch. Then you can access the vase through the reality composer file in Xcode and add components to the vase entity that will transform its position. All entities have a transform component, which is documented here.
RealityKit feels like it was designed to abstract as much code away from the programmer as possible, so a lot of the underlying architecture isn't really exposed or documented. It's also not organized the way that I would organize it, but I don't have a lot of familiarity with the Entity-Component design pattern.
If you don't like how RealityKit is laid out you also have the option to use SceneKit as a renderer. That does not abstract away as much of the functionality and allows you to use Core Animation commands directly on objects.

How to execute multiple custom animation functions synchronously in Swift 4

I am working on an IOS App that is supposed to help people in a tourist region to make sense of means of transport. To make it crystal clear to them how to get from A to B, routes are animated with Annotation objects. For example once the user chooses to see the route from A to D, a cable car object slides from A to B. Once it finishes, a Shuttle Bus object moves along a road from B to C, followed by a boat sliding from C to D.
So I wrote the following functions.
This one lets a transportMode object (small image of a boat, cable car, etc.) slide in a straight line from A to B or B to A.
func animateLinearRoute(transportMode: TransportAnnot, startcoor:
CLLocationCoordinate2D, destcoor: CLLocationCoordinate2D){
UIView.animate(withDuration: 3, animations:
{
if (transportMode.coordinate.latitude == startcoor.latitude && transportMode.coordinate.longitude == startcoor.longitude) {
transportMode.coordinate = destcoor
} else {
transportMode.coordinate = startcoor
}
})
}
For moving an object along a nonlinear route (usually a road) drawn on a map I use the following function:
// pass mode of transport and coordinates along the travel route
func animateNonLinearRoute(transportMode: TransportAnnot, animroute: [CLLocationCoordinate2D]){
let path = UIBezierPath()
// get start point of route from coordinates and start drawing route
let point = self.mapView.convert(animroute[0], toPointTo: self.mapView)
path.move(to: point)
// translate each coordinate along the route into a point in the view for drawing
for coor in animroute {
let point = self.mapView.convert(coor, toPointTo: self.mapView)
path.addLine(to: point)
}
// create keyframe animation to move annotation along the previously drawn path
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = path.cgPath
animation.duration = 5.0
animation.isRemovedOnCompletion = false
let transportview = self.mapView.view(for: transportMode)
transportview?.layer.add(animation, forKey: "animate position along path")
transportMode.coordinate = animroute[animroute.count - 1]
CATransaction.commit()
}
Now the full route can consist of an arbitrary chain of these methods. For example the user may chose to get to a point that requires a linear route -> nonlinear route -> linear route -> nonlinear -> nonlinear.
Ultimately the animations need to be executed in a strictly consecutive manner so the user won't be confused (the second animation should not start unless the first one has finished, etc.).
One consideration would be a keyframe animation like this:
UIView.animateKeyframes(withDuration: 8, delay: 0, options: .calculationModeLinear, animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 5/8, animations: {
self.animateNonLinearRoute(transportMode: self.bus, animroute: self.br.coordinates)
})
UIView.addKeyframe(withRelativeStartTime: 5/8, relativeDuration: 3/8, animations: {
self.animateLinearRoute(transportMode: self.cableCar, startcoor: self.lowerstation, destcoor: self.midstation)
})
// dynamically fill up as needed using appropriate relative start times and durations
}, completion: nil)
That doesn't execute the code synchronously though. I guess it conflicts with the timings and keyframes defined within the functions.
I've been messing around with custom completion closures and then put each method in the completion closure of the previous one as well as with dispatch queues. But I don't really seem to understand them because I wasn't able to achieve the desired effects. And as routes get longer nested completion closures don't seem to be an ideal option as they make the program unnecessarily complex. Any suggestions are highly appreciated.
I guess I traced down the problem.
Since animateNonLinearRoute triggers an animation on the CALayer, the function returns after triggering the animation without waiting for its completion. Therefore all functions in the methods completion handler will be executed, after the CALayer Animation HAS BEEN TRIGGERED, NOT AFTER IT HAS FINISHED.
A simple hacky solution would be to wrap the functions that should be executed after the CALayer Animations have finished, into a CATransaction block:
CATransaction.begin()
CATransaction.setCompletionBlock({
self.animateLinearRoute(transportMode: self.cableCar, startcoor: self.lowerstation, destcoor: self.midstation)
})
self.animateNonLinearRoute(transportMode: self.bus, animroute: self.br.coordinates)
CATransaction.commit()
I would love to hear a better explanation from someone who understands multithreading and concurrency in Swift and a suggestion for a clean way to chain several of these function calls.

Accessing properties of multiple SKShapeNodes

In my program I have a method called addObstacle, which creates a rectangular SKShapeNode with an SKPhysicsBody, and a leftward velocity.
func addObstacle(bottom: CGFloat, top: CGFloat, width: CGFloat){
let obstacleRect = CGRectMake(self.size.width + 100, bottom, width, (top - bottom))
let obstacle = SKShapeNode(rect: obstacleRect)
obstacle.name = "obstacleNode"
obstacle.fillColor = UIColor.grayColor()
obstacle.physicsBody = SKPhysicsBody(edgeLoopFromPath: obstacle.path!)
obstacle.physicsBody?.dynamic = false
obstacle.physicsBody?.affectedByGravity = false
obstacle.physicsBody?.contactTestBitMask = PhysicsCatagory.Ball
obstacle.physicsBody?.categoryBitMask = PhysicsCatagory.Obstacle
obstacle.physicsBody?.usesPreciseCollisionDetection = true
self.addChild(obstacle)
obstacle.runAction(SKAction.moveBy(obstacleVector, duration: obstacleSpeed))
}
In a separate method, called endGame, I want to fade out all the obstacles currently in existence on the screen. All the obstacle objects are private, which makes accessing their properties difficult. If there is only one on the screen, I can usually access it by its name. However, when I say childNodeWithName("obstacleNode")?.runAction(SKAction.fadeAlphaBy(-1.0, duration: 1.0)), only one of the "obstacles" fades away; the rest remain completely opaque. Is there a good way of doing this? Thanks in advance (:
You could probably go with:
self.enumerateChildNodesWithName("obstacleNode", usingBlock: {
node, stop in
//do your stuff
})
More about this method can be found here.
In this example I assumed that you've added obstacles to the scene. If not, then instead of scene, run this method on obstacle's parent node.
And one side note...SKShapeNode is not performant solution in many cases because it requires at least one draw pass to be rendered by the scene (it can't be drawn in batches like SKSpriteNode). If using a SKShapeNode is not "a must" in your app, and you can switch them with SKSpriteNode, I would warmly suggest you to do that because of performance.
SpriteKit can render hundreds of nodes in a single draw pass if you are using same atlas and same blending mode for all sprites. This is not the case with SKShapeNodes. More about this here. Search SO about this topic, there are some useful posts about all this.