Mobile Game Development — Unity & Godot for Mobile
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
Why use object pooling on mobile? Avoids GC spikes (which cause frame drops). Allocation on mobile is 10-100x more expensive than desktop.
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).
How do you handle device fragmentation? Target the 50th percentile device (e.g., Snapdragon 765G), use quality settings tiers, test on 5+ real devices.
What is dynamic resolution scaling? Lowering render resolution during intense scenes to maintain frame rate. Uses
ScalableBufferManagerin Unity.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.
Related Tutorials
- Unity Guide — Unity game engine fundamentals
- Godot — Godot Engine basics
- Game Optimization — General optimization techniques
- Next: Game QA Testing — Testing & Debugging Games Guide
- Previous: Game Narrative Design — Storytelling in Games Guide
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro