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
- Create Project: Start a new Unity project using the Mobile template, or convert an existing project
- 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
- 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
- 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
- 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
- Design for Touch: Create UI elements that are finger-friendly (at least 44x44 pixels)
- Handle Multiple Input Types: Support both simple taps and more complex gestures
- Create Input Feedback: Provide visual/audio feedback for touches
- Test on Real Devices: Touch input feels different on actual devices vs. simulators
- 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
- Design for Touch: Create UI elements that are finger-friendly (at least 44x44 pixels)
- Handle Multiple Input Types: Support both simple taps and more complex gestures
- Create Input Feedback: Provide visual/audio feedback for touches
- Test on Real Devices: Touch input feels different on actual devices vs. simulators
- 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
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