Spritekit scaling for universal app - ios

I am having trouble properly setting up my app so that it displays correctly on all devices. I want my game to look best for iPhone and I understand that setting my scene size using GameScene(size: CGSize(width: 1334, height: 750)) and use .aspectFill means that on iPads there will be less space to display things which I'm fine with. The problem is, how do I position my nodes so that they are relative to the each devices frame height and width? I use self.frame.height, self.frame.width, self.frame.midX, etc. for positioning my nodes and when I run my game, it positions things properly considering I run on my iPhone 6, but on my iPad, everything seems blown up and nodes are off the screen. I'm going crazy trying to figure this out

I solved this in my game by using scaleFactors, numbers which tell my app how much to enlarge each length, width, height etc. Then I just make my game look well for one phone, and use that phone's width and height to calculate with which factor I need to enlarge it for other devices. In this example I use the iPhone 4 as a base, but you can use any device just change the numbers according to that device.
Portrait mode:
var widthFactor = UIScreen.main.bounds.width/320.0 //I divide it by the default iPhone 4 width
var heightFactor = UIScreen.main.bounds.height/480.0
Landscape mode:
var widthFactor = UIScreen.main.bounds.width/480.0 //I divide it by the default iPhone 4 landscape width
var heightFactor = UIScreen.main.bounds.height/320.0
Then when you make a node, a coin image for example, multiply its coordinates or width/height by the scaleFactors:
let coin = SKSpriteNode(imageNamed: "coin")
coin.position = CGPoint(x: 25 * widthFactor, y: self.size.height - 70 * heightFactor)

I think what you're looking for might be in this answer: https://stackoverflow.com/a/34878528/6728196
Specifically I think this part is what you're looking for (edited to fit your example):
if UIDevice.current.userInterfaceIdiom == .pad {
// Set things only for iPad
// Example: Adjust y positions using += or -=
buttonNode.position.y += 100
labelNode.position.y -= 100
}
Basically, this just adds or subtracts a certain amount from the iPhone position if the user is using an iPad. It's not too complicated, and you can increase or decrease both x and y values of position by a certain value of percentage of the screen (self.size.width * decimalPercentage).
Another benefit of using this way is that you're just modifying the iPhone positions, so it starts by using the default values that you set. Then if on iPad, it will make changes.
If this is hard to understand let me know so I can clear up the explanation

Related

How are co-ordinates calculated in Swift

I've been trying to get the following code to display a rectangle at the bottom of an iPhone simulator in landscape mode:
let size = CGSize(width: 100, height: 10)
let myRect = SKShapeNode(rectOf: size)
myRect.position = CGPoint(x:self.frame.midX, y:self.frame.maxY - 50)
myRect.strokeColor = SKColor(red: 0.0/255.0, green: 0.0/255.0, blue: 200.0/255.0, alpha: 1.0)
myRect.lineWidth = 4
myRect.physicsBody = SKPhysicsBody(rectangleOf: size)
myRect.physicsBody?.affectedByGravity = false
myRect.physicsBody?.isDynamic = true
The result of this is that the rectangle is not drawn inside the visible screen; I then inserted this debug statement:
print(self.frame.minY, self.frame.maxY, self.frame.minX, self.frame.maxX, self.frame.width, self.frame.height)
Which outputs this:
-667.0 667.0 -375.0 375.0 750.0 1334.0
Further, when I changed the co-ordinates to y:100 I noticed the shape move up - not down. So, my question is: when the phone is in landscape mode, do I need to manually translate the X & Y co-ordinates and, do I need to know which way it has been rotated so that I can tell up from down?
Try to understand the difference between Scene and Screen. These are 2 independent constructs in iPhone development.
When creating a SpriteKit game in XCode 11 (Others may vary), the default Scene size is 750x1334. This is the "Standard" size now.
What this means is on a standard size iPhone device, you will get pixel perfect graphics, everything else will either scale or resize the scene to fit the screen.
There are 4 modes to accomplish this, .aspectFill, .aspectFit, .fill, .resize and are set via scaleMode. I am not going to go into all of these, you can look it up, but the default in the template is .aspectFill.
What .aspectFill means is preserve the aspect ratio and scale the image until all edges are covered, leaving no black bars. This means cropping will happen if your scene does not meet the aspect ratio of your screen.
Now when dealing with orientation, you need to find some way to handle this, because the scene will still stay the same size, and be forced to scale or resize.
So what is happening, is your scene is in an aspect ratio of 9:16, but your screen is in an aspect ratio of 16:9. this means it is going to scale your scene to a factor of 16:28.44~ to make sure that all screen edges are covered. This means 68% (19.44~/28.44~) of your scene is now getting cropped to fit the scene.
To remedy this, you have a few choices.
1) You can support only 1 orientation, and set the scene size to properly fit it.
2) You can design your scene in a square, and handle cropping on both sides.
3) You can design multiple scenes to handle aspect ratio
4) You can manually keep changing the size and work on a percentage based positioning.
Of these choices, I recommend either doing the 1 or 2

Why are SKSpriteNodes only partially rendering?

I've coded a basic layout of "cards" for level selection in a game using Swift and SpriteKit. It's basically just 6 level selection cards side by side that have a picture of the level that the user can select. To create them I am running a for-loop and placing the first one in the center of the screen, a padding, then the second one, then padding etc. Each card is an SKSpriteNode from a png image. Each card is approx a 3rd of the device wide and about a 3rd of the devices height.
I create all six of them and then created an action that moves all 6 cards left or right to select which on the player would like. The one in the center is the one that is selected.
Everything works great on iphone simulators (tested on iPhone 6, iPhone 6 plus, iPhone 7 Plus and iPhone 5... all work great). On iPad simulators the first and last card have a portion of the image that doesn't render at all. The first card has about 1/4 of it lost on the left side, the last card has about 1/4 or it lost on the far right side. I tried running it on a physical iPad as well and it has the same issue. When I run it on an iPad Pro 12.7 it gets worse... it cuts off more of the image.
If I choose only to display 5 of the 6 cards they all render great.
If I choose to shrink them down to about 1/4 of the device width and 1/4 of the device height and lessen the padding they render fine.
I tried playing with the Scene and View sizes and scales and didn't have any improvement.
I've tried using different images and there is no changes at all.
I've double checked all zPositions and found no improvement.
I've tried systematically removing all other objects in the scene and still have the problem.
I've put them on their own "layer" which is an SKEffectNode named cardNode. (it's an SKEffectNode because I choose to later blur it when an alert screen comes up in front of it). I thought that putting them onto their own layer might help but it didn't.
I've put physics bodies on the cards just to make sure that they are still "there" and the physics bodies appear in the correct places. If I click on part of the node that isn't rendered it still does behave properly as though it was still rendered in that area.
I can't figure out where to go from here to fix this. Ideally I would like to add more cards yet in the future but getting stuck on this problem.
Here is the code that I have for creating the cards.
let cardNode = SKEffectNode()
let levelCardArray: [String] = [
"BlackBoxLevelCard.png"
,"FruitLevelCard.png"
,"SportsLevelCard.png"
,"BarnLevelCard.png"
,"SeaLevelCard.png"
,"SpaceLevelCard.png"
]
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
let w10 = screenWidth * 0.10
let w40 = screenWidth * 0.40
let w50 = screenWidth * 0.50
let w60 = screenWidth * 0.60
let h50 = screenHeight * 0.50
let cardMargin = w10
let cardSize = CGSize(width: w40, height: w60)
let startPosition = CGPoint(x: w50, y: h50)
override func didMove(to view: SKView) {
let scene = levelSelectionScene(size: view.bounds.size)
scene.backgroundColor = UIColor.black
let skView = view as SKView
skView.ignoresSiblingOrder = true
cardNode.zPosition = 100
self.addChild(cardNode)
for i in 1...levelCardArray.count {
let currentArrayValue = i - 1
let cardSprite = SKSpriteNode(imageNamed: levelCardArray[currentArrayValue])
cardSprite.size = cardSize
cardSprite.position = CGPoint(x: startPosition.x + (CGFloat(currentArrayValue) * (cardSize.width + cardMargin)), y: startPosition.y)
cardSprite.zPosition = cardNode.zPosition
cardSprite.name = "levelCardObject"
cardNode.addChild(cardSprite)
}
Any help or insight would be greatly appreciated. Thanks guys!
I tried all day today to try to find a way to make this work through resizing the scenes and views and haven't had any luck with it at all.
My solution is to check for the device type and if it's an iPad I am reducing the image sizes and buffer between images until it doesn't cut them off. I don't consider this a very good solution, really just a work around until I can find a better way to do it. Thank you guys for your thoughts though. I definitely appreciate it!

Getting actual view size in Swift / IOS

Even after reading several posts in frame vs view I still cannot get this working. I open Xcode (7.3), create a new game project for IOS. On the default scene file, right after addChild, I add the following code:
print(self.view!.bounds.width)
print(self.view!.bounds.height)
print(self.frame.size.width)
print(self.frame.size.height)
print(UIScreen.mainScreen().bounds.size.width)
print(UIScreen.mainScreen().bounds.size.height)
I get following results when I run it for iPhone 6s:
667.0
375.0
1024.0
768.0
667.0
375.0
I can guess that first two numbers are Retina Pixel size at x2. I am trying to understand why frame size reports 1024x768 ?
Then I add following code to resize a simple background image to fill the screen:
self.anchorPoint = CGPointMake(0.5,0.5)
let theTexture = SKTexture(imageNamed: "intro_screen_phone")
let theSizeFromBounds = CGSizeMake(self.view!.bounds.width, self.view!.bounds.height)
let theImage = SKSpriteNode(texture: theTexture, color: SKColor.clearColor(), size: theSizeFromBounds)
I get an image smaller than the screen size. Image is displayed even smaller if I choose landscape mode.
I tried multiplying bounds width/height with two, hoping to get actual screen size but then the image gets too big. I also tried frame size which makes the image slightly bigger than the screen.
Main reason for my confusion, besides lack of knowledge is the fact that I've seen this exact example on a lesson working perfectly. Either I am missing something obvious or ?
The frame is given as 1024x768 because it is defined in points, not pixels.
If you want your scene to be the same size as your screen, before the scene is presented in your GameViewController, before:
skView.presentScene(scene)
use this:
scene.size = self.view.frame.size
which will make the scene the exact size of the screen.
Then you could easily make an image fill the scene like so:
func addBackground() {
let bgTexture = SKTexture(imageNamed: "NAME")
let bgSprite = SKSpriteNode(texture: bgTexture, color: SKColor.clearColor(), size: scene.size)
bgSprite.anchorPoint = CGPoint(x: 0, y: 0)
bgSprite.position = self.frame.origin
self.addChild(bgSprite)
}
Also, you may want to read up on the difference between a view's bounds and it's frame.

SpriteKit - Basic Positioning of a label in a background

Very new to Sprite Kit and i'm doing some reading now, but wanted to ask about something that I haven't found the best answer to yet.
I'm doing a tutorial with some code that creates a background, and then adds a label to show a score. I started changing the label code to position it on the top-left corner of the screen.
Here is the code (with my edits to the label, gameLabel):
let screenSize = UIScreen.mainScreen().bounds
let screenWidth = screenSize.width
let screenHeight = screenSize.height
let background = SKSpriteNode(imageNamed: "background")
background.position = CGPoint(x: screenWidth / 10, y: (screenHeight / 15) - 100)
background.blendMode = .Replace
background.zPosition = -1
addChild(background)
gameScore = SKLabelNode(fontNamed: "Chalkduster")
gameScore.text = "Score: 0"
gameScore.position = CGPoint(x: screenWidth / 10, y: (screenHeight / 15) - 100)
gameScore.horizontalAlignmentMode = .Left
gameScore.verticalAlignmentMode = .Top
gameScore.fontSize = 48
addChild(gameScore)
So the label is not displaying in any case, and my assumption is this:
Because i'm adding the background first, the label is getting added within the confines of the background, therefore it needs to be positioned differently, perhaps using the label size instead of the screen size?
My questions:
How can I get the label to always appear in the top left?
The author chose a hard-coded CGPoint for this background image and then said it has to be on an iPad, but i'd like to do it for iPhone 6/plus in landscape as well as iPad. Is there a way I can just make it work on both devices without having to specify a CGPoint like that? Can't it just scale and work regardless?
Thanks and apologies if these are basic questions - i'm going to do my best to continue reading on the subject..
Your question has a simple answer to it. You have forgotten and missed out somethings in your code, and I can show you these things.
In your code you set the background ZPosition to -1, the smallest number will always appear at the back. So if you set the SKLabelNode to a bigger zPosition it will always appear at the front, as maybe there may be a problem with rendering, as I have also experience like these, I fix it this way:
Before you add the LabelNode set it's property to this:
gamescore.zPosition = 0
0, In this case could just be anything bigger than the backgrounds(or the node that you want to appear at the back). So this just tells the compiler that you want the LabelNode to appear at the front, or in front of the Background.
If you want to make a universal app or game, with SpriteKit you will need to add some extra code to your game or app. Since I think that it is better to give you a good tutorial to show you instead of showing you some basics, I will give you a good link on how to do this:
SpriteKit: how to make a universal app
I hope this helps, and don't worry this took me some time to figure out my self.

Obtaining width and height of an SKScene using Swift

I am developing a little game using Xcode 6.0.1 for iOS 8. I couldn't find anything useful on the web and I tried everything. I need an explanation to why I am getting 0.000 instead of the width and height in pixels of the device I'm developing for.
To let you know what I did:
used self.frame.size.width, self.size.width and many more.
override func didMoveToView(view: SKView) {
let testImage:SKSpriteNode = SKSpriteNode(imageNamed: "GamePreview3.png")
testImage.position = CGPointMake(self.size.width/2, self.size.height/2)
self.addChild(testImage)
NSLog("Width: %f", CGRectGetWidth(self.frame))
NSLog("Height: %f", CGRectGetHeight(self.frame))
}
Width: 0.000000
Height: 0.000000
Is it because now with the new Xcode engine the screen size doesn't matter anymore?
I believe the frame of the scene is always 0-sized because it has no particular "frame" (its content is not limited to a virtual frame). You should use the size property of the scene instead because that'll give you the size the scene was initialized with, which is typically (but doesn't have to be) the SKView's size.

Resources