Search code examples
iosswiftsprite-kitskactioncgpath

SKAction followPath not working as expected with two circles (Eight Shape)


I am trying to create a CGPath with the shape of an eight or infinity sign. I do think the path creation should return the desired result but I will still post it here:

let path = CGMutablePath()

path.addEllipse(in: CGRect(x: -height/4, y: -height/2, width: height/2, height: height/2))
path.addEllipse(in: CGRect(x: -height/4, y: 0.0, width: height/2, height: height/2))

path.closeSubpath()

return path

Now I have another Node that should follow this path, which I thought would just work by using the SKAction like so:

highlighter.run(SKAction.repeatForever(SKAction.follow(shape.path!, asOffset: false, orientToPath: true, duration: 5.0)))

highlighter being the name of my node, shape being the name of the SKShapeNode I have assigned my path to. However the node only draws one of the circles and not the desired shape. I have been googling the issue but could not find any similar cases and have now been sitting on this for over a day so I might not see something obvious. Any insight would be much appreciated!


Solution

  • If you draw the figure 8 you have generated, you'll see that your sprite is only following the bottom circle, as this is the way you have drawn the path.

    You need to draw half of either the top or the bottom circle, from top/bottom to the middle, then add the complete other circle starting and ending at the middle, and finally another half circle back to where you started.

    Here is an example, which also draws the bounding rectangle and the figure-8 path to follow:

    class GameScene: SKScene {
    
        //    var sprite = SKSpriteNode()
        var figureEightPath = CGMutablePath()
    
        override func didMove(to view: SKView) {
    
            let boundingRect = CGRect(x: -200, y: -400, width: 400, height: 800)
            addChild(drawSKShapeNode(fromRect: rect, withWidth: 2, inColour: SKColor.green, called: "rect"))
    
            figureEightPath = drawFigureEight(in: boundingRect)
            addChild(drawSKShapeNode(fromPath: figureEightPath, withWidth: 2, inColour: SKColor.red, called: "path"))
    
            let sprite = SKSpriteNode(color: SKColor.yellow, size: CGSize(width: 50, height: 50))
            addChild(sprite)
    
            let moveAction = SKAction.follow(figureEightPath, asOffset: false, orientToPath: true, speed: 200)
            sprite.run(SKAction.repeatForever(moveAction))
    
        }
    
        func drawSKShapeNode(fromPath path: CGMutablePath,
                             withWidth width: CGFloat,
                             inColour colour: SKColor,
                             called name: String) -> SKShapeNode {
    
            let shape = SKShapeNode(path: path, centered: true)
            shape.lineWidth = width
            shape.strokeColor = colour
            shape.name = name
            return shape
        }
    
        func drawSKShapeNode(fromRect rect: CGRect,
                             withWidth width: CGFloat,
                             inColour colour: SKColor,
                             called name: String) -> SKShapeNode {
    
            let shape = SKShapeNode(rect: rect)
            shape.lineWidth = width
            shape.strokeColor = colour
            shape.name = name
            return shape
        }
    
        func drawFigureEight(in rect: CGRect) -> CGMutablePath {
            let bottomCentre = CGPoint(x: rect.midX, y: rect.height/4+rect.minY)
            let topCentre = CGPoint(x: rect.midX, y:(rect.height/4)*3+rect.minY)
            let radius = rect.height/4
    
            let bottom = CGPoint(x: rect.midX, y: rect.minY)
    
            let path = CGMutablePath()
            path.move(to: bottom)
    //Draw a semi-circle from the bottom counter-clockwise to the middle
            path.addArc(center: bottomCentre, radius: radius, startAngle: CGFloat(Double.pi*1.5), endAngle: CGFloat(Double.pi/2), clockwise: false)
    //Draw to top circle in a clockwise direction
            path.addArc(center: topCentre, radius: radius, startAngle: CGFloat(Double.pi*1.5), endAngle: CGFloat(-Double.pi/2), clockwise: true)
    //Draw the rest of the bottom circle by drawing a semi-circle from the middle to the bottom counter-clockwise.
            path.addArc(center: bottomCentre, radius: radius, startAngle: CGFloat(Double.pi/2), endAngle: CGFloat(-Double.pi/2), clockwise: false)
    
            return path
        }
    
        override func update(_ currentTime: TimeInterval) {
            // Called before each frame is rendered
        }
    }
    

    There's a couple of helper function - drawSKShapeNode - which you won't need but are useful for debugging as they 'draw' a CGPath or CGRect by creating an SKShapeNode from what they are given.