Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Check if SCNNode SCNAction is finished

I have created a SceneKit 3D maze world in which a player can move. Some of the moves like jumping involve moving the camera up and down while changing to view direction over a period of time of several seconds. During this time I would like to ignore taps and swipes by the user that would normally result in other types of movements like turning and moving forward.

I could create a timer that matches the jump duration and sets a Bool but I was hoping for a simplier way of checking the SCNNode for the camera.

Is there a simple way to see if the SCNNode for the camera is no longer running the SCNAction for the jump so I can add this logic in front of other tap and swipe actions?

Or perhaps there is an SCNAction that could set the Bool that I could put at the start and finish of my jump sequence?

Here is my jump code:

        let jumpUp: SCNAction = SCNAction.move(to: SCNVector3Make(Float(Int(-yPos)), Float(Int(xPos)), jumpHeight), duration: jumpTime)
        let jumpAppex: SCNAction = SCNAction.wait(duration: jumpWaitTime)
        let fallDown: SCNAction = SCNAction.move(to: SCNVector3Make(Float(Int(-yPos)), Float(Int(xPos)), cameraHeight), duration: jumpTime)

        var lookDown: SCNAction = SCNAction.rotateTo(x: 0, y: 0, z: CGFloat(π), duration: jumpTurnTime)
        let noLook: SCNAction = SCNAction.wait(duration: jumpTime*2.0)
        var lookBack: SCNAction = SCNAction.rotateTo(x: 0, y: 0, z: 0, duration: jumpTurnTime)

        switch playerDirection.direction
        {
            case .south:
                lookDown = SCNAction.rotateTo(x: 0, y: 0, z: CGFloat(southZ), duration: jumpTurnTime)
                lookBack = SCNAction.rotateTo(x: CGFloat(π/2), y: 0, z: CGFloat(southZ), duration: jumpTurnTime)
            case .north:
                lookDown = SCNAction.rotateTo(x: 0, y: 0, z: CGFloat(northZ), duration: jumpTurnTime)
                lookBack = SCNAction.rotateTo(x: CGFloat(π/2), y: 0, z: CGFloat(northZ), duration: jumpTurnTime)
            case .east:
                lookDown = SCNAction.rotateTo(x: 0, y: 0, z: CGFloat(eastZ), duration: jumpTurnTime)
                lookBack = SCNAction.rotateTo(x: CGFloat(π/2), y: 0, z: CGFloat(eastZ), duration: jumpTurnTime)
            case .west:
                lookDown = SCNAction.rotateTo(x: 0, y: 0, z: CGFloat(westZ), duration: jumpTurnTime)
                lookBack = SCNAction.rotateTo(x: CGFloat(π/2), y: 0, z: CGFloat(westZ), duration: jumpTurnTime)
        }

        let sequenceJump = SCNAction.sequence([jumpUp, jumpAppex, fallDown])
        let sequenceLook = SCNAction.sequence([lookDown, noLook, lookBack])

        mazeScene.mazeCamera.runAction(sequenceJump)
        mazeScene.mazeCamera.runAction(sequenceLook)

Thanks

Greg

like image 522
Greg Robertson Avatar asked Dec 02 '16 10:12

Greg Robertson


2 Answers

Actions are pretty weird things, I think.

The idea is heavily inspired (basically 1:1 mapping and 'theft') from cocos2D, where they were used as a way to avoid interacting directly with a game-loop, state and time (sort of). Actions provide a kind of modularity and abstraction for handling both time and the creation and invoking of activity, plus a very primitive handling of their conclusions.

You can see just how similar SpriteKit and SceneKit's Actions are to the original ideas when reading here: http://python.cocos2d.org/doc/programming_guide/actions.html

As a result of the divergent and evolving nature of their creation, someone forgot to give them awareness of their own state, despite the fact they influence some aspects of the state of nodes. It would make near perfect sense that an Action object have a "running" state. But they don't, so far as I know.

Instead, you can check to see if a Node has a given Action, and if it does, it's inferred that the Action is running.

Despite most actions doing something across a duration of time, you also can't query them about how far they've made it through their duration, nor what values they've just fired, or will send next. But there are actions that do this, specifically, to cope with the fact this is missing for all Actions means that if you want this, you need to use a special Action that provides this.

And this kind of recursiveness upon and back to self for facilities that should have been innate is the most annoying part of Actions not having been thought through from the perspective of someone familiar with a timeline and keyframes.

Holistically, and in summary: You can't query an action's state, nor its progress, or the value it just did or will use next. This is, absolutely, ridiculous.

I don't know how this was overlooked in the various evolutions of Actions... they're time management and modular activity creators. It seems so logical to include state and progress reporting within them that... well, I don't know... just bizarre that they don't.

So, to answer your question:

  1. You can either use completion handlers, which are the invoking of some code when an Action finishes, to set values or call other functions or clean up stuff, or whatever you want..

  2. Sequence actions, in an SCNAction.sequence... which is a way to have an Action run sequentially, and inside use some Actions that run blocks of code when you want, that call into setting what you need, when you need, in the sequence of Actions. All of which could be avoided if there was transparency to the values the Actions currently had of both time and properties... but...

  3. You can also use the few special actions that have some awareness of the value they're editing by virtue of the changes made to them. I'm only familiar with the float setting abilities in SKEaseKit, but you probably can do this (if you're a much better coder than me) within SceneKit and SpriteKit. SKEaseKit is hiding a couple of value changing Actions that are super useful.

I use it, for example, like this, wherein this mess changes a value of 0 to 1 across a duration (time), in this case linearly, and each frame (hopefully) updates the .xScale of the node upon which this growAction is run:

let growAction = SKEase.createFloatTween(
                start: 0,
                ender: 1,
                timer: time,
                easer: SKEase.getEaseFunction(.curveTypeLinear,
             easeType: .easeTypeOut),
          setterBlock: {(node, i) in
            node.xScale = i}
            )
like image 181
Confused Avatar answered Sep 22 '22 12:09

Confused


I ended up using a .customAction:

I added a class variable isJumping

then at front of the function code I added:

 isJumping = true

and added the SCNAction:

 let jumpDone: SCNAction = SCNAction.customAction(duration: 0, action: {_,_ in self.isJumping = false})

then changed the sequence to:

 let sequenceLook = SCNAction.sequence([lookDown, noLook, lookBack, jumpDone])

then I just do an if on isJumping to see if the jump movement has completed.

like image 33
Greg Robertson Avatar answered Sep 20 '22 12:09

Greg Robertson