Consent Management Platform (CMP)
The CMP plugin collects user consent (GDPR / IAB TCF) and exposes it through playOps.Cmp(). It shows the consent form when required, reports the outcome, and hands you the stored consent data so you can drive your own ads/attribution decisions.
The current implementation is powered by Google’s User Messaging Platform (UMP). All vendor-specific wiring is handled by the plugin — your code only deals with the vendor-neutral surface below, so the CMP backend can change without touching your integration.
Requirements
1. External Dependency Manager for Unity (EDM4U)
The CMP plugin pulls Android (Maven) and iOS (CocoaPods) native libraries through Google’s External Dependency Manager — Android user-messaging-platform, iOS the Google-Mobile-Ads-SDK pod (which includes UMP). Without it, builds fail at the linker stage with missing UMP 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 dependency is written to your gradle template / Assets/Plugins/Android/.
2. AdMob account, registered app, and a published consent message
UMP forms are authored server-side and fetched against an AdMob App ID:
- An AdMob (or Google Ad Manager) account with the app registered.
- A consent message published under the account’s Privacy & messaging tab.
- The AdMob App ID present in the build (see Set the AdMob App ID).
3. Order it correctly with ATT and the MMP
On iOS, request App Tracking Transparency before collecting consent, and start the MMP after consent resolves so the first attribution request carries the final consent state. A typical boot sequence is ATT → CMP → MMP:
playOps.AppTracking().RequestAuthorization(_ =>{ playOps.Cmp().ShowConsentFormIfRequired(_ => { playOps.Mmp()?.Start(); });});Install
The Tactile scoped registry must already be configured. Add the plugin to Packages/manifest.json dependencies:
"com.tactilegames.playops-plugins.cmp": "1.0.0"The plugin transitively pulls its UMP backend (com.tactilegames.google-ump-sdk) and, via EDM4U, the native libraries — no separate install step.
Set the AdMob App ID
UMP fetches the consent message tied to the AdMob App ID baked into the build. The plugin does not set it for you — inject it from a build post-processor on each platform:
- Android — add the
<meta-data>to the generated manifest from anIPostGenerateGradleAndroidProjecteditor script:<meta-data android:name="com.google.android.gms.ads.APPLICATION_ID"android:value="ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy"/> - iOS — set
GADApplicationIdentifierin the builtInfo.plistfrom a[PostProcessBuild]script (PlistDocument.root.SetString("GADApplicationIdentifier", …)).
Configure
Build a PlayOpsCmpSettings and pass it to the PlayOpsSDK constructor.
using PlayOps;using PlayOps.Analytics;using PlayOps.Cmp;
var cmpSettings = new PlayOpsCmpSettings( tagForUnderAgeOfConsent: false, debugGeography: CmpDebugGeography.Disabled, // EEA/NotEEA only affect registered test devices testDeviceHashedIds: null);
var playOps = new PlayOpsSDK( new PlayOpsSettings(cloudUrl: "https://…", gameSecret: "…"), new AnalyticsSettings(appId: "yourgame"), cmpSettings);
playOps.Initialize();Passing the PlayOpsCmpSettings is what enables the plugin and registers the playOps.Cmp() facade.
Collect consent
ShowConsentFormIfRequired is the one-call flow: it refreshes consent info, then shows the form only if the backend reports consent is required. The callback fires once with the final status.
playOps.Cmp().ShowConsentFormIfRequired(result =>{ if (result.IsSuccess) { // consent resolved — safe to start attribution }});Call it at a studio-chosen moment — most games do it at boot, before starting the MMP (see ordering). All callbacks are Action<ConsentResult> and fire on the Unity main thread.
Lower-level calls are also available when you want explicit control:
| Member | Purpose |
|---|---|
RequestConsentInfoUpdate(cb) | Refresh status from the backend. Shows no UI. |
ShowConsentForm(cb) | Load + show the consent form unconditionally. |
Status / IsConsentRequired | Current status (Unknown / NotRequired / Required / Obtained). |
CanRequestAds | Backend signal that ads may be requested under the current consent. |
IsConsentFormAvailable | Whether a form has been fetched and is ready to show. |
Read the stored consent
GetConsent() returns a CmpUserConsent read from the standard IAB TCF v2 keys in device storage (Android SharedPreferences, iOS NSUserDefaults):
var consent = playOps.Cmp().GetConsent();// consent.GdprApplies // 1 when GDPR applies, 0 when not, null when unknown// consent.TcString // the raw IAB TCF v2 consent string// consent.PublisherCC // publisher country code// consent.HasPurposeConsent(3) // 1-based IAB TCF purposeThis is data, not policy — turning purposes into an ads/attribution decision is your game’s call.
Coordinating with the MMP
The CMP plugin has no dependency on the MMP and never calls it — you sequence them. Two patterns:
- TCF auto-read (recommended for AppsFlyer). UMP writes the IAB TCF string to device storage when the form resolves; the MMP plugin picks it up automatically when
PlayOpsMmpSettings.EnableTcfDataCollectionistrue. You only control ordering — resolve consent, then start the MMP. - Explicit push. Build an
MmpConsentfromGetConsent()and pass it toMmp().UpdateConsent(...)beforeStart.
See MMP → Advanced: overriding consent for both in detail.
Re-consent (“Manage privacy”)
When IsPrivacyOptionsRequired is true, surface a “Manage privacy” entry point so the user can change a prior decision, and present the form with ShowPrivacyOptionsForm:
if (playOps.Cmp().IsPrivacyOptionsRequired){ // show a "Manage privacy" button that calls: playOps.Cmp().ShowPrivacyOptionsForm(_ => { /* re-consent done */ });}If you drive the MMP via explicit consent, re-read GetConsent() and call Mmp().UpdateConsent(...) after the form is dismissed.
Testing without a published message
Google’s public sample App ID serves a generic Google test consent form and exercises the full flow on device — in a regulated region (EEA / UK / Switzerland) the form renders and consent resolves. The demo project (PlayOpsDemo) wires the sample IDs via build post-processors as a reference.
Outside a regulated region, force the form for testing by registering your device and setting the debug geography:
var cmpSettings = new PlayOpsCmpSettings( debugGeography: CmpDebugGeography.EEA, testDeviceHashedIds: new[] { "YOUR_DEVICE_HASH" });The device hash is printed in the device log on the first RequestConsentInfoUpdate (look for … addTestDeviceHashedId("…")). DebugGeography has no effect on devices that are not registered as test devices.
Public surface
Reached via playOps.Cmp() once a PlayOpsCmpSettings is passed to the SDK constructor:
namespace PlayOps.Cmp{ public enum CmpConsentStatus { Unknown, Required, NotRequired, Obtained }
public enum CmpDebugGeography { Disabled = 0, EEA = 1, NotEEA = 2 }
// Outcome of a consent operation, delivered to the callbacks below. public class ConsentResult { bool IsSuccess { get; } CmpConsentStatus Status { get; } int ErrorCode { get; } // 0 when IsSuccess string ErrorMessage { get; } // null when IsSuccess }
// IAB TCF v2 consent data, returned by GetConsent(). public class CmpUserConsent { int? GdprApplies { get; } // 1 = applies, 0 = not, null = unknown string TcString { get; } // raw IAB TCF v2 consent string string PurposeConsents { get; } // 1-based purpose bitstring string PurposeLegitimateInterests { get; } string PublisherCC { get; } // publisher country code bool HasPurposeConsent(int purposeId); // 1-based IAB TCF purpose }
public class PlayOpsCmp { CmpConsentStatus Status { get; } bool IsConsentRequired { get; } bool IsConsentFormAvailable { get; } bool CanRequestAds { get; } bool IsPrivacyOptionsRequired { get; }
void RequestConsentInfoUpdate(Action<ConsentResult> onComplete = null); void ShowConsentForm(Action<ConsentResult> onComplete = null); void ShowConsentFormIfRequired(Action<ConsentResult> onComplete = null); void ShowPrivacyOptionsForm(Action<ConsentResult> onComplete = null); CmpUserConsent GetConsent(); }
public static class PlayOpsExtension { // Registered when a PlayOpsCmpSettings is passed to the PlayOpsSDK constructor. public static PlayOpsCmp Cmp(this PlayOpsSDK playOps); }}