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.
Important:This is an[adventure post],where I will write a postWHILEI 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 theSOLUTIONsection.
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 toLunar Landertext-based game from back in the day. And when I say "back in the day" I mean like1969.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 inREAL-TIMElooked 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 thefuelGauge,as we are in space, and we really need to manage our spending you know?! 💰
In case you are not familiar withSpriteKit,let me give you a quick rundown. We have one mainUIViewControllerthat is holding our scene. TheSKSceneis 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 thefuelGauge:
- Add it in the ViewController like a regularUIImageVieworUISlider
- Add it to the GameScene as anSKSpriteNode
Adding it to the viewController is not a problem at all. I can use a delegate and modify that image height or even use aUISlider.
BUT, I really don't want to use the slider since myfuelGaugeneeds to be quite custom, and honestly IREALLYwant to useSKCropNodeand just mask the current fuel state by moving that node up and down. Masking in SpriteKit works really well so let's use that to 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 ourGameScene!
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 that's 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 friendDuckDuckGoand see what we can find! 🐥🐥🕵️♂️
The bug hunt 🐞
So I found thisposton StackOverflow which mentions a similar issue. It says that we need to define the playableArea so gameScene knows what it's working with:
classGameScene:SKScene{
letplayableArea:CGRect
//....
Then, use the following code in the GameScene initialiser:
overrideinit(size:CGSize){
//1. Get the aspect ratio of the device
letdeviceWidth=UIScreen.mainScreen().bounds.width
letdeviceHeight=UIScreen.mainScreen().bounds.height
letmaxAspectRatio:CGFloat=deviceWidth/deviceHeight
//3. For portrait orientation, use this*****
letplayableWidth=size.height/maxAspectRatio
letplayableMargin=(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 initialization for theGameScenein our main viewController and forward the size of the viewController that is creating it.
//...
ifletview=self.viewas!SKView?{
letscene=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's place thatfuelGaugesmack 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,BUTeverything is the same sizeANDin 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 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 usingplayableArea.maxY - 160for 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 thefuelGaugeis 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 widthANDthe height of thefuelGauge.In my opinion taking 3rd of the screen is quite sufficient.
letfuelGaugeHeight=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 is260/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)
ThatfuelGaugeHeight/1.5is the same as the- 160we 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 aneffinng 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! OurfuelGaugehas a good placement and next, I will work on the peskySKCropNode,so stay tuned!
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 onTwitter.
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.