Back to Gaming

Unity Game Engine Series Part 8: Building & Publishing

March 31, 2026 Wasil Zafar 40 min read

Transform your Unity project into a shippable product. Master the build pipeline from IL2CPP compilation to platform-specific optimization, navigate store submission requirements for Steam, mobile, and consoles, and implement monetization strategies that respect your players.

Table of Contents

  1. Build Pipeline
  2. Build Optimization
  3. Testing & QA
  4. Publishing Platforms
  5. Monetization
  6. History & Evolution
  7. Exercises & Self-Assessment
  8. Build Checklist Generator
  9. Conclusion & Next Steps

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.

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:

  1. Builds for Windows (IL2CPP, Release), Android (AAB), and WebGL from a single menu command
  2. Automatically increments the version number for each build
  3. Outputs build size and time to a log file
  4. Creates a separate "Development" build with profiler autoconnect enabled
  5. 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:

  1. Create a scene with 1000 cube instances using the same material
  2. Measure draw calls with the Frame Debugger (Window > Analysis > Frame Debugger)
  3. Enable GPU Instancing on the material — measure draw calls again
  4. Mark half the cubes as Static and build — compare static vs dynamic batching
  5. 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:

  1. Install the Addressables package (com.unity.addressables)
  2. Move assets out of the Resources folder and mark them as Addressable
  3. Create address groups: "Core" (always loaded), "Level01", "Level02" (loaded on demand)
  4. Implement async loading with a progress bar UI
  5. Compare memory usage before and after the migration using the Memory Profiler
Exercise 4

Reflective Questions

  1. Why is IL2CPP required for iOS and console builds but optional for PC? What fundamental platform constraints drive this requirement?
  2. 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.
  3. 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?
  4. 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.
  5. 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?

Build & Publish Checklist Generator

Generate a comprehensive build and publishing checklist document. Download as Word, Excel, PDF, or PowerPoint.

Draft auto-saved

All data stays in your browser. Nothing is sent to or stored on any server.

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.

Gaming