66
\$\begingroup\$

I've gotten a lot of answers to this question, but they are all generic and generally not very useful. None of the tutorials talk about aspect ratio and dealing with mobile devices and there are a zillion ways to do it, all seem to have gotcha's and flaws.

I really would love to know what successful games have used to handle different aspect ratios on iOS and Android without making a zillion different sized assets.

I am strictly speaking mobile, not desktop, specifically with Unity and I don't care about the UI, I only care about the gameplay canvas.

Issues I have in mind is when there are key things that have to be in certain places and cannot fall off the screen. Using black bars on top or bottom is unacceptable these days.

\$\endgroup\$
  • 3
    \$\begingroup\$ This question is very broad, as the right way depends on almost anything. What have you tried? Why did it not work? \$\endgroup\$ – Anko Jun 30 '14 at 20:05
  • 3
    \$\begingroup\$ I've tried all sorts of things, I've tried adjusting ortho camera size, I've tried attaching all sprites to list and scaling them by difference in aspect ratio, setting ortho size to screen.height/2/100, many other ideas. Some work, but all of them have issues. I know different games handle it different but there is absolutely no discussion of this topic anywhere and it isn't as easy as "just letting unity handle it" as many claim. \$\endgroup\$ – Michael Jun 30 '14 at 20:34
  • 1
    \$\begingroup\$ So, why did they not work? What would a good solution look like? (By the way, you can edit the question to clarify too.) \$\endgroup\$ – Anko Jun 30 '14 at 20:44
  • 7
    \$\begingroup\$ Some distorted the images, some didn't line up right. Many different issues, but 65% of games developed with Unity are 2D, and they made it work. I just want to know what people are using, and not having to reinvent the wheel. No one talks about it and there is no guides or docs on how to handle it. Yet you can't get far in a mobile project without having a system in place to do it. \$\endgroup\$ – Michael Jun 30 '14 at 22:38
  • 1
    \$\begingroup\$ "Issues I have in mind is when there are key things that have to be in certain places and cannot fall off the screen. Using black bars on top or bottom is unacceptable these days." Guarantee of elements not falling off screen, zero distortion, but no letter/pillar-boxing (black bars and the like). These requirements are irreconcilable. The last requirement is probably the least important, or can be hidden by padding out the canvas beyond what must be on-screen. Most games I've seen with such strict requirements will have decorated pillarbox/borders. \$\endgroup\$ – Jibb Smart Sep 2 '15 at 3:39

12 Answers 12

34
\$\begingroup\$

What you want is to constrain the camera viewport on portrait or landscape(depending on your needs), by computing camera.orthographicSize property, so you can build your 2d scene regardless of aspect ratio and resolution:

// Attach this script on your main ortohgraphic camera:

/* The MIT License (MIT)

Copyright (c) 2014, Marcel Căşvan

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. */

using System;
using System.Collections;
using UnityEngine;

[ExecuteInEditMode]
[RequireComponent (typeof (Camera))]
public class ViewportHandler : MonoBehaviour
{
    #region FIELDS
    public Color wireColor = Color.white;
    public float UnitsSize = 1; // size of your scene in unity units
    public Constraint constraint = Constraint.Portrait;
    public static ViewportHandler Instance;
    public new Camera camera;

    private float _width;
    private float _height;
    //*** bottom screen
    private Vector3 _bl;
    private Vector3 _bc;
    private Vector3 _br;
    //*** middle screen
    private Vector3 _ml;
    private Vector3 _mc;
    private Vector3 _mr;
    //*** top screen
    private Vector3 _tl;
    private Vector3 _tc;
    private Vector3 _tr;
    #endregion

    #region PROPERTIES
    public float Width {
        get {
            return _width;
        }
    }
    public float Height {
        get {
            return _height;
        }
    }

    // helper points:
    public Vector3 BottomLeft {
        get {
            return _bl;
        }
    }
    public Vector3 BottomCenter {
        get {
            return _bc;
        }
    }
    public Vector3 BottomRight {
        get {
            return _br;
        }
    }
    public Vector3 MiddleLeft {
        get {
            return _ml;
        }
    }
    public Vector3 MiddleCenter {
        get {
            return _mc;
        }
    }
    public Vector3 MiddleRight {
        get {
            return _mr;
        }
    }
    public Vector3 TopLeft {
        get {
            return _tl;
        }
    }
    public Vector3 TopCenter {
        get {
            return _tc;
        }
    }
    public Vector3 TopRight {
        get {
            return _tr;
        }
    }
    #endregion

    #region METHODS
    private void Awake()
    {
        camera = GetComponent<Camera>();
        Instance = this;
        ComputeResolution();
    }

    private void ComputeResolution()
    {
        float leftX, rightX, topY, bottomY;

        if(constraint == Constraint.Landscape){
            camera.orthographicSize = 1f / camera.aspect * UnitsSize / 2f;    
        }else{
            camera.orthographicSize = UnitsSize / 2f;
        }

        _height = 2f * camera.orthographicSize;
        _width = _height * camera.aspect;

        float cameraX, cameraY;
        cameraX = camera.transform.position.x;
        cameraY = camera.transform.position.y;

        leftX = cameraX - _width / 2;
        rightX = cameraX + _width / 2;
        topY = cameraY + _height / 2;
        bottomY = cameraY - _height / 2;

        //*** bottom
        _bl = new Vector3(leftX, bottomY, 0);
        _bc = new Vector3(cameraX, bottomY, 0);
        _br = new Vector3(rightX, bottomY, 0);
        //*** middle
        _ml = new Vector3(leftX, cameraY, 0);
        _mc = new Vector3(cameraX, cameraY, 0);
        _mr = new Vector3(rightX, cameraY, 0);
        //*** top
        _tl = new Vector3(leftX, topY, 0);
        _tc = new Vector3(cameraX, topY , 0);
        _tr = new Vector3(rightX, topY, 0);           
    }

    private void Update()
    {
        #if UNITY_EDITOR
        ComputeResolution();
        #endif
    }

    void OnDrawGizmos() {
        Gizmos.color = wireColor;

        Matrix4x4 temp = Gizmos.matrix;
        Gizmos.matrix = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one);
        if (camera.orthographic) {
            float spread = camera.farClipPlane - camera.nearClipPlane;
            float center = (camera.farClipPlane + camera.nearClipPlane)*0.5f;
            Gizmos.DrawWireCube(new Vector3(0,0,center), new Vector3(camera.orthographicSize*2*camera.aspect, camera.orthographicSize*2, spread));
        } else {
            Gizmos.DrawFrustum(Vector3.zero, camera.fieldOfView, camera.farClipPlane, camera.nearClipPlane, camera.aspect);
        }
        Gizmos.matrix = temp;
    }
    #endregion

    public enum Constraint { Landscape, Portrait }
}

If you need more info on this please ask and I will reply. ;) Regards and cheers.

UPDATE: Use Eliot Lash's object anchoring script together with this one to place objects at key positions on the screen if needed(relative to screen corners/borders). If you do, rename "CameraFit" to "ViewportHandler".

Preview simulating various aspect ratios screens: enter image description here

\$\endgroup\$
  • 4
    \$\begingroup\$ @Eliot Added MIT license above. Good luck with your projects! \$\endgroup\$ – androidu Jan 7 '15 at 12:16
  • 7
    \$\begingroup\$ Fantastic, thanks! One good turn deserves another, so here's an open-source component I just wrote for simple anchoring of GameObjects to the helper points defined by your script: gist.github.com/fadookie/256947788c364400abe1 \$\endgroup\$ – Eliot Jan 7 '15 at 12:39
  • 1
    \$\begingroup\$ MarcelCăşvan Very nice script! I added an enum for selecting if you want to define unit size for potrait for landscape (height/width). Just had to add a couple of lines to the script, and I use GetComponent<Camera>().orthographicSize = UnitSize / 2f; for potrait/defining height units \$\endgroup\$ – am_ Mar 26 '15 at 14:11
  • 2
    \$\begingroup\$ @MarcelCăşvan great stuff! Thanks. I see that the deviceWidth and deviceHeight variables from ComputeFunction() are unused. Perhaps consider deleting these. \$\endgroup\$ – user2313267 Apr 20 '16 at 10:26
  • 1
    \$\begingroup\$ CameraAnchor is throwing the error: "CameraFit is not defined in this context" -- In case someone else finds this answer later it seems that "CameraFit" just renamed "ViewportHandler" since this was originally posted. If you just rename the class from ViewportHandler back to CameraFit. \$\endgroup\$ – Lenny Aug 7 '17 at 23:23
5
\$\begingroup\$

You typically don't need different sizes of assets - imported textures and sprites with automatically-generated mip maps will look nice when rendered at any size less than or equal to the original pixel size of the image.

The scene layout is the challenge. One good approach is as follows (and FYI I use a 3D camera looking at 2D content positioned at z=0):

  1. Arbitrarily decide on a minimum "logical" display size in either pixels or tiles. This doesn't need to correspond to any real-world resolution, but it should reflect the narrowest/shortest aspect ratio that you want to support. For example, for a landscape game I wouldn't choose 480x320 because that's a wider aspect ratio than the iPad. So I might pick 1024x768 - or even 480x360, which gives me an original iPhone-sized coordinate system to work with and the same aspect ratio as every iPad (including iPad Air 2, etc.). Also note you can just as easily work in tile coordinates rather than pixel coordinates - 15x11.25 for example.
  2. Program your game logic so that everything important is (or can be) positioned within your minimum display size but be prepared to fill extra room on the sides with additional content, even if it's just decorative filler.
  3. Determine how much you need to scale your content so that either the width or the height matches the minimum value and the other axis is larger than or equal to the minimum needed. To do this "scale to fit", divide the screen pixel size by the minimum display size and take the smaller of the resulting scale values to be your overall view scale.
  4. Use the view scale to calculate the effective (actual) display size for game logic purposes.
  5. Actually scale your content by moving the camera along the Z axis.

In code form:

  // Adjust the camera to show world position 'centeredAt' - (0,0,0) or other - with
  // the display being at least 480 units wide and 360 units high.

  Vector3 minimumDisplaySize = new Vector3( 480, 360, 0 );

  float pixelsWide = camera.pixelWidth;
  float pixelsHigh = camera.pixelHeight;

  // Calculate the per-axis scaling factor necessary to fill the view with
  // the desired minimum size (in arbitrary units).
  float scaleX = pixelsWide / minimumDisplaySize.x;
  float scaleY = pixelsHigh / minimumDisplaySize.y;

  // Select the smaller of the two scale factors to use.
  // The corresponding axis will have the exact size specified and the other 
  // will be *at least* the required size and probably larger.
  float scale = (scaleX < scaleY) ? scaleX : scaleY;

  Vector3 displaySize = new Vector3( pixelsWide/scale, pixelsHigh/scale, 0 );

  // Use some magic code to get the required distance 'z' from the camera to the content to display
  // at the correct size.
  float z = displaySize.y /
            (2 * Mathf.Tan((float)camera.fieldOfView / 2 * Mathf.Deg2Rad));

  // Set the camera back 'z' from the content.  This assumes that the camera
  // is already oriented towards the content.
  camera.transform.position = centeredAt + new Vector3(0,0,-z);

  // The display is showing the region between coordinates 
  // "centeredAt - displaySize/2" and "centeredAt + displaySize/2".

  // After running this code with minimumDisplaySize 480x360, displaySize will
  // have the following values on different devices (and content will be full-screen
  // on all of them):
  //    iPad Air 2 - 480x360
  //    iPhone 1 - 540x360
  //    iPhone 5 - 639x360
  //    Nexus 6 - 640x360

  // As another example, after running this code with minimumDisplaySize 15x11
  // (tile dimensions for a tile-based game), displaySize will end up with the following 
  // actual tile dimensions on different devices (every device will have a display
  // 11 tiles high and 15+ tiles wide):
  //    iPad Air 2 - 14.667x11
  //    iPhone 1 - 16.5x11
  //    iPhone 5 - 19.525x11
  //    Nexus 6 - 19.556x11
\$\endgroup\$
2
\$\begingroup\$

If you come around to using the bars it's actually pretty simple to implement (I'm posting this even though the OP stated the opinion of it being unacceptable because it has the benefit of being not near as bad on mobile and it's a simple solution that requires no code whatsoever)

Camera.orthographicSize is a variable within the ortho camera (which most 2D games use) that fits the measured amount of game units vertically on the screen (divided by 2) (source). Thus, pick an aspect ratio that fits the vast majority of devices (I chose 16:9 as most screens I researched are 16:9, 16:10, 3:2) and add a mask that overlays that at a ratio.

Example:

In my game (not listed here as this is not an ad, can ask in comments if desired) we use portrait mode. To do a nice simple 16:9 I made my Ortho camera at size 16. This means the camera will adapt 32 game units of height (y: 16 through -16 in my case) into the device's vertical of the screen.

I then placed black masks with a game between -9 and +9. Voila, the game's screen looks the exact same on all devices and a little skinnier on devices that are a little wider. I've had absolutely no negative feedback regarding the masks. To do landscape simply flip those values and then you'd make the camera of size 9. Change the values to match whatever you've decided is your game unit scale.

The only place we've observed the black bar to show up significantly is on the iPad at 3:2. Even then, I've had no complaints.

\$\endgroup\$
1
\$\begingroup\$

I'm doing this in a game I am currently working on. I have a background image that is 1140x720. The most important bits (the ones that should never get cropped) are contained in the 960x640 middle area. I run this code on the start function of my camera:

    float aspect = (float)Screen.width / (float)Screen.height;

    if (aspect < 1.5f)
        Camera.main.orthographicSize = 3.6f;
    else
        Camera.main.orthographicSize = 3.2f;

    float vertRatio = Screen.height / 320.0f;
    fontSize = (int)(12 * vertRatio);

I also define sizes other than font size for buttons and such. It works well on every aspect ratio I've tested. It's been a while since I set it up, so I may be missing a step. Let me know if it doesn't work as expected and I'll see if I left anything out.

\$\endgroup\$
1
\$\begingroup\$

@Marcel's answer and code are great and helped me understand what was happening. It's the definitive answer. Just thought someone might also find useful what I ended up doing for my specific case: since I wanted something really really simple, one sprite to be always on screen, I came up with these few lines:

public class CameraFit : MonoBehaviour {

    public SpriteRenderer spriteToFitTo;

    void Start () { // change to Update to test by resizing the Unity editor window
        var bounds = spriteToFitTo.bounds.extents;
        var height = bounds.x / camera.aspect;
        if (height < bounds.y)
            height = bounds.y;
        camera.orthographicSize = height;
    }
}

I added this to the camera and dragged my sprite (it's my background) to the script's only property. If you don't want any black bars (horizontal or vertical) you can put a bigger background behind this one...

\$\endgroup\$
0
\$\begingroup\$

There are a few ways to tackle this issue, and there isn't a perfect solution (at least I haven't found one yet) and the type of solution you go with is going to depend greatly on the type of game you're developing.

Regardless of you what you do, you should start off by picking the lowest possible resolution you'd like to support and build your sprites to that resolution. So if you're interested in developing for iOS, according to http://www.iosres.com/ the lowest iOS device resolution is 480x320.

From there, you can start scaling up the sprites to meet the higher resolutions. The caveat to this is that you'll eventually start to notice the sprites begin to blur the farther up you scale, in which case you can switch to another set of sprites built for the higher resolutions.

Or, you can ignore scaling completely, and just decide to display more of the game screen for higher resolutions (I believe this is how Terraria does it, for example). However, for many games, this isn't appropriate; competitive games for example.

Using black bars on top or bottom is unacceptable these days.

Is it? Lots of games that want to force 4:3 aspect ratios do this. Since you're using Unity, you can make use of the AspectRatioEnforcer script to aid in this.

\$\endgroup\$
  • \$\begingroup\$ I'm targeting mobile, so I cannot change the aspect ratio or resolution. Using low resolution assets is not an acceptable solution. Some games (especially 3D) it isn't an issue, you just show more or less. But games like Kingdom Rush, tower defense, and other games like Alex you want to see the same thing regardless of the device. If things do not show up in the right locations, the game won't work properly. \$\endgroup\$ – Michael Jul 6 '14 at 21:00
  • \$\begingroup\$ Yes you can docs.unity3d.com/ScriptReference/Screen.SetResolution.html \$\endgroup\$ – Cooper Jul 6 '14 at 21:44
0
\$\begingroup\$

There are multiple ways to handle this issue, it depends on your game and what will work best. For example, on our game Tyrant Unleashed we simply made wide maps with unimportant detailing on the sides, so that it's okay to cut off the sides on narrower devices. However other games might be better with an approach where you actually shift buttons around or something to fit the screen better.

(also part of our approach is also keeping a consistent height on all devices, only varying the width. This certainly makes life easier for us, but again this may or may not be any good for your specific game. It doesn't matter in our art style if the images are scaled a bit on different screens, whereas this might matter for something like pixel art. Basically, this is the "let Unity handle it" approach)

\$\endgroup\$
  • \$\begingroup\$ Have you done any projects where that wasn't an option? Almost all the tower defense style games I've seen and many other styles you can see the entire game view on the device without scrolling, and this is consistent across devices. On IOS you can make assets and swap them, but that becomes impossible (and really just a big PITA anyway) on Android. \$\endgroup\$ – Michael Jul 6 '14 at 21:55
  • \$\begingroup\$ No I haven't. btw I just added some more detail about screen size \$\endgroup\$ – jhocking Jul 6 '14 at 23:02
0
\$\begingroup\$

I'm using the following script which adds a targetAspect parameter to the camera and adapts its orthographicSize with respect to the screen ratio (more details in this blog post):

using UnityEngine;
using System.Collections;

public class AspectRatioScript : MonoBehaviour {

    public float targetAspect;

    void Start () 
    {
        float windowAspect = (float)Screen.width / (float)Screen.height;
        float scaleHeight = windowAspect / targetAspect;
        Camera camera = GetComponent<Camera>();

        if (scaleHeight < 1.0f)
        {  
            camera.orthographicSize = camera.orthographicSize / scaleHeight;
        }
    }
}
\$\endgroup\$
0
\$\begingroup\$

My method is mostly similar to the solutions given by others :) I'll try to explain in detail the approach which I take to make the game independent of the screen size.

Screen Orientation

Depending upon the screen orientation (Landscape or Portrait) , you need to consider whether the camera will scale with a fixed height or fixed width. Mostly I choose fixed width for landscape oriented games and fixed height for portrait oriented games.

Camera Scaling

As discussed this can be either fixed height or fixed width.

Fixed Height: The vertical area of the game will always fit to the screen height. And as the screen aspect ratio changes there will be extra space added to the left and right of the screen. To implement this you don't need to code anything, it is the default behaviour of unity camera.

Fixed Width: The horizontal area of the game will always fit the screen width. And extra space will be added to the top and bottom as the aspect ratio of the screen changes. To implement this you need to write a small piece of code. Later on make sure you remove the code form update function, and place it in awake.

using UnityEngine;

[ExecuteInEditMode]
public class ScaleWidthCamera : MonoBehaviour {

    public int targetWidth = 640;
    public float pixelsToUnits = 100;

    void Update() {

        int height = Mathf.RoundToInt(targetWidth / (float)Screen.width * Screen.height);

        camera.orthographicSize = height / pixelsToUnits / 2;
    }
}

In the editor you can change the targetWidth to define the world space area you want to display. This code is explained in the following video along with many other practices for 2D games :)

Unite 2014 - 2D Best Practices In Unity

Aspect Ratio

Following aspect ratio listed from widest to narrowest, covers almost all the screen sizes for both android and iOS

  • 5:4
  • 4:3
  • 3:2
  • 16:10
  • 16:9

I usually set all of these aspect ratios in the given order under the game window, as it is handy while testing for different screen sizes :)

Expendable Area

This is the area which gets added to the screen to the sides or top/bottom depending upon the camera scaling you have chosen.

For fixed height all the game elements should preferably fit in 16:9 ratio which is the narrowest. And the background should extend till it covers the 5:4 ratio. Which makes sure that your game never has black strips to the sides.

For fixed width it almost the same but here the elements should fit in 5:4 ratio and the BG should extend till 16:9.

Bounds

Sometimes we cannot use the expendable area approach as we need to utilise the whole available screen for the game play.

For example , consider a portrait game with fixed height , catching the coins falling from the sky. In this we need to give the player ability to move horizontally over the available screen width.

Hence we need the bounds of the camera in terms of world co-ordinates to know where exactly the left,right,top or bottom of the camera clips at the world position.
We can also use these bounds to anchor game elements or UI to a desired side of the camera.

Using Camera.ViewportToWorldPoint we can get the bounds.Viewport space is normalized and relative to the camera. The bottom-left of the camera is (0,0); the top-right is (1,1). The z position is in world units from the camera. For 2D/orthographic the z doesn't matter.

Vector3 leftBottom = camera.ViewportToWorldPoint(new Vector3(0, 0, camera.nearClipPlane));
Vector3 rightTop = camera.ViewportToWorldPoint(new Vector3(1, 1, camera.nearClipPlane));

float left = leftBottom.x;
float bottom = leftBottom.y;
float right = rightTop.x;
float top = rightTop.y;

UI

For UI we can apply the same concepts that we used for the game elements. After introduction of Unity5 UI and availability of plugins like NGUI this won't be much of a problem :)

\$\endgroup\$
0
\$\begingroup\$

I created the Unity Asset 'Resolution Magic 2D' to solve this very problem (ad: you can get it from the Unity Asset Store, or see more details at grogansoft.com).

The way I tackled the problem was as follows...

Define an area of the screen that must always be visible regardless of aspect ratio/resolution, and use a simple rectangle transform to 'stencil out' this region. This is your ideal screen shape. Using a simple algorithm I then zoom the camera until the region blocked out by the rectangle is as large as possible while still being 100% visible to the camera.

Then your main game area is always taking up as much screen as possible. And as long as there is sufficient 'extra' content (e.g. your background) outside of the rectangle area you defined before, players whose screen is not the same aspect ratio as your 'ideal' rectangle will see the extra content where black bars would otherwise go.

My asset also includes some logic for placing UI, but that is mostly obsolete due to Unity's new UI system.

My asset provides this out of the box with minimal setup, and it works really well on all platforms.

If you use this technique (or my asset), just make sure you design your game to have 'optional' space all around to accommodate screens that are wider or taller than your ideal (to avoid black bars).

I don't personally make 3D games, so I don't know if this would work in 3D (or if it is even necessary).

It's really hard to explain without pictures, so please visit my website (Resolution Magic 2D)

\$\endgroup\$
0
\$\begingroup\$

The best solution for me is to use the theorem of intersecting lines so that there is neither a cut off on the sides nor a distortion of the game view. That means that you have to step back or forward depending on the different aspect ratio.

\$\endgroup\$
-3
\$\begingroup\$

I created a AssetStore extension that allows for easier aspect switching called AspectSwitcher. It provides a system for allowing you to easily specify different properties for different aspects. There are generally two method that most people use to switch aspects. One is to provide different game objects for each aspect. The other is to create custom code that modifies the properties of a single game object based on the current aspect. That generally requires a lot of custom coding. My extension attempts to alleviate a lot of that pain.

\$\endgroup\$
  • 2
    \$\begingroup\$ This answer would be better, and seem less spammy, if you elaborated on the actual techniques one might use to address the OP's question. \$\endgroup\$ – Josh Oct 15 '14 at 17:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.