I'm building an app with ARkit and I'd like the user to have the ability to start and stop animations in the scene with the use of a button in the viewcontroller. However, I am having trouble finding an example of this code. Any pointers would be appreciated. For reference, my code is as follows for animating the dae file. I had a version working where I could stop the animation, but could not get it to restart. Thanks in advance.
func loadModels(atom: String) {
// Create a new scene
let scene = SCNScene(named: "art.scnassets/" + atom + ".scn")!
// Set the scene to the view
sceneViewAr?.scene = scene
let mainnode = scene.rootNode.childNode(withName: "mainnode", recursively: true)
mainnode?.runAction(SCNAction.rotateBy(x: 10, y: 0, z: 0, duration: 0))
let orbit = scene.rootNode.childNode(withName: "orbit", recursively: true)
orbit?.runAction(SCNAction.rotateBy(x: 0, y: 2 * 50, z: 0, duration: 100))
if let orbittwo = scene.rootNode.childNode(withName: "orbit2", recursively: true) {
orbittwo.runAction(SCNAction.rotateBy(x: 0, y: -2 * 50, z: 0, duration: 100))
}
if let orbitthree = scene.rootNode.childNode(withName: "orbit3", recursively: true) {
orbitthree.runAction(SCNAction.rotateBy(x: 0, y: 2 * 50, z: 0, duration: 100))
}
}
I dont think there is a way to pause and stop in the way that you want per se.
Having said this you can make use of the following SCNAction functions:
func action(forKey key: String) -> SCNAction?
func removeAction(forKey key: String)
Essentially you are going to have to stop and then recreate the actions again.
As such you could do something like so:
Create a variable under your class declaration for your SCNActions so you dont have to rewrite them every time e.g:
let rotateXAction = SCNAction.rotateBy(x: 10, y: 0, z: 0, duration: 10)
Then Create 2 functions one to add and the other to remove the actions e.g:
/// Adds An SCNAction To A Specified Node With A Key
///
/// - Parameters:
/// - action: SCNAction
/// - node: SCNNode
/// - key: String
func addAction(_ action: SCNAction, toNode node: SCNNode, forKey key: String){
node.runAction(action, forKey: key)
}
/// Removes An SCNAction For A Specified Node With A Key
///
/// - Parameters:
/// - action: SCNAction
/// - node: SCNNode,
/// - key: String
func removeActionFromNode(_ node: SCNNode, forKey key: String){
node.removeAction(forKey: key)
}
Which you could then test like so:
/// Tests Whether The `SCNActions` Can Be Stated & Stopped
func testActions(){
//1. Create An Array Of Colours
let colours: [UIColor] = [.red, .green, .blue, .orange, .purple, .black]
//2. Create An Array Of Face Materials
var faceArray = [SCNMaterial]()
//3. Create An SCNNode With An SCNBox Geometry
let firstNode = SCNNode(geometry: SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0))
for faceIndex in 0..<colours.count{
let material = SCNMaterial()
material.diffuse.contents = colours[faceIndex]
faceArray.append(material)
}
firstNode.geometry?.materials = faceArray
//4. Add It To The Scene
self.augmentedRealityView.scene.rootNode.addChildNode(firstNode)
firstNode.position = SCNVector3(0 , 0, -1)
//5. Run The Action
addAction(rotateXAction, toNode: firstNode, forKey: "rotateX")
//6. Stop It After 4 Seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
self.removeActionFromNode(firstNode, forKey: "rotateX")
}
//7. Start It Again After 8
DispatchQueue.main.asyncAfter(deadline: .now() + 8) {
self.addAction(self.rotateXAction, toNode: firstNode, forKey: "rotateX")
}
}
In your context you can easily tweak these functions to apply for a specific IBAction etc.
This is just a starter and in no way a refactored or refined example, however it should point you in the right direction...
Related
Working on a game right now, I've faced a problem regarding management of SkSpriteNodes. I have a SpriteNode whose texture size is lower than physicsBody size assigned to it. It is possible to move, thanks to something similar to an SKAction, only the texture of a node and not its physicsBody?
To explain the problem in other terms, I will give you a graphic example:
As you can see, what I want to achieve is not to modify physicsBody proprieties(in order to not affect collision or having problems with continuos reassigning of PhysicsBody entities), but changing its texture length and adjusting its position. How can I achieve this programmatically?
A bit of code for context, which is just illustrative of the problem:
let node = SKSpriteNode(color: .red, size: CGSize(width: 8, height: 60)
node.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 8, height: 60)
self.addChild(node)
//what I've tried is something like that
//It causes glitches in visualisation... and I need to move the object since resizing is towards the center.
let resize = SKAction.scaleY(to: 0.5, duration: 5)
let move = SKAction.move(to: node.position - CGPoint(x:0, y:node.size.height*0.5), duration: 5)
let group = SKAction.group([resize, move])
node.run(group)
//And this is even worse if I add, in this specific example, another point fixed to the previous node
let node2 = SKSpriteNode(color: .blue, size: CGSize(width: 8, length: 8)
node2.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 8, height: 8))
node2.position = CGPoint(x: 0, y: -node.height)
self.addChild(node2)
self.physicsWorld.add(SKPhysicsJointFixed.joint(withBodyA: node.physicsBody! , bodyB: node2.physicsBody!, anchor: CGPoint(x: 0, y: -node.size.height)))
I get your problem.
let resize = SKAction.scaleY(to: 0.5, duration: 5)
This line will cause the physicsBody to scale the x and y axis uniformly. While your texture will just scale the y axis.
Its not so straight forward changing physicsBody shapes to match actions
One way to do it though would be to call a method from
override func didEvaluateActions()
Something like this:
var group1: SKAction? = nil
var group2: SKAction? = nil
var touchCnt = 0
var test = SKSpriteNode(texture: SKTexture(imageNamed: "circle"), color: .blue, size: CGSize(width: 100, height: 100))
func setActions() {
let newPosition = CGPoint(x: -200, y: 300)
let resize1 = SKAction.scaleY(to: 0.5, duration: 5)
let move1 = SKAction.move(to: newPosition, duration: 5)
group1 = SKAction.group([resize1, move1])
let resize2 = SKAction.scaleY(to: 1, duration: 5)
let move2 = SKAction.move(to: position, duration: 5)
group2 = SKAction.group([resize2, move2])
}
func newPhysics(node: SKSpriteNode) {
node.physicsBody = SKPhysicsBody(texture: node.texture!, size: node.size)
node.physicsBody?.affectedByGravity = false
node.physicsBody?.allowsRotation = false
node.physicsBody?.usesPreciseCollisionDetection = false
}
override func sceneDidLoad() {
physicsWorld.contactDelegate = self
test.position = CGPoint(x: 0, y: 300)
setActions()
newPhysics(node: test)
addChild(test)
}
override func didEvaluateActions() {
newPhysics(node: test)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if touchCnt == 0 {
if !test.hasActions() {
test.run(group1!)
touchCnt += 1
}
} else {
if !test.hasActions() {
test.run(group2!)
touchCnt -= 1
}
}
}
if you put the above code in your gameScene, taking care to replace any duplicated methods and replacing the test node texture. then when you tap the screen the sprite should animate as you want while keeping the physics body is resized at the same time. There are a few performance issues with this though. As it changes the physics body on each game loop iteration.
I am making a simple measuring app. Currently I place spheres as SCNNodes around the place and between nodes appears a label that displays the length of the line from node 1 to node 2.
This is how the labels are created:
func addLabel() {
let plane = SCNPlane(width: 0.07, height: 0.02)
plane.cornerRadius = plane.height / 10
let sks = SKScene(size: CGSize(width: plane.width * 10e3, height: plane.height * 10e3))
sks.backgroundColor = UIColor(red: 0.1, green: 0.1, blue: 0.1, alpha: 0.7)
currentLbl = SKLabelNode(text: "")
currentLbl.fontSize = 110
currentLbl.fontName = "Helvetica"
currentLbl.verticalAlignmentMode = .center
currentLbl.position = CGPoint(x: sks.frame.midX, y: sks.frame.midY)
currentLbl.fontColor = .white
sks.addChild(currentLbl)
let material = SCNMaterial()
material.isDoubleSided = true
material.diffuse.contents = sks
material.diffuse.contentsTransform = SCNMatrix4Translate(SCNMatrix4MakeScale(1, -1, 1), 0, 1, 0)
plane.materials = [material]
let node = SCNNode(geometry: plane)
node.constraints = [SCNBillboardConstraint()]
node.position = SCNVector3Make(0, 0, 0)
let (minBound, maxBound) = node.boundingBox
node.pivot = SCNMatrix4MakeTranslation( (maxBound.x + minBound.x)/2, minBound.y, 0.02/2)
lblNodes.append(node)
currentLblNode = node
sceneView.scene.rootNode.addChildNode(node)
}
I would like to apply a mathematical equation to the scale of these label nodes (in my update function) to maintain readability from a couple of metres.
var myNodes: [SCNNode] = []
let s = getMagicScalingNumber()
Say I obtained my scale factor as above and I have an array of SCNNodes, how can I scale all the nodes and their respective children so they stay visually proportional.
If SCNTransformConstraint() is an option for this, I would appreciate an example of how to implement it.
Edit: Just to clarify, I have tried
currentLblNode.scale = SCNVector3Make(s, s, s)
which does not seem to work.
I know this is very late, however I have put together an example which should point you in the right direction.
In your example you are essentially creating a holder SCNNode which contains your labels etc.
You can store these into an array of [SCNNode] and then transform the scale of these like so:
/// Updates The Contents Of Each Node Added To Our NodesAdded Array
///
/// - Parameters:
/// - nodes: [SCNNode]
/// - pointOfView: SCNNode
func updateScaleFromCameraForNodes(_ nodes: [SCNNode], fromPointOfView pointOfView: SCNNode){
nodes.forEach { (node) in
//1. Get The Current Position Of The Node
let positionOfNode = SCNVector3ToGLKVector3(node.worldPosition)
//2. Get The Current Position Of The Camera
let positionOfCamera = SCNVector3ToGLKVector3(pointOfView.worldPosition)
//3. Calculate The Distance From The Node To The Camera
let distanceBetweenNodeAndCamera = GLKVector3Distance(positionOfNode, positionOfCamera)
//4. Animate Their Scaling & Set Their Scale Based On Their Distance From The Camera
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
switch distanceBetweenNodeAndCamera {
case 0 ... 0.5:
node.simdScale = simd_float3(0.25, 0.25, 0.25)
case 0.5 ... 1:
node.simdScale = simd_float3(0.5, 0.5, 0.5)
case 1 ... 1.5:
node.simdScale = simd_float3(1, 1, 1)
case 1.5 ... 2:
node.simdScale = simd_float3(1.5, 1.5, 1.5)
case 2 ... 2.5:
node.simdScale = simd_float3(2, 2, 2)
case 2.5 ... 3:
node.simdScale = simd_float3(2.5, 2.5, 2.5)
default:
print("Default")
}
SCNTransaction.commit()
}
}
Here I am just setting 'random' values to illustrate the concept of scale depending on the distance from the camera.
To put this better into context here is a little demo I have put together:
//--------------------------
// MARK: - ARSCNViewDelegate
//--------------------------
extension ViewController: ARSCNViewDelegate{
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
if !nodesAdded.isEmpty, let currentCameraPosition = self.sceneView.pointOfView {
updateScaleFromCameraForNodes(nodesAdded, fromPointOfView: currentCameraPosition)
}
}
}
class ViewController: UIViewController {
#IBOutlet var sceneView: ARSCNView!
var nodesAdded = [SCNNode]()
//-----------------------
// MARK: - View LifeCycle
//-----------------------
override func viewDidLoad() {
super.viewDidLoad()
//1. Generate Our Three Box Nodes
generateBoxNodes()
//2. Run The Session
let configuration = ARWorldTrackingConfiguration()
sceneView.session.run(configuration)
sceneView.delegate = self
}
/// Generates Three SCNNodes With An SCNBox Geometry & Places Them In A Holder Node
func generateBoxNodes(){
//1. Create An SCNNode To Hold All Of Our Content
let holderNode = SCNNode()
//2. Create An Array Of Colours For Each Face
let colours: [UIColor] = [.red, .green, .blue, .purple, .cyan, .black]
//3. Create An SCNNode Wih An SCNBox Geometry
let boxNode = SCNNode()
let boxGeometry = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.01)
boxNode.geometry = boxGeometry
//4. Create A Different Material For Each Face
var materials = [SCNMaterial]()
for i in 0..<5{
let faceMaterial = SCNMaterial()
faceMaterial.diffuse.contents = colours[i]
materials.append(faceMaterial)
}
//5. Set The Geometries Materials
boxNode.geometry?.materials = materials
//6. Create Two More Nodes By Cloning The First One
let secondBox = boxNode.flattenedClone()
let thirdBox = boxNode.flattenedClone()
//7. Position Them In A Line & Add To The Scene
boxNode.position = SCNVector3(-0.2, 0, 0)
secondBox.position = SCNVector3(0, 0, 0)
thirdBox.position = SCNVector3(0.2, 0, 0)
holderNode.addChildNode(boxNode)
holderNode.addChildNode(secondBox)
holderNode.addChildNode(thirdBox)
holderNode.position = SCNVector3(0, 0, -1)
self.sceneView.scene.rootNode.addChildNode(holderNode)
nodesAdded.append(holderNode)
}
/// Updates The Contents Of Each Node Added To Our NodesAdded Array
///
/// - Parameters:
/// - nodes: [SCNNode]
/// - pointOfView: SCNNode
func updateScaleFromCameraForNodes(_ nodes: [SCNNode], fromPointOfView pointOfView: SCNNode){
nodes.forEach { (node) in
//1. Get The Current Position Of The Node
let positionOfNode = SCNVector3ToGLKVector3(node.worldPosition)
//2. Get The Current Position Of The Camera
let positionOfCamera = SCNVector3ToGLKVector3(pointOfView.worldPosition)
//3. Calculate The Distance From The Node To The Camera
let distanceBetweenNodeAndCamera = GLKVector3Distance(positionOfNode, positionOfCamera)
//4. Animate Their Scaling & Set Their Scale Based On Their Distance From The Camera
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
switch distanceBetweenNodeAndCamera {
case 0 ... 0.5:
node.simdScale = simd_float3(0.25, 0.25, 0.25)
case 0.5 ... 1:
node.simdScale = simd_float3(0.5, 0.5, 0.5)
case 1 ... 1.5:
node.simdScale = simd_float3(1, 1, 1)
case 1.5 ... 2:
node.simdScale = simd_float3(1.5, 1.5, 1.5)
case 2 ... 2.5:
node.simdScale = simd_float3(2, 2, 2)
case 2.5 ... 3:
node.simdScale = simd_float3(2.5, 2.5, 2.5)
default:
print("Default")
}
SCNTransaction.commit()
}
}
override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) }
override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) }
override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() }
}
Hopefully it helps...
Here is an illustration of what I am trying to do:
Here is my code so far:
import SpriteKit
class GameScene: SKScene {
let mySquare1 = SKShapeNode(rectOfSize:CGSize(width: 50, height: 50))
let mySquare2 = SKShapeNode(rectOfSize:CGSize(width: 50, height: 50))
override func didMoveToView(view: SKView) {
mySquare1.position = CGPoint(x: 100, y:100)
mySquare2.position = CGPoint(x: 300, y:100)
mySquare1.fillColor = SKColor.blueColor()
mySquare2.fillColor = SKColor.blueColor()
self.addChild(mySquare1)
self.addChild(mySquare2)
let moveAction1 = SKAction.moveTo(CGPoint(x:250, y:100), duration: 1)
mySquare1.runAction(moveAction1)
let moveAction2 = SKAction.moveTo(CGPoint(x:300, y:350), duration: 1)
mySquare2.runAction(moveAction2)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
}
override func update(currentTime: CFTimeInterval) {
}
}
My problem is, I am trying to move the rectangles synchronously (not asynchronously). That is, I want my first rectangle start moving, finish its movement, stop. And then, start my second rectangle moving, finish its movement and stop.
Currently what happens is that, when I run my program, they both start moving at the same time.
I also found SKAction.sequence for actions to play in order, however, I only can use this for actions on the same object. Not in different objects like in my example.
If you want to move the two rectangles sequentially (not in parallel), you could use the completion property of the first action like this (apple docs):
import SpriteKit
class GameScene: SKScene {
let mySquare1 = SKShapeNode(rectOfSize:CGSize(width: 50, height: 50))
let mySquare2 = SKShapeNode(rectOfSize:CGSize(width: 50, height: 50))
override func didMoveToView(view: SKView) {
mySquare1.position = CGPoint(x: 100, y:100)
mySquare2.position = CGPoint(x: 300, y:100)
mySquare1.fillColor = SKColor.blueColor()
mySquare2.fillColor = SKColor.blueColor()
self.addChild(mySquare1)
self.addChild(mySquare2)
let move1 = SKAction.moveToX(250, duration: 1.0)
let move2 = SKAction.moveToY(250, duration: 1.0)
self.mySquare1.runAction(move1,completion: {
self.mySquare2.runAction(move2)
})
}
}
You could use the SKAction for running blocks. Then sequence them from the scene.
For example:
func example() {
let firstRectMove = SKAction.runBlock({
let move1 = SKAction.moveToX(20, duration: 1.0)
let move2 = SKAction.moveToY(20, duration: 1.0)
rec1.runAction(SKAction.sequence([move1,move2]))
})
let actionWait = SKAction.waitForDuration(2.0)
let secondRectMove = SKAction.runBlock({
let move1 = SKAction.moveToX(20, duration: 1.0)
let move2 = SKAction.moveToY(20, duration: 1.0)
rec2.runAction(SKAction.sequence([move1,move2]))
})
//self is an SKScene or any other node really...
self.runAction(SKAction.sequence([firstRectMove,actionWait,secondRectMove]))
}
As Alessandro Ornano suggests, a way to accomplish this is run your first action with a completion block, where a block of code runs after the completion of the original action. A downside to this approach is that it can create a pyramid of doom when you need to chain together more than two actions at a time.
An alternative way to avoid to the "pyramid" is to define a method that calls itself recursively for each subsequent action in an array:
func runInSequence(actions:[(node:SKNode,action:SKAction)], index:Int) {
if index < actions.count {
let node = actions[index].node
let action = actions[index].action
node.runAction(action) {
// Avoid retain cycle
[weak self] in
self?.runInSequence(actions, index: index+1)
}
}
}
To use this, define/create an array that stores the nodes and actions you want to run in sequence, and call the method with the starting index:
let nodesAndActions:[(node:SKNode,action:SKAction)] = [
(mySquare1,moveAction1),
(mySquare2,moveAction2)
]
runInSequence(nodesAndActions, index: 0)
The idea is that I am creating blocks that are falling out of the sky.
To do this I need an custom action that does four things:
Create an node with my block class
Set the position of that node
add the node to the scene
after an delay go to point one
I am wondering if you actually can create a SKAction.customActionWithDuration to do this things.
Thanks in advance
The following method creates an SKAction that should fit your needs.
func buildAction() -> SKAction {
return SKAction.runBlock {
// 1. Create a node: replace this line to use your Block class
let node = SKShapeNode(circleOfRadius: 100)
// 2. Set the position of that node
node.position = CGPoint(x: 500, y: 300)
// 3. add the node to the scene
self.addChild(node)
// 4. after a delay go to point one
let wait = SKAction.waitForDuration(3)
let move = SKAction.moveTo(CGPoint(x: 500, y: 0), duration: 1)
let sequence = SKAction.sequence([wait, move])
node.runAction(sequence)
}
}
Thanks to #appsYourLife.
I've made a couple of changes below:
I've adjusted for swift 3
I've added a parameter called parent so you can either use buildAction(parent: self) or if you want to attach the node to some otherNode you can use buildAction(parent: otherNode)
func buildAction(parent: SKNode) -> SKAction {
return SKAction.run {
// 1. Create a node: replace this line to use your Block class
let node = SKShapeNode(circleOfRadius: 100)
// 2. Set the position of that node
node.position = CGPoint(x: 500, y: 300)
// 3. add the node to the scene
parent.addChild(node)
// 4. after a delay go to point one
let wait = SKAction.wait(forDuration: 3)
let move = SKAction.move(to: CGPoint(x: 500, y: 0), duration: 1)
let sequence = SKAction.sequence([wait, move])
node.run(sequence)
}
}
I am making a game in SpriteKit and I have a node that is moving back and forth across the screen and repeating using the code:
let moveRight = SKAction.moveByX(frame.size.width/2.8, y: 0, duration: 1.5)
let moveLeft = SKAction.moveByX(-frame.size.width/2.8, y: 0, duration: 1.5)
let texRight = SKAction.setTexture(SKTexture(imageNamed: "Drake2"))
let texLeft = SKAction.setTexture(SKTexture(imageNamed: "Drake1"))
let moveBackAndForth = SKAction.repeatActionForever(SKAction.sequence([texRight, moveRight, texLeft, moveLeft,]))
Drake1.runAction(moveBackAndForth)
I am trying to figure out what method I can use to randomize the duration. Every time moveBackandForth runs, I want it to rerun using a different duration, (within games, not between). If someone could give me some example code to try I would really appreciate it.
Also arc4Random works fine, but it doesn't randomize within the game, only between games.
When you run actions like from your example and randomize duration parameter with something like arc4Random this is actually happening:
Random duration is set and stored in action.
Then action is reused in a sequence with a given duration.
Because the action is reused as it is, duration parameter remains the same over time and moving speed is not randomized.
One way to solve this (which I prefer personally) would be to create a "recursive action", or it is better to say, to create a method to run desired sequence and to call it recursively like this :
import SpriteKit
class GameScene: SKScene {
let shape = SKSpriteNode(color: UIColor.redColor(), size: CGSize(width: 20, height: 20))
override func didMoveToView(view: SKView) {
shape.position = CGPointMake(CGRectGetMidX(self.frame) , CGRectGetMidY(self.frame)+60 )
self.addChild(shape)
move()
}
func randomNumber() ->UInt32{
var time = arc4random_uniform(3) + 1
println(time)
return time
}
func move(){
let recursive = SKAction.sequence([
SKAction.moveByX(frame.size.width/2.8, y: 0, duration: NSTimeInterval(randomNumber())),
SKAction.moveByX(-frame.size.width/2.8, y: 0, duration: NSTimeInterval(randomNumber())),
SKAction.runBlock({self.move()})])
shape.runAction(recursive, withKey: "move")
}
}
To stop the action, you remove its key ("move").
I don't have any project where I can try right now.
But you might want to try this :
let action = [SKAction runBlock:^{
double randTime = 1.5; // do your arc4random here instead of fixed value
let moveRight = SKAction.moveByX(frame.size.width/2.8, y: 0, duration: randTime)
let moveLeft = SKAction.moveByX(-frame.size.width/2.8, y: 0, duration: randTime)
let texRight = SKAction.setTexture(SKTexture(imageNamed: "Drake2"))
let texLeft = SKAction.setTexture(SKTexture(imageNamed: "Drake1"))
let sequence = SKAction.sequence([texRight, moveRight, texLeft, moveLeft])
Drake1.runAction(sequence)
}];
let repeatAction = SKAction.repeatActionForever(action)
Drake1.runAction(repeatAction)
Let me know if it helped.