Emin Grbo, May 14 2020

SpriteKit Node positioning on all screen sizes

Positioning nodes on the same exact spot can be a problem in SpriteKit. Perfect time to do an experiment. [πŸ—Ί adventure post]

Important: This is an [adventure post], where I will write a post WHILE I am working on the issue. So, all failed attempts, good and bad, will remain here. If you are here for a quick fix, just scroll to the SOLUTION section you lazy bastard. If however you would like to go on this adventure with me, strap on your best Swift shoes, get that warm drink in your hand and let's see where we might be swept off to. (mind your feet)

History

So, I am making this game which is an homage to Lunar Lander text-based game from back in the day. And when I say "back in the day" I mean like 1969. I wasn't even close to being alive then, I think my mother was like 5 years old...soooooo game is from beyond back in the day you might say. 😁

For some reason, first time I saw it, I was enchanted. So simple, so clear...but yet so challenging! Just to give you an idea of how old this game is, this is what the computer that was designed to play the game in REAL-TIME looked like. And by the way, this is 4 years after the initial LunarLander text based game.


So I decided to get to it, and create a modern version that will keep the simplicity, and have some "advanced" pixel art graphics to go with it, because nothing makes nostalgia come alive more than some good ol' pixel-art! ◻️

Intro

Our journey begins with the working demo. Have a look at this little guys first flight below! πŸš€

⬅️ Tap the left side of the screen, it goes left
➑️ Tap right side of the screen, it goes right
⬆️ And if you tap with two fingers (position not important) it goes straight up!

Now, next thing I want to add is the fuelGauge, as we are in space, and we really need to manage our spending you know?! πŸ’°

In case you are not familiar with SpriteKit, let me give you a quick rundown. We have one main UIViewController that is holding our scene. The SKScene is where all the gaming πŸ§™β€β™€οΈmagic happens, and viewController is there to present that scene to the user. Here is a quick glimpse.


So I have 2 options for adding the fuelGauge:

  • Add it in the ViewController like a regular UIImageView or UISlider
  • Add it to the GameScene as an SKSpriteNode

Adding it to the viewController is not a problem at all. I can use a delegate and modify that image height or even use a UISlider.

BUT, I really don't want to use the slider since my fuelGauge needs to be quite custom, and honestly I REALLY want to use SKCropNode and just mask the current fuel state by moving that node up an down. Masking in SpriteKit works really well so let's use that yo our advantage! We don't plan to have a lot of nodes so I think this is fine.


Our journey begins!

This sounds like a breeze, let's add the node to our GameScene!

fuelGauge = SKSpriteNode()
fuelGauge.size = CGSize(width: 11, height: 260)
fuelGauge.position = CGPoint(x: 100, y: screenHeight - 260)
fuelGauge.texture = SKTexture(imageNamed: "fuelGauge")
addChild(fuelGauge)

I already added the image to my assets called "fuelGauge" so that is where the texture comes from. Let's build that on few devices and see what we have.


Now thats not quite right. Our gauge seems to be way to the right, not to mention that the steps are going towards the middle quite a bit more than they should on iPhone 7 and SE2! Oh well, let's fire up our good friend DuckDuckGo and see what we can find! πŸ₯πŸ₯πŸ•΅οΈβ€β™‚οΈ


The bug hunt 🐞

So I found this post on StackOverflow which mentions a similar issue. It says that we need to define the playableArea so gameScene knows what it's working with:

class GameScene: SKScene {
     let playableArea: CGRect
     //....

Then, use the following code in the GameScene initialiser:

override init(size: CGSize) {

    //1. Get the aspect ratio of the device
    let deviceWidth = UIScreen.mainScreen().bounds.width
    let deviceHeight = UIScreen.mainScreen().bounds.height
    let maxAspectRatio: CGFloat = deviceWidth / deviceHeight

    //3. For portrait orientation, use this*****
    let playableWidth = size.height / maxAspectRatio
    let playableMargin = (size.width - playableWidth) / 2.0
    playableArea = CGRect(x: playableMargin, y: 0, width: playableWidth, height: size.height)

    super.init(size: size)
}

So let me change the initialisation for the GameScene in our main viewController and forward the size of the viewController that is creating it.

// ...
if let view = self.view as! SKView? {

let scene = GameScene(size: view.frame.size)
    // Set the scale mode to scale to fit the window
    scene.scaleMode = .aspectFit
// ...

Just to make sure everything kinda works, and before we do any "serious" math, let place that fuelGauge smack middle of the screen:

//...
fuelGauge.position = CGPoint(x: playableArea.midX, y: playableArea.midY)
//...

Ok, let's see what we have so far!


Alright! Now we're talkin'! It's not perfect, and everything seems to be a lot bigger, BUT everything is the same size AND in the same position! Which I really like, since we can adjust the size based on the screen and that would make so much sense.

So, now I think we can really make the fuelGauge position exactly the way we want it. For some reason I feel the the top left corner makes the most sense, as it won't be blocked with your fingers which I am assuming will be on the bottom part of the screen. That way, you can always keep an eye on the fuel!

With all that in place, now we can just use the appropriate values to place our texture in the right spot:

fuelGauge = SKSpriteNode()
fuelGauge.size = CGSize(width: 11, height: 260)
fuelGauge.position = CGPoint(x: 16, y: playableArea.maxY - 160)
fuelGauge.texture = SKTexture(imageNamed: "fuelGauge")
addChild(fuelGauge)

Note that I am using playableArea.maxY - 160 for the Y position. Since anchor-point for that node is in the middle, I am using 130 to move it down by the upper half, and them adding some more (30) to give it some breathing room.


Solution

This is all good so far. But one thing is bothering me. Height of the fuelGauge is the same on both screens. I know I said that I like that initially, and I still do. But I wan't to control the size of it depending on the screen height. On smaller phones this will be almost half the screen! Which is not something I want.

Therefore let's try and make it so that with one constant we can control the width AND the height of the fuelGauge. In my opinion taking 3rd of the screen is quite sufficient.

let fuelGaugeHeight = playableArea.maxY / 3

And now that we have that, what are we to do with our width which was hardcoded to 11points? Well, since our texture is 11x260, we could calculate the ratio which is 260/11 = 23

With that in mind, our new code for the fuel texture is this:

fuelGauge = SKSpriteNode()
fuelGauge.size = CGSize(width: fuelGaugeHeight/23, height: fuelGaugeHeight)
fuelGauge.position = CGPoint(x: 16, y: playableArea.maxY - safeZoneTop - fuelGaugeHeight/1.5)
fuelGauge.texture = SKTexture(imageNamed: "fuelGauge")
addChild(fuelGauge)

That fuelGaugeHeight/1.5 is the same as the - 160 we had before, we are just adding a bit more than the half of the height to have a bit of space.

Now, I realise that using "magic numbers" and hardcoding bunch of stuff is not recommended, but at the moment we are only tied in to that left spacing and the height ratio. I prefer to be precise and make all my screens look as similar as I can, and I am quite happy with this outcome!

HELLS YEAH!! Now that is what a call an effinng consistency! If I had a mic I would drop it! πŸŽ€πŸ‘‡

I love this current look, and everything seems to be in order. I suspect iPad would be a different beast, but right now this is only planned for the iPhone. Who knows, with some adjustments iPad can join in, but we'll see. πŸ•΅οΈβ€β™‚οΈ


And that's it! Our fuelGauge has a good placement and next, I will work on the pesky SKCropNode, so stay tuned! (khm...subscribe!)

This has been an experiment post, and I am really interested if you liked it...or not!
I would really appreciate if you let me know on Twitter.
Even if you just send me a message "It sucks dude", it would mean the world to me 🌎.
No hard feelings, I appreciate honesty more than you can imagine.

Tagged with: