I have a Direction
enum that I use to position UI layers in my adventure game. It supports 8 compass directions plus a case for centre:
public enum Direction : String, CaseIterable, Codable { case center, north, northeast, east, southeast, south, southwest, west, northwest }
I want it to be able to tell me what position I should give a child node so that it’s flush against a particular edge or corner given a parent’s size and anchor point.
The Problem
I can’t just assume that, for example, southwest
is going to be 0, 0. This would be true if both the child and the parent’s anchor points were 0, 0.
However, if the parent’s anchor point was 0.5, 0.5, then the child would appear with its bottom left point at the centre of the screen.
Finding the Centre
What I need is a formula to find the centre point of the parent node given its size and anchor point:
let centreX : CGFloat = -(parentSize.width / 2) + (parentSize.width * (1 - anchorPoint.x)) let centreY : CGFloat = -(parentSize.height / 2) + (parentSize.height * (1 - anchorPoint.y)) let centrePoint : CGPoint = CGPoint(x: centreX, y: centreY)
This will give me the position I need to place in any child nodes to make sure their anchor point is aligned to the centre of the parent, whatever happens to the parent’s size and anchor point.
Positioning The Child
However, this position does not take into account the anchor point of the child. I still have this problem:
Most of the time, this is expected behaviour. I choose the anchor point of the child because I want it to appear a particular way. For example, having an anchor point of CGPoint(x: 0.5, y: 0)
makes positioning the sprite as if it’s standing on a surface as easy as setting the position of the child to the top of that surface.
For my Direction
enum, however, I want the sprite to be flush against a corner or an edge and not have to think about what its anchor point is.
I need to apply the same formula to the child’s properties as well:
let childPointX = -(childSprite.size.width / 2) + (childSprite.size.height * (1 - child.anchorPoint.x)) let childPointY = -(childSprite.size.height / 2) + (childSprite.size.height * (1 - child.anchorPoint.y)) let childCentre = CGPoint(x: childPointX, y: childPointY)
Finally, I’ll subtract the child’s position from the parent’s and then apply this position to the child:
let centreX = parentCentreX - childPointX let centreY = parentCentreY - childPointY childSprite.position = CGPoint(x: centreX, y: centreY)
This will ensure that the child node will always be centred.
Adding The Compass Points
To get it to stick to each compass point, I need to calculate the offset from the centre. This is always going to be half the parent’s size minus half the child’s size:
let offsetX = ((parentSize.width / 2) - (childWidth / 2)) let offsetY = ((parentSize.height / 2) - (childHeight / 2))
Then it’s a case of adding or subtracting the relevant offsets to get the correct position:
let point : CGPoint switch self { case .center: point = CGPoint(x: finalX, y: finalY) case .north: point = CGPoint(x: finalX, y: finalY + offsetY) case .northeast: point = CGPoint(x: finalX + offsetX, y: finalY + offsetY) case .east: point = CGPoint(x: finalX + offsetX, y: finalY) case .southeast: point = CGPoint(x: finalX + offsetX, y: finalY - offsetY) case .south: point = CGPoint(x: finalX, y: finalY - offsetY) case .southwest: point = CGPoint(x: finalX - offsetX, y: finalY - offsetY) case .west: point = CGPoint(x: finalX - offsetX, y: finalY) case .northwest: point = CGPoint(x: finalX - offsetX, y: finalY + offsetY) } childSprite.position = CGPoint(x: centreX, y: centreY)
Example Playground
To test this, I assigned the tap gesture to randomly generate the parent’s anchor point while cycling through all of the available Direction cases.
It’s a neat way to be able to ensure that children1 will be placed correctly while still being able to use the anchor point and size for the parent that is most appropriate for a game.
All of this is in a Playground that’s available on Github.
- I have further adapted it to take any node, not just an
SKSpriteNode
. For any other node type, it assumes that the width and height is 0 so some additional work is required to place things correctly (e.g. for anSKLabelNode
, setting the correct horizontal and vertical alignments is up to the caller). ↩︎