Skip to content

Mobile Game Development — Unity & Godot for Mobile

DodaTech Updated 2026-06-21 10 min read

In this tutorial, you'll learn about Mobile Game Development. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Mobile Game Development adapts game engines like Unity and Godot for smartphones and tablets, addressing unique constraints — touch input, smaller screens, battery life, thermal throttling, and fragmented device hardware — while maintaining engaging gameplay.

What You'll Learn

You'll create a mobile game in Unity and Godot with touch controls, optimize for 60fps on mid-range devices, manage memory for texture and audio, handle device rotations and lifecycle events, and build platform-specific releases for iOS and Android.

Why Mobile Game Development Matters

Mobile gaming generates $90B+ annually — over 50% of the entire gaming market. Unlike PC/console, mobile games must run on hundreds of device configurations. A game that runs at 60fps on a Snapdragon 8 Gen 3 might stutter at 20fps on a MediaTek Helio. At DodaTech, we apply mobile optimization techniques from Game Development to our Doda Browser — ensuring smooth 60fps scrolling even on budget devices.

Real-World Use Case

A hyper-casual game launches with 15MB APK size, runs at 60fps on 85% of devices, and drains 5% battery per 30 minutes. The competitor's similar game is 80MB, runs at 30fps on mid-range devices, and drains 15% battery per 30 minutes. The optimized game achieves 3x higher retention due to smooth performance.

Unity vs Godot for Mobile

Feature Unity Godot 4
Build Size 15-30MB (minimal) 10-20MB
Scripting C# GDScript, C#, C++
2D Performance Excellent Excellent
3D Performance Better on low-end Needs optimization
Mobile Plugins Mature ecosystem Growing ecosystem
Learning Curve Steeper Gentler

Touch Controls in Unity

using UnityEngine;

public class TouchController : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 5f;
    [SerializeField] private float swipeThreshold = 50f;
    
    private Vector2 touchStartPos;
    private float startTime;
    private Camera mainCamera;
    
    void Start()
    {
        mainCamera = Camera.main;
        Application.targetFrameRate = 60;
    }
    
    void Update()
    {
        if (Input.touchCount == 0) return;
        
        Touch touch = Input.GetTouch(0);
        
        switch (touch.phase)
        {
            case TouchPhase.Began:
                touchStartPos = touch.position;
                startTime = Time.time;
                break;
                
            case TouchPhase.Moved:
                // Drag to move character
                Vector3 touchPos = mainCamera.ScreenToWorldPoint(
                    new Vector3(touch.position.x, touch.position.y, 10f));
                Vector3 direction = (touchPos - transform.position).normalized;
                transform.position += direction * moveSpeed * Time.deltaTime;
                break;
                
            case TouchPhase.Ended:
                // Detect swipe
                Vector2 swipeDelta = touch.position - touchStartPos;
                float swipeDuration = Time.time - startTime;
                
                if (swipeDelta.magnitude > swipeThreshold && swipeDuration < 0.5f)
                {
                    HandleSwipe(swipeDelta.normalized);
                }
                
                // Detect tap (short touch, minimal movement)
                if (swipeDelta.magnitude < 10f && swipeDuration < 0.3f)
                {
                    HandleTap(touch.position);
                }
                break;
        }
    }
    
    void HandleSwipe(Vector2 direction)
    {
        if (Mathf.Abs(direction.x) > Mathf.Abs(direction.y))
        {
            // Horizontal swipe
            if (direction.x > 0)
                Debug.Log("Swiped RIGHT — dash!");
            else
                Debug.Log("Swiped LEFT — dodge!");
        }
        else
        {
            // Vertical swipe
            if (direction.y > 0)
                Debug.Log("Swiped UP — jump!");
            else
                Debug.Log("Swiped DOWN — slide!");
        }
    }
    
    void HandleTap(Vector2 screenPos)
    {
        Ray ray = mainCamera.ScreenPointToRay(screenPos);
        if (Physics.Raycast(ray, out RaycastHit hit))
        {
            if (hit.collider.CompareTag("Enemy"))
            {
                Debug.Log($"Tapped enemy: {hit.collider.name}");
                // Trigger attack animation
            }
        }
    }
}

Expected output: Player touches screen to drag-move character. Swipes trigger dash/dodge/jump/slide. Taps on enemies trigger attacks. Frame rate locked to 60fps.

Godot Touch Input (GDScript)

extends CharacterBody2D

@export var move_speed := 300.0
@export var jump_velocity := -400.0
@export var swipe_threshold := 50

var touch_start := Vector2.ZERO
var was_swipe := false

func _input(event):
    if event is InputEventScreenTouch:
        if event.pressed:
            touch_start = event.position
            was_swipe = false
        else:
            # Touch released
            var delta = event.position - touch_start
            if delta.length() > swipe_threshold and not was_swipe:
                handle_swipe(delta)
            elif delta.length() < 10:
                handle_tap(event.position)
    
    elif event is InputEventScreenDrag:
        # Continuous drag for joystick-style control
        var drag_delta = event.relative
        # Move character based on drag
        velocity = drag_delta * 2
        was_swipe = true

func handle_swipe(delta: Vector2):
    if abs(delta.x) > abs(delta.y):
        # Horizontal
        velocity.x = sign(delta.x) * move_speed * 3  # Dash
        print("Dash: ", "right" if delta.x > 0 else "left")
    else:
        # Vertical
        if delta.y < 0:
            velocity.y = jump_velocity  # Jump
            print("Jump!")

func handle_tap(pos: Vector2):
    # Check if tapping on enemy (using area detection)
    var space_state = get_world_2d().direct_space_state
    var query = PhysicsPointQueryParameters2D.new()
    query.position = get_global_mouse_position()  # Would need camera transform
    var results = space_state.intersect_point(query)
    for result in results:
        if result.collider.has_method("take_damage"):
            result.collider.take_damage(10)
            print("Attacked enemy!")

Expected output: In Godot, touch and drag moves the character with velocity, swipes trigger dashes/jumps, and taps attack enemies.

Mobile Optimization Techniques

Texture Atlasing and Compression

// Unity — Texture optimization script
using UnityEditor;
using UnityEngine;

public static class MobileTextureOptimizer
{
    [MenuItem("Tools/Optimize Textures for Mobile")]
    public static void OptimizeAllTextures()
    {
        string[] guids = AssetDatabase.FindAssets("t:Texture");
        int count = 0;
        
        foreach (string guid in guids)
        {
            string path = AssetDatabase.GUIDToAssetPath(guid);
            TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter;
            
            if (importer == null) continue;
            
            // Mobile-optimized settings
            importer.maxTextureSize = 1024;  // Cap at 1024x1024
            importer.compressionQuality = 50;
            importer.textureCompression = TextureImporterCompression.Compressed;
            
            // Platform-specific overrides
            TextureImporterPlatformSettings androidSettings = 
                importer.GetPlatformTextureSettings("Android");
            androidSettings.overridden = true;
            androidSettings.format = TextureImporterFormat.ASTC_6x6;
            androidSettings.maxTextureSize = 1024;
            importer.SetPlatformTextureSettings(androidSettings);
            
            TextureImporterPlatformSettings iosSettings = 
                importer.GetPlatformTextureSettings("iPhone");
            iosSettings.overridden = true;
            iosSettings.format = TextureImporterFormat.ASTC_6x6;
            iosSettings.maxTextureSize = 1024;
            importer.SetPlatformTextureSettings(iosSettings);
            
            importer.SaveAndReimport();
            count++;
        }
        
        Debug.Log($"Optimized {count} textures for mobile");
    }
}

Expected output: Running the menu item processes all textures — reducing total texture memory by 60-80% with minimal visual quality loss on mobile screens.

Object Pooling for Mobile

using System.Collections.Generic;
using UnityEngine;

public class MobileObjectPool : MonoBehaviour
{
    [SerializeField] private GameObject prefab;
    [SerializeField] private int poolSize = 20;
    
    private Queue<GameObject> pool = new Queue<GameObject>();
    
    void Awake()
    {
        // Pre-instantiate objects (avoids runtime allocation)
        for (int i = 0; i < poolSize; i++)
        {
            GameObject obj = Instantiate(prefab, transform);
            obj.SetActive(false);
            pool.Enqueue(obj);
        }
    }
    
    public GameObject Get()
    {
        if (pool.Count == 0)
        {
            // Pool exhausted — recycle oldest (or expand)
            Debug.LogWarning("Pool exhausted, consider increasing poolSize");
            return null;
        }
        
        GameObject obj = pool.Dequeue();
        obj.SetActive(true);
        return obj;
    }
    
    public void Return(GameObject obj)
    {
        obj.SetActive(false);
        obj.transform.SetParent(transform);
        pool.Enqueue(obj);
    }
}

// Usage in bullet spawner
public class BulletSpawner : MonoBehaviour
{
    [SerializeField] private MobileObjectPool bulletPool;
    
    void Fire()
    {
        GameObject bullet = bulletPool.Get();
        if (bullet != null)
        {
            bullet.transform.position = transform.position;
            // Configure bullet
        }
    }
    
    void OnBulletExpired(GameObject bullet)
    {
        bulletPool.Return(bullet);
    }
}

Expected output: No Garbage Collection spikes during gameplay — bullets are recycled instead of destroyed/created. Zero allocation during gameplay.

Mermaid Diagram: Mobile Game Optimization Pipeline

flowchart LR
    A[Art Assets] --> B[Texture Atlasing]
    B --> C[ASTC Compression]
    C --> D[1024px Max]
    D --> E[Build Asset Bundles]
    
    F[Code] --> G[Object Pooling]
    G --> H[No GC Allocations]
    H --> I[Fixed Timestep]
    
    J[Audio] --> K[Compressed Vorbis/MP3]
    K --> L[Load on Demand]
    
    E & I & L --> M[Mobile Build]
    M --> N{Test on Devices}
    N -->|60fps| O[Release]
    N -->|<60fps| A
    
    style A fill:#d4edda
    style M fill:#fff3cd
    style O fill:#cce5ff

Device-Specific Optimizations

Optimization Impact Implementation
Dynamic Resolution 2x performance Lower render scale when thermal throttling
Texture Streaming 50% less RAM Load mipmap levels based on camera distance
Audio Compression 80% smaller Use Vorbis at 96kbps for mobile
LOD Groups 3x draw calls 3 LOD levels (100%, 50%, 10% triangles)
Batching 5x draw calls Static/GPU instancing for repeated objects

Common Mobile Game Development Errors

1. Ignoring Thermal Throttling

Problem: Game runs at 60fps for 5 minutes, then drops to 20fps as device heats up. Fix: Monitor temperature via SystemInfo.deviceModel, dynamically reduce render scale when hot.

2. No Touch Feedback

Problem: Player taps but no visual/audio feedback — feels unresponsive. Fix: Add tap particle effects, button press animations, haptic feedback on supported devices.

3. Oversized APK

Problem: 200MB APK — 40% of users on slow connections abandon download. Fix: Use Android App Bundle (150MB limit), asset delivery (Play Asset Delivery), texture compression.

4. Notch/Cutout Ignorance

Problem: UI hidden under phone notch or punch-hole camera. Fix: Use Screen.safeArea in Unity or Godot's DisplayServer.window_get_safe_area().

5. Portrait-Only Without Rotation

Problem: Game fixed to portrait, but tablet users expect landscape. Fix: Support both orientations with responsive UI or clearly lock orientation with message.

6. Memory Leaks from Asset Loading

Problem: RAM grows 50MB per level switch, eventually crashes. Fix: Unload unused assets: Resources.UnloadUnusedAssets(), use addressables with ref counting.

Practice Questions

  1. Why use object pooling on mobile? Avoids GC spikes (which cause frame drops). Allocation on mobile is 10-100x more expensive than desktop.

  2. What is ASTC texture compression? Adaptive Scalable Texture Compression — the best quality/size ratio for mobile. Supported on all modern GPUs (Adreno, Mali, Apple GPU).

  3. How do you handle device fragmentation? Target the 50th percentile device (e.g., Snapdragon 765G), use quality settings tiers, test on 5+ real devices.

  4. What is dynamic resolution scaling? Lowering render resolution during intense scenes to maintain frame rate. Uses ScalableBufferManager in Unity.

  5. Why is draw call batching important for mobile? Each draw call has CPU overhead. Mobile CPUs have fewer cores and lower clock speeds — target <100 draw calls for 60fps.

Challenge

Build a complete mobile endless runner in Unity or Godot: implement touch swipe controls (left/right lanes, jump, slide), object pool enemies and obstacles, optimize with texture atlasing and LOD, support safe area for notched devices, and profile to achieve 60fps on a mid-range Android device (e.g., Moto G Power).

Real-World Task

Your mobile game has a 180MB APK and 3-star rating citing "lag on my phone". Analyze: 85% of textures are 2048x2048 uncompressed. Compress to ASTC 6x6, reduce max to 1024, implement object pooling for projectiles, add dynamic resolution targeting 30fps minimum. Measure: APK size target 60MB, 60fps on Snapdragon 690+.

Mini Project: Mobile Performance Monitor

using UnityEngine;
using UnityEngine.UI;

public class MobilePerformanceHUD : MonoBehaviour
{
    [SerializeField] private Text fpsText;
    [SerializeField] private Text memoryText;
    [SerializeField] private Text batteryText;
    
    private float deltaTime = 0;
    private int frameCount = 0;
    private float fpsAccumulator = 0;
    
    void Update()
    {
        // Smooth FPS calculation
        frameCount++;
        fpsAccumulator += Time.unscaledDeltaTime;
        if (fpsAccumulator >= 0.5f)
        {
            float fps = frameCount / fpsAccumulator;
            fpsText.text = $"FPS: {fps:F0}";
            
            // Color code based on performance
            fpsText.color = fps switch
            {
                >= 55 => Color.green,
                >= 30 => Color.yellow,
                _ => Color.red
            };
            
            frameCount = 0;
            fpsAccumulator = 0;
        }
        
        // Memory
        long memoryMB = System.GC.GetTotalMemory(false) / (1024 * 1024);
        memoryText.text = $"RAM: {memoryMB}MB";
        memoryText.color = memoryMB < 200 ? Color.green : Color.yellow;
        
        // Battery (Android only)
        if (Application.platform == RuntimePlatform.Android)
        {
            using (var player = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
            {
                using (var activity = player.GetStatic<AndroidJavaObject>("currentActivity"))
                {
                    using (var filter = new AndroidJavaObject("android.content.IntentFilter", 
                           "android.intent.action.BATTERY_CHANGED"))
                    {
                        using (var battery = activity.Call<AndroidJavaObject>(
                               "registerReceiver", null, filter))
                        {
                            int level = battery.Call<int>("getIntExtra", "level", -1);
                            int scale = battery.Call<int>("getIntExtra", "scale", -1);
                            int percent = level * 100 / scale;
                            batteryText.text = $"Battery: {percent}%";
                        }
                    }
                }
            }
        }
    }
}

Expected output: A HUD overlay showing real-time FPS, RAM usage, and battery level — essential for detecting performance regressions during mobile testing.

  • Unity Guide — Unity game engine fundamentals
  • GodotGodot Engine basics
  • Game Optimization — General optimization techniques
  • Next: Game QA Testing — Testing & Debugging Games Guide
  • Previous: Game Narrative Design — Storytelling in Games Guide
Should I develop for iOS or Android first?

Android first (wider user base, easier testing, no review process). Port to iOS after core gameplay is validated. Use cross-platform frameworks (Unity, Godot) to minimize porting effort.

How do I test on real mobile devices?

Use Unity Remote or Godot's one-click deploy for rapid iteration. For final testing, run on 5+ physical devices covering low/mid/high tiers. Use Firebase Test Lab or AWS Device Farm for automated testing.

What frame rate should I target for mobile?

60fps for premium devices, 30fps as minimum for low-end. Never ship below 30fps. Use adaptive quality: start at 60fps, drop to 30fps if thermal throttling detected.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro