unity-monetization
Unity Monetization
Wire ads into a casual iOS game without breaking the build or lying about whether ads serve. The deliverable is an AdsManager that no-ops safely off-device (behind an SDK-agnostic Ads facade), WHEN-to-show factored into a pure unit-tested cadence policy, and a build that carries whatever provisioning the SDK needs — plus an honest statement of what is provisioned vs coded. Built on unity-mcp-bridge (Editor actions) and cross-linked to unity-qa-release (the CocoaPods / pod-install / .xcworkspace build mechanics).
The path we ship now is Google AdMob (com.google.ads.mobile via OpenUPM) — see "Google AdMob (iOS)" below. The Unity Ads direct material that follows is still proven and kept as the alternative path (and as the source of the INTERNAL_ERROR / LevelPlay-conflict lore).
Doctrine (these override convenience)
- "Code compiles" ≠ "ads serve." Never claim ads work without seeing one render on-device. A clean build, a valid Game ID, and green listeners prove nothing about delivery. The only proof is an ad frame on a real device (or TestFlight). Until then say "integration wired, delivery unverified."
- A brand-new ad account/app/unit is almost certainly NOT serving real ads yet. Freshly created Unity Ads / AppLovin / ironSource accounts sit in "pending approval". AdMob is the same in a sharper form: a new AdMob app/unit returns no-fill for real ads until Google approves the app (often only once it's live / linked to a store listing) and the unit warms up (hours to a couple days). Flipping
UseTestAdstrue→false too early makes ads disappear — that's expected, not a bug. Google test unit IDs always serve regardless of approval. When a valid ID yields no ad, suspect provisioning/no-fill first, not C#. Code must handle no-fill gracefully (log + retry next opportunity, never block gameplay). - Pick ONE ad path. Mixing leaves conflicting CocoaPods. We evaluated three in one project; friction order was LevelPlay (
com.unity.services.levelplay, mediation, account pending) → AppLovin MAX (.unitypackage, account pending) → Unity Ads direct (com.unity.ads), which is the path we shipped on. Two SDKs pulling the same pod at different versions breakspod install(see the LevelPlay trap below). - The iOS Xcode project is REGENERATED every build. Anything you'd hand-edit in Xcode (Info.plist ATT/SKAdNetwork keys, entitlements) must be re-applied by a
[PostProcessBuild]editor script, or it's gone next build. Never hand-edit the generated project. - Bootstrap ads on device only, and no-op until configured. The manager must do nothing in the Editor / off-device and nothing until a real Game ID + placement exist, so the rest of the game (and tests) never depends on an ad SDK being present or reachable.
- Gameplay never references the ad SDK — go through a tiny
Adsfacade. A static facade (Ads.NotifyGameFinished()→Ads.GameFinishedHandler?.Invoke()) that the concreteAdsManagerregisters at launch keeps gameplay assemblies SDK-free and makes swapping ad networks a one-file change. Off-device the handler is null → no-op. (The reference project swapped Unity Ads → AdMob by rewriting onlyAdsManager.cs.) - WHEN to show is a pure, unit-tested policy — not logic buried in the MonoBehaviour. Put frequency capping in an engine-free class with injected time so it's deterministic and testable; the MonoBehaviour owns the real clock + SDK. Separate the decision from the commit (
MarkShownonly on real display) so a not-yet-loaded ad doesn't reset the counters.
Confirm the Editor is reachable (mcpforunity://editor/state, ready_for_tools==true) before driving any of this through MCP; for the CocoaPods build itself, route to unity-qa-release.