Run SCNActions sequence with different nodes in SceneKit - swift

I know how to create a sequence of SCNActions in a single node, one by one, in SceneKit. But I would like to know, how can I make a sequence of SCNActions with different nodes?
For example
Move forward node A
Move forward node B
Wait 1 second
Move backward node A
I found an example with SpriteKit but I can not use it, this...
Run SKActions sequence with different nodes
The code of a sequence is as follows
var sequence = [SCNAction] ()
let force = SCNVector3(0.0, 0.0, -1.0)
let move = SCNAction.move(by: force!, duration: 1.5)
squence.append(move)
let actions = SCNAction.sequence(squence)
nodeSelected?.runAction(actions)

you can use customAction.
let actionA = SCNAction.customAction(duration: 1.5) { (node, elapsedTime) in
nodeA.position.z -= 1.0
}
let actionB = SCNAction.customAction(duration: 1.5) { (node, elapsedTime) in
nodeB.position.z -= 1.0
}
let sequence = SCNAction.sequence([actionA,actionB])
anyNode.runAction(sequence)
Running action with any node will first move nodeA then nodeB.

Related

Adding SCNNode multiple time shows only once

I am trying to add SCNNode mutiple time in a loop at different position but I can see same type of node at once with the last position.
Below is the code
let entityArray:[entity] = [.coin, .coin, .coin, .brick, .coin, .coin, .coin, .brick]
func setupworld() {
let scene = SCNScene(named: "art.scnassets/MainScene.scn")!
var zPosition = -10
var count = 0
let delta = -4
for entity in entityArray {
var node = SCNNode()
switch entity {
case .coin:
node = scene.rootNode.childNode(withName: "coin", recursively: true) ?? node
node.position = SCNVector3(0, -5, zPosition)
case .brick:
node = scene.rootNode.childNode(withName: "brick", recursively: true) ?? node
node.position = SCNVector3(0, 0, zPosition)
}
self.sceneView.scene.rootNode.addChildNode(node)
zPosition += delta
count += 1
}
}
It shows one coin and one brick at last positions.
I am new to scenekit so would be doing something wrong, Please help me.
Building on from the other comments and as #rmaddy has said an SCNNode has a clone() function (which is the approach you should take here) and which simply:
Creates a copy of the node and its children.
One thing to be aware of when using this however, is that each cloned Node will share the same geometry and materials.
That's to say that if you wanted at any point to have some bricks with a red colour and some with a green colour you wouldn't be able to do it with this method since:
changes to the objects attached to one node will affect
other nodes that share the same attachments.
To achieve this e.g. to render two copies of a node using different materials, you must copy both the node and its geometry before assigning a new material, which you can read more about here: Apple Discussion
The reason you are only ever seeing one instance of either the coin or brick is because each time you are iterating through your loop you are saying that the newly created node is equal to either the coin or the brick, so naturally the last element in that loop will be the one that references that element from your scene.
Putting this into practice and solving your issue therefor, your setupWorld function should look like something like this:
/// Sets Up The Coins & Bricks
func setupworld(){
//1. Get Our SCNScene
guard let scene = SCNScene(named: "art.scnassets/MainScene.scn") else { return }
//2. Store The ZPosition
var zPosition = -10
//3. Store The Delta
let delta = -4
//4. Get The SCNNodes We Wish To Clone
guard let validCoin = scene.rootNode.childNode(withName: "coin", recursively: true),
let validBrick = scene.rootNode.childNode(withName: "brick", recursively: true) else { return }
//5. Loop Through The Entity Array & Create Our Nodes Dynamically
var count = 0
for entity in entityArray {
var node = SCNNode()
switch entity{
case .coin:
//Clone The Coin Node
node = validCoin.clone()
node.position = SCNVector3(0, -5, zPosition)
case .brick:
//Clone The Brick Node
node = validBrick.clone()
node.position = SCNVector3(0, 0, zPosition)
}
//6. Add It To The Scene
self.sceneView.scene.rootNode.addChildNode(node)
//7. Adjust The zPosition
zPosition += delta
count += 1
}
}

Understanding Spritekit run block, variable not as expected

I'm looking for some help understanding why my speedToPoint variable is always 1 when it gets to the wait action.
This is part of an SKSpriteNode extension i wrote for making a bird fly to a random point in a predefined rectangle. The speedToPoint is also randomized between 1 and 4 and used as the duration for the moveTo action. However, i also need to use that TimeInterval for my wait block in the action sequence.
speedToPoint is indeed being randomized in the run block (i've confirmed). How can i use that same randomized number in the wait block in the next part of the sequence?
var speedToPoint:TimeInterval = 1
self.run(SKAction.repeatForever(
SKAction.sequence([
SKAction.run{
speedToPoint = TimeInterval(Globals.sharedInstance.randomF(min: 1, max: 4))
var pointX = Globals.sharedInstance.randomF(min: left,max: right)
let pointY = Globals.sharedInstance.randomF(min: top,max:bottom)
while abs(self.position.x.distance(to: pointX)) < 200 {
pointX = Globals.sharedInstance.randomF(min: left,max: right)
}
self.xScale = pointX < self.position.x ? -1 : 1
self.run(SKAction.move(to: CGPoint(x:pointX,y:pointY),duration: speedToPoint))
},
SKAction.wait(forDuration: speedToPoint)])
),withKey: "inFrame")
To clarify, what i'm really tring to do is have the bird fly to a point, once it's arrived at that point, fly to another point. I'm still wrapping my heard around action sequences and whether or not they actually wait for completion to move on. Which from what i've read, they do, but not for any move actions. That's why the wait is in there. Perhaps there is another way?
An SKAction, once created, can't be modified, and it is meant to be reused eg. you can't modify the duration parameter, or change other passed parameters. This means that you have to re-create it if you need it changed. Of course you can change the speed property of an existing action, or you can pause the action but that's pretty much it when it comes to modifying the existing action.
To solve your issue, you could do next:
1) Create an action which moves a sprite to a specific location
2) Once the action is completed, you create a new one which does the same
you can do this using recursion, like this (just copy & paste the code to see how it works):
import SpriteKit
class GameScene: SKScene, SKPhysicsContactDelegate {
private var bird = SKSpriteNode(color: .purple, size: CGSize(width: 100, height: 100))
override func didMove(to view: SKView) {
addChild(bird)
recursive()
}
func recursive(){
let sequence = SKAction.sequence([
SKAction.move(to: self.randomPoint(inRect: self.frame), duration: TimeInterval(self.random(between: 1, and: 3))),
SKAction.run({[unowned self] in NSLog("Block executed"); self.recursive()})
])
self.bird.run(sequence, withKey: "aKey")
}
func random(between minimum: CGFloat, and maximum: CGFloat) -> CGFloat{
return CGFloat(arc4random()) / CGFloat(UINT32_MAX) * abs(minimum - maximum) + min(minimum, maximum)
}
func randomPoint(inRect rect:CGRect)->CGPoint{
let x = random( between: -rect.size.width / 2.0 , and: rect.origin.x + rect.size.width/2.0)
let y = random(between: -rect.size.height / 2.0 , and: rect.origin.y + rect.size.height/2.0)
return CGPoint(x: x, y: y)
}
}
To stop this action, remove the key associated with it.

Choosing to spawn different SKSpriteNodes

I am making a game and I have objects which fall from the top of the screen to the bottom. I want to spawn choose between the objects and drop one of them. I currently the drop all at the same time.
func ShapePicker() -> SKSpriteNode{
let shapeArray = [purpleOctagon, coin, greenTriangle, orangeHexagon]
let MaxValue = self.size.width / 2 - 200
let MinValue = self.size.width / 3 * 0.95
let rangeMax = UInt32(MaxValue)
let rangeMin = UInt32(MinValue)
purpleOctagon.position = CGPoint(x: CGFloat(arc4random_uniform(rangeMin) + rangeMax), y: self.size.height)
self.addChild(purpleOctagon)
greenTriangle.position = CGPoint(x: CGFloat(arc4random_uniform(rangeMin) + rangeMax), y: self.size.height)
self.addChild(greenTriangle)
coin.position = CGPoint(x: CGFloat(arc4random_uniform(rangeMin) + rangeMax), y: self.size.height)
self.addChild(coin)
return shapeArray[Int(arc4random_uniform(UInt32(shapeArray.count)))]
}
I would like the program to randomly .addChild because right now it just puts them on the screen.
Your code implies that you want all of them to be on the screen, and then one randomly drops... So you do want to continue to .addChild. What you want, is for them to NOT drop all at once.
So, you need to change the .physicsBody.pinned to true to keep them at the top of the screen.
Then, in your update() you can check for how much time has passed, etc, and after a certain # of seconds you can do an arc4random_uniform and use that result to change the .pinned property of one of the nodes (thus causing that one and that one only to fall).
So, if the coin is 0, triangle is 1, and octagon is 2, then, in your .update keep track of the time elapsed and after say 3 seconds, do a random check 0-2, and that check will perform:
switch result {
case 0: // coin
coin.physicsBody?.pinned = false // makes it drop
case 1: // triangle
...
Just make sure that your nodes are in the proper scope so that you can do the logic in update()
If I read your Q wrong, and you only want to spawn and then drop just one, then you would still need the above switch statement, but instead of changing physicsBody you would do .addChild instead.
so inside of your func would be more like:
// This can be global or inside of your GameScene:
var myGlobalCurrentTime: CFTimeInterval
override func update(currentTime: CFTimeInterval) {
myGlobalCurrentTime = myTimerUpdateTime(currentTime)
func myDropFunc() {
... // Initialize your nodes
let result = myRandomNumber(3)
switch result {
case 0:
coin.position
= CGPoint(x: CGFloat(arc4random_uniform(rangeMin) + rangeMax),
y: self.size.height)
self.addChild(coin)
case 1:
...
}
}
// Execute the func:
myDropFunc()
}
I'm still a bit confused by your code and question, so please clarify in the comments so I can update this answer if needed.
You can move the addChild call out of this function, it returns a SKSpriteNode, which you can then add.
let sprite = ShapePicker()
addChild(sprite)
Or, just add the one randomly chosen and return it. You don't need to addChild nodes that you're not planning on using

How to wait for a duration before a sequence is run?

I'm making a game with SpriteKit, and when it starts, I have nodes that spawn and fall from the top of the screen:
let wait = SKAction.waitForDuration(0.2, withRange: 0.19)
let spawn = SKAction.runBlock {
self.addTears()
}
let sequence = SKAction.sequence([wait, spawn])
self.runAction(SKAction.repeatActionForever(sequence))
Before these nodes spawn, I want to wait for a duration of 1 second, but only when the game starts. I tried to add a waitForDuration before I run the sequence but it didn't work.
Try:
let otherWait = SKAction.waitForDuration(1)
let otherSequence = SKAction.sequence([otherWait, SKAction.repeatActionForever(sequence)])
runAction(otherSequence)

Spawning a Spritekit node at a random time

I'm making a game where I have a node that is spawning and falling from the top of the screen. However I want to make the nodes spawn at random time intervals between a period of 3 seconds. So one spawns in 1 second, the next in 2.4 seconds, the next in 1.7 seconds, and so forth forever. I am struggling with what the code should be for this.
Code I currently have for spawning node:
let wait = SKAction.waitForDuration(3, withRange: 2)
let spawn = SKAction.runBlock { addTears()
}
let sequence = SKAction.sequence([wait, spawn])
self.runAction(SKAction.repeatActionForever(spawn))
The code for my addTears() function is:
func addTears() {
let Tears = SKSpriteNode (imageNamed: "Tear")
Tears.position = CGPointMake(Drake1.position.x, Drake1.position.y - 2)
Tears.zPosition = 3
addChild(Tears)
//gravity
Tears.physicsBody = SKPhysicsBody (circleOfRadius: 150)
Tears.physicsBody?.affectedByGravity = true
//contact
Tears.physicsBody = SKPhysicsBody (circleOfRadius: Tears.size.width/150)
Tears.physicsBody!.categoryBitMask = contactType.Tear.rawValue
Tears.physicsBody!.contactTestBitMask = contactType.Bucket.rawValue
}
It's not advisable that you use NSTimer with SpriteKit (see SpriteKit - Creating a timer). Instead, to generate random times you could use SKAction.waitForDuration:withRange:
Creates an action that idles for a randomized period of time.
When the action executes, the action waits for the specified amount of
time, then ends...
Each time the action is executed, the action computes a new random
value for the duration. The duration may vary in either direction by
up to half of the value of the durationRange parameter...
To spawn nodes at random times you could combine waitForDuration:withRange with runBlock: together in an SKAction sequence. For example:
// I'll let you adjust the numbers correctly...
let wait = SKAction.wait(forDuration: 3, withRange: 2)
let spawn = SKAction.run {
// Create a new node and it add to the scene...
}
let sequence = SKAction.sequence([wait, spawn])
self.run(SKAction.repeatForever(sequence))
// self in this case would probably be your SKScene subclass.