Introduction: From Project to Product
Series Overview: This is Part 8 of our 16-part Unity Game Engine Series. We transition from creating game content to shipping it — covering build configuration, optimization for multiple platforms, store submission workflows, and monetization strategies.
1
Unity Basics & Interface
Editor overview, assets, prefabs, architecture
2
C# Scripting Fundamentals
MonoBehaviour, coroutines, input systems, patterns
3
GameObjects & Components
Transforms, renderers, custom components
4
Physics & Collisions
Rigidbody, colliders, raycasting, forces
5
UI Systems
Canvas, uGUI, UI Toolkit, responsive design
6
Animation & State Machines
Animator, blend trees, IK, Timeline
7
Audio & Visual Effects
AudioSource, particles, VFX Graph, post-processing
8
Building & Publishing
Build pipeline, optimization, platforms, monetization
You Are Here
9
Rendering Pipelines
URP, HDRP, Shader Graph, lighting systems
10
Data-Oriented Tech Stack
ECS, Jobs System, Burst Compiler
11
AI & Gameplay Systems
NavMesh, FSMs, behavior trees, procedural gen
12
Multiplayer & Networking
Netcode, RPCs, latency, prediction
13
Tools & Editor Scripting
Custom editors, debug tools, CI/CD
14
Architecture & Clean Code
Service locators, DI, ScriptableObject architecture
15
Performance Optimization
CPU/GPU profiling, memory, object pooling
16
Production & Industry Practices
Git, Agile, asset pipelines, debugging at scale
Building a great game is only half the battle — shipping it is where many projects fail. The gap between "it works on my machine" and "it's live on Steam/App Store/PlayStation" is enormous, filled with platform-specific requirements, optimization challenges, certification hurdles, and business decisions that can make or break your project.
This part covers the entire journey from clicking File > Build Settings to seeing your game listed in a store. Whether you're a solo indie targeting Steam or a studio aiming for multiplatform release, mastering the build pipeline is a non-negotiable skill for any professional Unity developer.
Key Insight: The build pipeline is not something you configure once at the end of development. Smart teams set up automated builds from day one, test on target hardware weekly, and treat the build process as a first-class part of their development workflow. Early and frequent building catches platform-specific issues before they compound.
1. Build Pipeline
Unity's build pipeline transforms your project's assets, scenes, and scripts into a platform-specific executable. Understanding each step of this pipeline gives you control over build size, performance, and compatibility.
1.1 Build Settings Window
The Build Settings window (File > Build Settings or Ctrl+Shift+B) is your central control panel for builds. It contains three critical sections:
| Section |
Purpose |
Key Considerations |
| Scenes in Build |
Ordered list of scenes included in the build |
Scene 0 loads first. Only checked scenes are included. Order matters for SceneManager.LoadScene(index) |
| Platform List |
Target platform selection |
Switching platforms reimports all assets (can take 30+ minutes on large projects). Install platform modules via Unity Hub |
| Build Options |
Configuration toggles |
Development Build, Autoconnect Profiler, Deep Profiling, Script Debugging, Compression Method |
// Automated build script — essential for CI/CD pipelines
using UnityEditor;
using UnityEditor.Build.Reporting;
using UnityEngine;
public class BuildAutomation
{
// Called from command line: Unity -executeMethod BuildAutomation.BuildWindows
[MenuItem("Build/Windows 64-bit")]
public static void BuildWindows()
{
BuildPlayerOptions options = new BuildPlayerOptions
{
scenes = GetEnabledScenes(),
locationPathName = "Builds/Windows/MyGame.exe",
target = BuildTarget.StandaloneWindows64,
options = BuildOptions.None
};
BuildReport report = BuildPipeline.BuildPlayer(options);
BuildSummary summary = report.summary;
if (summary.result == BuildResult.Succeeded)
{
Debug.Log($"Build succeeded: {summary.totalSize / (1024 * 1024):F1} MB");
Debug.Log($"Build time: {summary.totalTime.TotalSeconds:F1}s");
Debug.Log($"Warnings: {summary.totalWarnings}, Errors: {summary.totalErrors}");
}
else
{
Debug.LogError($"Build failed with {summary.totalErrors} errors");
foreach (var step in report.steps)
{
foreach (var msg in step.messages)
{
if (msg.type == LogType.Error)
Debug.LogError($"[{step.name}] {msg.content}");
}
}
}
}
[MenuItem("Build/Android APK")]
public static void BuildAndroid()
{
// Set Android-specific settings
PlayerSettings.Android.bundleVersionCode++;
EditorUserBuildSettings.buildAppBundle = false; // APK for testing
BuildPlayerOptions options = new BuildPlayerOptions
{
scenes = GetEnabledScenes(),
locationPathName = "Builds/Android/MyGame.apk",
target = BuildTarget.Android,
options = BuildOptions.None
};
BuildPipeline.BuildPlayer(options);
}
private static string[] GetEnabledScenes()
{
var scenes = new System.Collections.Generic.List<string>();
foreach (var scene in EditorBuildSettings.scenes)
{
if (scene.enabled)
scenes.Add(scene.path);
}
return scenes.ToArray();
}
}
1.2 IL2CPP vs Mono — Scripting Backends
The scripting backend determines how your C# code is compiled and executed at runtime. This is one of the most impactful build decisions you'll make:
| Feature |
Mono |
IL2CPP |
| Compilation |
JIT (Just-In-Time) at runtime |
AOT (Ahead-Of-Time) — C# to C++ to native code |
| Build time |
Fast (seconds to minutes) |
Slow (minutes to hours for large projects) |
| Runtime performance |
Good — JIT can optimize hot paths |
Better — native code, 1.5-3x faster for CPU-bound code |
| Build size |
Smaller (includes Mono runtime ~5MB) |
Larger (native binaries, but code stripping helps) |
| Platform support |
PC, Mac, Linux, Android |
All platforms (required for iOS, consoles, WebGL) |
| Code security |
Easily decompiled (IL code) |
Much harder to reverse-engineer (native binary) |
| Reflection |
Full support |
Limited — AOT can't generate code at runtime |
| Use for |
Development iteration, quick testing |
Release builds, all shipping products |
IL2CPP Gotcha: IL2CPP uses AOT compilation, which means it cannot generate code at runtime. This breaks certain reflection patterns, System.Reflection.Emit, and some serialization libraries. If you rely on runtime code generation, you must use link.xml to preserve types from being stripped, or refactor to avoid runtime codegen entirely. Test with IL2CPP early — don't discover these issues at ship time.
// link.xml — Preserve types from IL2CPP code stripping
// Place in Assets/ folder
/*
<linker>
<!-- Preserve entire assembly -->
<assembly fullname="MyGameAssembly" preserve="all"/>
<!-- Preserve specific types used via reflection -->
<assembly fullname="Assembly-CSharp">
<type fullname="MyNamespace.SaveData" preserve="all"/>
<type fullname="MyNamespace.NetworkMessage" preserve="all"/>
</assembly>
<!-- Preserve JSON serialization types -->
<assembly fullname="Newtonsoft.Json" preserve="all"/>
</linker>
*/
1.3 Player Settings & Build Compression
Player Settings (Edit > Project Settings > Player) contain platform-specific configuration that affects how your build appears and behaves:
| Setting Category |
Key Settings |
Impact |
| Company/Product |
Company Name, Product Name, Version |
Displayed in store listings, about dialogs, crash reports |
| Icon |
Default icon, platform-specific icons |
App icon on desktop/home screen. Each platform has size requirements |
| Resolution |
Default screen size, fullscreen mode, aspect ratios |
Startup behavior, supported display configurations |
| Splash Screen |
Logos, background, animation |
Unity Personal shows Unity watermark; Pro/Plus can customize fully |
| API Compatibility |
.NET Standard 2.1 vs .NET Framework |
.NET Standard = smaller builds, fewer APIs. Framework = full API access, larger builds |
| Managed Stripping |
Disabled / Low / Medium / High |
Higher stripping = smaller builds but may strip needed code. Use link.xml to preserve |
// Setting Player Settings programmatically for CI/CD
using UnityEditor;
public static class BuildConfigurator
{
public static void ConfigureForRelease()
{
// Scripting backend
PlayerSettings.SetScriptingBackend(
BuildTargetGroup.Standalone, ScriptingImplementation.IL2CPP);
// API compatibility
PlayerSettings.SetApiCompatibilityLevel(
BuildTargetGroup.Standalone, ApiCompatibilityLevel.NET_Standard_2_0);
// Managed stripping for smaller builds
PlayerSettings.SetManagedStrippingLevel(
BuildTargetGroup.Standalone, ManagedStrippingLevel.High);
// Version
PlayerSettings.bundleVersion = "1.0.0";
// Resolution defaults
PlayerSettings.defaultIsNativeResolution = true;
PlayerSettings.fullScreenMode = FullScreenMode.FullScreenWindow;
// Strip engine code for smaller builds
PlayerSettings.stripEngineCode = true;
}
public static void ConfigureForAndroid()
{
PlayerSettings.SetScriptingBackend(
BuildTargetGroup.Android, ScriptingImplementation.IL2CPP);
// Target ARM64 for modern devices (required by Google Play)
PlayerSettings.Android.targetArchitectures = AndroidArchitecture.ARM64;
// Minimum API level
PlayerSettings.Android.minSdkVersion = AndroidSdkVersions.AndroidApiLevel24;
// App bundle for Google Play (smaller downloads via split APKs)
EditorUserBuildSettings.buildAppBundle = true;
// Texture compression targeting
PlayerSettings.Android.textureCompressionFormats =
new[] { TextureCompressionFormat.ASTC, TextureCompressionFormat.ETC2 };
}
}
2. Build Optimization
Optimization is the art of doing more with less. For builds targeting mobile and WebGL, every megabyte and every draw call matters. Even on powerful PCs, optimization separates professional releases from amateur ones.
2.1 Draw Call Batching
A draw call is a command sent from the CPU to the GPU to render a mesh with a specific material. Each draw call has overhead — reducing them is one of the most impactful optimizations you can make. Think of draw calls like trips to the grocery store: it's far more efficient to buy everything in one trip than to make 500 separate trips for each item.
| Technique |
How It Works |
Requirements |
Best For |
| Static Batching |
Combines static meshes into a single large mesh at build time |
Objects marked as Static, same material |
Environment geometry (buildings, terrain props, walls) |
| Dynamic Batching |
Combines small moving meshes at runtime each frame |
<300 vertices per mesh, same material, no multi-pass shaders |
Small particles, UI elements, simple props |
| GPU Instancing |
Renders many copies of the same mesh in one draw call via hardware instancing |
Same mesh and material, shader must support instancing |
Forests (trees), grass, crowds, bullet casings |
| SRP Batcher |
Caches material properties on GPU, reduces CPU overhead per draw call |
URP/HDRP, SRP-compatible shaders |
Everything in URP/HDRP projects — always enable this |
Pro Tip: Use texture atlasing to combine multiple textures into one large texture. This allows objects with different textures to share a single material, making them eligible for batching. Tools like TexturePacker or Unity's Sprite Atlas handle this automatically for 2D games.
// GPU Instancing example — render 10,000 objects efficiently
using UnityEngine;
public class GPUInstancingDemo : MonoBehaviour
{
[SerializeField] private Mesh instanceMesh;
[SerializeField] private Material instanceMaterial; // Must have "Enable GPU Instancing" checked
[SerializeField] private int instanceCount = 10000;
private Matrix4x4[][] batches;
private MaterialPropertyBlock propertyBlock;
private static readonly int ColorID = Shader.PropertyToID("_Color");
private void Start()
{
// GPU instancing supports max 1023 instances per draw call
int batchCount = Mathf.CeilToInt(instanceCount / 1023f);
batches = new Matrix4x4[batchCount][];
int remaining = instanceCount;
for (int b = 0; b < batchCount; b++)
{
int count = Mathf.Min(remaining, 1023);
batches[b] = new Matrix4x4[count];
for (int i = 0; i < count; i++)
{
Vector3 pos = new Vector3(
Random.Range(-50f, 50f),
0f,
Random.Range(-50f, 50f)
);
Quaternion rot = Quaternion.Euler(0, Random.Range(0f, 360f), 0);
Vector3 scale = Vector3.one * Random.Range(0.5f, 1.5f);
batches[b][i] = Matrix4x4.TRS(pos, rot, scale);
}
remaining -= count;
}
propertyBlock = new MaterialPropertyBlock();
}
private void Update()
{
// Render all batches — each batch is a single draw call!
foreach (var batch in batches)
{
Graphics.DrawMeshInstanced(
instanceMesh, 0, instanceMaterial,
batch, batch.Length, propertyBlock
);
}
// 10,000 objects rendered in ~10 draw calls instead of 10,000
}
}
2.2 LOD System (Level of Detail)
The LOD (Level of Detail) system automatically swaps high-poly meshes for lower-poly versions as objects move further from the camera. This is one of the most effective GPU optimization techniques — why render 50,000 triangles for a tree that's 200 meters away when 500 triangles look identical at that distance?
| LOD Level |
Typical Screen % |
Triangle Reduction |
Use Case |
| LOD 0 |
60-100% of screen |
Full detail (100%) |
Close-up: hero asset, main character, nearby objects |
| LOD 1 |
30-60% |
50% of LOD 0 |
Mid-range: still visible but not scrutinized |
| LOD 2 |
10-30% |
25% of LOD 0 |
Far: silhouette matters, detail doesn't |
| Culled |
<10% |
Not rendered |
Too small to see — render nothing, save everything |
// Programmatically configure LOD Groups
using UnityEngine;
public class LODConfigurator : MonoBehaviour
{
[SerializeField] private Mesh highPolyMesh; // 10,000 triangles
[SerializeField] private Mesh medPolyMesh; // 3,000 triangles
[SerializeField] private Mesh lowPolyMesh; // 500 triangles
[SerializeField] private Material sharedMaterial;
private void Start()
{
LODGroup lodGroup = gameObject.AddComponent<LODGroup>();
// Create LOD renderers
LOD[] lods = new LOD[3];
// LOD 0: Full detail when taking 60%+ of screen
var lod0Go = CreateLODChild("LOD0", highPolyMesh);
lods[0] = new LOD(0.6f, lod0Go.GetComponentsInChildren<Renderer>());
// LOD 1: Medium detail at 30-60%
var lod1Go = CreateLODChild("LOD1", medPolyMesh);
lods[1] = new LOD(0.3f, lod1Go.GetComponentsInChildren<Renderer>());
// LOD 2: Low detail at 10-30%, culled below 10%
var lod2Go = CreateLODChild("LOD2", lowPolyMesh);
lods[2] = new LOD(0.1f, lod2Go.GetComponentsInChildren<Renderer>());
lodGroup.SetLODs(lods);
lodGroup.RecalculateBounds();
}
private GameObject CreateLODChild(string name, Mesh mesh)
{
var child = new GameObject(name);
child.transform.SetParent(transform, false);
var filter = child.AddComponent<MeshFilter>();
filter.mesh = mesh;
var renderer = child.AddComponent<MeshRenderer>();
renderer.material = sharedMaterial;
return child;
}
}
2.3 Addressables & Asset Management
The Addressables system is Unity's modern approach to asset loading and management. It replaces the old Resources folder with a powerful, flexible system that supports remote hosting, content updates without app store resubmission, and memory-efficient loading.
Why Addressables? The Resources folder loads everything into memory at startup. For a game with 2GB of assets, that's catastrophic on mobile. Addressables let you load and unload specific asset bundles on demand, keeping memory usage tight and enabling post-launch content delivery.
// Addressables — load assets on demand
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
public class AddressableLoader : MonoBehaviour
{
// Load a single asset by address
public async void LoadPrefab(string address)
{
AsyncOperationHandle<GameObject> handle =
Addressables.LoadAssetAsync<GameObject>(address);
await handle.Task;
if (handle.Status == AsyncOperationStatus.Succeeded)
{
Instantiate(handle.Result, transform.position, Quaternion.identity);
Debug.Log($"Loaded: {address}");
}
else
{
Debug.LogError($"Failed to load: {address}");
}
}
// Load all assets with a label (e.g., "level_01_assets")
public async void LoadLevelAssets(string label)
{
var handle = Addressables.LoadAssetsAsync<GameObject>(
label, (obj) => {
// Called for each loaded asset
Debug.Log($"Loaded asset: {obj.name}");
});
await handle.Task;
Debug.Log($"All {label} assets loaded: {handle.Result.Count} items");
}
// Unload when done to free memory
public void UnloadLevel(AsyncOperationHandle handle)
{
Addressables.Release(handle);
Debug.Log("Level assets released from memory");
}
// Download remote content (DLC, updates)
public async void DownloadRemoteContent(string label)
{
// Check download size first
var sizeHandle = Addressables.GetDownloadSizeAsync(label);
await sizeHandle.Task;
long downloadSize = sizeHandle.Result;
if (downloadSize > 0)
{
Debug.Log($"Downloading {downloadSize / (1024 * 1024):F1} MB...");
var downloadHandle = Addressables.DownloadDependenciesAsync(label);
while (!downloadHandle.IsDone)
{
float progress = downloadHandle.PercentComplete;
Debug.Log($"Download progress: {progress * 100:F0}%");
await System.Threading.Tasks.Task.Yield();
}
}
}
}
3. Testing & QA
3.1 Debug vs Release Builds
Understanding the difference between debug and release builds is critical for both development efficiency and shipping quality:
| Aspect |
Development Build |
Release Build |
| Debug.Log |
Active — messages visible in console |
Still executes but no console. Strip with conditional compilation |
| Profiler |
Can connect remotely for live profiling |
No profiler connection (unless manually enabled) |
| Code stripping |
Minimal — preserves debug symbols |
Aggressive — removes unused code, smaller binaries |
| Performance |
Slower — debug overhead, extra checks |
Full speed — optimized native code (IL2CPP) |
| Script debugging |
Can attach Visual Studio/Rider debugger |
No debugger attachment |
| Build size |
Larger — includes debug symbols and metadata |
Smaller — stripped and optimized |
// Conditional compilation for debug-only code
using UnityEngine;
using System.Diagnostics;
public static class DebugHelper
{
// [Conditional] attribute completely strips the method in release
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
public static void Log(string message)
{
UnityEngine.Debug.Log($"[DBG] {message}");
}
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
public static void DrawGizmoBox(Vector3 center, Vector3 size, Color color)
{
UnityEngine.Debug.DrawLine(
center - size / 2, center + size / 2, color, 0.1f);
}
// Use preprocessor directives for blocks of code
public static void InitializeDebugSystems()
{
#if DEVELOPMENT_BUILD || UNITY_EDITOR
// In-game debug console
GameObject.Instantiate(Resources.Load("DebugConsole"));
// FPS counter
GameObject.Instantiate(Resources.Load("FPSCounter"));
// Cheat system
CheatManager.Enable();
#endif
}
}
3.2 Deep Profiling & Device Testing
The Unity Profiler is your primary tool for identifying performance bottlenecks in builds. Connecting to a development build running on a target device gives you accurate performance data that editor profiling cannot provide.
Critical: Never trust editor performance numbers for shipping decisions. The editor adds significant overhead (50-200%+). A game running at 60fps in the editor might run at 120fps in a build — or 20fps on a mobile device. Always profile on target hardware with a development build.
// Custom profiler markers for detailed performance analysis
using Unity.Profiling;
using UnityEngine;
public class GameplaySystem : MonoBehaviour
{
// Create custom profiler markers
static readonly ProfilerMarker s_AIUpdateMarker =
new ProfilerMarker("GameplaySystem.AIUpdate");
static readonly ProfilerMarker s_PhysicsQueryMarker =
new ProfilerMarker("GameplaySystem.PhysicsQueries");
static readonly ProfilerMarker s_PathfindingMarker =
new ProfilerMarker(ProfilerCategory.Ai, "Pathfinding");
private void Update()
{
// Wrap expensive operations with profiler markers
using (s_AIUpdateMarker.Auto())
{
UpdateAllEnemyAI();
}
using (s_PhysicsQueryMarker.Auto())
{
PerformEnvironmentQueries();
}
}
private void UpdateAllEnemyAI()
{
// Each sub-system gets its own marker for drill-down
using (s_PathfindingMarker.Auto())
{
// Expensive pathfinding calculations
// Now visible as a named block in the Profiler timeline
}
}
private void PerformEnvironmentQueries() { /* ... */ }
}
4. Publishing Platforms
4.1 PC: Steam, Epic, itch.io
PC remains the most accessible platform for indie developers. Each store has unique requirements and opportunities:
| Store |
Fee / Revenue Split |
Key Requirements |
Notes |
| Steam |
$100 listing fee + 30/25/20% cut (tiered) |
Steamworks SDK integration, achievements, cloud saves, controller support recommended, Steam Deck verification |
Largest PC market. Steamworks provides matchmaking, voice chat, DRM, workshops. Steam Deck compatibility expands audience |
| Epic Games Store |
12% revenue cut |
Epic Online Services integration optional, minimum quality bar, curated submissions |
Lower cut than Steam. Exclusivity deals available for qualifying titles. Smaller audience but growing |
| itch.io |
You choose (0-100%, default suggested) |
No minimum quality requirements, any format accepted |
Best for prototypes, game jams, niche titles. Direct fan relationship. Pay-what-you-want support |
// Steamworks integration example (using Steamworks.NET)
using Steamworks;
using UnityEngine;
public class SteamManager : MonoBehaviour
{
private static SteamManager instance;
private bool initialized;
private void Awake()
{
if (instance != null) { Destroy(gameObject); return; }
instance = this;
DontDestroyOnLoad(gameObject);
// Initialize Steam API
if (!Packsize.Test())
{
Debug.LogError("Steamworks Packsize test failed!");
return;
}
try
{
initialized = SteamAPI.Init();
if (initialized)
{
string playerName = SteamFriends.GetPersonaName();
Debug.Log($"Steam initialized. Welcome, {playerName}!");
}
}
catch (System.DllNotFoundException e)
{
Debug.LogError($"Steam DLL not found: {e.Message}");
}
}
// Unlock a Steam Achievement
public static void UnlockAchievement(string achievementId)
{
if (!instance.initialized) return;
SteamUserStats.SetAchievement(achievementId);
SteamUserStats.StoreStats();
}
// Save to Steam Cloud
public static void CloudSave(string filename, byte[] data)
{
if (!instance.initialized) return;
SteamRemoteStorage.FileWrite(filename, data, data.Length);
}
private void Update()
{
if (initialized) SteamAPI.RunCallbacks();
}
private void OnApplicationQuit()
{
if (initialized) SteamAPI.Shutdown();
}
}
4.2 Mobile: iOS & Android
Mobile publishing has the highest barrier to entry in terms of store requirements, but reaches the largest audience — over 3 billion active mobile gamers worldwide.
| Requirement |
iOS (App Store) |
Android (Google Play) |
| Developer Account |
$99/year Apple Developer Program |
$25 one-time Google Play Console fee |
| Build Format |
.ipa via Xcode (requires Mac) |
.aab (Android App Bundle, required since 2021) |
| Signing |
Code signing certificate + provisioning profile |
Keystore + app signing by Google Play |
| Architecture |
ARM64 only (required since 2020) |
ARM64 required for Google Play (32-bit dropped) |
| Review Process |
Strict review (1-7 days), common rejections for UI/UX issues |
Automated review (hours to days), policy checks |
| Size Limit |
4GB max (requires on-demand resources for large games) |
150MB AAB limit (use Play Asset Delivery for rest) |
| Revenue Split |
30% (15% for first $1M/year via Small Business Program) |
30% (15% for first $1M/year) |
Mobile Optimization Checklist: Target 30fps minimum (60fps ideal), keep initial download under 100MB, use ASTC texture compression, implement loading screens for async operations, handle interruptions (calls, notifications), support both portrait and landscape if applicable, and test on low-end devices (2GB RAM baseline).
4.3 Console & WebGL
Console publishing requires formal developer relationships and certification processes:
| Platform |
Requirements |
Certification Notes |
| PlayStation (PS5/PS4) |
Sony Partners registration, PS5 dev kit, TRC compliance |
Technical Requirements Checklist (TRC) — 200+ rules covering trophies, save data, error handling, controller disconnection |
| Xbox (Series X|S) |
ID@Xbox program or managed publisher, Xbox dev kit, XR compliance |
Xbox Requirements (XR) — achievements, Game Pass integration, Smart Delivery for cross-gen |
| Nintendo Switch |
Nintendo Developer Portal registration, Switch dev kit, Lotcheck |
Lotcheck certification — strict performance requirements, 30fps minimum, no crashes. Extended review period |
WebGL builds run directly in the browser but come with significant limitations:
- No multithreading — JavaScript is single-threaded, so Job System and async operations behave differently
- Memory limited — browsers typically cap WASM memory at 2GB (browser-dependent)
- No file system access — use IndexedDB or PlayerPrefs for persistent data
- Shader limitations — WebGL 2.0 supports most features, but compute shaders and some post-processing effects are unavailable
- Compression essential — use Brotli compression for smallest download, Gzip as fallback. Server must send correct Content-Encoding headers
5. Monetization
5.1 Ads & In-App Purchases
Unity provides first-party solutions for both advertising and in-app purchases, making monetization integration straightforward:
// Unity Ads integration — rewarded video ads
using UnityEngine;
using UnityEngine.Advertisements;
public class RewardedAdsManager : MonoBehaviour, IUnityAdsLoadListener, IUnityAdsShowListener
{
[SerializeField] private string androidAdUnitId = "Rewarded_Android";
[SerializeField] private string iOSAdUnitId = "Rewarded_iOS";
private string adUnitId;
private void Awake()
{
#if UNITY_IOS
adUnitId = iOSAdUnitId;
#elif UNITY_ANDROID
adUnitId = androidAdUnitId;
#endif
}
public void LoadAd()
{
Debug.Log("Loading rewarded ad...");
Advertisement.Load(adUnitId, this);
}
public void ShowAd()
{
Advertisement.Show(adUnitId, this);
}
// IUnityAdsLoadListener
public void OnUnityAdsAdLoaded(string placementId)
{
Debug.Log($"Ad loaded: {placementId}");
}
public void OnUnityAdsFailedToLoad(string placementId, UnityAdsLoadError error, string msg)
{
Debug.LogError($"Ad load failed: {error} - {msg}");
}
// IUnityAdsShowListener
public void OnUnityAdsShowComplete(string placementId, UnityAdsShowCompletionState state)
{
if (state == UnityAdsShowCompletionState.COMPLETED)
{
// Player watched the full ad — grant reward
Debug.Log("Ad completed! Granting reward.");
GameEconomy.AddGems(50);
LoadAd(); // Pre-load next ad
}
}
public void OnUnityAdsShowFailure(string placementId, UnityAdsShowError error, string msg)
{
Debug.LogError($"Ad show failed: {error} - {msg}");
}
public void OnUnityAdsShowStart(string placementId) { }
public void OnUnityAdsShowClick(string placementId) { }
}
// Unity IAP (In-App Purchases) integration
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Extension;
public class IAPManager : MonoBehaviour, IDetailedStoreListener
{
private static IAPManager instance;
private IStoreController storeController;
// Product IDs (must match App Store Connect / Google Play Console)
private const string PRODUCT_GEMS_100 = "com.studio.game.gems100";
private const string PRODUCT_REMOVE_ADS = "com.studio.game.removeads";
private const string PRODUCT_VIP_PASS = "com.studio.game.vipmonthly";
private void Awake()
{
if (instance != null) { Destroy(gameObject); return; }
instance = this;
DontDestroyOnLoad(gameObject);
InitializePurchasing();
}
private void InitializePurchasing()
{
var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
// Consumable — can be purchased multiple times
builder.AddProduct(PRODUCT_GEMS_100, ProductType.Consumable);
// Non-consumable — purchased once, persists forever
builder.AddProduct(PRODUCT_REMOVE_ADS, ProductType.NonConsumable);
// Subscription — recurring payment
builder.AddProduct(PRODUCT_VIP_PASS, ProductType.Subscription);
UnityPurchasing.Initialize(this, builder);
}
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
storeController = controller;
Debug.Log("IAP initialized successfully");
// Check if player already owns non-consumable
if (HasPurchased(PRODUCT_REMOVE_ADS))
AdManager.DisableAds();
}
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
{
string productId = args.purchasedProduct.definition.id;
if (productId == PRODUCT_GEMS_100)
{
GameEconomy.AddGems(100);
Debug.Log("Purchased 100 gems!");
}
else if (productId == PRODUCT_REMOVE_ADS)
{
AdManager.DisableAds();
PlayerPrefs.SetInt("AdsRemoved", 1);
Debug.Log("Ads removed!");
}
return PurchaseProcessingResult.Complete;
}
public bool HasPurchased(string productId)
{
var product = storeController?.products.WithID(productId);
return product != null && product.hasReceipt;
}
public void OnInitializeFailed(InitializationFailureReason error) { }
public void OnInitializeFailed(InitializationFailureReason error, string message) { }
public void OnPurchaseFailed(Product product, PurchaseFailureDescription desc) { }
}
5.2 Business Models
| Model |
Description |
Best For |
Considerations |
| Premium |
One-time purchase price ($5-$70) |
Story-driven games, deep gameplay, PC/console titles |
Higher perceived quality, lower player count, demo/trial helps conversion |
| Free-to-Play + Ads |
Free download, revenue from ads |
Casual mobile games, hyper-casual, wide audiences |
Needs massive volume (100K+ DAU), rewarded ads convert best, interstitials annoy players |
| Free-to-Play + IAP |
Free download, cosmetic/convenience purchases |
Competitive games, RPGs, live-service titles |
~5% of players ("whales") generate 50%+ revenue. Ethical design matters |
| Subscription |
Monthly recurring fee for premium content |
MMOs, content-heavy games, battle passes |
Predictable revenue, but requires constant content updates to retain subscribers |
6. History & Evolution of the Build Pipeline
| Era |
Build Pipeline State |
Key Changes |
| 2005-2012 |
Simple export, few platforms |
Mac/Windows builds. Web Player (browser plugin). Manual asset management |
| 2013-2015 |
IL2CPP introduced, mobile explosion |
iOS/Android become primary targets. IL2CPP replaces Mono for iOS (Apple mandate). Asset Bundles for DLC |
| 2016-2018 |
WebGL replaces Web Player |
NPAPI plugin deprecation forces WebGL adoption. Scriptable Build Pipeline package. Cache server for teams |
| 2019-2021 |
Addressables system matures |
Addressables replaces legacy Resources/AssetBundles. Content Delivery Network integration. Build report improvements |
| 2022-2026 |
Unity 6, modern pipeline |
Improved build times, incremental builds, cloud build enhancements, platform-specific optimizations, Multiplayer build configuration |
Case Study
Among Us — Multiplatform Launch Strategy
Among Us by InnerSloth demonstrates the power of Unity's cross-platform capabilities. Originally released on mobile (June 2018) as a free-to-play title with ads and cosmetic IAP, the game exploded in popularity in 2020. The team then leveraged Unity's build pipeline to ship on PC (Steam), Nintendo Switch, PlayStation, and Xbox — all from a single codebase. Key decisions:
- Mobile-first development ensured the game ran on low-end hardware, making porting up to PC/consoles trivial
- Different monetization per platform: free with ads on mobile, $5 premium on PC (later also free-to-play)
- Cross-play between all platforms required unified networking code
- The team of just 3 people shipped to 5+ platforms using Unity's abstraction layers
Cross-Platform
3-Person Team
500M+ Downloads
Mobile-First
Case Study
Genshin Impact — Mobile Optimization Masterclass
Genshin Impact by miHoYo (HoYoverse) is a technical marvel — an open-world action RPG running at high fidelity across PC, PS4/PS5, iOS, and Android from a single Unity project. Their optimization approach includes:
- Aggressive LOD system — 4+ LOD levels for every asset, with custom LOD selection curves per platform
- Dynamic resolution scaling — GPU-bound frames trigger lower resolution rendering, maintaining 30fps on mobile
- Asset streaming — the open world uses zone-based asset loading via a custom Addressables-like system
- Platform-specific shader variants — desktop gets full PBR, mobile gets simplified lighting models
- Texture compression per platform — ASTC for mobile, BC7 for PC, each at appropriate quality levels
Open World
Mobile AAA
$4B+ Revenue
Cross-Platform
Exercises & Self-Assessment
Exercise 1
Build Pipeline Setup
Create an automated build script that:
- Builds for Windows (IL2CPP, Release), Android (AAB), and WebGL from a single menu command
- Automatically increments the version number for each build
- Outputs build size and time to a log file
- Creates a separate "Development" build with profiler autoconnect enabled
- Compare IL2CPP vs Mono build sizes and startup times for your project
Exercise 2
LOD & Batching Optimization
Build a test scene and measure the impact of optimization:
- Create a scene with 1000 cube instances using the same material
- Measure draw calls with the Frame Debugger (
Window > Analysis > Frame Debugger)
- Enable GPU Instancing on the material — measure draw calls again
- Mark half the cubes as Static and build — compare static vs dynamic batching
- Add LODGroup to a complex mesh with 3 LOD levels — measure triangle count at different camera distances
Exercise 3
Addressables Migration
Migrate a project from Resources.Load to Addressables:
- Install the Addressables package (
com.unity.addressables)
- Move assets out of the Resources folder and mark them as Addressable
- Create address groups: "Core" (always loaded), "Level01", "Level02" (loaded on demand)
- Implement async loading with a progress bar UI
- Compare memory usage before and after the migration using the Memory Profiler
Exercise 4
Reflective Questions
- Why is IL2CPP required for iOS and console builds but optional for PC? What fundamental platform constraints drive this requirement?
- A QA tester reports that your game runs at 60fps in the editor but 15fps on a target Android device. List the top 5 things you would investigate, in order of likelihood.
- Your game's initial APK is 280MB but Google Play requires 150MB maximum. What strategies would you use to reduce the download size without removing content?
- Compare the economics of premium ($4.99) vs F2P with ads for a casual puzzle game expecting 100,000 downloads in the first month. Calculate estimated revenue for each model.
- You're porting a PC game to Nintendo Switch and it fails Lotcheck certification. What are the most common certification failure reasons, and how would you proactively prevent them?
Conclusion & Next Steps
You now understand the complete journey from Unity project to published product. Here are the key takeaways from Part 8:
- IL2CPP is the production scripting backend — use Mono for fast iteration, IL2CPP for release builds. Watch for AOT limitations with reflection
- Draw call optimization (static batching, GPU instancing, SRP Batcher) is often the single biggest performance win
- LOD systems give you free GPU performance — invest in creating LOD meshes for every 3D asset
- Addressables replace the Resources folder for professional asset management, enabling on-demand loading and remote content updates
- Always profile on target hardware — editor performance is misleading. Use development builds with the Profiler connected
- Each platform has unique requirements — from Apple's strict review process to Sony's TRC checklist to WebGL's threading limitations
- Monetization strategy should be decided early — it affects game design, not just business
Next in the Series
In Part 9: Rendering Pipelines, we dive into Unity's rendering architecture — choosing between URP and HDRP, mastering the Shader Graph visual editor, understanding lighting systems from baked GI to real-time ray tracing, and writing custom HLSL shaders for unique visual effects.
Continue the Series
Part 9: Rendering Pipelines
Master URP, HDRP, Shader Graph, and lighting systems for stunning visuals across all platforms.
Read Article
Part 10: Data-Oriented Tech Stack (DOTS)
Unlock massive performance with ECS, the Jobs System, and the Burst Compiler for handling 100K+ entities.
Read Article
Part 7: Audio & Visual Effects
AudioSource, particle systems, VFX Graph, and post-processing for polished game feel.
Read Article