Search code examples
uibezierpath

What algorithm is available to correlate a SKSpriteNode's position with the UIBezierPath's currentPoint?


What algorithm is available to correlate a SKSpriteNode's position with the UIBezierPath's currentPoint?

I have this code (GameViewController) which implements the following of a SKSpriteNode along a UIBezierPath:

func createTrainPath() {
    
    trackRect = CGRect(x: tracksPosX - tracksWidth/2,
                       y: tracksPosY,
                       width: tracksWidth,
                       height: tracksHeight)
    trainPath = UIBezierPath(ovalIn: trackRect)
                  
}   // createTrainPath


func startFollowTrainPath() {
                       
    var trainAction = SKAction.follow(
                                  trainPath.cgPath,
                                  asOffset: false,
                                  orientToPath: true,
                                  speed: theSpeed)
    trainAction = SKAction.repeatForever(trainAction)
    myTrain.run(trainAction, withKey: runTrainKey)

}   // startFollowTrainPath


func stopFollowTrainPath() {
    
    guard myTrain == nil else {
        myTrain.removeAction(forKey: runTrainKey)
        savedTrainPosition = getPositionFor(myTrain, orPath: trainPath)
        return
    }
    
}   // stopFollowTrainPath

The user of this segment can bring up another SKScene .. at which action I call stopFollowTrainPath() and save the position of the SKSpriteNode to the global savedTrainPosition via:

func getPositionFor(_ node: SKSpriteNode, orPath: UIBezierPath) -> CGPoint {
    
    if (useNode) {
        return node.position
    }
    else {
        return orPath.currentPoint
    }
    
}   // getPositionFor

(1) When the user returns from looking at the other SKScene, I want the moving (following) SKSpriteNode to continue moving around the UIBezierPath starting where I stopped the Node before presenting the other SKScene.

(2) If the SKSpriteNode is not moving before the other SKScene is brought up, I need the position of the SKSpriteNode to remain where it was before the new SKScene was presented.

What is the best method for doing these?


Solution

  • Because you will be re-sizing the oval on device rotation, and, when you show a different scene and then "come back" you want to:

    • "save" the current train position
    • show a new scene
    • show the train scene (possibly after device was rotated while viewing the other scene)
    • re-start the animation at the saved position

    You probably need to generate an array of points defining the oval path (instead of using ovalIn:).

    Let's look at an oval path using only 10 points, with array[0] at 3 o'clock:

    enter image description here

    the animation always begins at the first point of the path:

    enter image description here

    now the train animates a little bit:

    enter image description here

    and the user taps to go to the "Directions" scene.

    We find the index of the closest point in the array, save it, and then on return re-create the path using that index as the starting point:

    enter image description here

    We let the animation run, and the user taps to see the "Credits" scene:

    enter image description here

    Again, we find the index of the closest point in the array, save it, and then on return re-create the path using that index as the starting point:

    enter image description here

    To get a smoother oval path, we'll use more points:

    enter image description here

    We can still use the current train position to find the closest point, and re-create the path using that point as the starting point:

    enter image description here

    enter image description here

    The more points we use, the smoother the oval will be. Here it is with 180 points (the labels would be a mess):

    enter image description here

    To get the train on the inside of the oval, and "right-side-up," we can replace the texture with a horizontally flipped image when the node is below the centerline of the oval:

    enter image description here

    Re-creating the path from a new index is very fast:

        var ptsCopy = Array(thePoints[startIDX...])
        ptsCopy.append(contentsOf: thePoints[0..<startIDX])
        
        let pth = UIBezierPath()
        pth.move(to: ptsCopy.removeFirst())
        while !ptsCopy.isEmpty {
            pth.addLine(to: ptsCopy.removeFirst())
        }
        pth.close()
    

    and, unless you're doing something you haven't talked about in this and your other related posts, we don't have to repeatedly generate the "oval array of points" ... You'll only have two sizes - one for portrait orientation and one for landscape.

    I updated the GitHub repo I had provided you before: https://github.com/DonMag/SpriteKitRotation

    and here's how it looks when running: https://www.youtube.com/watch?v=ziDqt-ApuSU


    Edit:

    With only minor code change, and with one (or more) "overhead" views of the train, you could also do this with the animation to make it a little less abrupt:

    enter image description here


    Edit 2:

    Sometimes, we have to experiment to understand how things work.

    Let's try using these two images:

    enter image description here enter image description here

    The yellow/red arrow shape is your "train." The Black arrow is pointing to the top of the image.

    When we animate a node along a path, and we use orientToPath: true, SceneKit positions the center of the node on the path, and orients the top of the node in the direction of movement.

    So, let's define an octagonal path (so we can see the direction/angle changes):

    enter image description here

    We'll create two of these, and use the "horizontal" image for one path and the "vertical" image for the other. We position the nodes at the starting points of the paths, but we do not yet add animation:

    enter image description here

    As soon as we start the animation, we see that the Black Arrow - which points to the top of the image/node - is oriented in the direction of movement:

    enter image description here

    As it "travels":

    enter image description here

    We see that it rotates as it should.

    For your usage, though, you don't want the built-in behavior... you want the node flipped -- based on your own desired behavior. You want the "yellow top of the train" to be oriented "up" at different locations on the path:

    enter image description here

    So, it's not a matter of SceneKit not doing what Apple's docs says it will do... it's a misunderstanding of how these things work. And, as I said, Sometimes, we have to experiment to reach that understanding.


    Edit 3:

    Multiple sprite nodes, using the same points-defined-oval-path, starting at different "index" points (ignore the "jump" across the top... had to keep the gif under 2mb):

    enter image description here