Apply a sequence of different SKActions to different SKNodes - ios

I want to create a sequence of SKActions, each of which is for a different SKSpriteNode. I want to fade my layer node, remove it from the parent, wait for 3 seconds, then start a move SKAction for my snake node. Here's some code:
func startGame() {
layer.run(SKAction.sequence([
SKAction.fadeAlpha(to: 0, duration: 1),
SKAction.removeFromParent(),
SKAction.wait(forDuration: 1),
]))
//Here move the snake node
}
The problem is, if I add snake.run(SKAction.move(...)) at the place of the comment, it gets executed at the same time with layer.run(...).

You can run a block of code or function as an action (because in Swift closures are also function types), so you can add SKAction.runBlock(moveSnake) as the last SKAction on the array of actions run in layer.
Then in func moveSnake() {} you can run your actions to move the snake.

Related

How to add SCNNodes without blocking main thread?

I'm creating and adding a large number of SCNNodes to a SceneKit scene, which causes the app to freeze for a second or two.
I thought I could fix this by putting all the action in a background thread using DispatchQueue.global(qos: .background).async(), but no dice. It behaves exactly the same.
I saw this answer and put the nodes through SCNView.prepare() before adding them, hoping it would slow down the background thread and prevent blocking. It didn't.
Here's a test function that reproduces the problem:
func spawnNodesInBackground() {
// put all the action in a background thread
DispatchQueue.global(qos: .background).async {
var nodes = [SCNNode]()
for i in 0...5000 {
// create a simple SCNNode
let node = SCNNode()
node.position = SCNVector3(i, i, i)
let geometry = SCNSphere(radius: 1)
geometry.firstMaterial?.diffuse.contents = UIColor.white.cgColor
node.geometry = geometry
nodes.append(node)
}
// run the nodes through prepare()
self.mySCNView.prepare(nodes, completionHandler: { (Bool) in
// nodes are prepared, add them to scene
for node in nodes {
self.myRootNode.addChildNode(node)
}
})
}
}
When I call spawnNodesInBackground() I expect the scene to continue rendering normally (perhaps at a reduced frame rate) while new nodes are added at whatever pace the CPU is comfortable with. Instead the app freezes completely for a second or two, then all the new nodes appear at once.
Why is this happening, and how can I add a large number of nodes without blocking the main thread?
I don't think this problem is solvable using the DispatchQueue. If I substitute some other task instead of creating SCNNodes it works as expected, so I think the problem is related to SceneKit.
The answers to this question suggest that SceneKit has its own private background thread that it batches all changes to. So regardless of what thread I use to create my SCNNodes, they all end up in the same queue in the same thread as the render loop.
The ugly workaround I'm using is to add the nodes a few at a time in SceneKit's delegated renderer(_:updateAtTime:) method until they're all done.
I poked around on this and didn't solve the freeze (I did reduce it a bit).
I expect that prepare() is going to exacerbate the freeze, not reduce it, because it's going to load all resources into the GPU immediately, instead of letting them be lazily loaded. I don't think you need to call prepare() from a background thread, because the doc says it already uses a background thread. But creating the nodes on a background thread is a good move.
I did see pretty good performance improvement by moving the geometry outside the loop, and by using a temporary parent node (which is then cloned), so that there's only one call to add a new child to the scene's root node. I also reduced the sphere's segment count to 10 (from the default of 48).
I started with the spinning spaceship sample project, and triggered the addition of the spheres from the tap gesture. Before my changes, I saw 11 fps, 7410 draw calls per frame, 8.18M triangles. After moving the geometry out of the loop and flattening the sphere tree, I hit 60 fps, with only 3 draw calls per frame and 1.67M triangles (iPhone 6s).
Do you need to build these objects at run time? You could build this scene once, archive it, and then embed it as an asset. Depending on the effect you want to achieve, you might also consider using SCNSceneRenderer's present(_:with:incomingPointOfView:transition:completionHandler) to replace the entire scene at once.
func spawnNodesInBackgroundClone() {
print(Date(), "starting")
DispatchQueue.global(qos: .background).async {
let tempParentNode = SCNNode()
tempParentNode.name = "spheres"
let geometry = SCNSphere(radius: 0.4)
geometry.segmentCount = 10
geometry.firstMaterial?.diffuse.contents = UIColor.green.cgColor
for x in -10...10 {
for y in -10...10 {
for z in 0...20 {
let node = SCNNode()
node.position = SCNVector3(x, y, -z)
node.geometry = geometry
tempParentNode.addChildNode(node)
}
}
}
print(Date(), "cloning")
let scnView = self.view as! SCNView
let cloneNode = tempParentNode.flattenedClone()
print(Date(), "adding")
DispatchQueue.main.async {
print(Date(), "main queue")
print(Date(), "prepare()")
scnView.prepare([cloneNode], completionHandler: { (Bool) in
scnView.scene?.rootNode.addChildNode(cloneNode)
print(Date(), "added")
})
// only do this once, on the simulator
// let sceneData = NSKeyedArchiver.archivedData(withRootObject: scnView.scene!)
// try! sceneData.write(to: URL(fileURLWithPath: "/Users/hal/scene.scn"))
print(Date(), "queued")
}
}
}
I have an asteroid simulation with 10000 nodes and ran into this issue myself. What worked for me was creating the container node, then passing it to a background process to fill it with child nodes.
That background process uses an SCNAction on that container node to add each of the generated asteroids to the container node.
let action = runBlock {
Container in
// generate nodes
/// then For each node in generatedNodes
Container.addChildNode(node)
}
I also used a shared level of detail node with an uneven sided block as its geometry so that the scene can draw those nodes in a single pass.
I also pre-generate 50 asteroid shapes that get random transformations applied during the background generation process. That process simply has to grab at random a pregen block apply a random simd transformation then stored for adding scene later.
I’m considering using a pyramid for the LOD but the 5 x 10 x 15 block works for my purpose. Also this method can be easily throttled to only add a set amount of blocks at a time by creating and passing multiple actions to the node. Initially I passed each node as an action but this way works too.
Showing the entire field of 10000 still affects the FPS slightly by 10 a 20 FPS but At that point the container nodes own LOD comes into effect showing a single ring.
Add all of them when application started but position them where camera dont see. When you need them change their position where they should be.

Apply a current running SCNAction to another SCNNode

I two SCNNodes A and B in my 3D world. A is moved by the user, while B has an action that makes it move from left to right. This is the action on B:
let moveDirection: Float = position.x > 0.0 ? -1.0 : 1.0
let moveDistance = levelData.gameLevelWidth()
let moveAction = SCNAction.moveBy(SCNVector3(x: moveDistance * moveDirection, y: 0.0, z: 0.0), duration: 10.0)
let action = SCNAction.runBlock { node -> Void in
}
carNode.runAction(SCNAction.sequence([moveAction, action]))
A and B can collide, and if a touches B, then A has to start moving with B as if they were one single object, until the user uses the commands to move A. The collision is detected in the method
func physicsWorld(world: SCNPhysicsWorld, didBeginContact contact: SCNPhysicsContact)
but I have no idea what approach I should take to make A move with B. I was thinking to apply a copy of the action with new positions, or if possible, apply the currently executing action to node A. I also thought of making A a child of B and then moving it back to the root node, but it doesn't seem to work.
The solution was simply accessing the current running SCNAnimations on the node that was involved in the collision with this code:
let action: SCNAction = contact.nodeB.actionForKey("MovementAction")!
contact.nodeA.runAction(action)
Once you have a reference to the current action, you can just run it on the other node, and it will be mirrored exactly.
note: remember to specify the key for the action when you create it, otherwise this method won't work.

Sprite Kit - Creating node from didBeginContact()

I am using a custom brick class:
import SpriteKit
class Brick: SKSpriteNode {
enum type { case Normal }
convenience init (type: Brick.type) {
self.init(color: .greenColor(), size: CGSizeMake(75, 25))
physicsBody.SKPhysicsBody(rectangleOfSize: size)
physicsBody!.mass = 9999
physicsBody!.affectedByGravity = false
position = CGPointMake(100, 50)
// Category and collision bitmasks ...
// Action to move the brick upwards ...
}
// Stuff
}
In my scene, I'm creating one initial brick, and everything works just fine. However, when I try to add a brick in the didBeginContact() function, I get unexpected results.
I have a SKNode at 1/5 height of the screen. So everytime a brick reaches that height, it will make contact with this invisible node and create a new brick:
// Inside SKScene
func didBeginContact(contact: SKPhysicsContact) {
// I set physics body A and B ...
if a.categoryBitMask == Category.brick && b.categoryBitMask == Category.brickSpawner {
addChild(Brick(type: .Normal))
}
}
So the problem is: when I create a new brick inside this function, the position is set to (0, 0) instead of (100, 50) as defined in the Brick class. Actually, if I go ahead and use println(brick.position) I get (100, 50), but in the screen it looks positioned at (0, 0).
If I create a brick anywhere else in the code, for example in the touchesBegan() or update() functions, the position is set correctly. This problem only happens when creating the node from didBeginContact().
This should help, it explains the steps in a single frame.
https://developer.apple.com/library/ios/documentation/GraphicsAnimation/Conceptual/SpriteKit_PG/Introduction/Introduction.html
You are creating it at the wrong time, so one of the steps behind the scenes is resetting it due to a few steps being missing. Queue that process up for the didFinishUpdate command. ( I would just create the object in the did contact, then throw it into an array, then on didFinishUpdate, go through the array and add them to the scene, and clear the array)

Triggering the end of a while loop using the completion of SKAction

I'm trying to use a "while loop" while my SKSpritenode is animating and I want to trigger the end of the SKAction to trigger the end of the loop. This is my attempt
while (orgNode.hasActions) {
//render the background
}
what I want to do is create a bottom up sweep effect that changes the sprite nodes texture underneath the originNode's position, so that the user sees the sweeping motion of the node changing the background color.
You can override the update or didEvaluateActions function of the SKScene if you want to update the node every frame. didEvaluateActions function is called in each frame after all the SKActions are evaluated. The update function is called before all actions are evaluated.
override func didEvaluateActions() {
if node.hasActions()
{
println("Running Action")
}
}
The following image shows which all functions are called in each frame and in what order.
For more information : https://developer.apple.com/library/ios/documentation/GraphicsAnimation/Conceptual/SpriteKit_PG/Introduction/Introduction.html

How do SKAction durations work in relation to frame rates?

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.

Resources