Easy, fool-proof UI scaling for mobile and tablets.

Hey Gals and Boys. I have decided that I will write this article after ratio, resolution, aspects and “notches” almost drove me mad. As an indie developer for mobile whether it is Android or iOS (especially true for Android devices), you are more than aware that there are a plethora of different phones out there. In 2015, there were over 24000 models in circulation! And that is only mobile phones, on top of that, we also have tablets. Tablets might be a much smaller part of the mobile market but HEY, we want to be able to show our games to as big of an audience as possible. We want our games to be playable on almost if not all devices out there. For us, here at LoOnar Forge studio, it was always a huge pain in the neck to deal with this problem. How can you test your game against so many devices? How to take into account ones that are not out yet? Should we make an .APK for each most common set of screen ratio/resolution? Don’t despair! Help is coming! Help is coming in threefold:

Simulator Window, green rectangle is called Safe Area
  1. I don’t know is it me but it seems that whenever we face a major problem/obstacle to deal while developing our games, Unity3d announces that in the next version there will be a change, new tool, or update that makes this problem go away, or at least it becomes much less of a hustle. They always provide! Thanks, Unity! As of version 2020.1, Unity3D has a new package (it is in preview mode still, so if you want to install it make sure you check in preview packages) called the “Simulator”. It allows you to change your Game View into a Device View! From the list in the top left corner, you can choose from almost 70 devices (both Android and iOS, mobile and tablet) with different screen ratios, different resolutions, with “notches” or without! Besides, if you don’t find your device on the list you can add your own(not tested by me) or simply change the resolution of a device that is already on the list. Unity already stated that they will be adding more devices as time goes. Also, you can rotate your phone to see how your game will behave in different layouts. The biggest help comes from the SafeArea option. If you check this option, on your virtual device you will see a green rectangle. This is the Safe Area. Each phone has it defined, it is an area that will not be obstructed by a notch or anything else when rotating your phone or just in general, using it. By sticking to the Safe Area only, you make sure your game won’t be missing half of a button or will only show half of the kills that you have scored! Hmmm, but how do we stick to the Safe Area with our UI elements? AHAAAaaa!
  2. Next, let me introduce Adriaan. He is a fantastic game developer (known for Hidden Folks game, amongst others) that was kind enough to share some of his code on the Unity forums. It is a single script that can be attached to your Canvas object in the Scene. If you have more than one Canvas in your scene you can attach it to as many as you need. But more on that later. Here you can find the link to the page with Adriaans original post (link) about it. And here you can visit his website (I hope he doesn’t mind) to see what he is up to. I really like his new project called Secret Shuffle! It is a party/dancing game. Still, W.I.P. but the idea is great. Check it out! (Adriaandejongh.nl). Now, lets get to the third and most important part of the equation, which is me!
  3. Only kidding. I am merely a person that has decided to write a post about it, hopefully, whoever stumbles upon the same problem will have an easier time dealing with it. We have spent quite a lot of time trying to get it to work for our android game called Sheep Happens with mixed results. This solution by far gives the best results, also it is very easy to apply. It is really simple, lets get to it.

STEP 1:

Create a C# script called CanvasHelper:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
 
[RequireComponent(typeof(Canvas))]
public class CanvasHelper : MonoBehaviour
{
    private static List<CanvasHelper> helpers = new List<CanvasHelper>();
 
    public static UnityEvent OnResolutionOrOrientationChanged = new UnityEvent();
 
    private static bool screenChangeVarsInitialized = false;
    private static ScreenOrientation lastOrientation = ScreenOrientation.Landscape;
    private static Vector2 lastResolution = Vector2.zero;
    private static Rect lastSafeArea = Rect.zero;
 
    private Canvas canvas;
    private RectTransform rectTransform;
    private RectTransform safeAreaTransform;
 
    void Awake()
    {
        if(!helpers.Contains(this))
            helpers.Add(this);
   
        canvas = GetComponent<Canvas>();
        rectTransform = GetComponent<RectTransform>();
   
        safeAreaTransform = transform.Find("SafeArea") as RectTransform;
   
        if(!screenChangeVarsInitialized)
        {
            lastOrientation = Screen.orientation;
            lastResolution.x = Screen.width;
            lastResolution.y = Screen.height;
            lastSafeArea = Screen.safeArea;
   
            screenChangeVarsInitialized = true;
        }
       
        ApplySafeArea();
    }
 
    void Update()
    {
        if(helpers[0] != this)
            return;
   
        if(Application.isMobilePlatform && Screen.orientation != lastOrientation)
            OrientationChanged();
   
        if(Screen.safeArea != lastSafeArea)
            SafeAreaChanged();
   
        if(Screen.width != lastResolution.x || Screen.height != lastResolution.y)
            ResolutionChanged();
    }
 
    void ApplySafeArea()
    {
        if(safeAreaTransform == null)
            return;
   
        var safeArea = Screen.safeArea;
   
        var anchorMin = safeArea.position;
        var anchorMax = safeArea.position + safeArea.size;
        anchorMin.x /= canvas.pixelRect.width;
        anchorMin.y /= canvas.pixelRect.height;
        anchorMax.x /= canvas.pixelRect.width;
        anchorMax.y /= canvas.pixelRect.height;
   
        safeAreaTransform.anchorMin = anchorMin;
        safeAreaTransform.anchorMax = anchorMax;
    }
 
    void OnDestroy()
    {
        if(helpers != null && helpers.Contains(this))
            helpers.Remove(this);
    }
 
    private static void OrientationChanged()
    {
        //Debug.Log("Orientation changed from " + lastOrientation + " to " + Screen.orientation + " at " + Time.time);
   
        lastOrientation = Screen.orientation;
        lastResolution.x = Screen.width;
        lastResolution.y = Screen.height;
 
        OnResolutionOrOrientationChanged.Invoke();
    }
 
    private static void ResolutionChanged()
    {
        //Debug.Log("Resolution changed from " + lastResolution + " to (" + Screen.width + ", " + Screen.height + ") at " + Time.time);
   
        lastResolution.x = Screen.width;
        lastResolution.y = Screen.height;
 
        OnResolutionOrOrientationChanged.Invoke();
    }
 
    private static void SafeAreaChanged()
    {
        // Debug.Log("Safe Area changed from " + lastSafeArea + " to " + Screen.safeArea.size + " at " + Time.time);
   
        lastSafeArea = Screen.safeArea;
   
        for (int i = 0; i < helpers.Count; i++)
        {
            helpers[i].ApplySafeArea();
        }
    }
}

STEP 2:

Add this script to your Canvas game object. As you can see, in this scene we have 2 Canvas objects, you just add the script to both of them.

STEP 3.

For each Canvas that you have in your project, you need to create a child object called SafeArea, not Safe Area (like I did and wasted 3 hours trying to figure out why it is broken) but SafeArea.

STEP 4.

Make sure that the SafeArea object has the same “Anchors” values in the inspector like here, namely, it has to range from 0 to 1. Also, in the Scene View, stretch the SafeArea object all the way to the borders of the Canvas it is attached to, in other words, all four anchors have to be all the way out, in the corners. That last part is very important, if you don’t stretch the anchors all the way, UI will change with different devices.

STEP 5.

Make sure that your Canvas and Canvas Scaler component that is located on your Canvas object is set like in the picture below. I have changed the Reference Resolution to 1500×1200. This way UI behaves as intended on all mobile devices that we have tested and also, the ratio is not too bad when you install your game on a tablet. Play with it to see what suits your game best or ignore it if you don’t care about tablets. We want to try for the same .APK to be usable on both small mobile devices and large tablets.

STEP 6.

And that is it, unless you had some animations on your UI and now they are broken! If you have animations that need fixing please follow to STEP 7. If not, you can add all of your UI objects to be children of SafeArea object and enjoy how beautifully they scale and stay in their positions. Remember, you still have to properly construct your UI to make it work, Hierarchy has to be orderly, anchors have to be used. This is not a magic wand solution 😉 Happy coding!

STEP 7.

If you have animations, they will be all broken and marked yellow, like on the picture below. Worry not. It is because now there is another object in the Hierarchy (namely: SafeArea) and Unity can’t figure it out by itself.. what you need to do is change the path for each object that is yellow. Select it with your mouse, press F2 to edit the path/name, and paste SafeArea/ at the beginning of the name. Yes, that includes the slash symbol as well. If you change all of the objects in the Animation Window properly, their names will be black and not yellow. Like in the bottom half of the picture below (part under the red line).

THANKS FOR READING. If you have any questions regarding this issue please do not hesitate to ask us. Talk to us on Twitter here:

LoOnarForgeTwitter

CHEERIO !