Lesson 6: Mobile UI/UX Design

Welcome back! Now that you have engaging puzzle mechanics working, it's time to make your game beautiful and intuitive. In this lesson, you'll learn how to design professional mobile UI/UX that works perfectly on all screen sizes and creates an enjoyable user experience.

Cute Gadget & Technology📲🖥️⌚ Cute Gadget & Technology📲🖥️⌚ by Dribbble Artist

What You'll Learn

  • Design intuitive mobile interfaces that players love
  • Create responsive layouts that work on all screen sizes
  • Implement touch-friendly UI elements and interactions
  • Build navigation systems that are easy to use
  • Design for different orientations (portrait and landscape)
  • Optimize UI for performance and battery life
  • Create accessible interfaces for all players

Prerequisites

  • Unity 2022.3 LTS or newer
  • Completed Lesson 5: Puzzle Mechanics & Level Design
  • Basic understanding of Unity UI system
  • Your mobile puzzle game project from previous lessons

Why Mobile UI/UX Matters

Great mobile UI/UX is the difference between a game players delete and one they keep playing. The best mobile games share these characteristics:

  • Intuitive navigation - Players know where everything is
  • Touch-friendly - Buttons and elements are easy to tap
  • Responsive design - Works on all screen sizes
  • Fast loading - UI appears instantly
  • Clear feedback - Players always know what's happening
  • Beautiful visuals - Attractive design keeps players engaged

Poor mobile UI/UX leads to:

  • Player frustration - Confusing navigation and hard-to-tap buttons
  • Low retention - Players quit when UI is difficult to use
  • Negative reviews - Bad UI gets called out in app stores
  • Missed opportunities - Great gameplay wasted on poor interface

Understanding Mobile UI Design Principles

Before building your UI, understand these core principles:

1. Touch Target Size

  • Minimum size: 44x44 pixels (iOS) or 48x48 dp (Android)
  • Recommended size: 60x60 pixels for important buttons
  • Spacing: At least 8 pixels between touch targets
  • Thumb zone: Place important buttons in easy-to-reach areas

2. Screen Real Estate

  • Safe areas: Account for notches, status bars, and navigation bars
  • Content priority: Most important content in center and top
  • Minimal chrome: Reduce UI elements that don't add value
  • Full-screen mode: Use full screen for gameplay when possible

3. Information Hierarchy

  • Visual hierarchy: Most important information is largest and most prominent
  • Progressive disclosure: Show information when needed, hide when not
  • Consistent patterns: Use familiar UI patterns players recognize
  • Clear labels: Every button and element should have clear purpose

4. Performance Considerations

  • UI batching: Group UI elements to reduce draw calls
  • Texture compression: Compress UI textures for mobile
  • Canvas optimization: Use separate canvases for static and dynamic UI
  • Animation efficiency: Use efficient animation methods

Step 1: Set Up Unity Canvas System

Unity's Canvas system is the foundation for all mobile UI. Set it up correctly from the start.

Canvas Setup

public class MobileCanvasSetup : MonoBehaviour
{
    [Header("Canvas References")]
    public Canvas mainCanvas;
    public CanvasScaler canvasScaler;
    public GraphicRaycaster graphicRaycaster;

    void Start()
    {
        SetupCanvas();
    }

    private void SetupCanvas()
    {
        // Set canvas render mode
        mainCanvas.renderMode = RenderMode.ScreenSpaceOverlay;

        // Configure canvas scaler for mobile
        canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
        canvasScaler.referenceResolution = new Vector2(1080, 1920); // Portrait reference
        canvasScaler.matchWidthOrHeight = 0.5f; // Balance between width and height

        // Set screen match mode
        canvasScaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;

        // Configure for different aspect ratios
        canvasScaler.referencePixelsPerUnit = 100;
    }
}

Multiple Canvas Strategy

Use separate canvases for different UI layers:

public class CanvasManager : MonoBehaviour
{
    [Header("Canvas Layers")]
    public Canvas backgroundCanvas;    // Static background elements
    public Canvas gameplayCanvas;     // Gameplay UI (score, moves)
    public Canvas popupCanvas;         // Popups and dialogs
    public Canvas overlayCanvas;       // Top overlay (menus, settings)

    void Start()
    {
        // Set different sort orders
        backgroundCanvas.sortingOrder = 0;
        gameplayCanvas.sortingOrder = 10;
        popupCanvas.sortingOrder = 20;
        overlayCanvas.sortingOrder = 30;

        // Optimize each canvas
        OptimizeCanvas(backgroundCanvas, false); // Static, no raycasting
        OptimizeCanvas(gameplayCanvas, true);    // Dynamic, needs raycasting
        OptimizeCanvas(popupCanvas, true);       // Dynamic, needs raycasting
        OptimizeCanvas(overlayCanvas, true);    // Dynamic, needs raycasting
    }

    private void OptimizeCanvas(Canvas canvas, bool needsRaycasting)
    {
        // Disable raycasting for static canvases
        GraphicRaycaster raycaster = canvas.GetComponent<GraphicRaycaster>();
        if (raycaster != null)
        {
            raycaster.enabled = needsRaycasting;
        }

        // Set canvas to update only when needed
        canvas.additionalShaderChannels = AdditionalCanvasShaderChannels.None;
    }
}

Step 2: Design Responsive Layouts

Mobile devices come in many sizes. Your UI must work on all of them.

Anchor System

Use Unity's anchor system for responsive layouts:

public class ResponsiveLayout : MonoBehaviour
{
    [Header("UI Elements")]
    public RectTransform topBar;
    public RectTransform bottomBar;
    public RectTransform gameArea;
    public RectTransform sidePanel;

    void Start()
    {
        SetupResponsiveLayout();
    }

    private void SetupResponsiveLayout()
    {
        // Top bar - anchored to top
        topBar.anchorMin = new Vector2(0, 1);
        topBar.anchorMax = new Vector2(1, 1);
        topBar.anchoredPosition = Vector2.zero;
        topBar.sizeDelta = new Vector2(0, 120); // 120 pixels tall

        // Bottom bar - anchored to bottom
        bottomBar.anchorMin = new Vector2(0, 0);
        bottomBar.anchorMax = new Vector2(1, 0);
        bottomBar.anchoredPosition = Vector2.zero;
        bottomBar.sizeDelta = new Vector2(0, 100);

        // Game area - fills space between top and bottom
        gameArea.anchorMin = new Vector2(0, 0);
        gameArea.anchorMax = new Vector2(1, 1);
        gameArea.offsetMin = new Vector2(0, bottomBar.sizeDelta.y);
        gameArea.offsetMax = new Vector2(0, -topBar.sizeDelta.y);

        // Side panel - anchored to right (for landscape)
        sidePanel.anchorMin = new Vector2(1, 0);
        sidePanel.anchorMax = new Vector2(1, 1);
        sidePanel.anchoredPosition = new Vector2(-sidePanel.sizeDelta.x / 2, 0);
    }
}

Safe Area Handling

Handle device safe areas (notches, status bars):

public class SafeAreaHandler : MonoBehaviour
{
    private RectTransform rectTransform;
    private Rect lastSafeArea = new Rect(0, 0, 0, 0);

    void Start()
    {
        rectTransform = GetComponent<RectTransform>();
        ApplySafeArea();
    }

    void Update()
    {
        // Check if safe area changed (device rotation, etc.)
        if (lastSafeArea != Screen.safeArea)
        {
            ApplySafeArea();
        }
    }

    private void ApplySafeArea()
    {
        Rect safeArea = Screen.safeArea;
        Vector2 anchorMin = safeArea.position;
        Vector2 anchorMax = safeArea.position + safeArea.size;

        // Convert to canvas coordinates
        Canvas canvas = GetComponentInParent<Canvas>();
        RectTransform canvasRect = canvas.GetComponent<RectTransform>();

        anchorMin.x /= canvasRect.rect.width;
        anchorMin.y /= canvasRect.rect.height;
        anchorMax.x /= canvasRect.rect.width;
        anchorMax.y /= canvasRect.rect.height;

        // Apply safe area
        rectTransform.anchorMin = anchorMin;
        rectTransform.anchorMax = anchorMax;

        lastSafeArea = safeArea;
    }
}

Aspect Ratio Adaptation

Adapt UI for different aspect ratios:

public class AspectRatioAdapter : MonoBehaviour
{
    [Header("Layout Settings")]
    public float portraitAspectRatio = 9f / 16f; // 9:16
    public float landscapeAspectRatio = 16f / 9f; // 16:9

    private float currentAspectRatio;

    void Start()
    {
        currentAspectRatio = (float)Screen.width / Screen.height;
        AdaptLayout();
    }

    void Update()
    {
        float newAspectRatio = (float)Screen.width / Screen.height;
        if (Mathf.Abs(newAspectRatio - currentAspectRatio) > 0.01f)
        {
            currentAspectRatio = newAspectRatio;
            AdaptLayout();
        }
    }

    private void AdaptLayout()
    {
        bool isPortrait = currentAspectRatio < 1f;

        if (isPortrait)
        {
            // Portrait layout
            SetupPortraitLayout();
        }
        else
        {
            // Landscape layout
            SetupLandscapeLayout();
        }
    }

    private void SetupPortraitLayout()
    {
        // Stack UI elements vertically
        // Use full screen width
        // Optimize for one-handed use
    }

    private void SetupLandscapeLayout()
    {
        // Arrange UI elements horizontally
        // Use side panels for additional info
        // Optimize for two-handed use
    }
}

Step 3: Create Touch-Friendly UI Elements

Mobile UI elements must be easy to tap and interact with.

Button Design

public class MobileButton : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
{
    [Header("Button Settings")]
    public float touchScale = 0.95f;
    public float animationDuration = 0.1f;
    public AudioClip clickSound;

    private RectTransform rectTransform;
    private Vector3 originalScale;
    private Button button;

    void Start()
    {
        rectTransform = GetComponent<RectTransform>();
        originalScale = rectTransform.localScale;
        button = GetComponent<Button>();

        // Ensure button is large enough for touch
        if (rectTransform.sizeDelta.x < 60 || rectTransform.sizeDelta.y < 60)
        {
            rectTransform.sizeDelta = new Vector2(
                Mathf.Max(60, rectTransform.sizeDelta.x),
                Mathf.Max(60, rectTransform.sizeDelta.y)
            );
        }
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        // Scale down on press
        LeanTween.scale(gameObject, originalScale * touchScale, animationDuration)
            .setEase(LeanTweenType.easeOutQuad);

        // Play haptic feedback
        if (SystemInfo.supportsVibration)
        {
            Handheld.Vibrate();
        }
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        // Scale back up
        LeanTween.scale(gameObject, originalScale, animationDuration)
            .setEase(LeanTweenType.easeOutQuad);

        // Play sound
        if (clickSound != null)
        {
            AudioSource.PlayClipAtPoint(clickSound, Camera.main.transform.position);
        }
    }
}

Swipe Gestures

Implement swipe gestures for navigation:

public class SwipeDetector : MonoBehaviour, IDragHandler, IEndDragHandler
{
    [Header("Swipe Settings")]
    public float swipeThreshold = 50f;
    public float swipeTime = 0.5f;

    private Vector2 startPosition;
    private float startTime;
    private bool isDragging = false;

    public System.Action<Vector2> OnSwipe;

    public void OnDrag(PointerEventData eventData)
    {
        if (!isDragging)
        {
            startPosition = eventData.position;
            startTime = Time.time;
            isDragging = true;
        }
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        if (!isDragging) return;

        Vector2 endPosition = eventData.position;
        Vector2 swipeDirection = endPosition - startPosition;
        float swipeDuration = Time.time - startTime;

        // Check if swipe is fast enough and far enough
        if (swipeDuration < swipeTime && swipeDirection.magnitude > swipeThreshold)
        {
            // Determine swipe direction
            Vector2 normalizedDirection = swipeDirection.normalized;

            if (Mathf.Abs(normalizedDirection.x) > Mathf.Abs(normalizedDirection.y))
            {
                // Horizontal swipe
                if (normalizedDirection.x > 0)
                {
                    OnSwipe?.Invoke(Vector2.right);
                }
                else
                {
                    OnSwipe?.Invoke(Vector2.left);
                }
            }
            else
            {
                // Vertical swipe
                if (normalizedDirection.y > 0)
                {
                    OnSwipe?.Invoke(Vector2.up);
                }
                else
                {
                    OnSwipe?.Invoke(Vector2.down);
                }
            }
        }

        isDragging = false;
    }
}

Touch Feedback

Provide visual and haptic feedback:

public class TouchFeedback : MonoBehaviour
{
    [Header("Feedback Settings")]
    public GameObject touchEffectPrefab;
    public float hapticIntensity = 0.1f;

    public void ShowTouchFeedback(Vector2 screenPosition)
    {
        // Visual feedback
        if (touchEffectPrefab != null)
        {
            Vector3 worldPosition = Camera.main.ScreenToWorldPoint(
                new Vector3(screenPosition.x, screenPosition.y, 10f)
            );
            GameObject effect = Instantiate(touchEffectPrefab, worldPosition, Quaternion.identity);
            Destroy(effect, 0.5f);
        }

        // Haptic feedback
        if (SystemInfo.supportsVibration)
        {
            #if UNITY_ANDROID
            Handheld.Vibrate();
            #elif UNITY_IOS
            // iOS haptic feedback
            #endif
        }
    }
}

Step 4: Build Navigation Systems

Create intuitive navigation that players understand immediately.

Menu Navigation

public class MenuNavigation : MonoBehaviour
{
    [Header("Menu Panels")]
    public GameObject mainMenuPanel;
    public GameObject settingsPanel;
    public GameObject levelSelectPanel;
    public GameObject shopPanel;

    private Stack<GameObject> navigationStack = new Stack<GameObject>();

    void Start()
    {
        // Start with main menu
        ShowPanel(mainMenuPanel);
    }

    public void ShowPanel(GameObject panel)
    {
        // Hide current panel
        if (navigationStack.Count > 0)
        {
            navigationStack.Peek().SetActive(false);
        }

        // Show new panel
        panel.SetActive(true);
        navigationStack.Push(panel);

        // Animate transition
        AnimatePanelTransition(panel, true);
    }

    public void GoBack()
    {
        if (navigationStack.Count > 1)
        {
            // Hide current panel
            GameObject currentPanel = navigationStack.Pop();
            AnimatePanelTransition(currentPanel, false);
            currentPanel.SetActive(false);

            // Show previous panel
            GameObject previousPanel = navigationStack.Peek();
            previousPanel.SetActive(true);
            AnimatePanelTransition(previousPanel, true);
        }
    }

    private void AnimatePanelTransition(GameObject panel, bool show)
    {
        RectTransform rectTransform = panel.GetComponent<RectTransform>();

        if (show)
        {
            // Slide in from right
            rectTransform.anchoredPosition = new Vector2(Screen.width, 0);
            LeanTween.moveX(rectTransform, 0, 0.3f)
                .setEase(LeanTweenType.easeOutQuad);
        }
        else
        {
            // Slide out to right
            LeanTween.moveX(rectTransform, Screen.width, 0.3f)
                .setEase(LeanTweenType.easeInQuad);
        }
    }
}

Tab Navigation

public class TabNavigation : MonoBehaviour
{
    [Header("Tabs")]
    public List<TabButton> tabs;
    public List<GameObject> tabPanels;

    private int currentTabIndex = 0;

    void Start()
    {
        // Initialize tabs
        for (int i = 0; i < tabs.Count; i++)
        {
            int index = i; // Capture for closure
            tabs[i].button.onClick.AddListener(() => SwitchTab(index));
        }

        // Show first tab
        SwitchTab(0);
    }

    public void SwitchTab(int tabIndex)
    {
        // Validate index
        if (tabIndex < 0 || tabIndex >= tabs.Count) return;

        // Hide current tab panel
        tabPanels[currentTabIndex].SetActive(false);
        tabs[currentTabIndex].SetActive(false);

        // Show new tab panel
        currentTabIndex = tabIndex;
        tabPanels[currentTabIndex].SetActive(true);
        tabs[currentTabIndex].SetActive(true);

        // Animate transition
        AnimateTabSwitch();
    }

    private void AnimateTabSwitch()
    {
        // Fade transition
        foreach (GameObject panel in tabPanels)
        {
            CanvasGroup canvasGroup = panel.GetComponent<CanvasGroup>();
            if (canvasGroup == null)
            {
                canvasGroup = panel.AddComponent<CanvasGroup>();
            }

            if (panel.activeSelf)
            {
                canvasGroup.alpha = 0;
                LeanTween.alphaCanvas(canvasGroup, 1, 0.2f);
            }
        }
    }
}

[System.Serializable]
public class TabButton
{
    public Button button;
    public Image icon;
    public Color activeColor;
    public Color inactiveColor;

    public void SetActive(bool active)
    {
        icon.color = active ? activeColor : inactiveColor;
    }
}

Step 5: Design for Different Orientations

Support both portrait and landscape modes.

Orientation Manager

public class OrientationManager : MonoBehaviour
{
    [Header("Orientation Settings")]
    public bool allowPortrait = true;
    public bool allowLandscape = true;

    [Header("UI Layouts")]
    public GameObject portraitLayout;
    public GameObject landscapeLayout;

    private ScreenOrientation currentOrientation;

    void Start()
    {
        // Set allowed orientations
        if (allowPortrait && allowLandscape)
        {
            Screen.orientation = ScreenOrientation.AutoRotation;
            Screen.autorotateToPortrait = true;
            Screen.autorotateToPortraitUpsideDown = false;
            Screen.autorotateToLandscapeLeft = true;
            Screen.autorotateToLandscapeRight = true;
        }
        else if (allowPortrait)
        {
            Screen.orientation = ScreenOrientation.Portrait;
        }
        else if (allowLandscape)
        {
            Screen.orientation = ScreenOrientation.LandscapeLeft;
        }

        currentOrientation = Screen.orientation;
        UpdateLayout();
    }

    void Update()
    {
        // Check for orientation change
        if (Screen.orientation != currentOrientation)
        {
            currentOrientation = Screen.orientation;
            UpdateLayout();
        }
    }

    private void UpdateLayout()
    {
        bool isPortrait = Screen.width < Screen.height;

        if (isPortrait)
        {
            // Show portrait layout
            if (portraitLayout != null) portraitLayout.SetActive(true);
            if (landscapeLayout != null) landscapeLayout.SetActive(false);
        }
        else
        {
            // Show landscape layout
            if (portraitLayout != null) portraitLayout.SetActive(false);
            if (landscapeLayout != null) landscapeLayout.SetActive(true);
        }
    }
}

Step 6: Optimize UI Performance

Mobile UI must be fast and efficient.

UI Optimization Tips

public class UIOptimizer : MonoBehaviour
{
    [Header("Optimization Settings")]
    public bool useObjectPooling = true;
    public int maxPoolSize = 50;

    private Dictionary<string, Queue<GameObject>> objectPools = new Dictionary<string, Queue<GameObject>>();

    public GameObject GetPooledObject(GameObject prefab)
    {
        string key = prefab.name;

        if (!objectPools.ContainsKey(key))
        {
            objectPools[key] = new Queue<GameObject>();
        }

        Queue<GameObject> pool = objectPools[key];

        if (pool.Count > 0)
        {
            GameObject obj = pool.Dequeue();
            obj.SetActive(true);
            return obj;
        }
        else
        {
            GameObject obj = Instantiate(prefab);
            return obj;
        }
    }

    public void ReturnToPool(GameObject obj)
    {
        string key = obj.name.Replace("(Clone)", "");

        if (!objectPools.ContainsKey(key))
        {
            objectPools[key] = new Queue<GameObject>();
        }

        if (objectPools[key].Count < maxPoolSize)
        {
            obj.SetActive(false);
            objectPools[key].Enqueue(obj);
        }
        else
        {
            Destroy(obj);
        }
    }
}

Canvas Optimization

public class CanvasOptimizer : MonoBehaviour
{
    void Start()
    {
        Canvas canvas = GetComponent<Canvas>();

        // Use separate canvas for static elements
        if (canvas.renderMode == RenderMode.ScreenSpaceOverlay)
        {
            // Disable pixel perfect for better performance
            canvas.pixelPerfect = false;
        }

        // Optimize canvas scaler
        CanvasScaler scaler = GetComponent<CanvasScaler>();
        if (scaler != null)
        {
            // Use scale with screen size for better performance
            scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
        }

        // Disable unnecessary raycasting
        GraphicRaycaster raycaster = GetComponent<GraphicRaycaster>();
        if (raycaster != null && GetComponent<Button>() == null)
        {
            // Only enable raycasting if needed for interaction
            raycaster.enabled = false;
        }
    }
}

Mini Challenge: Build Your Mobile UI

Create a complete mobile UI system with:

  1. Main menu - Start screen with navigation
  2. Gameplay UI - Score, moves, timer display
  3. Settings panel - Options and preferences
  4. Level select - Grid of available levels
  5. Responsive design - Works on different screen sizes
  6. Touch feedback - Visual and haptic feedback

Success Criteria:

  • All buttons are at least 60x60 pixels
  • UI adapts to different screen sizes
  • Navigation is intuitive and clear
  • Touch feedback feels responsive
  • Performance is smooth (60 FPS)

Common Mistakes to Avoid

1. Buttons Too Small

  • Use minimum 60x60 pixels for touch targets
  • Add padding between interactive elements
  • Test on actual devices, not just editor

2. Ignoring Safe Areas

  • Account for notches and status bars
  • Test on devices with different safe areas
  • Use safe area handlers

3. Poor Information Hierarchy

  • Most important info should be most prominent
  • Use size, color, and position to show importance
  • Don't clutter the screen

4. No Orientation Support

  • Support both portrait and landscape
  • Adapt layouts for each orientation
  • Test rotation transitions

5. Performance Issues

  • Optimize canvas settings
  • Use object pooling for dynamic UI
  • Minimize draw calls

Pro Tips

Design for Thumbs

  • Place important buttons in thumb-friendly zones
  • Keep navigation within easy reach
  • Consider one-handed vs two-handed use

Use Familiar Patterns

  • Follow platform design guidelines (iOS/Android)
  • Use familiar UI patterns players recognize
  • Don't reinvent the wheel

Test on Real Devices

  • Editor preview isn't enough
  • Test on multiple devices and screen sizes
  • Get feedback from real users

Optimize Early

  • Don't wait until the end to optimize
  • Profile UI performance regularly
  • Fix performance issues as you go

Troubleshooting

UI looks different on different devices

  • Check canvas scaler settings
  • Use anchor system properly
  • Test on multiple aspect ratios

Buttons don't respond to touch

  • Check button size (minimum 60x60)
  • Verify raycasting is enabled
  • Check if other UI is blocking touches

UI is too slow

  • Optimize canvas settings
  • Reduce number of UI elements
  • Use object pooling for dynamic elements

Layout breaks on rotation

  • Use anchor system for responsive layouts
  • Handle safe area changes
  • Test rotation transitions

What's Next?

Congratulations! You've created a professional mobile UI/UX system for your puzzle game. In the next lesson, you'll learn how to add audio and visual polish that makes your game feel complete and professional.

Coming Up: Lesson 7 will cover audio design, visual effects, haptic feedback, and creating that polished, professional feel that makes players want to keep playing.

Resources

Community Support

  • Discord Server: Share your UI designs and get feedback
  • GitHub Repository: Find UI templates and examples
  • Game Design Forums: Learn from other mobile game developers
  • Course Discussion: Show off your mobile UI

Ready to make your game look and feel amazing? Continue to Lesson 7: Audio & Visual Polish and learn how to add that final layer of polish.