Apply a current running SCNAction to another SCNNode - ios

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.

Related

Can't detect collision between rootNode and pointOfView child nodes in SceneKit / ARKit

In an AR app, I want to detect collisions between the user walking around and the walls of an AR node that I construct. In order to do that, I create an invisible cylinder right in front of the user and set it all up to detect collisions.
The walls are all part of a node which is a child of sceneView.scene.rootNode.
The cylinder, I want it to be a child of sceneView.pointOfView so that it would always follow the camera.
However, when I do so, no collisions are detected.
I know that I set it all up correctly, because if instead I set the cylinder node as a child of sceneView.scene.rootNode as well, I do get collisions correctly. In that case, I continuously move that cylinder node to always be in front of the camera in a renderer(updateAtTime ...) function. So I do have a workaround, but I'd prefer it to be a child of pointOfView.
Is it impossible to detect collisions if nodes are children of different root nodes?
Or maybe I'm missing something in my code?
The contactDelegate is set like that:
sceneView.scene.physicsWorld.contactDelegate = self so maybe this only includes sceneView.scene, but will exclude sceneView.pointOfView???
Is that the issue?
Here's what I do:
I have a separate file to create and configure my cylinder node which I call pov:
import Foundation
import SceneKit
func createPOV() -> SCNNode {
let pov = SCNNode()
pov.geometry = SCNCylinder(radius: 0.1, height: 4)
pov.geometry?.firstMaterial?.diffuse.contents = UIColor.blue
pov.opacity = 0.3 // will be set to 0 when it'll work correctly
pov.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil)
pov.physicsBody?.isAffectedByGravity = false
pov.physicsBody?.mass = 1
pov.physicsBody?.categoryBitMask = BodyType.cameraCategory.rawValue
pov.physicsBody?.collisionBitMask = BodyType.wallsCategory.rawValue
pov.physicsBody?.contactTestBitMask = BodyType.wallsCategory.rawValue
pov.simdPosition = simd_float3(0, -1.5, -0.3) // this position only makes sense when setting as a child of pointOfView, otherwise the position will always be changed by renderer
return pov
}
Now in my viewController.swift file I call this function and set is as a child of either root nodes:
pov = createPOV()
sceneView.pointOfView?.addChildNode(pov!)
(Don't worry right now about not checking and unwrapping).
The above does not detect collisions.
But if instead I add it like so:
sceneView.scene.rootNode.addChildNode(pov!)
then collisions are detected just fine.
But then I need to always move this cylinder to be in front of the camera and I do it like that:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
guard let pointOfView = sceneView.pointOfView else {return}
let currentPosition = pointOfView.simdPosition
let currentTransform = pointOfView.simdTransform
let orientation = SCNVector3(-currentTransform.columns.2.x, -currentTransform.columns.2.y, -currentTransform.columns.2.z)
let currentPositionOfCamera = orientation + SCNVector3(currentPosition)
DispatchQueue.main.async {
self.pov?.position = currentPositionOfCamera
}
}
For completeness, here's the code I use to configure the node of walls in ViewController (they're built elsewhere in another function):
node?.physicsBody = SCNPhysicsBody(type: .dynamic, shape: SCNPhysicsShape(node: node!, options: nil))
node?.physicsBody?.isAffectedByGravity = false
node?.physicsBody?.mass = 1
node?.physicsBody?.damping = 1.0 // remove linear velocity, needed to stop moving after collision
node?.physicsBody?.angularDamping = 1.0 // remove angular velocity, needed to stop rotating after collision
node?.physicsBody?.velocityFactor = SCNVector3(1.0, 0.0, 1.0) // will allow movement only in X and Z coordinates
node?.physicsBody?.angularVelocityFactor = SCNVector3(0.0, 1.0, 0.0) // will allow rotation only around Y axis
node?.physicsBody?.categoryBitMask = BodyType.wallsCategory.rawValue
node?.physicsBody?.collisionBitMask = BodyType.cameraCategory.rawValue
node?.physicsBody?.contactTestBitMask = BodyType.cameraCategory.rawValue
And here's my physycsWorld(didBegin contact) code:
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
if contact.nodeA.physicsBody?.categoryBitMask == BodyType.wallsCategory.rawValue || contact.nodeB.physicsBody?.categoryBitMask == BodyType.wallsCategory.rawValue {
print("Begin COLLISION")
contactBeginLabel.isHidden = false
}
}
So I print something to the console and I also turn on a label on the view so I'll see that collision was detected (and the walls indeed move as a whole when it works).
So Again, it all works fine when the pov node is a child of sceneView.scene.rootNode, but not if it's a child of sceneView.pointOfView.
Am I doing something wrong or is this a limitation of collision detection?
Is there something else I can do to make this work, besides the workaround I already implemented?
Thanks!
Regarding the positioning of your cyliner:
instead to use the render update at time, you better use a position constraint for your cylinder node to move with the point of view. the result will be the same, as if it were a child of the point of view, but collisions will be detected, because you add it to the main rootnode scenegraph.
let constraint = SCNReplicatorConstraint(target: pointOfView) // must be a node
constraint.positionOffset = positionOffset // some SCNVector3
constraint.replicatesOrientation = false
constraint.replicatesScale = false
constraint.replicatesPosition = true
cylinder.constraints = [constraint]
There is also an influence factor you can configure. By default the influence is 100%, the position will immediatly follow.

Animate specific dae objects based on condition

For 2 dae objects in the scene, how can one initiate animations for each one of them at different conditions?
Since all objects are part of the SCNScene, I am unable to refer to individual ones based on a condition. They all render properly, but they animate all the same time. Can we put a condition to make specific objects in the scene animate at a time?
Thanks in advance!
let idleScene = SCNScene(named: "art.scnassets/Avatar_1.dae")!
// This node will be parent of all the animation models
let node = SCNNode()
// Add all the child nodes to the parent node
for child in idleScene.rootNode.childNodes {
node.addChildNode(child)
}
// Set up some properties
node.position = SCNVector3(hitTestResult.worldTransform.columns.3.x+0.5,hitTestResult.worldTransform.columns.3.y, hitTestResult.worldTransform.columns.3.z)
node.scale = SCNVector3(0.2, 0.2, 0.2)
// Add the node to the scene
sceneView.scene.rootNode.addChildNode(node)
For another avatar (Avatar_2.dae), how do we add it in the scene but give another reference name.
Also how can we individually play/pause the animations for each avatars?
This one if for scene, but is there one for individual avatars?
sceneView.scene.isPaused = play
By explicitly naming the elements in your SCNScene and then designating them as distinct nodes by using rootNode.childNode(withName:):
let scannerScene = SCNScene(named: "Scanner.scn")
let sectorField: SCNNode = (scannerScene?.rootNode.childNode(withName: "sectorField", recursively: true))!
let scanBeam: SCNNode = (scannerScene?.rootNode.childNode(withName: "scanBeam", recursively: true))!
Once you've done this, the individual nodes can be animated independently:
// start scanBeam
let rotateAction = SCNAction.rotateBy(x: 0, y: CGFloat(2*Float.pi), z: 0, duration: 1.5)
let perpetualRotation = SCNAction.repeatForever(rotateAction)
scanBeam.runAction(perpetualRotation)
To stop a specific animation (as opposed to all animations in the entire scene) simply remove the action
scanBeam.removeAction(forKey: String)

Swift 4 and SpriteKit: Move node to a new touch location

I'm trying to move a node to a touch location using physicsBody (as opposed to SKAction). I am trying to use applyForce, but I may be going about this wrong.
In my touchesBegan function I record the touch location in a constant and pass it into my movePlayer function. The initial touch works as expected. However, subsequent touches seem to move from a relative position, which makes me think that I should recalculate my vector. I'm just not sure how to do this. Any help is appreciated.
func movePlayer(to touchPoint: CGPoint) {
let vector = CGVector(dx: touchPoint.x, dy: touchPoint.y)
player.physicsBody?.applyForce(vector, at: player.position)
player.physicsBody?.velocity = vector
playerIsMoving = true
}

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).

Does adding new action for the scene remove automatically the one before in SpriteKit?

I want to know if I made a sequence action and while this sequence is running a new action added to the scene , Does the new action stop the sequence ?
If yes , how i can make both of them working if the new action is added in swift ?
If you just give it a try, I'm sure you'll find the answer yourself.
But anyway, I tried it for you:
node.run(SKAction.sequence([SKAction.moveBy(x: 100, y: 0, duration: 3), SKAction.moveBy(x: 0, y: 100, duration: 3)]))
node.run(SKAction.rotate(byAngle: CGFloat(M_PI * 2), duration: 6))
And what I see is that the node both moves and rotates. So each subsequent action you tell a node to run will be run simultenuously.
Another way to run actions at the same time is to use SKAction.group.
The only time a new action will interfere with any running actions is if both actions share the same key. If you do not assign a key, then every time you add an action, it gets added to the action pool and will run concurrently.
I've follow your comment, seems you are in the situation of overlap of the actions.
When you have a node and you want to launch one or more action, especially a sequence of actions where your node are involved in movements, you should be sure that these actions are finished.
To do it, for example to self:
let seq = SKAction.sequence([action1,action2,..])
if self.action(forKey: "moveToRoof") == nil {
self.run(seq, withKey:"moveToRoof")
}
You can also do:
let group1 = SKAction.group([action1, action2,..])
let group2 = SKAction.group([action1, action2,..])
let addNewNode = SKAction.run{
self.addChild(node)
}
let seq = SKAction.sequence([action1, group1, action2, addNewNode, group2,..])
if self.action(forKey: "moveToGround") == nil {
self.run(seq, withKey:"moveToGround")
}
In your case seems you want to add nodes to a node that following the position of his parent..
override func update(_ currentTime: TimeInterval) {
if let child = myNode1, let parent = child.parent { // if exist follow the parent position
child.position = parent.position
}
}

Resources