Easy Learn C#

Unity Mobile Game Development

Mobile game development with Unity offers the opportunity to reach billions of potential players on iOS and Android platforms. This tutorial will guide you through the essential aspects of creating, optimizing, and deploying mobile games using Unity and C#.

Setting Up Your Mobile Project

Proper project setup is crucial for successful mobile development. Let's start with configuring Unity for mobile platforms.

Implementation Guidelines

  1. Create Project: Start a new Unity project using the Mobile template, or convert an existing project
  2. Configure Build Settings:
    • Open File → Build Settings
    • Select either Android or iOS as the target platform
    • Click "Switch Platform" to change Unity's build target
  3. Set Up Development Environment:
    • For Android: Install Android Studio, configure SDK/NDK paths in Unity (Edit → Preferences → External Tools)
    • For iOS: You'll need a Mac with Xcode installed
  4. Configure Player Settings:
    • Set proper app identification (bundle ID/package name)
    • Configure minimum OS versions
    • Set default orientation (portrait/landscape)
    • Configure icon and splash screen
  5. Establish Project Structure: Organize your project with folders for Scripts, Prefabs, Scenes, etc.

Mobile Configuration Helper

using UnityEngine;

// Helper class to manage mobile-specific settings at runtime
public class MobileConfigManager : MonoBehaviour
{
    [Header("Screen Settings")]
    public bool dontSleepScreen = true;    // Prevent screen from sleeping
    public bool fullScreen = true;         // Run in fullscreen mode
    
    [Header("Quality Settings")]
    [Range(0, 5)]
    public int defaultQualityLevel = 3;    // Default quality level (0-5)
    public bool dynamicResolution = true;  // Enable dynamic resolution scaling
    
    [Header("Frame Rate")]
    public int targetFrameRate = 60;       // Target frame rate (30/60)
    public bool vSyncEnabled = true;       // Enable VSync
    
    [Header("Debug Options")]
    public bool showFpsCounter = false;    // Show FPS counter in development builds
    
    // Track performance metrics
    private float deltaTime = 0.0f;
    private GUIStyle fpsStyle;
    
    private void Awake()
    {
        // Apply settings on startup
        ApplyMobileSettings();
    }
    
    private void ApplyMobileSettings()
    {
        // Screen sleep and display
        Screen.sleepTimeout = dontSleepScreen ? SleepTimeout.NeverSleep : SleepTimeout.SystemSetting;
        Screen.fullScreen = fullScreen;
        
        // Quality settings
        QualitySettings.SetQualityLevel(defaultQualityLevel);
        
        // Frame rate settings
        Application.targetFrameRate = targetFrameRate;
        QualitySettings.vSyncCount = vSyncEnabled ? 1 : 0;
        
        // Set up dynamic resolution if enabled
        if (dynamicResolution)
        {
            #if UNITY_2019_3_OR_NEWER
            DynamicResolutionHandler.SetDynamicResolutionEnabled(true);
            #endif
        }
        
        // Log applied settings
        Debug.Log($"Mobile settings applied: Quality Level {QualitySettings.GetQualityLevel()}, " +
                 $"Target FPS {Application.targetFrameRate}, VSync {QualitySettings.vSyncCount}");
        
        // Create FPS counter style
        if (showFpsCounter)
        {
            fpsStyle = new GUIStyle();
            fpsStyle.alignment = TextAnchor.UpperRight;
            fpsStyle.fontSize = Screen.height / 40;
            fpsStyle.normal.textColor = Color.white;
        }
    }
    
    private void Update()
    {
        // Update frame time for FPS counter
        if (showFpsCounter)
        {
            deltaTime += (Time.unscaledDeltaTime - deltaTime) * 0.1f;
        }
    }
    
    private void OnGUI()
    {
        // Draw FPS counter if enabled (development builds only)
        if (showFpsCounter && Debug.isDebugBuild)
        {
            float msec = deltaTime * 1000.0f;
            float fps = 1.0f / deltaTime;
            string text = string.Format("{0:0.0} ms ({1:0.} fps)", msec, fps);
            
            Rect rect = new Rect(20, 20, Screen.width - 40, Screen.height - 40);
            GUI.Label(rect, text, fpsStyle);
        }
    }
    
    // Detect when app is paused/resumed (when user switches apps)
    private void OnApplicationPause(bool pauseStatus)
    {
        if (pauseStatus)
        {
            // App is paused - save state if needed
            Debug.Log("Application paused");
        }
        else
        {
            // App is resumed - restore state if needed
            Debug.Log("Application resumed");
        }
    }
    
    // Called before app is terminated
    private void OnApplicationQuit()
    {
        // Save any necessary data before the app is closed
        Debug.Log("Application quitting");
    }
}

Device Compatibility Best Practices

Mobile devices vary greatly in capabilities. Here are some best practices for ensuring compatibility:

  • Test on Multiple Devices: Test on both high-end and low-end devices to ensure performance
  • Create Adaptive UI: Design UI that adapts to different screen sizes and aspect ratios
  • Implement Fallbacks: Provide fallbacks for features not supported on all devices
  • Target Reasonable Specs: Don't require the latest hardware features; maintain backward compatibility
  • Use Device Simulators: Unity's Device Simulator package helps test various screen sizes and orientations

Touch Input and Mobile Controls

Mobile games rely on touch input rather than keyboard and mouse. Let's implement responsive touch controls.

Implementation Guidelines

  1. Design for Touch: Create UI elements that are finger-friendly (at least 44x44 pixels)
  2. Handle Multiple Input Types: Support both simple taps and more complex gestures
  3. Create Input Feedback: Provide visual/audio feedback for touches
  4. Test on Real Devices: Touch input feels different on actual devices vs. simulators
  5. Consider Screen Areas: Place controls where they won't be blocked by thumbs

Basic Touch Input Manager

using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Events;

// Handles various touch input types for mobile games
public class TouchInputManager : MonoBehaviour
{
    [System.Serializable]
    public class TouchEvent : UnityEvent {}
    
    [Header("Single Touch Events")]
    public TouchEvent onTap;               // Fired when player taps the screen
    public TouchEvent onDoubleTap;         // Fired on double-tap
    public TouchEvent onLongPress;         // Fired when player holds finger down
    
    [Header("Gesture Settings")]
    public float doubleTapTimeThreshold = 0.3f;   // Max time between taps for double-tap
    public float longPressTimeThreshold = 0.7f;   // Time needed to trigger long press
    public float swipeThreshold = 50f;            // Min distance for swipe detection
    
    [Header("Swipe Events")]
    public TouchEvent onSwipeLeft;         // Fired on left swipe
    public TouchEvent onSwipeRight;        // Fired on right swipe
    public TouchEvent onSwipeUp;           // Fired on up swipe
    public TouchEvent onSwipeDown;         // Fired on down swipe
    
    [Header("Pinch/Zoom")]
    public UnityEvent onPinch;      // Fired during pinch with delta value

    // Touch state tracking
    private float lastTapTime;
    private Vector2 touchStartPosition;
    private float touchStartTime;
    private bool isLongPressing;
    private int longPressFingerID = -1;
    private List trackedFingers = new List();
    
    private void Update()
    {
        // Track touch count
        int touchCount = Input.touchCount;
        
        // Process multi-touch gestures first (pinch)
        if (touchCount >= 2)
        {
            ProcessPinchGesture();
        }
        
        // Process single touch events
        if (touchCount > 0)
        {
            foreach (Touch touch in Input.touches)
            {
                ProcessTouch(touch);
            }
        }
        
        // For editor/debug: simulate touch with mouse
        #if UNITY_EDITOR
        SimulateTouchWithMouse();
        #endif
    }
    
    private void ProcessTouch(Touch touch)
    {
        switch (touch.phase)
        {
            case TouchPhase.Began:
                // Store initial position and time
                if (!trackedFingers.Contains(touch.fingerId))
                {
                    trackedFingers.Add(touch.fingerId);
                    touchStartPosition = touch.position;
                    touchStartTime = Time.time;
                    
                    // Start tracking for long press
                    if (longPressFingerID == -1)
                    {
                        longPressFingerID = touch.fingerId;
                        isLongPressing = true;
                        Invoke("TriggerLongPress", longPressTimeThreshold);
                    }
                }
                break;
                
            case TouchPhase.Moved:
                // Cancel long press if finger moved too much
                if (isLongPressing && touch.fingerId == longPressFingerID)
                {
                    if (Vector2.Distance(touchStartPosition, touch.position) > 20)
                    {
                        CancelLongPress();
                    }
                }
                break;
                
            case TouchPhase.Ended:
                if (trackedFingers.Contains(touch.fingerId))
                {
                    // Cancel long press detection for this finger
                    if (touch.fingerId == longPressFingerID)
                    {
                        CancelLongPress();
                    }
                    
                    // Calculate touch duration and distance
                    float touchDuration = Time.time - touchStartTime;
                    float touchDistance = Vector2.Distance(touchStartPosition, touch.position);
                    
                    // Determine gesture type
                    if (touchDistance > swipeThreshold)
                    {
                        // This was a swipe
                        ProcessSwipe(touchStartPosition, touch.position);
                    }
                    else if (touchDuration < longPressTimeThreshold)
                    {
                        // This was a tap or double tap
                        float timeSinceLastTap = Time.time - lastTapTime;
                        
                        if (timeSinceLastTap <= doubleTapTimeThreshold)
                        {
                            // Double tap
                            onDoubleTap?.Invoke(touch.position);
                            lastTapTime = 0; // Reset to avoid triple-tap
                        }
                        else
                        {
                            // Single tap
                            onTap?.Invoke(touch.position);
                            lastTapTime = Time.time;
                        }
                    }
                    
                    // Remove from tracked fingers
                    trackedFingers.Remove(touch.fingerId);
                }
                break;
                
            case TouchPhase.Canceled:
                // Touch was canceled, clean up
                if (touch.fingerId == longPressFingerID)
                {
                    CancelLongPress();
                }
                
                if (trackedFingers.Contains(touch.fingerId))
                {
                    trackedFingers.Remove(touch.fingerId);
                }
                break;
        }
    }
    
    private void TriggerLongPress()
    {
        // Ensure we're still in a long press state
        if (isLongPressing && longPressFingerID != -1)
        {
            // Find the current position of the finger
            foreach (Touch touch in Input.touches)
            {
                if (touch.fingerId == longPressFingerID)
                {
                    onLongPress?.Invoke(touch.position);
                    break;
                }
            }
            
            isLongPressing = false;
        }
    }
    
    private void CancelLongPress()
    {
        if (isLongPressing)
        {
            CancelInvoke("TriggerLongPress");
            isLongPressing = false;
            longPressFingerID = -1;
        }
    }
    
    private void ProcessSwipe(Vector2 startPos, Vector2 endPos)
    {
        // Calculate swipe direction
        Vector2 direction = endPos - startPos;
        
        // Determine primary direction
        if (Mathf.Abs(direction.x) > Mathf.Abs(direction.y))
        {
            // Horizontal swipe
            if (direction.x > 0)
            {
                onSwipeRight?.Invoke(endPos);
            }
            else
            {
                onSwipeLeft?.Invoke(endPos);
            }
        }
        else
        {
            // Vertical swipe
            if (direction.y > 0)
            {
                onSwipeUp?.Invoke(endPos);
            }
            else
            {
                onSwipeDown?.Invoke(endPos);
            }
        }
    }
    
    private void ProcessPinchGesture()
    {
        // Need exactly 2 touches for pinch
        if (Input.touchCount != 2)
            return;
            
        Touch touch1 = Input.GetTouch(0);
        Touch touch2 = Input.GetTouch(1);
        
        // Check if either touch just began or ended
        if (touch1.phase == TouchPhase.Began || touch2.phase == TouchPhase.Began ||
            touch1.phase == TouchPhase.Ended || touch2.phase == TouchPhase.Ended)
        {
            // Just store the current distance
            return;
        }
        
        // Calculate current and previous touch positions
        Vector2 touch1PrevPos = touch1.position - touch1.deltaPosition;
        Vector2 touch2PrevPos = touch2.position - touch2.deltaPosition;
        
        // Calculate distance between touches (current and previous frame)
        float prevTouchDeltaMag = (touch1PrevPos - touch2PrevPos).magnitude;
        float touchDeltaMag = (touch1.position - touch2.position).magnitude;
        
        // Calculate the difference in distances
        float deltaMagnitudeDiff = touchDeltaMag - prevTouchDeltaMag;
        
        // Trigger pinch event with delta value
        onPinch?.Invoke(deltaMagnitudeDiff);
    }
    
    #if UNITY_EDITOR
    // Mouse simulation for testing in editor
    private bool isMouseDown = false;
    private Vector2 mouseDownPosition;
    private float mouseDownTime;
    
    private void SimulateTouchWithMouse()
    {
        // Mouse button down - simulate touch begin
        if (Input.GetMouseButtonDown(0))
        {
            isMouseDown = true;
            mouseDownPosition = Input.mousePosition;
            mouseDownTime = Time.time;
            
            // Start long press detection
            isLongPressing = true;
            longPressFingerID = 0; // Use 0 for mouse
            Invoke("TriggerLongPress", longPressTimeThreshold);
        }
        
        // Mouse moved while button down - simulate touch move
        if (isMouseDown && Input.GetMouseButton(0))
        {
            // Check if moved far enough to cancel long press
            if (isLongPressing && Vector2.Distance(mouseDownPosition, (Vector2)Input.mousePosition) > 20)
            {
                CancelLongPress();
            }
        }
        
        // Mouse button up - simulate touch end
        if (Input.GetMouseButtonUp(0) && isMouseDown)
        {
            // Cancel long press
            if (isLongPressing)
            {
                CancelLongPress();
            }
            
            // Calculate duration and distance
            float clickDuration = Time.time - mouseDownTime;
            float clickDistance = Vector2.Distance(mouseDownPosition, Input.mousePosition);
            
            // Determine gesture type
            if (clickDistance > swipeThreshold)
            {
                // This was a swipe
                ProcessSwipe(mouseDownPosition, Input.mousePosition);
            }
            else if (clickDuration < longPressTimeThreshold)
            {
                // This was a tap or double tap
                float timeSinceLastTap = Time.time - lastTapTime;
                
                if (timeSinceLastTap <= doubleTapTimeThreshold)
                {
                    // Double tap
                    onDoubleTap?.Invoke(Input.mousePosition);
                    lastTapTime = 0; // Reset to avoid triple-tap
                }
                else
                {
                    // Single tap
                    onTap?.Invoke(Input.mousePosition);
                    lastTapTime = Time.time;
                }
            }
            
            isMouseDown = false;
        }
    }
    #endif

Touch Input and Mobile Controls

Mobile games rely on touch input rather than keyboard and mouse. Let's implement responsive touch controls.

Implementation Guidelines

  1. Design for Touch: Create UI elements that are finger-friendly (at least 44x44 pixels)
  2. Handle Multiple Input Types: Support both simple taps and more complex gestures
  3. Create Input Feedback: Provide visual/audio feedback for touches
  4. Test on Real Devices: Touch input feels different on actual devices vs. simulators
  5. Consider Screen Areas: Place controls where they won't be blocked by thumbs

Basic Touch Input Manager

using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Events;

// Handles various touch input types for mobile games
public class TouchInputManager : MonoBehaviour
{
    [System.Serializable]
    public class TouchEvent : UnityEvent {}
    
    [Header("Single Touch Events")]
    public TouchEvent onTap;               // Fired when player taps the screen
    public TouchEvent onDoubleTap;         // Fired on double-tap
    public TouchEvent onLongPress;         // Fired when player holds finger down
    
    [Header("Gesture Settings")]
    public float doubleTapTimeThreshold = 0.3f;   // Max time between taps for double-tap
    public float longPressTimeThreshold = 0.7f;   // Time needed to trigger long press
    public float swipeThreshold = 50f;            // Min distance for swipe detection
    
    [Header("Swipe Events")]
    public TouchEvent onSwipeLeft;         // Fired on left swipe
    public TouchEvent onSwipeRight;        // Fired on right swipe
    public TouchEvent onSwipeUp;           // Fired on up swipe
    public TouchEvent onSwipeDown;         // Fired on down swipe
    
    [Header("Pinch/Zoom")]
    public UnityEvent onPinch;      // Fired during pinch with delta value

    // Touch state tracking
    private float lastTapTime;
    private Vector2 touchStartPosition;
    private float touchStartTime;
    private bool isLongPressing;
    private int longPressFingerID = -1;
    private List trackedFingers = new List();
    
    private void Update()
    {
        // Track touch count
        int touchCount = Input.touchCount;
        
        // Process multi-touch gestures first (pinch)
        if (touchCount >= 2)
        {
            ProcessPinchGesture();
        }
        
        // Process single touch events
        if (touchCount > 0)
        {
            foreach (Touch touch in Input.touches)
            {
                ProcessTouch(touch);
            }
        }
        
        // For editor/debug: simulate touch with mouse
        #if UNITY_EDITOR
        SimulateTouchWithMouse();
        #endif
    }
    
    private void ProcessTouch(Touch touch)
    {
        switch (touch.phase)
        {
            case TouchPhase.Began:
                // Store initial position and time
                if (!trackedFingers.Contains(touch.fingerId))
                {
                    trackedFingers.Add(touch.fingerId);
                    touchStartPosition = touch.position;
                    touchStartTime = Time.time;
                    
                    // Start tracking for long press
                    if (longPressFingerID == -1)
                    {
                        longPressFingerID = touch.fingerId;
                        isLongPressing = true;
                        Invoke("TriggerLongPress", longPressTimeThreshold);
                    }
                }
                break;
                
            case TouchPhase.Moved:
                // Cancel long press if finger moved too much
                if (isLongPressing && touch.fingerId == longPressFingerID)
                {
                    if (Vector2.Distance(touchStartPosition, touch.position) > 20)
                    {
                        CancelLongPress();
                    }
                }
                break;
                
            case TouchPhase.Ended:
                if (trackedFingers.Contains(touch.fingerId))
                {
                    // Cancel long press detection for this finger
                    if (touch.fingerId == longPressFingerID)
                    {
                        CancelLongPress();
                    }
                    
                    // Calculate touch duration and distance
                    float touchDuration = Time.time - touchStartTime;
                    float touchDistance = Vector2.Distance(touchStartPosition, touch.position);
                    
                    // Determine gesture type
                    if (touchDistance > swipeThreshold)
                    {
                        // This was a swipe
                        ProcessSwipe(touchStartPosition, touch.position);
                    }
                    else if (touchDuration < longPressTimeThreshold)
                    {
                        // This was a tap or double tap
                        float timeSinceLastTap = Time.time - lastTapTime;
                        
                        if (timeSinceLastTap <= doubleTapTimeThreshold)
                        {
                            // Double tap
                            onDoubleTap?.Invoke(touch.position);
                            lastTapTime = 0; // Reset to avoid triple-tap
                        }
                        else
                        {
                            // Single tap
                            onTap?.Invoke(touch.position);
                            lastTapTime = Time.time;
                        }
                    }
                    
                    // Remove from tracked fingers
                    trackedFingers.Remove(touch.fingerId);
                }
                break;
                
            case TouchPhase.Canceled:
                // Touch was canceled, clean up
                if (touch.fingerId == longPressFingerID)
                {
                    CancelLongPress();
                }
                
                if (trackedFingers.Contains(touch.fingerId))
                {
                    trackedFingers.Remove(touch.fingerId);
                }
                break;
        }
    }
    
    private void TriggerLongPress()
    {
        // Ensure we're still in a long press state
        if (isLongPressing && longPressFingerID != -1)
        {
            // Find the current position of the finger
            foreach (Touch touch in Input.touches)
            {
                if (touch.fingerId == longPressFingerID)
                {
                    onLongPress?.Invoke(touch.position);
                    break;
                }
            }
            
            isLongPressing = false;
        }
    }
    
    private void CancelLongPress()
    {
        if (isLongPressing)
        {
            CancelInvoke("TriggerLongPress");
            isLongPressing = false;
            longPressFingerID = -1;
        }
    }
    
    private void ProcessSwipe(Vector2 startPos, Vector2 endPos)
    {
        // Calculate swipe direction
        Vector2 direction = endPos - startPos;
        
        // Determine primary direction
        if (Mathf.Abs(direction.x) > Mathf.Abs(direction.y))
        {
            // Horizontal swipe
            if (direction.x > 0)
            {
                onSwipeRight?.Invoke(endPos);
            }
            else
            {
                onSwipeLeft?.Invoke(endPos);
            }
        }
        else
        {
            // Vertical swipe
            if (direction.y > 0)
            {
                onSwipeUp?.Invoke(endPos);
            }
            else
            {
                onSwipeDown?.Invoke(endPos);
            }
        }
    }
    
    private void ProcessPinchGesture()
    {
        // Need exactly 2 touches for pinch
        if (Input.touchCount != 2)
            return;
            
        Touch touch1 = Input.GetTouch(0);
        Touch touch2 = Input.GetTouch(1);
        
        // Check if either touch just began or ended
        if (touch1.phase == TouchPhase.Began || touch2.phase == TouchPhase.Began ||
            touch1.phase == TouchPhase.Ended || touch2.phase == TouchPhase.Ended)
        {
            // Just store the current distance
            return;
        }
        
        // Calculate current and previous touch positions
        Vector2 touch1PrevPos = touch1.position - touch1.deltaPosition;
        Vector2 touch2PrevPos = touch2.position - touch2.deltaPosition;
        
        // Calculate distance between touches (current and previous frame)
        float prevTouchDeltaMag = (touch1PrevPos - touch2PrevPos).magnitude;
        float touchDeltaMag = (touch1.position - touch2.position).magnitude;
        
        // Calculate the difference in distances
        float deltaMagnitudeDiff = touchDeltaMag - prevTouchDeltaMag;
        
        // Trigger pinch event with delta value
        onPinch?.Invoke(deltaMagnitudeDiff);
    }
    
    #if UNITY_EDITOR
    // Mouse simulation for testing in editor
    private bool isMouseDown = false;
    private Vector2 mouseDownPosition;
    private float mouseDownTime;
    
    private void SimulateTouchWithMouse()
    {
        // Mouse button down - simulate touch begin
        if (Input.GetMouseButtonDown(0))
        {
            isMouseDown = true;
            mouseDownPosition = Input.mousePosition;
            mouseDownTime = Time.time;
            
            // Start long press detection
            isLongPressing = true;
            longPressFingerID = 0; // Use 0 for mouse
            Invoke("TriggerLongPress", longPressTimeThreshold);
        }
        
        // Mouse moved while button down - simulate touch move
        if (isMouseDown && Input.GetMouseButton(0))
        {
            // Check if moved far enough to cancel long press
            if (isLongPressing && Vector2.Distance(mouseDownPosition, (Vector2)Input.mousePosition) > 20)
            {
                CancelLongPress();
            }
        }
        
        // Mouse button up - simulate touch end
        if (Input.GetMouseButtonUp(0) && isMouseDown)
        {
            // Cancel long press
            if (isLongPressing)
            {
                CancelLongPress();
            }
            
            // Calculate duration and distance
            float clickDuration = Time.time - mouseDownTime;
            float clickDistance = Vector2.Distance(mouseDownPosition, Input.mousePosition);
            
            // Determine gesture type
            if (clickDistance > swipeThreshold)
            {
                // This was a swipe
                ProcessSwipe(mouseDownPosition, Input.mousePosition);
            }
            else if (clickDuration < longPressTimeThreshold)
            {
                // This was a tap or double tap
                float timeSinceLastTap = Time.time - lastTapTime;
                
                if (timeSinceLastTap <= doubleTapTimeThreshold)
                {
                    // Double tap
                    onDoubleTap?.Invoke(Input.mousePosition);
                    lastTapTime = 0; // Reset to avoid triple-tap
                }
                else
                {
                    // Single tap
                    onTap?.Invoke(Input.mousePosition);
                    lastTapTime = Time.time;
                }
            }
            
            isMouseDown = false;
        }
    }
    #endif
}

Mobile Joystick Controller

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

// Virtual joystick for mobile character movement
public class VirtualJoystick : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler
{
    [Header("Components")]
    public RectTransform backgroundRect;   // The outer background circle
    public RectTransform handleRect;       // The inner handle/knob
    
    [Header("Settings")]
    public float joystickRange = 50f;      // Maximum distance handle can move from center
    public bool hideJoystickUntilTouched = true; // Hide joystick until used
    public bool resetHandleOnRelease = true;     // Reset handle to center on release
    public bool isDynamicJoystick = false;       // Joystick appears at touch position
    
    [Header("Output")]
    public Vector2 inputVector;             // Current input direction vector (normalized)
    public float inputMagnitude;            // Input strength (0-1)
    
    // Private variables
    private Vector2 joystickCenter;         // Center position of joystick
    private Canvas parentCanvas;            // Reference to parent canvas
    private CanvasGroup canvasGroup;        // For opacity control
    
    private void Awake()
    {
        // Get component references
        parentCanvas = GetComponentInParent();
        canvasGroup = GetComponent();
        
        // Create CanvasGroup if needed
        if (canvasGroup == null && hideJoystickUntilTouched)
        {
            canvasGroup = gameObject.AddComponent();
        }
        
        // Initialize joystick center
        joystickCenter = backgroundRect.position;
        
        // Hide joystick if configured
        if (hideJoystickUntilTouched && canvasGroup != null)
        {
            canvasGroup.alpha = 0f;
        }
        
        // Reset input
        inputVector = Vector2.zero;
        inputMagnitude = 0f;
    }
    
    // Called when pointer touches the joystick area
    public void OnPointerDown(PointerEventData eventData)
    {
        // Show joystick
        if (hideJoystickUntilTouched && canvasGroup != null)
        {
            canvasGroup.alpha = 1f;
        }
        
        // For dynamic joystick, move the background to touch position
        if (isDynamicJoystick)
        {
            // Convert screen position to canvas position
            Vector2 touchPos;
            RectTransformUtility.ScreenPointToLocalPointInRectangle(
                parentCanvas.GetComponent(),
                eventData.position,
                parentCanvas.worldCamera,
                out touchPos
            );
            
            // Set new position
            backgroundRect.localPosition = touchPos;
            joystickCenter = backgroundRect.position;
        }
        
        // Process the drag
        OnDrag(eventData);
    }
    
    // Called when pointer is dragged over the joystick
    public void OnDrag(PointerEventData eventData)
    {
        // Convert screen position to canvas position
        Vector2 touchPos;
        if (RectTransformUtility.ScreenPointToLocalPointInRectangle(
            backgroundRect,
            eventData.position,
            eventData.pressEventCamera,
            out touchPos))
        {
            // Calculate input vector (direction from center to touch)
            Vector2 direction = touchPos;
            
            // Normalize input based on joystick range
            inputMagnitude = Mathf.Clamp01(direction.magnitude / joystickRange);
            
            // Normalize direction
            inputVector = (direction.magnitude > 0.01f) ? direction.normalized : Vector2.zero;
            
            // Move handle to touch position, clamped to joystick range
            Vector2 clampedPosition = inputVector * (joystickRange * inputMagnitude);
            handleRect.anchoredPosition = clampedPosition;
        }
    }
    
    // Called when pointer is released from joystick
    public void OnPointerUp(PointerEventData eventData)
    {
        // Reset handle position if configured
        if (resetHandleOnRelease)
        {
            handleRect.anchoredPosition = Vector2.zero;
        }
        
        // Hide joystick if configured
        if (hideJoystickUntilTouched && canvasGroup != null)
        {
            canvasGroup.alpha = 0f;
        }
        
        // Reset input
        inputVector = Vector2.zero;
        inputMagnitude = 0f;
    }
    
    // Method to get input for movement
    public Vector2 GetMovementInput()
    {
        return inputVector * inputMagnitude;
    }
}

Best Practices for Mobile Controls

Creating effective mobile controls requires careful design:

  • Keep it Simple: Mobile controls should be intuitive and minimal
  • Provide Options: Let players customize controls and sensitivity
  • Give Feedback: Visual and haptic feedback helps confirm input
  • Use Native Features: Consider gyroscope, accelerometer, and other sensors
  • Place Controls Thoughtfully: Position controls for thumbs in portrait/landscape mode