Universal game position - ios

Explanation
I'm building a game in a iPhone 5s, but now I want to make it universal, so it can run in all iPhones (or at least 4s ahead) and all iPads (or at least iPad 2 ahead).
By now, I pretty much created those 3 images (1x, 2x and 3x). So there's a 50x50 square (#1x), a 100x100 square (#2x) and a 150x150 square (#3x).
This is the sample project (download here) I'm testing in:
import SpriteKit
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
scene!.scaleMode = SKSceneScaleMode.ResizeFill //usually at GameViewController, not GameScene
let square = SKSpriteNode(imageNamed: "square")
square.anchorPoint = CGPointZero
square.position = CGPoint(x: self.frame.width / 1.095, y: self.frame.height / 1.1875) //superior right on iPhone 5/5s
addChild(square)
}
}
and these are the images:
Testing
When I run on each device, this is happens:
iPhone 4s - wrong position
iPhone 5/5s - right position (it was set up here)
iPhone 6/6s - wrong position
iPhone 6+/6s+ - wrong position
iPad 2 - wrong position
iPad Air/iPad Air 2 - wrong position
iPad Pro - wrong position
iPad Retina - wrong position
You can see better what happens by clicking on the image below.
Question
Basically, my question is: how can I make this universal? I mean, how can I make the square be positioned at the same relative place on the devices above?
Attempts
Michael Austin's attempt (download here)
import SpriteKit
// MARK: Screen Dimensions
let screenWidth = CGFloat(1024)
let screenHeight = CGFloat(768)
// MARK: Node Sizes
let square = SKSpriteNode(imageNamed: "square")
let nodeConstantWidth = screenWidth/square.size.width * 0.088
let nodeConstantHeight = screenHeight/square.size.height * 0.158
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
scene!.scaleMode = SKSceneScaleMode.Fill //usually at GameViewController, not GameScene
square.xScale = nodeConstantWidth
square.yScale = nodeConstantHeight
square.anchorPoint = CGPointZero
square.position = CGPoint(x: screenWidth / 1.5, y: screenHeight / 1.5)
addChild(square)
}
}
Timmy Sorensen's attempt (download here)
import SpriteKit
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
scene!.scaleMode = SKSceneScaleMode.ResizeFill //usually at GameViewController, not GameScene
let square = SKSpriteNode(imageNamed: "square")
square.anchorPoint = CGPointZero
square.position = CGPoint(x: self.frame.width / 1.095, y: self.frame.height / 1.1875) //superior right on iPhone 5/5s
addChild(square)
}
}
the square isn't positioning at the same place on every device, only on the device where position was set up. I made a little comparison below.

Try mathematically setting the size. Use UIScreen.mainScreen().bounds.width and UIScreen.mainScreen().bounds.height to find the device size. Use a little math to set up a proportion between your .sks scene and the device's size. Then use node.setScale to set the scale of your Sprite
Here is some sample code. Declare these as universal constants:
// MARK: Screen Dimensions
let screenSize = UIScreen.mainScreen().bounds
let screenWidth = screenSize.size.width
let screenHeight = screenSize.size.height
// MARK: Node Sizes
let node = SKSpriteNode(imageNamed: "image")
let nodeConstantWidth = screenWidth/node.size.width*0.3
If you were to move the declaration of nodeConstantWidth to GameScene, It will continually scale itself each time the scene is rendered. To avoid this, we just declare it once universally. Mathematically, the equation above will set the node's width to 30% of the screen's width on any device, and the height will be calculated accordingly. Change the '0.3' to any number to adjust this. Then in GameScene:
node.setScale(nodeConstantWidth)

This is how I placed my objects
scoreLbl.position = CGPoint(x: scene!.frame.width / 2, y: scene!.frame.height / 1.3)
airDefense.positon = CGPoint(x: frame.size.width / 2, y: frame.size.height / 5
This takes the screen and divides it out so its always in the same place no matter how big the screen is.
It is the same as using
CGPoint(x: self.frame.width / 2, y: self.frame.height / 2)
to place something in the middle of the screen.

Related

Resizing screen for iPhone X using SpriteKit

So, there are tens of questions on Stack Overflow referencing about fitting a SpriteKit scene properly on the iPhone X. I could not find anyone however, where there is the same gap.
Is there anyway I can resize the screen just for the iPhone X? Or do I have to create another scene, JUST for the iPhone X to fit the screen?
Here is what I am getting using .aspectFit:
I want to extend that scene towards just under the notch and just above where the screen corner curves are. I see many games which fit perfectly, but have not found a suitable solution.
Note: I cannot use aspectFill as some parts of the game go out of view.
Any links or answers will be greatly appreciated!
EDIT
resizeFill is too big, everything gets enlarged
I have tried all 4 options for sizing the screen, but none did the job. Anyone got suggestions?
I feel like the solution is going to be done pragmatically because it will need to be relative to those borders.
Add this to your code it will set the Playable Game Area for both the iPhone X and the regular iPhones and then use the gameArea constant to define how far things can move.
//MARK: Playable Game Araa
let gameArea: CGRect
override init(size: CGSize) {
if UIDevice().userInterfaceIdiom == .phone && UIScreen.main.nativeBounds.height == 2436 {
//iPhone X
let maxAspectRatio: CGFloat = 19.5/9.0
let playableWidth = size.height / maxAspectRatio
let margin = (size.width - playableWidth) / 2
gameArea = CGRect(x: margin, y: 0, width: playableWidth, height: size.height)
} else {
let maxAspectRatio: CGFloat = 16.0/9.0
let playableWidth = size.height / maxAspectRatio
let margin = (size.width - playableWidth) / 2
gameArea = CGRect(x: margin, y: 0, width: playableWidth, height: size.height)
}
super.init(size: size)
}
We (me and snapbackdev) found a solution, suitable for any phones now or any coming in the future to set the screen to fit the whole view, without stretching or empty spaces.
We ended up doing everything programmatically, which made everything work.
In GameViewController.swift, this code loads the GameScene:
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
// Load a GameScene with the size of the view
let scene = GameScene(size: view.frame.size)
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
// Present the scene
view.presentScene(scene)
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
This may also be done in viewDidLayoutSubviews instead, depending on what you are doing.
The only code needed to setup the scene in GameScene.swift is:
override func didMove(to view: SKView) {
// Set (0, 0) as the centre of the screen
scene?.anchorPoint = CGPoint(x: 0.5, y: 0.5)
// Create the frame
let frameEdge = SKPhysicsBody(edgeLoopFrom: frame)
self.physicsBody = frameEdge
}

SpriteKit Sprites Not Scaling Properly

The sprites in my SpriteKit game, though properly scaled, are being rendered extremely small in my scenes. I've been following a few different tutorials using similarly sized sprites and theirs are scaled fine in the simulator. Below are some code snippets and screenshots. For example, the pause button on the top right of the screen is crazy small, yet the actual resolutions of the assets are standard in size.
GameScene.swift
private let pauseTex = SKTexture(imageNamed: "PauseButton")
...
private func setupPause() {
pauseBtn.texture = pauseTex
pauseBtn.size = pauseTex.size()
pauseBtn.anchorPoint = CGPoint(x: 1.0, y: 1.0)
pauseBtn.position = CGPoint(x: size.width, y: size.height)
pauseBtn.zPosition = Layer.Foreground.rawValue
worldNode.addChild(pauseBtn)Btn)
...
}
GameVC.swift
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
...
if let skView = self.view as? SKView {
if skView.scene == nil {
let aspectRatio = view.bounds.size.height / view.bounds.size.width
let scene = MenuScene(size: CGSize(width: 750, height: 750 * aspectRatio))
scene.scaleMode = .aspectFill
skView.ignoresSiblingOrder = true
...
}
}
}
Basically what has happened here is Mike got confused with scene size vs screen size. He was thinking that since the sprite was 32 pixels, it should be taking up 10% of the iPhone SE Screen since it has a 1x width of 320 (Natively it is 640 [2x]) But in reality, his scene is 750 pixels wide, so it was only showing at 5%.
He switched his scene size to be 375x667 (iPhone 6 non retina resolution) to properly use the retina graphics, and now everything should be aligning up for him.
The only issue he is going to come across with now, is cropping on iPad devices. He just needs to work in safe zones for this.
The size of your assets will not always reflect the desired size on-screen due to resolution discrepancies between different devices (this is true if the SKScene's scaleMode is set to resizeFill). If you want a specific size for your sprite, you need to set the SKNode's size property to your desired value.
What this would look like in your method:
private func setupPause() {
pauseBtn.anchorPoint = CGPoint(x: 1.0, y: 1.0)
// Set size property (arbitrarily selected below)
pauseBtn.size = CGSize(width: 50, height: 50)
pauseBtn.position = CGPoint(x: size.width, y: size.height)
pauseBtn.zPosition = Layer.Foreground.rawValue
worldNode.addChild(pauseBtn)
}

SKSpriteNode not scalling properly

I'm developing a small game using Swift & SpriteKit. When I add a SKSpriteNode for Restart button, it doesn't scale properly.
The size of Restart button is 100px in height and width. If I don't set scale , it covers the whole screen and make the screen appear white. I figured out that if I setScale to 0.005 only than it appears on screen, but not in proper size.
import Foundation
import SpriteKit
class EndScene: SKScene {
var restartBtn = SKSpriteNode()
override func didMoveToView(view: SKView) {
background()
restartGame()
}
func restartGame() {
restartBtn = SKSpriteNode(imageNamed: "restartBtn")
restartBtn.setScale(0.005)
restartBtn.position = CGPoint(x: self.size.width / 2, y: self.size.height / 4)
restartBtn.zPosition = 1
self.addChild(restartBtn)
}
func background() {
let bkg = SKSpriteNode(imageNamed: "Background")
bkg.size = self.frame.size
bkg.position = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2)
bkg.zPosition = -2
self.addChild(bkg)
}
}
Here is the output of this code,
Restart Button Output
UPDATE
I put scene!.scaleMode = .AspectFill right inside didMoveToView function and it helped in rendering the shape of the SpriteNode properly. But still I have to setScale(0.001) to make the size of Restart button fit in the screen. Can anyone assist me what line of code I'm still missing?
Instead of using .setScale try using restartBtn.size = CGSize(Width: 50, Height: 50)
This uses the Sprite Kit's resize formula.
I ran into this as well. It happened when I forgot to specify the size of the scene. Instead of calling initWithSize I just wrote init and the scene started to scale down all nodes by x1000. Nodes were visible, but they required a scale of 0.001

iPhone 4, 5, 5s and 6+ simulators misaligning nodes

Apologies if I've already asked this, but Im very stuck; the first question was very unclear. Created a game in SpriteKit on the iPhone 6 simulator and when run the other sized simulators, everything is thrown out of alignment. Im not using any .sks, .xib or storyboarding. Is there a way to programmatically resize the game so that all screen sizes have the correct positioning? Will post code if necessary.
Edit: GameViewController:
import UIKit
import SpriteKit
var sceneSize = UIScreen.mainScreen().bounds
var screenWidth = sceneSize.width
var screenHeight = sceneSize.height
class GameViewController: UIViewController {
var scene: GameScene!
override func viewDidLayoutSubviews() {
super.viewWillLayoutSubviews()
let skView = self.view as! SKView
skView.multipleTouchEnabled = false
scene = GameScene(size: skView.bounds.size)
//scene.scaleMode = .AspectFill
scene.scaleMode = SKSceneScaleMode.ResizeFill
skView.presentScene(scene)
scene.anchorPoint = CGPointMake(0, 0)
skView.showsFPS = false
skView.showsNodeCount = false
skView.ignoresSiblingOrder = true
let completionBlock: () -> Void = {
}
let errorBlock: (NSError!) -> Void = { error in
print("error")
}
RevMobAds.startSessionWithAppID("", withSuccessHandler: completionBlock, andFailHandler: errorBlock);
}
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return .AllButUpsideDown
} else {
return .All
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
Edit 2 GameScene:
import Foundation
import SpriteKit
import UIKit
var sceneSize = UIScreen.mainScreen().bounds
var screenWidth = sceneSize.width
var screenHeight = sceneSize.height
class GameScene: SKScene {
player.position = CGPointMake(screenWidth / 5, screenHeight / 3)
}
First, set the GameScene SKSceneScalode (this is found pre-programmed into the GameViewController to .ResizeFill
Then copy the following code into GameScene:
Import UIKit
var screenSize = UIScreen.mainscreen().bounds
var screenWidth = screenSize.width
var screenHeight = screenSize.height
This will store the width and height of the screen in the variables screenWidth and screenHeight. Now when you create your spriteNodes and other objects, use these two variables as your basis. Here's an example
someNode.position = CGPoint(x: screenWidth/5 y: screenHeight/3)
Are you the same person that posted on the Swift section of Reddit? Here's my code that works for all iPhone devices, as I answered it well on the Reddit post the same way. Here's the more profound answer.
In your View Controller, enter the following code inside the viewDidLoad().
//Create a scene object with the max size of an iPhone 6 Plus at 1920 x 1080
//That means it can easily scale down to the 6, 5, and 4S.
let mainScene = GameScene(size: CGSize(width: 1920, height: 1080))
//To fill the scene and fit for all sizes
mainScene.scaleMode = .AspectFill
//Prepare the view
let mainView = self.view as! SKView
mainView.showsFPS = true
mainView.showsNodeCount = true
mainView.ignoresSiblingOrder = true
//Move to the required scene
mainView.presentScene(mainScene, transition: SKTransition.fadeWithDuration(1))
In the class scene, add the following variables at the top.
//Will be used to get the ratio of the device
let deviceWidth = UIScreen.mainScreen().bounds.width
let deviceHeight = UIScreen.mainScreen().bounds.height
let maxAspectRatio: CGFloat
let playableArea: CGRect
If you don't already have an override init inside the class scene that the view is moving to, add the following inside the class.
//Overrides the superclasses' SKScene init
override init(size: CGSize) {
//Initializes the maxAspectRatio variable
maxAspectRatio = deviceHeight / deviceWidth
print("The ratio of the device is: \(maxAspectRatio)")
//Prepares the rectangle view that is viewable by the user (ONLY FOR PORTRAIT MODE)
let playableWidth = size.height / maxAspectRatio
let playableMargin = (size.width - playableWidth) / 2.0
playableArea = CGRect(x: playableMargin, y: 0, width: playableWidth, height: size.height)
/*USE THIS CODE IF YOUR APP IS IN LANDSCAPE MODE*****************************************
let playableHeight = size.width / maxAspectRatio
let playableMargin = (size.height - playableHeight) / 2.0
playableArea = CGRect(x: 0, y: playableMargin, width: size.width, height: playableHeight)
*///*************************************************************************************
//Find out what kind of device the user is using
if maxAspectRatio == (4 / 3) {
//The iPhone's 4S ratio is 4:3
print("The user is using an iPhone 4S")
} else {
//That means the ratio is not 4:3, so it probably is 16:9
print("The user is using an iPhone 5, 6, 6S, or 6 Plus.")
}
//Assigns the size of the scene to the superclass init of SKScene
//Must be called after all variables are initialed in this class
super.init(size: size)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Add the following function inside the scene class too so you can see the viewable area that is seen by the user. It draws a red rectangle. Then, just call this function inside the didMoveToView() function. Now to position objects, all you have to do is use CGRectGet... properties on the playableArea to position the objects correctly.
//Used to draw the rectangle or the view of the phone
func drawPlayableArea() {
let shape = SKShapeNode()
let path = CGPathCreateMutable()
CGPathAddRect(path, nil, playableArea)
shape.path = path
shape.strokeColor = SKColor.redColor()
shape.lineWidth = 8
addChild(shape)
}
override func didMoveToView(view: SKView) {
drawPlayableArea()
//Examples used to position objects on a screen related to the playable area or viewable area
//This works for all iPhone devices as well as iPods.
let label = SKLabelNode(fontNamed: "Arial")
label.fontColor = SKColor.whiteColor()
label.text = "Hello, Redditor!"
label.position = CGPoint(x: CGRectGetMidX(playableArea), y: CGRectGetMidY(playableArea))
addChild(label)
let score = SKLabelNode(fontNamed: "Arial")
score.verticalAlignmentMode = .Top
score.horizontalAlignmentMode = .Left
score.fontColor = SKColor.whiteColor()
score.text = "Score: 100"
score.position = CGPoint(x: CGRectGetMinX(playableArea) + 20, y: CGRectGetMaxY(playableArea) - 20)
addChild(score)
backgroundColor = SKColor.blackColor()
}
Here are the github files if you need to understand the code more.
Screenshots of positions in an 4S and a 6 Plus. Notice how the objects stay positioned relative to the scene size.
OK I had time to reread this, and I think I know what is going on, I will leave my old answer in here as well.
New Answer:
What is going on here is you are setting your scene size at the time of creation of view, before any auto constraints get applied. So your view size is not the same as your screen size. Thus, your scene becomes the same size regardless of device, then you apply the math based on your screen, and everything gets thrown off. What you need to do is 1) Verify that my theory is right, and that at the time of creating your scene, the size is not the same as screen size, then 2) create the scene at the time the view does get resized for the device.
Old Answer:
The reason everything is thrown out of alignment is because your game scene is being resized based on device. You need to set a single constant size for doing things the way you are doing
let scene = GameScene(size:CGSizeMake(400,300))
Where 400 x 300 can be changed based on your needs. this will give you a play area of fixed size to work with, so your nodes will be placed the same. Then, to handle the screen size, you set your scenes scale mode to aspect fill or aspect fit, depending on if you want to lose information, but have no black bars, or have black bars and show all information. (resize fill will distort your image by stretching if that is what you want). to do this it is just scene.scaleMode = .AspectFill
Now you will have to tweak numbers and images if you are getting undesirable results, but the same logic should be applied to all devices. If you start grabbing things like the screen size again, things will start misaligning.
Inside your scene code, use the scenes width and height to manipulate your nodes, not the screen
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
player.position = CGPointMake(scene.frame.width / 5, scene.frame.height / 3)
}
Best way to think about it is the Game scene gets treated as its own screen, and in the end you want to take this screen and scale it to meet the various device settings.

Swift & SpriteKit: Full screen image

I'm making a first scene with SpriteKit in swift. I created a background image for 4 inch iPhone with dimensions 640x1136. I placed the image as sprite in centre of the screen, but the image is not full screen, there are edges missing.
Next, I tried to resize the image within app. When I did image.size.height = self.size.height the height got properly resized along full iPhone screen.
But when I did the same with width image.size.width = self.size.width then the picture got extremely stretched width wise.
I printed the dimensions of self.size and it turns out the dimension for my iPhone 5s are 1024, 768. This is total nonsense as the screen can't be wider than its height on iPhone. The app is universal but the only orientation is set to portrait.
Edit: This is the code I use to put the image on the screen
class GameScene: SKScene
{
let menuImage = SKSpriteNode(imageNamed: "menuImage")
override func didMoveToView(view: SKView)
{
/* Setup your scene here */
menuImage.anchorPoint = CGPointMake(0.5, 0.5)
menuImage.size.height = self.size.height
menuImage.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
self.addChild(menuImage)
}
You need to add
scene.size = skView.bounds.size
or for Swift 5
scene.size = self.view.bounds.size
just before skView.presentScene(scene) in your gameViewController file
This is what worked for me to stretch the background image to fit the whole screen in Swift 5.
background.size = CGSize (width: frame.maxX, height: frame.maxY)

Resources