SKSpriteNode positioning math escapes me?

29 Views Asked by At

SKSpriteNode positioning math escapes me ?

More specifically, the positioning required for creating a UIBezierPath from a CGRect.

A very few short code snippets follow ...

roomWidth = UIScreen.main.bounds.width
roomHeight = UIScreen.main.bounds.height

// trackOffset = a constant
tracksWidth  = roomWidth - 2*trackOffset   // inset from screen edge
tracksHeight = 0.40*roomHeight - trackOffset

// the `SKSpriteNode` = myTracks
myTracks.anchorPoint = CGPoint(x: 0.5, y: 0.5)

tracksPosX = 0.0
tracksPosY = -roomHeight/2 + trackOffset + tracksHeight/2
myTracks.position = CGPoint(x: tracksPosX, y: tracksPosY)

Later, I create a UIBezierPath from my trackRect defined as follows:

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

But something is wrong with y: tracksPosY because with iOS, the origin is in the upper-left corner and the rectangle extends towards the lower-right corner.

Given this spec from Apple, shouldn't it read:

y: tracksPosY + tracksHeight/2?

1

There are 1 best solutions below

0
DonMag On

We need to try and simplify things -- and take them step-by-step.

So, let's use a simple game scene, where we'll define a rect and add a SKShapeNode with a rectangle path, and a SKShapeNode with an oval path:

import UIKit
import GameplayKit

class SimpleGameScene: SKScene {
    
    override func didMove(to view: SKView) {
        
        guard let thisSKView = self.view else { return }
        
        let sz = thisSKView.frame.size
        
        let offset: CGFloat = 80.0
        
        let x: CGFloat = offset
        let y: CGFloat = offset
        let w: CGFloat = sz.width - (offset * 2.0)
        let h: CGFloat = sz.height * 0.4
        
        let myRect: CGRect = .init(x: x, y: y, width: w, height: h)
        
        let shapeNodeRect = SKShapeNode(path: UIBezierPath(rect: myRect).cgPath)
        shapeNodeRect.lineWidth = 7
        shapeNodeRect.strokeColor = .darkGray
        addChild(shapeNodeRect)
        
        let shapeNodeOval = SKShapeNode(path: UIBezierPath(ovalIn: myRect).cgPath)
        shapeNodeOval.lineWidth = 5
        shapeNodeOval.strokeColor = .systemBlue
        addChild(shapeNodeOval)
    }
    
}

Looks like this:

enter image description here

and, as we know, y-axis is "inverted" with scene kit, so we have an origin of 80,80 and positive values for width and height:

enter image description here

If we use the same values to define the rect in a "normal" app, using CAShapeLayers instead of scene kit nodes:

import UIKit

class ShapeTestVC: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .init(red: 0.148, green: 0.148, blue: 0.148, alpha: 1.0)
        
        let thisSKView = UIView()
        thisSKView.frame = view.bounds
        view.addSubview(thisSKView)
        
        let sz = thisSKView.frame.size
        
        let offset: CGFloat = 80.0
        
        let x: CGFloat = offset
        let y: CGFloat = offset
        let w: CGFloat = sz.width - (offset * 2.0)
        let h: CGFloat = sz.height * 0.4
        
        let myRect: CGRect = .init(x: x, y: y, width: w, height: h)
        
        let shapeNodeRect = CAShapeLayer()
        shapeNodeRect.path = UIBezierPath(rect: myRect).cgPath
        shapeNodeRect.lineWidth = 7
        shapeNodeRect.strokeColor = UIColor.darkGray.cgColor
        shapeNodeRect.fillColor = UIColor.clear.cgColor
        thisSKView.layer.addSublayer(shapeNodeRect)
        
        let shapeNodeOval = CAShapeLayer()
        shapeNodeOval.path = UIBezierPath(ovalIn: myRect).cgPath
        shapeNodeOval.lineWidth = 5
        shapeNodeOval.strokeColor = UIColor.systemBlue.cgColor
        shapeNodeOval.fillColor = UIColor.clear.cgColor
        thisSKView.layer.addSublayer(shapeNodeOval)
    }
    
}

we get this:

enter image description here

with non-inverted y-axis:

enter image description here

What may be confusing the issue is the fact that an inverted rectangle or oval doesn't look any different.

So, let's add an "arrow" bezier path shape:

    let p1: CGPoint = .init(x: myRect.midX - 60.0, y: myRect.minY + 80.0)
    let p2: CGPoint = .init(x: myRect.midX, y: myRect.minY)
    let p3: CGPoint = .init(x: myRect.midX + 60.0, y: myRect.minY + 80.0)
    let p4: CGPoint = .init(x: myRect.midX, y: myRect.maxY)
    let p5: CGPoint = .init(x: myRect.minX, y: myRect.maxY)
    let p6: CGPoint = .init(x: myRect.maxX, y: myRect.maxY)

    let pth = UIBezierPath()
    
    pth.move(to: p1)
    pth.addLine(to: p2)
    pth.addLine(to: p3)
    pth.move(to: p2)
    pth.addLine(to: p4)
    pth.move(to: p5)
    pth.addLine(to: p6)

enter image description here

Let's add that as another CAShapeLayer in our "normal" app:

    let shapeNodeArrow = CAShapeLayer()
    shapeNodeArrow.path = pth.cgPath
    shapeNodeArrow.lineWidth = 3
    shapeNodeArrow.strokeColor = UIColor.systemRed.cgColor
    shapeNodeArrow.fillColor = UIColor.clear.cgColor
    thisSKView.layer.addSublayer(shapeNodeArrow)

and we get this (as expected):

enter image description here

If we then add that arrow bezier path as another SKShapeNode in our scene kit app:

    let shapeNodeArrow = SKShapeNode(path: pth.cgPath)
    shapeNodeArrow.lineWidth = 3
    shapeNodeArrow.strokeColor = .systemRed
    addChild(shapeNodeArrow)

we get this result:

enter image description here

and now it's pretty easy to see that the inverted y-axis also inverts the path(s) we've created.


Again, simplify what you're doing to make it easier to learn and understand what's going on. It also helps to add print(...) statements, or Debug Breakpoints, to evaluate your calculations.

Using the code you posted, if I set trackOffset = 80.0 and then print(trackRect) at the end, I get (on an iPhone 15 Pro):

(-116.5, -215.6, 233.0, 260.8)

which is almost certainly not the rectangle origin you are going for.


Full code for each example...

"normal" app with CAShapeLayer:

class ShapeTestVC: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .init(red: 0.148, green: 0.148, blue: 0.148, alpha: 1.0)
        
        let thisSKView = UIView()
        thisSKView.frame = view.bounds
        view.addSubview(thisSKView)
        
        let sz = thisSKView.frame.size
        
        let offset: CGFloat = 80.0
        
        let x: CGFloat = offset
        let y: CGFloat = offset
        let w: CGFloat = sz.width - (offset * 2.0)
        let h: CGFloat = sz.height * 0.4
        
        let myRect: CGRect = .init(x: x, y: y, width: w, height: h)
        
        let shapeNodeRect = CAShapeLayer()
        shapeNodeRect.path = UIBezierPath(rect: myRect).cgPath
        shapeNodeRect.lineWidth = 7
        shapeNodeRect.strokeColor = UIColor.darkGray.cgColor
        shapeNodeRect.fillColor = UIColor.clear.cgColor
        thisSKView.layer.addSublayer(shapeNodeRect)
        
        let shapeNodeOval = CAShapeLayer()
        shapeNodeOval.path = UIBezierPath(ovalIn: myRect).cgPath
        shapeNodeOval.lineWidth = 5
        shapeNodeOval.strokeColor = UIColor.systemBlue.cgColor
        shapeNodeOval.fillColor = UIColor.clear.cgColor
        thisSKView.layer.addSublayer(shapeNodeOval)
        
        let p1: CGPoint = .init(x: myRect.midX - 60.0, y: myRect.minY + 80.0)
        let p2: CGPoint = .init(x: myRect.midX, y: myRect.minY)
        let p3: CGPoint = .init(x: myRect.midX + 60.0, y: myRect.minY + 80.0)
        let p4: CGPoint = .init(x: myRect.midX, y: myRect.maxY)
        let p5: CGPoint = .init(x: myRect.minX, y: myRect.maxY)
        let p6: CGPoint = .init(x: myRect.maxX, y: myRect.maxY)

        let pth = UIBezierPath()
        
        pth.move(to: p1)
        pth.addLine(to: p2)
        pth.addLine(to: p3)
        pth.move(to: p2)
        pth.addLine(to: p4)
        pth.move(to: p5)
        pth.addLine(to: p6)

        let shapeNodeArrow = CAShapeLayer()
        shapeNodeArrow.path = pth.cgPath
        shapeNodeArrow.lineWidth = 3
        shapeNodeArrow.strokeColor = UIColor.systemRed.cgColor
        shapeNodeArrow.fillColor = UIColor.clear.cgColor
        thisSKView.layer.addSublayer(shapeNodeArrow)
    }
    
}

Full code for each example...

scene kit app with SKShapeNode:

class SimpleGameScene: SKScene {
    
    override func didMove(to view: SKView) {
        
        guard let thisSKView = self.view else { return }
        
        let sz = thisSKView.frame.size
        
        let offset: CGFloat = 80.0
        
        let x: CGFloat = offset
        let y: CGFloat = offset
        let w: CGFloat = sz.width - (offset * 2.0)
        let h: CGFloat = sz.height * 0.4
        
        let myRect: CGRect = .init(x: x, y: y, width: w, height: h)
        
        let shapeNodeRect = SKShapeNode(path: UIBezierPath(rect: myRect).cgPath)
        shapeNodeRect.lineWidth = 7
        shapeNodeRect.strokeColor = .darkGray
        addChild(shapeNodeRect)
        
        let shapeNodeOval = SKShapeNode(path: UIBezierPath(ovalIn: myRect).cgPath)
        shapeNodeOval.lineWidth = 5
        shapeNodeOval.strokeColor = .systemBlue
        addChild(shapeNodeOval)
        
        let p1: CGPoint = .init(x: myRect.midX - 60.0, y: myRect.minY + 80.0)
        let p2: CGPoint = .init(x: myRect.midX, y: myRect.minY)
        let p3: CGPoint = .init(x: myRect.midX + 60.0, y: myRect.minY + 80.0)
        let p4: CGPoint = .init(x: myRect.midX, y: myRect.maxY)
        let p5: CGPoint = .init(x: myRect.minX, y: myRect.maxY)
        let p6: CGPoint = .init(x: myRect.maxX, y: myRect.maxY)
        
        let pth = UIBezierPath()
        
        pth.move(to: p1)
        pth.addLine(to: p2)
        pth.addLine(to: p3)
        pth.move(to: p2)
        pth.addLine(to: p4)
        pth.move(to: p5)
        pth.addLine(to: p6)
        
        let shapeNodeArrow = SKShapeNode(path: pth.cgPath)
        shapeNodeArrow.lineWidth = 3
        shapeNodeArrow.strokeColor = .systemRed
        addChild(shapeNodeArrow)
    }
    
}