6

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

Greg Robertson
  • 2,317
  • 1
  • 16
  • 30
  • 1
    There is a `runBlock` `SCNAction`, you could just add this to your sequence and set a bool inside the block. There is also a `runAction:completionHandler` method to run a block when the action has finished. – James P Dec 02 '16 at 14:49
  • Excuse Apple's inability to document its own best features, for this is what you're about to see. IMNSHO, this is the best and most powerful Action, and nearly completely undocumented, and never promoted for what it is, can be, and represents in terms of promise and potential: https://developer.apple.com/reference/scenekit/scnaction/1523692-customaction – Confused Dec 02 '16 at 15:29

3 Answers3

4

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}
            )
Confused
  • 6,048
  • 6
  • 34
  • 75
  • Agree 100% percent. So frustrating that you can't set up a sequence of actions where each action can use/query the current state of the node as part of it's action. the SCNAction.moveTo() must have something under the covers, and for some ungodly reason, we're not allowed to create our own subclasses. Cocos2D had this stuff done better. – PKCLsoft Jun 28 '18 at 03:20
  • If you're considering switching from cocos2D to native frameworks, take a good, long look at using SCNKit instead of SpriteKit, even if you're only working in 2D. Firstly, it gives you better stuff: better particles, better cameras and better scene editor, and much better post processing. Secondly, and exactly pertinent to what you're probably searching for, it gives far better animation options. You can use CAAnimation intrinsically. Perhaps start here: https://developer.apple.com/documentation/scenekit/scnanimatable?language=objc – Confused Jun 28 '18 at 04:34
  • 1
    @PKCLsoft and in my experiments, SceneKit performs better, is more stable, launches much faster and works in all the ways SpriteKit never does, and gets you all the size and native benefits. Plus it seems much better supported and more tuned as an initiative, even before ARKit. Much more so since then. It's also got those cocos2D style actions, too, if you ever need to fallback to them for something. The better particles, alone, are worth the effort, I think. One more thing, the physics are better, too. – Confused Jun 28 '18 at 04:36
  • And have a look at the second answer here, for how to get move info out a running CAAnimation: https://stackoverflow.com/questions/20244933/get-current-caanimation-transform-value/20245200 – Confused Jun 28 '18 at 04:43
  • thanks. I've used a combo of both SpriteKit and SceneKit in my game without huge problems though I do find it more problematic with both of these compared to Cocos2D. I put up this project on github to demonstrate some of the issues I had: https://github.com/pkclsoft/TestSKActionCalls – PKCLsoft Jun 28 '18 at 07:33
  • Oh. Don't ever blend the two. That "feature" never got past an early proof-of-concept phase. Some intern must have pressed publish, and we've been stuck with it ever since. It's a myriad of corner cases, at the core, in the middle of the screen, if you'll excuse the mixed metaphors. A good idea that never got much further than that. The problems that plagued it in the beginning are still there, plus more. SpriteKit, if at all possible, should be ignored and deplored. It is abhorrent, unfinished and unloved. The superficial similarity to cocos2D was a deliberate and cynical ploy. – Confused Jun 28 '18 at 09:41
  • @PKCLsoft read your github. So sorry you went through that. Apple should have the decency to warn everyone their stuff is mostly just gimmicks. Swift included, being pushed as a first programming language... for children... and it's part of the 'design' of the language, apparently. Could have fooled me. It's not near finished, and provides initialisation and optionals as a constantly entwined pair of linked labyrinths for the unwary and/or literal thinkers. SpriteKit was part of Apple's cancelled endeavour to become a game publisher. They've gone with karaoke in a car, and now Oprah, instead. – Confused Jun 28 '18 at 10:09
4

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.

Greg Robertson
  • 2,317
  • 1
  • 16
  • 30
2

There is a runAction(_:completionHandler:), where you can handle the completion. See the documentation.

So, you can pass completion block and check necessary conditions here:

mazeScene.mazeCamera.runAction(sequenceJump) { print("Sequence jump is completed") }

Olga Konoreva
  • 1,338
  • 13
  • 20
  • I saw [James P](https://stackoverflow.com/users/488611/james-p) comment under the question. And I'm sure that it should be a separate answer. At least, it could help me some time ago. – Olga Konoreva Aug 01 '17 at 10:01