Part of these game systems:
intermediate Touch & Mobile

Mobile Responsive UI Scaler

Dynamically adjusts UI scaling based on device aspect ratio, DPI, and orientation. Works with Unity's CanvasScaler to ensure readable fonts and proper layouts across all mobile devices.

Unity 2022.3+ · 4.0 KB · MobileResponsiveScaler.cs

How to Use

1

Attach the MobileResponsiveScaler script to your Canvas GameObject (requires a CanvasScaler)

2

Set the referenceResolution to your design resolution (e.g. 1080x1920)

3

Enable dpiAwareScaling for consistent sizing across different screen densities

4

Optionally assign portraitLayout and landscapeLayout GameObjects for orientation-based switching

5

Connect onOrientationChanged to update any layout-dependent logic

Features

  • Aspect ratio-based UI scaling
  • Reference resolution support
  • Font size scaling for readability
  • Layout switching (portrait/landscape)
  • DPI-aware scaling

When to Use This

Use in any mobile game that needs to look correct across phones and tablets with different aspect ratios and screen densities. Essential when your UI has both portrait and landscape layouts, or when font sizes need to stay readable on high-DPI tablets vs low-DPI phones.

Common Mistakes

This script overrides the CanvasScaler's matchWidthOrHeight at runtime, so don't manually set it in the Inspector or your value will be replaced on first frame. The referenceDPI defaults to 160, which is standard for Android mdpi — adjust this if your primary target is iOS (which uses 163 DPI as baseline). The IsTablet() method uses a 7-inch diagonal threshold, but Screen.dpi returns 0 on some Android devices, causing false negatives.

Source Code

MobileResponsiveScaler.cs
C#
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;

[RequireComponent(typeof(CanvasScaler))]
public class MobileResponsiveScaler : MonoBehaviour
{
    [Header("Reference Resolution")]
    [SerializeField] private Vector2 referenceResolution = new Vector2(1080, 1920);
    [SerializeField] private float referenceAspect = 0.5625f; // 9:16

    [Header("Scaling")]
    [SerializeField] private float minMatchWidthHeight = 0f;
    [SerializeField] private float maxMatchWidthHeight = 1f;
    [SerializeField] private float wideScreenThreshold = 0.6f;

    [Header("Font Scaling")]
    [SerializeField] private bool scaleFonts = true;
    [SerializeField] private float baseFontScale = 1f;
    [SerializeField] private float minFontScale = 0.7f;
    [SerializeField] private float maxFontScale = 1.5f;

    [Header("DPI")]
    [SerializeField] private bool dpiAwareScaling = true;
    [SerializeField] private float referenceDPI = 160f;

    [Header("Layout Switching")]
    [SerializeField] private bool enableLayoutSwitching = true;
    [SerializeField] private GameObject portraitLayout;
    [SerializeField] private GameObject landscapeLayout;

    [Header("Events")]
    public UnityEvent<bool> onOrientationChanged; // true = portrait
    public UnityEvent<float> onScaleChanged;

    public float CurrentFontScale { get; private set; } = 1f;
    public bool IsPortrait { get; private set; }
    public float CurrentAspect { get; private set; }
    public float CurrentDPI { get; private set; }

    private CanvasScaler canvasScaler;
    private Vector2Int lastScreenSize;
    private ScreenOrientation lastOrientation;

    private void Awake()
    {
        canvasScaler = GetComponent<CanvasScaler>();
        canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
        canvasScaler.referenceResolution = referenceResolution;

        UpdateScaling();
    }

    private void Update()
    {
        Vector2Int currentSize = new Vector2Int(Screen.width, Screen.height);
        ScreenOrientation currentOrientation = Screen.orientation;

        if (currentSize != lastScreenSize || currentOrientation != lastOrientation)
        {
            lastScreenSize = currentSize;
            lastOrientation = currentOrientation;
            UpdateScaling();
        }
    }

    private void UpdateScaling()
    {
        if (Screen.width <= 0 || Screen.height <= 0) return;

        CurrentAspect = (float)Screen.width / Screen.height;
        IsPortrait = Screen.height > Screen.width;
        CurrentDPI = Screen.dpi > 0 ? Screen.dpi : referenceDPI;

        UpdateCanvasScaler();
        UpdateFontScale();
        UpdateLayout();
    }

    private void UpdateCanvasScaler()
    {
        float aspectRatio = CurrentAspect;
        float aspectDelta = Mathf.Abs(aspectRatio - referenceAspect);

        // Wider screens should favor width matching, taller screens favor height
        float match;
        if (aspectRatio > wideScreenThreshold)
        {
            match = Mathf.Lerp(maxMatchWidthHeight, minMatchWidthHeight,
                (aspectRatio - wideScreenThreshold) / (1f - wideScreenThreshold));
        }
        else
        {
            // Scale adjustment based on how far we are from reference aspect
            match = Mathf.Lerp(maxMatchWidthHeight, minMatchWidthHeight, aspectDelta);
        }

        match = Mathf.Clamp(match, minMatchWidthHeight, maxMatchWidthHeight);
        canvasScaler.matchWidthOrHeight = match;

        onScaleChanged?.Invoke(match);
    }

    private void UpdateFontScale()
    {
        if (!scaleFonts) return;

        float screenDiagonal = Mathf.Sqrt(Screen.width * Screen.width + Screen.height * Screen.height);
        float referenceDiagonal = Mathf.Sqrt(
            referenceResolution.x * referenceResolution.x +
            referenceResolution.y * referenceResolution.y);

        float sizeRatio = screenDiagonal / referenceDiagonal;

        float dpiScale = 1f;
        if (dpiAwareScaling && CurrentDPI > 0)
        {
            dpiScale = referenceDPI / CurrentDPI;
        }

        CurrentFontScale = baseFontScale * sizeRatio * dpiScale;
        CurrentFontScale = Mathf.Clamp(CurrentFontScale, minFontScale, maxFontScale);
    }

    private void UpdateLayout()
    {
        if (!enableLayoutSwitching) return;

        bool portrait = IsPortrait;

        if (portraitLayout != null)
            portraitLayout.SetActive(portrait);

        if (landscapeLayout != null)
            landscapeLayout.SetActive(!portrait);

        onOrientationChanged?.Invoke(portrait);
    }

    public float GetScaledFontSize(float baseFontSize)
    {
        return baseFontSize * CurrentFontScale;
    }

    public void SetReferenceResolution(Vector2 resolution)
    {
        referenceResolution = resolution;
        canvasScaler.referenceResolution = resolution;
        UpdateScaling();
    }

    public float GetScreenDiagonalInches()
    {
        if (CurrentDPI <= 0) return 0f;
        float diagonalPixels = Mathf.Sqrt(
            Screen.width * Screen.width + Screen.height * Screen.height);
        return diagonalPixels / CurrentDPI;
    }

    public bool IsTablet()
    {
        return GetScreenDiagonalInches() >= 7f;
    }

    public void ForceUpdate()
    {
        UpdateScaling();
    }

    private void OnValidate()
    {
        if (canvasScaler == null)
            canvasScaler = GetComponent<CanvasScaler>();

        if (canvasScaler != null)
        {
            canvasScaler.referenceResolution = referenceResolution;
        }
    }
}