Sticking Nodes to Corners

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.

Image of a small purple rectangle on a large black rectangle. The bottom left corner of the purple rectangle is aligned with the bottom left corner of the image. There are two captions, one in the black rectangle that says Anchor Point: 0, 0 and one on the purple rectangle that says Anchor Point: 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.

Image of a small purple rectangle on a large black rectangle. The bottom left corner of the purple rectangle is aligned with the centre of the image. There are two captions, one in the black rectangle that says Anchor Point: 0.5, 0.5 and one on the purple rectangle that says Anchor Point: 0, 0

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:

Image of a small purple rectangle on a large black rectangle. The bottom left corner of the purple rectangle is aligned with the centre of the image. There are two captions, one in the black rectangle that says Anchor Point: 0.5, 0.5 and one on the purple rectangle that says Anchor Point: 0, 0

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.

An animated gif showing a red square on a dark grey background. As it animates, the square moves to be flush with each edge and corner.

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.


  1. 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 an SKLabelNode, setting the correct horizontal and vertical alignments is up to the caller). ↩︎