Ads
The Ads plugin shows rewarded video, interstitial, and banner ads and exposes them through playOps.Ads(). It loads and retries rewarded video and interstitial ads for you, reports the ad lifecycle to PlayOps Analytics, and forwards impression revenue to the MMP plugin when it is installed.
The current implementation is powered by AppLovin MAX mediation. All vendor-specific setup, including the mediation SDK, the network adapters, and the build-time wiring, is handled by the plugin. Your code only deals with the vendor-neutral surface described below.
Requirements
1. External Dependency Manager for Unity (EDM4U)
The mediation SDK and every network adapter declare their Android (Maven) and iOS (CocoaPods) native libraries through Google’s External Dependency Manager. Without it, builds fail at the linker stage with missing symbols.
EDM4U is distributed via OpenUPM. Add the scoped registry and the package to your Packages/manifest.json:
{ "scopedRegistries": [ { "name": "OpenUPM", "url": "https://package.openupm.com", "scopes": ["com.google.external-dependency-manager"] } ], "dependencies": { "com.google.external-dependency-manager": "1.2.187" }}After installing EDM4U, run Assets → External Dependency Manager → Android Resolver → Force Resolve at least once so the Android dependencies are written to your gradle template.
2. Platform minimums
- Android: Minimum API Level 24 or higher (Player Settings → Android → Other Settings). The build fails early with a clear message when it is lower; without that check Gradle would stop at the manifest merge with an opaque
uses-sdk:minSdkVersionerror. - iOS: deployment target 15.0 or higher (Player Settings → iOS → Other Settings → Target minimum iOS Version). Below that, CocoaPods aborts
pod installand the build fails.
3. Consent before the first ad request
The plugin sends no ad request until you call Ads().Start(). Call it only after your consent flow has resolved, so the mediation SDK initializes with the final consent state. On iOS, request App Tracking Transparency first, then collect consent with the CMP, then start ads when the CMP reports that ads may be requested:
playOps.AppTracking().RequestAuthorization(_ =>{ playOps.Cmp().ShowConsentFormIfRequired(_ => { playOps.Mmp().Start(); // only if the MMP plugin is installed
if (playOps.Cmp().CanRequestAds) { playOps.Ads().Start(); } });});4. A “Manage privacy” entry point
Users who gave consent must be able to change their mind. Surface a “Manage privacy” entry in your settings screen and drive it with the CMP’s re-consent flow. Nothing in the Ads plugin needs to change when consent changes; the mediation SDK reads the updated consent on its own.
Get your credentials
You need three kinds of values. They go into your project’s Ads configuration on the PlayOps dashboard, not into your code:
| Value | Format | One per |
|---|---|---|
| SDK key | long alphanumeric string | account |
| AdMob App ID | ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy (note the tilde) | platform |
| Ad unit ids | 16 hex characters | ad format and platform |
Create a rewarded video and an interstitial ad unit for every platform you ship on, and a banner ad unit if you show banners. If you already operate the underlying mediation platform for your title, take the values from your existing account. Otherwise request them from your Tactile integration contact, the same person who onboards you to PlayOps; Tactile creates the ad units for your game.
Install
The Tactile scoped registry must already be configured. Add the plugin to Packages/manifest.json dependencies:
"com.tactilegames.playops-plugins.ads": "1.1.0"The plugin requires PlayOps SDK 1.9.0 or later. If your manifest pins com.tactilegames.playops-sdk to an older version, raise it: the version in your manifest always wins over the one a package asks for. The plugin transitively pulls the mediation SDK and all network adapters (com.tactilegames.applovin-max-sdk). Do not install AppLovin’s own UPM packages or .unitypackage alongside it, and do not add AppLovin’s registry to your manifest.
Migrating from an existing MAX integration
- Before removing your MAX install, turn off Terms and Privacy Policy Flow in the AppLovin Integration Manager. Consent is owned by the CMP plugin, and two consent flows break the boot sequence. If the menu is already gone, set
"consentFlowEnabled": falseinProjectSettings/AppLovinInternalSettings.json. - Delete
Assets/MaxSdk/Scriptsif a.unitypackageleft it behind. - Remove any
com.applovin.*entries fromPackages/manifest.json. - Move the SDK key, AdMob App IDs, and ad unit ids you used with MAX into your project’s Ads configuration on the dashboard.
The plugin logs a warning in the Editor for each of the first three.
Seeing Assets/MaxSdk/Resources/AppLovinSettings.asset appear in your project is expected; the plugin creates it and re-stamps the SDK key and App IDs from your dashboard configuration on every build, so never edit it by hand. The plugin also hides the mediation SDK’s editor menu and turns off its auto-update prompt, because mediation and adapter versions ship with the package.
Configure
Enter the values in your project’s Ads configuration on the PlayOps dashboard, then press Get in PlayOps > Settings; see Connect to the dashboard. They do not go into your code. If a key your platform needs is missing, the build fails and names it.
| Key | Required | Used |
|---|---|---|
sdkKey | Always | At build time |
admobAppIdAndroid / admobAppIdIos | On that platform’s build | At build time |
rewardedVideoAdUnitIdAndroid / rewardedVideoAdUnitIdIos | On that platform’s build | At runtime |
interstitialAdUnitIdAndroid / interstitialAdUnitIdIos | On that platform’s build | At runtime |
bannerAdUnitIdAndroid / bannerAdUnitIdIos | No | At runtime; without one, the banner API is a no-op |
A key is required only on its own platform, so a single-platform game leaves the other platform’s keys out. Editor play mode and inhouse builds use your project’s development settings, and store builds use production, so set up the Ads configuration in both.
Installing the package is what enables the plugin and registers the playOps.Ads() facade; nothing ads-related is passed to the PlayOpsSDK constructor:
using PlayOps;using PlayOps.Ads;
var playOps = new PlayOpsSDK( cmpSettings, // the settings of the other plugins you use mmpSettings);
playOps.Initialize();Boot through new PlayOpsSDK(…). The obsolete constructors that take PlayOpsSettings cannot read the dashboard configuration, so Initialize() throws. Initialize() configures the plugin but does not start the mediation SDK yet; see Start ads.
To change a default, pass a PlayOpsAdsSettings with the other settings, for example new PlayOpsAdsSettings { MuteAds = true }:
| Setting | Required | Notes |
|---|---|---|
MuteAds | no | Starts ads muted. Default false. |
VerboseLogging | no | Turns on the mediation SDK’s verbose logging. Default false. |
Ad Review (AppLovin’s Quality Service) is always off: the plugin disables it on every build.
The mediation SDK is keyed on the PlayOps install id, the same id PlayOps Analytics uses, so user-level ad revenue joins with your analytics events. There is nothing to configure for it.
Upgrading from Ads 1.0.0
- Move the SDK key, AdMob App IDs, and ad unit ids from your
IAdsConfigProviderclass into your project’s Ads configuration on the dashboard, then press Get in PlayOps > Settings. - Delete your
IAdsConfigProviderclass; the interface no longer exists. - Stop passing ads settings to the
PlayOpsSDKconstructor. Keep aPlayOpsAdsSettingsonly to setMuteAdsorVerboseLogging;VerboseLoggingreplacesIsVerbose, andQualityServiceEnabledis gone. - Raise
com.tactilegames.playops-sdkto 1.9.0 if your manifest pins it, and follow Upgrading from an earlier SDK version.
Set the AdMob App ID
The mediation setup includes the Google (AdMob) network. The Google Mobile Ads SDK checks at app launch that its App ID is present in the Android manifest and the iOS Info.plist, and crashes the app when it is missing, before a single ad is requested.
Set admobAppIdAndroid and admobAppIdIos in your project’s Ads configuration on the dashboard. The plugin injects them into the manifest and Info.plist at build time, and fails the build with an actionable message when the App ID for the platform you are building is missing or is not an App ID (ad unit ids use a slash, the App ID uses a tilde).
Start ads
Ads().Start() initializes the mediation SDK and starts preloading rewarded video and interstitial ads.
playOps.Ads().Start();Call it after PlayOpsSDK.Initialize() and after your consent flow has resolved (see Requirements). Idempotent: calling Start more than once is a no-op after the first call. IsStarted tells you whether it has run.
Show ads
Every result callback fires exactly once, on the Unity main thread, so you can touch UI from it.
Rewarded video
if (playOps.Ads().IsRewardedVideoReady){ playOps.Ads().ShowRewardedVideo("shop_refill", result => { if (result == RewardedVideoResult.Completed) { // grant the reward } });}RewardedVideoResult | Meaning |
|---|---|
Completed | The user watched to the end and the network granted the reward. Grant it only here. |
Dismissed | The user closed the ad before earning the reward. |
Failed | No ad was ready, or the network failed to show it. |
Gate your reward button on IsRewardedVideoReady instead of calling ShowRewardedVideo blindly: when no ad is ready the callback fires immediately with Failed.
Interstitial
playOps.Ads().ShowInterstitial("level_complete", result =>{ // InterstitialResult.Completed or InterstitialResult.Failed ContinueToNextLevel();});IsInterstitialReady works the same way as its rewarded counterpart.
Banner
playOps.Ads().ShowBanner(BannerPosition.Bottom);// laterplayOps.Ads().HideBanner();Banners are opt-in: both calls are no-ops unless your Ads configuration has a banner ad unit for the platform. You can call ShowBanner before Start; the banner is shown once the mediation SDK is ready.
Placements
The placement string you pass to ShowRewardedVideo and ShowInterstitial names the spot in your game that shows the ad (level_failed_refill, level_complete). It is required and it segments every analytics event of that impression, so pick stable snake_case names.
Loading and retries
You never load ads yourself. After Start the plugin preloads rewarded video and interstitial ads, reloads each one after it is shown, and retries failed loads with exponential backoff. Banners are created when you call ShowBanner and refreshed by the mediation SDK.
Ad revenue
Every paid impression raises OnAdRevenue with the revenue the mediation SDK reported for it:
playOps.Ads().OnAdRevenue += data =>{ // data.Revenue, data.CurrencyCode ("USD"), data.NetworkName, data.AdFormat, data.Placement, …};OnAdRevenue fires on the Unity main thread. If the MMP plugin (1.3.0 or later) is installed, it forwards every paid impression to the MMP provider on its own; see MMP → Ad revenue for what it sends and how to turn it off. Do not forward OnAdRevenue to the MMP yourself as well, or revenue is counted twice.
Analytics
The plugin reports the ad lifecycle to PlayOps Analytics on its own; your game logs nothing. The events are adRequest, adLoadSucceeded, adLoadFailed, adImpressionStarted, adClicked, adFailedToShow, adImpressionCompleted, and adImpressionRevenue. All of them carry the ad format and the ad unit id. The impression events (adImpressionStarted, adImpressionCompleted, adImpressionRevenue) also carry the placement you passed to ShowRewardedVideo or ShowInterstitial and a transaction id that ties the three together, so impressions and revenue can be broken down per placement. Banners report only adImpressionRevenue.
These events are new to your analytics schema. After adding the plugin, regenerate the schema (any build, or PlayOps → Tactile Analytics → Export Analytics Schema) and upload it to the PlayOps dashboard as described in Uploading Schemas; until then the ad events are rejected.
Verify the integration on device
ShowMediationDebugger() opens the mediation SDK’s on-device diagnostic. Use it on a development build to confirm that every network adapter is integrated and that your ad units and App IDs are the ones you expect:
playOps.Ads().ShowMediationDebugger();It requires the mediation SDK to have finished initializing after Start; before that it logs a warning and shows nothing. On device, that warning is only visible when you pass PlayOpsSdkSettings with verboseLogging: true.
Advanced: US privacy opt-out
You can skip this section if your CMP already writes a US privacy signal; most games let their CMP handle US privacy the same way it handles GDPR.
Otherwise, you can opt the user out of the sale of personal information yourself. Call it from your privacy flow, before Start:
playOps.Ads().SetDoNotSell(true);Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Build fails with com.tactilegames.playops-plugins.ads requires section 'plugins.ads.applovin'… or …requires keys […] in section 'plugins.ads.applovin' | The Ads configuration is missing on the dashboard, or lacks a key the platform you build needs, for the environment of this build. | Set it up on the dashboard, press Get in PlayOps > Settings, and commit the updated files. |
Build fails with PlayOps Ads: no PlayOps Environment File was staged for this build | The project is not connected to the dashboard. | Connect to the dashboard. |
Build fails with PlayOps Ads: admobAppIdAndroid "…" is not an AdMob App ID (or admobAppIdIos) | An ad unit id was entered where the App ID belongs. | Copy the App ID from the AdMob console; it uses a tilde, ad unit ids use a slash. |
Initialize() throws 'AppLovinConfigSection' was requested from the Environment File, but none is available… | The game boots through an obsolete constructor that takes PlayOpsSettings. | Boot with new PlayOpsSDK(…). |
Initialize() throws in editor play mode, naming com.playops.plugins.ads.applovin and missing keys | The development settings have no Ads configuration, or lack a key for the active build target. | Set it up on the dashboard and press Get. |
playOps.Ads() returns null | The game boots through the shared-DI constructor new PlayOpsSDK(diContainer, …), which does not wire the plugin. | Boot with new PlayOpsSDK(…). |
playOps.Ads() throws PlayOpsNotInitializedException | It was called before playOps.Initialize(). | Call Initialize() first. A missing package shows up earlier, as a compile error on Ads(). |
iOS build fails with … already declares a post_install hook | Your project (or another package) adds its own post_install to the Podfile. CocoaPods keeps only the last hook, so the plugin refuses to add a second one. | Merge the framework-embedding step into your hook, or remove yours. |
Public surface
Reached via playOps.Ads() once the package is installed and the SDK boots through new PlayOpsSDK(…):
namespace PlayOps.Ads{ // Optional: pass it only to change a default. public class PlayOpsAdsSettings : ServiceSettings { bool MuteAds { get; set; } bool VerboseLogging { get; set; } }
public enum RewardedVideoResult { Completed, Dismissed, Failed } public enum InterstitialResult { Completed, Failed } public enum BannerPosition { Top, Bottom }
// Revenue snapshot for one paid impression, delivered through OnAdRevenue. public class AdRevenueData { double Revenue { get; } // in CurrencyCode units string CurrencyCode { get; } // ISO 4217, "USD" string MediationName { get; } string NetworkName { get; } string AdUnitId { get; } string AdFormat { get; } // "RewardedVideo", "Interstitial" or "Banner" string Placement { get; } // empty for banners string CreativeId { get; } string RevenuePrecision { get; } string NetworkPlacement { get; } string DspName { get; } }
public class PlayOpsAds { event Action<AdRevenueData> OnAdRevenue;
bool IsStarted { get; } bool IsRewardedVideoReady { get; } bool IsInterstitialReady { get; }
void Start(); void ShowRewardedVideo(string placement, Action<RewardedVideoResult> resultCallback); void ShowInterstitial(string placement, Action<InterstitialResult> resultCallback); void ShowBanner(BannerPosition position); void HideBanner(); void ShowMediationDebugger(); void SetDoNotSell(bool doNotSell); }
public static class PlayOpsExtension { // Registered whenever the plugin is installed and the SDK boots through new PlayOpsSDK(…). public static PlayOpsAds Ads(this PlayOpsSDK playOps); }}