Dealing With Different iOS Device Resolutions in Spritekit

Dealing with different iOS device resolutions in SpriteKit

Adding the following code will fix your problem (code is in Swift):

scene.scaleMode = SKSceneScaleMode.ResizeFill

Now if you want to know why this fixes your problem, what your problem actually is, and how to handle multiple resolutions – I suggest you continue reading.

There are three things that can impact the position of nodes in your scene.

1) Anchor Point

Make sure your scene's anchor point is set to (0,0) bottom left. By default the scene's anchor point starts at (0,0) so i'm assuming that is not causing the issue.

2) Size
Check the size of your scene. I typically make my scene size match the size of the device (i.e. iPad, iPhone 4-inch, iPhone 3.5 inch), then I place another layer in the scene for storing my nodes. This makes me able to do a scrolling effect for devices with smaller resolutions, but it depends on your game of-course. My guess is that your scene size might be set to 320, 480 which could be causing the positioning problems on your iPhone 5s.

3) Scale Mode
The scale mode has a huge effect on the positioning of nodes in your scene. Make sure you set the scale mode to something that makes sense for your game. The scale mode kicks in when your scene size does not match the size of the view. So the purpose of the scale mode is to let Sprite Kit know how to deal with this situation. My guess is that you have the scene size set to 320,480 and the scene is being scaled to match the iPhone 5 view which will cause positioning problems identical to what you described. Below are the various scale modes you can set for your scene.

SKSceneScaleMode.AspectFill

The scaling factor of each dimension is calculated and the larger of
the two is chosen. Each axis of the scene is scaled by the same
scaling factor. This guarantees that the entire area of the view is
filled, but may cause parts of the scene to be cropped.


SKSceneScaleMode.AspectFit

The scaling factor of each dimension is calculated and the smaller of
the two is chosen. Each axis of the scene is scaled by the same
scaling factor. This guarantees that the entire scene is visible, but
may require letterboxing in the view.


SKSceneScaleMode.Fill

Each axis of the scene is scaled independently so that each axis in
the scene exactly maps to the length of that axis in the view.


SKSceneScaleMode.ResizeFill

The scene is not scaled to match the view. Instead, the scene is
automatically resized so that its dimensions always matches those of
the view.


Conclusion

It looks like you want to remove the scaling of your scene, that way your positions in the scene will match the actual positions in the view. You can either set your scene's size to match the view size, in which case no scaling will take place. Or you can set your scene's scale mode to ResizeFill which will always make the scene's size match your view's size and it won't scale anything. In general I would stay away from any scaling and instead adjust the interface and the scene size to best suit each device. You may also want to add zoom and/or scrolling to allow devices with smaller resolutions to achieve the same view field.


But what if I want to scale my scene?

If however you need to scale your scene, but you still want positions to be relative to the view (i.e. You want (0,0) to be the bottom left of screen even when scene is cutoff) then see my answer here


Additional Info

See answer here for sample code showing how I layout nodes dynamically.

See answer here for more details about scaling to support multiple devices.

SpriteKit Physics Inconsistent On Different Screen Resolution

Several factors need to be considered in your question, including the different resolution of different devices, the gravity and the momentum. Why momentum? Because in applyImpulse:(CGVector)impulse, impulse is a vector describes how much momentum is added. Therefore, simply multiplies a scale factor on PLAYER_JUMP_HEIGHT is no use.

Thanks to this answer, we can use a ratio named 'pixel to unit' to calculate how much dy needed to make the impulse vector.

The following is to explain how to make a player jump for constant PLAYER_JUMP_HEIGHT. You may want to adjust it according to different resolution yourself and this time your scale factor should work.

#define kScale ([[UIScreen mainScreen] bounds].size.height) / 1024.0
const CGFloat PLAYER_JUMP_HEIGHT = 200.0;

- (void)didMoveToView:(SKView *)view
{
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];

// Print useful info
NSLog(@"scale: %.3f", kScale);
NSLog(@"total height: %.1f", self.frame.size.height);
NSLog(@"jump height: %.1f%%", kScale * PLAYER_JUMP_HEIGHT / self.frame.size.height * 100);

// Let player jump over the bar
SKSpriteNode *bar = [SKSpriteNode spriteNodeWithColor:[UIColor redColor] size:CGSizeMake(CGRectGetMaxY(self.frame), 1.0)];
bar.position = CGPointMake(CGRectGetMidX(self.frame), PLAYER_JUMP_HEIGHT * kScale);
[self addChild:bar];

self.player = [SKSpriteNode spriteNodeWithColor:[UIColor greenColor] size:CGSizeMake(kScale * 50.0, kScale * 50.0)];
self.player.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake(kScale * 50.0, kScale * 50.0)];
self.player.physicsBody.mass = 0.25;
self.player.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)/5.0);
[self addChild:self.player];

// Pixel to unit
ptu = 1.0 / sqrt([SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake(1.0, 1.0)].mass);
}

In the code above, we firstly set the const PLAYER_JUMP_HEIGHT, a bar indicated how high is 200 points on screen, a player and the ratio ptu.

Then calculate dy and apply it to the impulse:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// reset the velocity each jump, so that a constant impulse is always applied
self.player.physicsBody.velocity = CGVectorMake(0, 0);
self.physicsWorld.gravity = CGVectorMake(0, kScale * (-9.8));

// jump the player up
CGFloat dy = self.player.physicsBody.mass * sqrt(2 * (-self.physicsWorld.gravity.dy) * (kScale * PLAYER_JUMP_HEIGHT * ptu));

[self.player.physicsBody applyImpulse:CGVectorMake(0, dy)];
}

Here's the sample project (outdated) for demo, run and play it. You will find the box sprite doesn't actually jump over the red bar but a little lower. I think the reason is the gravity.

Code updated in several places:

  1. Define kScale macro for different devices for quick test. Works only in portrait orientation.

  2. Add kScale where needed (jump height, player size, and gravity!). It now should jump adaptive height at the same velocity.

Resizing screen for iPhone X using SpriteKit

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

}

iOS Universal Device App with SpriteKit, how to scale nodes for all views?

I initially tried to do the scaling myself with 2 games and it was just madness (scene size = view size or scene scale mode = .ResizeFill). You have to adjust all values e.g font size, sprite sizes, impulses etc for all devices and it will never be consistent.

So you have 2 options basically

1) Set scene size to 1024X768 (landscape) or 768x1024 (portrait). This was the default setting in Xcode 7.

You than usually just have/show some extra background at the top/bottom (landscape) or left/right (portrait) on iPads which gets cropped on iPhones.

Examples of games that show more on iPads / crop on iPhones:

Altos Adventure, Leos Fortune, Limbo, The Line Zen, Modern Combat 5.

2) Apple changed the default scene size in xCode 8 to iPhone 6/7 (7501334-Portait, 1337750-Landscape). This setting will crop your game on iPads.

Examples of games that show less on iPads:

Lumino City, Robot Unicorn Attack

Chosing between the 2 options is up to you and depends what game you are making. I usually prefer to use option 1 and show more background on iPads.

Regardless of scene size scale mode is usually best left at the default setting of .aspectFill.

To adjust specific things such as labels etc you can do it this way

 if UIDevice.current.userInterfaceIdiom == .pad {
...
}

You can try the scene scaling yourself, create a new SpriteKit sample game project. Run on all iPhones and you will notice the HelloWorld label looks perfect on all devices.

Now change the default settings to scene size = frame or use .ResizeFill, the HelloWorld label is not scaled properly anymore on all devices.

As a side note, the line

 if let scene = GameScene(fileNamed: "GameScene")

references the GameScene.sks file. You said you do everything programatically, therefore you can probably delete the GameScene.sks file and change the line to

 let skView = view as! SKView!
let scene = GameScene(size: CGSize(width: 1024, height: 768)) // 768 x 1024 if portrait

Update:

I am now using a slightly different variant as I had problems adapting my game to iPhoneX. I set scene size to 1334x750 and use aspect fit as scale mode. I than run some code to remove the black bars if needed e.g. on iPads or iPhone X. It’s based on this great article (link no longer works).

http://endlesswavesoftware.com/blog/spritekit-skscene-scalemode/

Displaying Images in iOS application

You wouldn't use SpriteKit just to display images. You would load the images as UIImage and then create a UIImageView that you can place on the screen wherever and however you want and then you just assign the UIImageView your UIImage. UIImageView has a lot of properties you can set how images are displayed (e.g. if they are scaled and how they are scaled or if they are not scaled, how they shall be aligned within the viewable area, and so on). You can draw a UIImageView on top of a SpriteKit scene, that is no problem on iOS (on iOS everything is drawn by OpenGL ES or Metal anyway).

Of course you can also embed any image as a sprite if you like:

UIImages * img = ...;
SKTexture * tex = [SKTexture textureWithImage:img];
SKSpriteNode * sprite = [[SKSpriteNode alloc] initWithTexture:tex];
// If you don't use ARC, I'd add the following below:
// [sprite autorelease];

Now you can perfectly integrate it into the scene in whatever way you like an perfectly align it will all your other sprites. Yet if you just want to paint an image over the scene:

SKScene * scene = ...;
SKView * sceneView = scene.view;

UIImageView * imgView = [[UIImageView alloc] init];
imgView.image = img;
// Whatever content mode you prefer
imgView.contentMode = UIViewContentModeScaleAspectFit;
// Where shall it be placed and how big shall it be.
imgView.frame = CGRectMake(posX, posY, width, height);
// If you'd use this, it will cover the whole scene:
// imgView.frame = sceneView.frame;

// Add it on top of your scene
[[sceneView parent] addSubview:imgView];

// If you don't use ARC, don't forget to release it:
// [imgView release];

If you load an UIImage from your application bundle with [UIImage imageNamed:@"blah"] and the image exists in different resolutions for retina devices (blah.png, blah@2.png, blah@3.png), the system will automatically load the image it considers most suitable for the screen of the current device. This is nothing you have to deal with.

If you need to convert between scene coordinates and view coordinates, SKScene offers -convertPointFromView: and -convertPointToView: for this case. If the scene fills the whole screen, then these actually convert between scene and screen coordinates.

Even if devices have different resolutions, your scene can always have the same "virtual size". So you can always say that the scene is 400x300, no matter what the real screen resolution is. In that case placing a sprite of virtual dimension 200x150 at the virtual coordinates (100,75) will always center it on the screen, no matter what device or how big the screen really is (well, assuming that the SKSceneView really covers exactly the whole screne, of course). The size of a SKScene is just the coordinate system you want to have for layouting your game, it can be whatever you want it to be, it can be bigger or smaller than the real screen.

The scene is always drawn into a SKSceneView. The size of the SKSceneView is the real size of your scene in screen coordinates. So if you SKScene is 480x320 and the size of the SKSceneView is 1440x960, then moving a sprite one pixel in your scene will in fact move it 3 pixels on the screen. Yet if your SKScene is 1136x640, but your SKSceneView is only 586x320, then moving a sprite two pixels in your scene will only move it one pixel on screen. Your scene is always scaled up or down as required.

Personally I'd either stick with the same size across all devices or maybe just make two or three device classes but not adopt the game for every single device and every existing screen resolution.



Related Topics



Leave a reply



Submit