Search code examples
iosswiftswift3skspritenodecgpath

Swift 3 Generate evenly-spaced SKSpriteNodes along path drawn by user


everyone! First of all, I'm aware that this question is very similar to Draw images evenly spaced along a path in iOS. However, that is in Objective-C (which I can't read) and it is in a normal ViewController working with CGImageRefs. I need it in swift and using SKSpriteNodes (not CGImageRefs). Here's my issue:

I'm trying to make a program that lets the user draw a simple shape (like a circle) and places SKSpriteNodes at fixed intervals along the path drawn by the user. I've got it working fine at a slow pace, but if the user draws too quickly then the nodes get placed too far apart. Here's an example of when I draw it slowly:

User-drawn barrier

User-drawn path with nodes placed approximately 60 pixels apart from each other. Blue is the start node, purple is the end node.

The goal is that each node would have a physicsBody that kept entities from crossing the line drawn by the user (those entities wouldn't be able to squeeze in between evenly spaced nodes). If the user draws too fast, however, there will be a gap in defenses that I can't fix. For example:

Node barrier with gap

Note the visibly larger gap between the 7th and 8th nodes. This occurred because I drew too quickly. Many people have questions that are slightly similar but are unhelpful for my task (e.g. place a specific amount of nodes evenly spaced along a path, rather than place as many nodes as neccessary to get them 60 pixels apart along the path).

In conclusion, here is my main question again: How can I place nodes perfectly spaced along a user-drawn path of any shape? Thank you in advance for your help! Here is my GameScene.swift file:

import SpriteKit

import GameplayKit

class GameScene: SKScene {

let minDist: CGFloat = 60 //The minimum distance between one point and the next

var points: [CGPoint] = []
var circleNodes: [SKShapeNode] = []

override func didMove(to view: SKView) {


}

func getDistance (fromPoint: CGPoint, toPoint: CGPoint) -> CGFloat {

    let deltaX = fromPoint.x - toPoint.x
    let deltaY = fromPoint.y - toPoint.y

    let deltaXSquared = deltaX*deltaX
    let deltaYSquared = deltaY*deltaY

    return sqrt(deltaXSquared + deltaYSquared) //Return the distance

}


func touchDown(atPoint pos : CGPoint) {

    self.removeAllChildren()

    //The first time the user touches, we need to place a point and mark that as the firstCircleNode
    print(pos)
    points.append(pos)
    //allPoints.append(pos)

    let firstCircleNode = SKShapeNode(circleOfRadius: 5.0)

    firstCircleNode.fillColor = UIColor.blue

    firstCircleNode.strokeColor = UIColor.blue

    firstCircleNode.position = pos

    circleNodes.append(firstCircleNode)

    self.addChild(firstCircleNode)

}

func touchMoved(toPoint pos : CGPoint) {

    let lastIndex = points.count - 1 //The index of the last recorded point

    let distance = getDistance(fromPoint: points[lastIndex], toPoint: pos)
        //vector_distance(vector_double2(Double(points[lastIndex].x), Double(points[lastIndex].y)), vector_double2(Double(pos.x), Double(pos.y))) //The distance between the user's finger and the last placed circleNode

    if distance >= minDist {
        points.append(pos)

        //Add a box to that point
        let newCircleNode = SKShapeNode(circleOfRadius: 5.0)

        newCircleNode.fillColor = UIColor.red

        newCircleNode.strokeColor = UIColor.red

        newCircleNode.position = pos

        circleNodes.append(newCircleNode)

        self.addChild(newCircleNode)

    }

}

func touchUp(atPoint pos : CGPoint) {

    //When the user has finished drawing a circle:

    circleNodes[circleNodes.count-1].fillColor = UIColor.purple //Make the last node purple

    circleNodes[circleNodes.count-1].strokeColor = UIColor.purple

    //Calculate the distance between the first placed node and the last placed node:
    let distance = getDistance(fromPoint: points[0], toPoint: points[points.count-1])
        //vector_distance(vector_double2(Double(points[0].x), Double(points[0].y)), vector_double2(Double(points[points.count - 1].x), Double(points[points.count - 1].y)))

    if distance <= minDist { //If the distance is closer than the minimum distance

        print("Successful circle")

    } else { //If the distance is too far

        print("Failed circle")

    }

    points = []
    circleNodes = []

}


override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    for t in touches { self.touchDown(atPoint: t.location(in: self)) }
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    for t in touches { self.touchMoved(toPoint: t.location(in: self)) }
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    for t in touches { self.touchUp(atPoint: t.location(in: self)) }
}

override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
    for t in touches { self.touchUp(atPoint: t.location(in: self)) }
}


override func update(_ currentTime: TimeInterval) {
    // Called before each frame is rendered
}

}


Solution

  • I've figured it out! I was inspired by Christian Cerri's suggestion so I used the following code to make what I wanted:

    import SpriteKit
    import GameplayKit
    
    // MARK: - GameScene
    
    class GameScene: SKScene {
    
    // MARK: - Allows me to work with vectors. Derived from https://www.raywenderlich.com/145318/spritekit-swift-3-tutorial-beginners
    
        func subtract(point: CGPoint, fromPoint: CGPoint) -> CGPoint {
    
            return CGPoint(x: point.x - fromPoint.x, y: point.y - fromPoint.y) //Returns a the first vector minus the second
    
        }
    
        func add(point: CGPoint, toPoint: CGPoint) -> CGPoint {
    
            return CGPoint(x: point.x + toPoint.x, y: point.y + toPoint.y) //Returns a the first vector minus the second
    
        }
    
        func multiply(point: CGPoint, by scalar: CGFloat) -> CGPoint {
    
            return CGPoint(x: point.x * scalar, y: point.y * scalar)
    
        }
    
        func divide(point: CGPoint, by scalar: CGFloat) -> CGPoint {
    
            return CGPoint(x: point.x / scalar, y: point.y / scalar)
    
        }
    
        func magnitude(point: CGPoint) -> CGFloat {
    
            return sqrt(point.x*point.x + point.y*point.y)
    
        }
    
        func normalize(aPoint: CGPoint) -> CGPoint {
    
            return divide(point: aPoint, by: magnitude(point: aPoint))
    
        }
    
        // MARK: - Properties
    
        let minDist: CGFloat = 60
    
        var userPath: [CGPoint] = [] //Holds the coordinates collected when the user drags their finger accross the screen
    
        override func didMove(to view: SKView) {
    
    
    
        }
    
        // MARK: - Helper methods
    
        func getDistance (fromPoint: CGPoint, toPoint: CGPoint) -> CGFloat
        {
    
            let deltaX = fromPoint.x - toPoint.x
            let deltaY = fromPoint.y - toPoint.y
    
            let deltaXSquared = deltaX*deltaX
            let deltaYSquared = deltaY*deltaY
    
            return sqrt(deltaXSquared + deltaYSquared) //Return the distance
    
        }
    
        func touchDown(atPoint pos : CGPoint) {
    
            userPath = []
            self.removeAllChildren()
    
            //Get the first point the user makes
            userPath.append(pos)
    
        }
    
        func touchMoved(toPoint pos : CGPoint) {
    
            //Get every point the user makes as they drag their finger across the screen
            userPath.append(pos)
    
        }
    
        func touchUp(atPoint pos : CGPoint) {
    
            //Get the last position the user was left touching when they've completed the motion
            userPath.append(pos)
    
            //Print the entire path:
            print(userPath)
            print(userPath.count)
    
            plotNodesAlongPath()
    
        }
    
        /**
         Puts nodes equidistance from each other along the path that the user placed
         */
        func plotNodesAlongPath() {
    
            //Start at the first point
            var currentPoint = userPath[0]
    
            var circleNodePoints = [currentPoint] //Holds the points that I will then use to generate circle nodes
    
            for i in 1..<userPath.count {
    
                let distance = getDistance(fromPoint: currentPoint, toPoint: userPath[i]) //The distance between the point and the next
    
                if distance >= minDist { //If userPath[i] is at least minDist pixels away
    
                    //Then we can make a vector that points from currentPoint to userPath[i]
                    var newNodePoint = subtract(point: userPath[i], fromPoint: currentPoint)
    
                    newNodePoint = normalize(aPoint: newNodePoint) //Normalize the vector so that we have only the direction and a magnitude of 1
    
                    newNodePoint = multiply(point: newNodePoint, by: minDist) //Stretch the vector to a length of minDist so that we now have a point for the next node to be drawn on
    
                    newNodePoint = add(point: currentPoint, toPoint: newNodePoint) //Now add the vector to the currentPoint so that we get a point in the correct position
    
                    currentPoint = newNodePoint //Update the current point. Next we want to draw a point minDist away from the new current point
    
                    circleNodePoints.append(newNodePoint) //Add the new node
    
                }
                //If distance was less than minDist, then we want to move on to the next point in line
    
            }
    
            generateNodesFromPoints(positions: circleNodePoints)
    
        }
    
        func generateNodesFromPoints(positions: [CGPoint]) {
            print("generateNodesFromPoints")
            for pos in positions {
    
                let firstCircleNode = SKShapeNode(circleOfRadius: 5.0)
    
                firstCircleNode.fillColor = UIColor.blue
    
                firstCircleNode.strokeColor = UIColor.blue
    
                firstCircleNode.position = pos //Put the node in the correct position
    
                self.addChild(firstCircleNode)
    
            }
    
        }
    
        // MARK: - Touch responders
    
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            for t in touches { self.touchDown(atPoint: t.location(in: self)) }
        }
    
        override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
            for t in touches { self.touchMoved(toPoint: t.location(in: self)) }
        }
    
        override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
            for t in touches { self.touchUp(atPoint: t.location(in: self)) }
        }
    
        override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
            for t in touches { self.touchUp(atPoint: t.location(in: self)) }
        }
    
    
        override func update(_ currentTime: TimeInterval) {
            // Called before each frame is rendered
        }
    }
    

    And this results in the following:

    Evenly placed nodes

    No matter how quickly the user moves their finger, it places nodes evenly along their path. Thanks so much for your help, and I hope it helps more people in the future!