SpriteKit universal app using SKScene scaleMode

Writing a SpriteKit game as a universal app that runs on all iPhones and iPads can be challenging due to all the different screen sizes and aspect ratios. Fortunately SpriteKit provides a scaleMode property on SKScene objects to help solve this problem. But even using scaleMode still requires planning and thought upfront to get things setup correctly. It is also important to do this planning early in your project as it will have an impact on your game’s coordinate system and art asset size. In this post I will share the approach I used in my latest SpriteKit game.

My latest SpriteKit game is a landscape only game and the approach I describe here is designed for landscape only. However the same approach could easily be adapted for portrait only games. Games that work both in landscape and portrait orientations would require additional work that I wont be covering here.

When setting up the scene configuration for my game I wanted the following characteristics:

  1. Have a single scene that can scale to fit any screen size. I don’t want separate scenes or different layouts for different screen sizes. Instead I want the scene content to scale to fit the screen.
  2. When the scene scales to fit the screen it should not stretch or distort, i.e. it must scale at the same aspect ratio. We don’t want to distort our artwork.
  3. Have a consistent coordinate system regardless of the screen size. The upper right corner of the scene should be the same coordinate regardless of the screen size.
  4. Want keep things as simple as possible. I want to avoid having to use point conversion routines and image scaling routines all over the code.

Understanding SKScene size and scaleMode

The key to understanding how to setup a scene to support different device resolutions is understanding how the SKScene size and scaleMode properties work. The scene’s size defines its visible area. It essentially creates the coordinate system for your scene. The scene’s visible coordinate space is (0,0) to (width,height), with the origin (0,0) in the lower left corner and the upper right corner at (width,height). After SpriiteKit renders a scene it copies its contents to the SKView to be displayed. If the scene is not the same size as the view, the scene will be scaled to fit the view. The scaleMode property determines how the scene is scaled to fit the view. There are four different values for the scaleMode property: .resizeFill, .fill, .aspectFill and .aspectFit.

.resizeFill – When scaleMode is set to .resizeFill, the system will change your SKScene size to exactly match the size of its parent view. This creates an inconsistent coordinate system for the scene. Each different screen size has a different coordinate system. So on an iPhone 4s the scene would be 320 pts high and 480 pts wide. But on an iPad Air the scene would be 768 pts high and 1024 pts wide. Placing a 10×10 sprite at location 512,100 would be horizontally centered on the iPad Air, but would be off the screen for the 4S.

.fill – When the scene scaleMode is set to .fill, the scene is stretched to fill the view but the scene’s size property does not change. This gives a consistent coordinate system and scaling we desire. Unfortunately this scaleMode does not honor the aspect ratio of the scene so the scene content will appear to be distorted on different devices. On iPads the content may appear stretched vertically and on iPhones it may appear to be stretched horizontally.

.aspectFill – The .aspectFill scale mode is the default scaleMode used by Xcode when it creates a new SpriteKit project. Using .aspectFill works like .fill in that it fills the view with the scene and does not change the scene’s size property. The difference is that it honors the aspect ratio of the scene and does not distort the scene content. However, it will crop any content that does not fit after scaling to fill the view. This means that content that is displayed on the left and right edges of an iPhone 5,6 or 7 would be cropped when displayed on an iPad.

.aspectFit.aspectFit works similar to .aspectFill but it uses letterboxing instead of cropping if the scene size is not the same aspect ratio as the view size. This mode gives us the desired consistent coordinate system and scaling we want, and does not crop content so that all of our content is always displayed regardless of the device size. However it does produce ugly letterboxing.

Configuring SKScene

The different scale modes allow for many different strategies to support universal apps. The approach I prefer is to use scaleMode set to .aspectFit and use a scene size that has the same aspect ratio as the iPhone 5, 6 and 7. In this example I use a scene size of 1136 x 640. Using this approach the scene content scales to fill the view while honoring the aspect ratio and with no clipping. So the right edge of the scene is always at 1136 and the top is always 640. For the iPhone 5, 6 and 7 the scene content scales to fit the screen perfectly and there is no letter boxing. However on iPads and the 4S it does cause letterboxing at the top and bottom of the screen. But I will show you later how to deal with letterboxing and make use of that space so that everything looks good on all devices.

To set the scene size open the scene sks file and set its size in the Attributes Inspector:

Setting SKScene size

If you dont use an sks file to create your scene, you can pass the size in scene initializer:

The scalingMode for the game’s initial scene is set in the viewDidLoad method the GameViewController, where the initial scene is created:

Here are some screen shots showing the scene running on an iPhone 6 and and iPad Air. I added a background image that shows the coordinate system.

iPhone 6

iPhone 6

iPad

iPad Air

Fixing the letter boxing

The letter boxing on the top and bottom of the iPad version does not look very good. It would look much better if we could extend the background image to fill the entire screen. To do that we need to extend the top and bottom of the scene so that it fits the screen, and also adjust the anchor point of the scene so that the origin does not move. The best place to do that is in the viewDidLayoutSubviews method of the GameViewController:

We also need to increase the height of our background image so that it can extend into the areas that were previously letter-boxed. For a width of 1136, the background image needs to be 852 pixels high to fill the screen on the iPad. With the new taller background image we now have a single background image that can be used on all screen sizes. It will be cropped on the top and bottom on the iPhone.

iPhone 6

iPhone 6

iPad Air

iPad Air

Resetting the scene height and origin in this way creates a “safe area” in the middle of the screen. Any content placed inside of the safe area will be visible on all screen types.

Using the full screen

Things are looking much better now, no more ugly letterboxing. However we now have empty unused space where the letterboxing used to be. To make things look better it would be nice to move some the game elements into that empty space. For example most of my games show the score and other status info along the top of the screen. For these types of items we want to position them relative to the top of the screen instead of using static coordinates. Similarly games often have game controls and action buttons that run along the bottom of the screen. We want to position those items relative to the bottom of the screen.

I created a simple SKScene extension that makes it easy to position items relative to the top or bottom of the screen. The positionFromTop method takes a point where the y coordinate is the distance from the top of the screen and returns that location in the scene coordinate space. For example, the following code positions the score label 25 pts from the top and 30 pts from the left:

Similarly we can position a pause button 45 pts from the bottom and near the right edge of the screen as follows:

Here are updated screen shots that include the score text and pause button.

iPhone 6

iPhone 6

iPad Air

iPad Air

Conclusion

We now have a scene setup that gives us a consistent coordinate system on all devices and correctly scales the game content for different screen sizes. It also allows us to position elements relative to the top and bottom of the screen, taking advantage of the extra space on the iPad. We have meet all our original goals, while keeping things relatively simple.

The code for the completed sample project, including the SKScene extension for positionFromTop and positionFromBottom, can be found at  https://github.com/tkier/SpriteKitSceneSize

Hopefully you find it useful. I am curious to hear other approaches to solve this problem. Feel free to leave a comment and share your solution.

This entry was posted in Game Programming and tagged , .

Leave a Reply

Your email address will not be published. Required fields are marked *