There are few moments in game development more frustrating than a successful build that fails to perform. You spend hours integrating Unity Ads, testing the reward logic, and verifying the placement in the Editor. Everything works flawlessly.
Then you push to an Android device. The build runs, the game plays, but when you tap the "Watch Ad" button—nothing happens. No video, no error popup, just silence.
If this scenario sounds familiar, you are likely a victim of aggressive code stripping or improper initialization sequencing. This guide moves beyond generic advice to fix the specific conflict between the Unity Ads SDK and the Android build pipeline (ProGuard/R8).
The Root Cause: The "Silent Stripper" (ProGuard/R8)
To understand why this breaks, you must understand how Unity compiles an Android application (APK/AAB).
When you build for the Editor, Unity runs the Mono backend. It includes almost everything, ensuring maximum compatibility for debugging. However, when you build for Android release, Unity (via Gradle) employs Minification and Code Stripping.
How Stripping Breaks Ads
Modern Unity versions use R8 (Google’s replacement for ProGuard) to shrink code size and obfuscate Java classes. R8 analyzes your code entry points and removes any class or method that doesn't appear to be statically referenced.
The Unity Ads SDK relies heavily on Reflection and JNI (Java Native Interface) to communicate between your C# scripts and the native Android Java layer. Because these calls happen dynamically at runtime, R8 cannot see them during the static analysis phase.
Consequently, R8 assumes the Unity Ads Java classes are unused garbage and deletes them from the final build. Your C# code calls out to the void, and the ad simply never loads.
The Fix: Preserving the SDK with ProGuard Rules
To fix this, we must explicitly tell the build system to keep the Unity Ads classes, regardless of whether it thinks they are used or not.
Step 1: Enable Custom ProGuard Files
- Open your project in Unity.
- Go to File > Build Settings.
- Ensure Platform is set to Android.
- Click Player Settings... (bottom left).
- Navigate to Player > Publishing Settings.
- Scroll down to the Build section.
- Check the box for Custom ProGuard File. (In newer Unity versions, this might be labeled Custom R8 Properties or minifyEnabled configurations).
Step 2: Add the Keep Rules
Once checked, Unity generates a file at Assets/Plugins/Android/proguard-user.txt. Open this file in your code editor and append the following rules:
# Keep Unity Ads Classes
-keep class com.unity3d.ads.** { *; }
-keep interface com.unity3d.ads.** { *; }
# Keep Unity Services (often required for mediation or analytics linked to ads)
-keep class com.unity3d.services.** { *; }
-keep interface com.unity3d.services.** { *; }
This acts as a whitelist, forcing R8 to bundle the Ads SDK into your final APK.
Step 3: Implement Robust Initialization
Code stripping isn't the only culprit. A race condition during initialization often causes failures on physical devices, where network latency is real (unlike the instant localhost connection in the Editor).
You must use the IUnityAdsInitializationListener interface. Do not call Advertisement.Show() until you are contractually guaranteed that initialization is complete.
Here is a modern, drop-in C# solution for Unity 2021+:
using UnityEngine;
using UnityEngine.Advertisements;
public class AdsInitializer : MonoBehaviour, IUnityAdsInitializationListener
{
[Header("Configuration")]
[SerializeField] private string _androidGameId = "1234567";
[SerializeField] private string _iOSGameId = "1234568";
[SerializeField] private bool _testMode = true;
private string _gameId;
void Awake()
{
InitializeAds();
}
public void InitializeAds()
{
#if UNITY_IOS
_gameId = _iOSGameId;
#elif UNITY_ANDROID
_gameId = _androidGameId;
#elif UNITY_EDITOR
_gameId = _androidGameId; // Default to Android ID for Editor testing
#endif
// Check if already initialized to prevent double-init errors
if (!Advertisement.isInitialized && Advertisement.isSupported)
{
Debug.Log($"Initializing Unity Ads... Game ID: {_gameId}");
Advertisement.Initialize(_gameId, _testMode, this);
}
}
// INTERFACE IMPLEMENTATION: SUCCESS
public void OnInitializationComplete()
{
Debug.Log("Unity Ads initialization complete.");
// Signal your AdManager that it is now safe to load ads
// Example: AdManager.Instance.LoadAd();
}
// INTERFACE IMPLEMENTATION: FAILURE
public void OnInitializationFailed(UnityAdsInitializationError error, string message)
{
Debug.LogError($"Unity Ads Initialization Failed: {error} - {message}");
// Implement retry logic here if necessary
}
}
Deep Dive: Why "Test Mode" Matters on Devices
After fixing the ProGuard rules, you might encounter a secondary issue: Error: No Fill.
In the Unity Editor, the "Ad" is a placeholder graphic. It always loads. On a physical device, the SDK attempts to fetch a real ad from the Unity Mediation Network.
If your game is not live, or if your device ID is not whitelisted in the Unity Dashboard, the ad network will refuse to serve a real ad to prevent click fraud.
The Behavior Difference
- Editor: Simulates a perfect fill rate.
- Device (Test Mode OFF): Requests a live ad. If eCPM floors are not met or inventory is low, you get nothing.
- Device (Test Mode ON): Requests a specific test ad. This ensures you can verify the technical implementation (display, close button, reward callback) without needing real ad inventory.
Crucial: Always keep _testMode = true in your code (as seen in the snippet above) until you are ready to upload the final build to the Google Play Store.
Common Pitfalls and Edge Cases
1. The "Managed Stripping Level" Setting
Aside from ProGuard, Unity has its own C# stripping.
- Go to Project Settings > Player > Other Settings.
- Find Managed Stripping Level.
- If set to High, try lowering it to Low or Minimal if issues persist. High stripping can remove C# delegates used for ad callbacks.
2. Missing Internet Permissions
While Unity usually handles this, verify your AndroidManifest.xml (located in Assets/Plugins/Android/). It must explicitly request network access:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
3. Wrong Placement IDs
A common mistake is using the "default" placement ID string found in old tutorials. Unity Ads moved to specific Placement IDs (e.g., Rewarded_Android vs Rewarded_iOS).
- Go to your Unity Ads Dashboard.
- Verify the exact string ID for your current platform.
- Passing
Rewarded_iOSwhile running on Android will result in a silent failure or a "Placement not found" error.
Conclusion
When Unity Ads work in the Editor but fail on Android, the issue is rarely with your C# logic. It is almost always an environment mismatch.
By defining strict ProGuard rules, you prevent the build pipeline from gutting the native Java bridge required for ads to function. By implementing IUnityAdsInitializationListener, you ensure your game respects the asynchronous nature of mobile networks. Apply these fixes, and your ads will start filling immediately.