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.
How to Use
Attach the MobileResponsiveScaler script to your Canvas GameObject (requires a CanvasScaler)
Set the referenceResolution to your design resolution (e.g. 1080x1920)
Enable dpiAwareScaling for consistent sizing across different screen densities
Optionally assign portraitLayout and landscapeLayout GameObjects for orientation-based switching
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
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;
}
}
}