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

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.

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)
}

Universal game position

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.

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

swift background color + button/image bug

I have just encountered a really odd bug when testing my app.
import Foundation
import SpriteKit
import AVFoundation
class EndScene : SKScene {
var Highscore : Int!
var ScoreLabel : UILabel!
var HighscoreLabel : UILabel!
var AudioPlayer = AVAudioPlayer()
override func didMoveToView(view: SKView) {
scene?.backgroundColor = UIColor.grayColor()
var EndGameMusic = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("endSong", ofType: "wav")!)
AudioPlayer = AVAudioPlayer(contentsOfURL: EndGameMusic, error: nil)
AudioPlayer.prepareToPlay()
AudioPlayer.play()
var scoreDefault = NSUserDefaults.standardUserDefaults()
var Score = scoreDefault.valueForKey("Score") as! NSInteger
var HighscoreDefault = NSUserDefaults.standardUserDefaults()
Highscore = HighscoreDefault.valueForKey("Highscore") as! NSInteger
NSLog("\(Highscore)")
ScoreLabel = UILabel(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 30))
ScoreLabel.center = CGPoint(x: view.frame.size.width, y: view.frame.size.width/4)
ScoreLabel.text = "Last Game:\(Score)"
self.view?.addSubview(ScoreLabel)
HighscoreLabel = UILabel(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 30))
HighscoreLabel.center = CGPoint(x: view.frame.size.width, y: view.frame.size.width/2)
HighscoreLabel.text = "Highscore: \(Highscore)"
self.view?.addSubview(HighscoreLabel)
let RestartBtn = SKSpriteNode(imageNamed: "RestartBtn")
RestartBtn.position = CGPointMake(size.width/2, size.height/2)
RestartBtn.name = "Restart"
addChild(RestartBtn)
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch: AnyObject in touches {
let touchLocation = touch.locationInNode(self)
let touchedNode = self.nodeAtPoint(touchLocation)
if(touchedNode.name == "Restart"){
Restart()
}
}
}
func Restart(){
self.view?.presentScene(InGame(), transition: SKTransition.crossFadeWithDuration(0.3))
HighscoreLabel.removeFromSuperview()
ScoreLabel.removeFromSuperview()
}
}
what happens is that when this scene is called, the two labels, highscore and score appear, but my background goes a yellow color and my image i'm using as a button, isn't present, yet if i touch anywhere on the screen (regardless of bounds, I've tried) it will activate the button and exit.
if i remove this code:
let RestartBtn = SKSpriteNode(imageNamed: "RestartBtn")
RestartBtn.position = CGPointMake(size.width/2, size.height/2)
RestartBtn.name = "Restart"
addChild(RestartBtn)
Then the bug doesn't occur and the background sets normally, so i know there is an issue with this code, and assumed that for some reason it was zoomed in on my image (cant post without 10 reputation...)
However, after all attempts at resizing and fixing this issue, nothing actually helped, but i still cant help thinking that this has a relatively simple solution, its just that this code was used in my previous scenes without any issue.
Any Suggestions?
EDIT: i am certain that its taken the color of my image and used that as the background, and made the entire background the restart button, i just tried it with another image styled differently.
EDIT 2: image size = 200x80, the image i used in my first edit i scaled down to 40x80
This is not a bug, so basically the way SKSpriteNode's work is they take on the size of your image file since you did not explicitly set the size anywhere else. From the code you have, I'm guessing your image is over 1000 by 1000 pixels, so it is taking up the entire screen. The iPhone 6's size for example, is something like 667 by 375 pixels within xCode so in a sense your restart button is actually larger than the screen. To fix this, set the xScale and yScale values low and see what works(makes the size of the sprite smaller)
RestartBtn.xScale = 0.01
RestartBtn.yScale = 0.01
That would make the button 100x smaller(or 1/100 the previous size), just play around with the values until you get a size you like.
Edit: So I had this issue before as well in spriteKit. For some reasons the size of the screen gets messed up unless you set its size where you declare the scene.
(What you called your screen).size = skView.bounds.size
For example if you did like
let skView = self.view as SKView!
myScene = GameScene()
myScene.size = skView.bounds.size
skView.presentScene(myScene)

Resources