This guide outlines how to implement ads across Android, iOS, and Desktop platforms in the MonoGame CardsStarterKit project, including a "Pay to Remove Ads" feature.
The project already follows excellent patterns (see 2-Core/Misc/AudioManager.cs:45-59 and 1-Framework/Utils/UIUtility.cs:19-24) that can be extended for ads.
1-Framework/Services/Ads/
├── IAdService.cs (interface)
├── AdPlacement.cs (enum: Banner, Interstitial, Rewarded)
└── AdConfiguration.cs (settings)
3-Games/Blackjack/Core/Services/
└── AdManager.cs (singleton, similar to AudioManager pattern)
3-Games/Blackjack/Platforms/Android/Services/
└── GoogleAdMobService.cs (Android implementation)
3-Games/Blackjack/Platforms/iOS/Services/
└── GoogleAdMobiOSService.cs (iOS implementation)
3-Games/Blackjack/Platforms/{Windows,Desktop}/Services/
└── NoAdService.cs (desktop stub implementation)
File: 1-Framework/Services/Ads/IAdService.cs
namespace CardsFramework.Services.Ads
{
public interface IAdService
{
bool IsInitialized { get; }
bool AdsEnabled { get; set; } // Based on purchase status
void Initialize(string appId);
void LoadBanner(AdPlacement placement);
void ShowBanner();
void HideBanner();
void LoadInterstitial();
void ShowInterstitial(Action onClosed, Action onFailed);
void LoadRewardedAd();
void ShowRewardedAd(Action<bool> onResult); // bool = watched completely
}
}
File: 1-Framework/Services/Ads/AdPlacement.cs
namespace CardsFramework.Services.Ads
{
public enum AdPlacement
{
TopBanner,
BottomBanner,
Interstitial,
RewardedVideo
}
}
File: 3-Games/Blackjack/Core/Services/AdManager.cs
using System;
using System.Collections.Concurrent;
using Microsoft.Xna.Framework;
using CardsFramework.Services.Ads;
namespace Blackjack.Core.Services
{
public class AdManager : GameComponent
{
private static AdManager _instance;
private readonly IAdService _adService;
private readonly ConcurrentQueue<Action> _pendingCallbacks = new();
public static AdManager Instance => _instance;
public bool AdsEnabled => _adService?.AdsEnabled ?? false;
private AdManager(Game game, IAdService adService) : base(game)
{
_adService = adService;
}
public static void Initialize(Game game, IAdService adService)
{
if (_instance != null)
return;
_instance = new AdManager(game, adService);
game.Components.Add(_instance);
}
public override void Update(GameTime gameTime)
{
base.Update(gameTime);
// Process ad callbacks on game thread
while (_pendingCallbacks.TryDequeue(out var callback))
{
callback?.Invoke();
}
}
public void ShowInterstitialAd(Action onClosed = null, Action onFailed = null)
{
if (!AdsEnabled || _adService == null)
{
onClosed?.Invoke();
return;
}
_adService.ShowInterstitial(
() => _pendingCallbacks.Enqueue(() => onClosed?.Invoke()),
() => _pendingCallbacks.Enqueue(() => onFailed?.Invoke())
);
}
public void ShowBanner()
{
if (AdsEnabled && _adService != null)
_adService.ShowBanner();
}
public void HideBanner()
{
_adService?.HideBanner();
}
}
}
File: 3-Games/Blackjack/Core/GameSettings.cs
Add this property to the existing GameSettings class:
public class GameSettings
{
// Add new property
public bool AdFreeVersion { get; set; } = false;
// Existing Save/Load methods handle persistence automatically
}
File: 3-Games/Blackjack/Platforms/Android/BlackJack.csproj
<ItemGroup>
<PackageReference Include="Xamarin.Google.Android.Play.Services.Ads" Version="23.0.0" />
</ItemGroup>
File: 3-Games/Blackjack/Platforms/Android/AndroidManifest.xml
Add inside <application> tag:
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-YOUR_ID~YOUR_APP_ID"/>
File: 3-Games/Blackjack/Platforms/Android/Services/GoogleAdMobService.cs
using Android.Gms.Ads;
using Android.Gms.Ads.Interstitial;
using CardsFramework.Services.Ads;
using System;
namespace Blackjack.Android.Services
{
public class GoogleAdMobService : IAdService
{
private InterstitialAd _interstitialAd;
private string _interstitialAdUnitId;
private Action _onInterstitialClosed;
private Action _onInterstitialFailed;
public bool IsInitialized { get; private set; }
public bool AdsEnabled { get; set; } = true;
public void Initialize(string appId)
{
MobileAds.Initialize(MainActivity.Instance, initStatus =>
{
IsInitialized = true;
});
}
public void LoadInterstitial()
{
if (!AdsEnabled) return;
var adRequest = new AdRequest.Builder().Build();
InterstitialAd.Load(
MainActivity.Instance,
_interstitialAdUnitId,
adRequest,
new InterstitialAdLoadCallback(this)
);
}
public void ShowInterstitial(Action onClosed, Action onFailed)
{
if (!AdsEnabled || _interstitialAd == null)
{
onClosed?.Invoke();
return;
}
_onInterstitialClosed = onClosed;
_onInterstitialFailed = onFailed;
_interstitialAd.FullScreenContentCallback = new FullScreenContentCallback(this);
_interstitialAd.Show(MainActivity.Instance);
}
// Implement other interface methods (Banner, Rewarded, etc.)
public void LoadBanner(AdPlacement placement) { }
public void ShowBanner() { }
public void HideBanner() { }
public void LoadRewardedAd() { }
public void ShowRewardedAd(Action<bool> onResult) { }
// Callback classes
private class InterstitialAdLoadCallback : Android.Gms.Ads.Interstitial.InterstitialAdLoadCallback
{
private readonly GoogleAdMobService _service;
public InterstitialAdLoadCallback(GoogleAdMobService service)
{
_service = service;
}
public override void OnAdLoaded(InterstitialAd ad)
{
_service._interstitialAd = ad;
}
public override void OnAdFailedToLoad(LoadAdError error)
{
_service._interstitialAd = null;
}
}
private class FullScreenContentCallback : Android.Gms.Ads.FullScreenContentCallback
{
private readonly GoogleAdMobService _service;
public FullScreenContentCallback(GoogleAdMobService service)
{
_service = service;
}
public override void OnAdDismissedFullScreenContent()
{
_service._interstitialAd = null;
_service._onInterstitialClosed?.Invoke();
_service.LoadInterstitial(); // Preload next ad
}
public override void OnAdFailedToShowFullScreenContent(AdError error)
{
_service._interstitialAd = null;
_service._onInterstitialFailed?.Invoke();
}
}
}
}
File: 3-Games/Blackjack/Platforms/Android/MainActivity.cs
using Blackjack.Android.Services;
using Blackjack.Core.Services;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
// Initialize ad service
var adService = new GoogleAdMobService();
adService.Initialize("ca-app-pub-YOUR_ID~YOUR_APP_ID");
// Create game with ad service
_game = new BlackjackGame();
AdManager.Initialize(_game, adService);
// Check if user purchased ad removal
var settings = GameSettings.LoadSettings();
adService.AdsEnabled = !settings.AdFreeVersion;
// Preload first interstitial
if (adService.AdsEnabled)
adService.LoadInterstitial();
// Existing code...
SetContentView(_game.Services.GetService(typeof(View)) as View);
_game.Run();
}
File: 3-Games/Blackjack/Platforms/iOS/BlackJack.csproj
<ItemGroup>
<PackageReference Include="Xamarin.Google.iOS.MobileAds" Version="11.0.0" />
</ItemGroup>
File: 3-Games/Blackjack/Platforms/iOS/Info.plist
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-YOUR_ID~YOUR_APP_ID</string>
<!-- For iOS 14+ App Tracking Transparency -->
<key>NSUserTrackingUsageDescription</key>
<string>This allows us to show you relevant ads</string>
<key>SKAdNetworkItems</key>
<array>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>cstr6suwn9.skadnetwork</string>
</dict>
<!-- Add more SKAdNetwork IDs as needed -->
</array>
File: 3-Games/Blackjack/Platforms/iOS/Services/GoogleAdMobiOSService.cs
using Google.MobileAds;
using CardsFramework.Services.Ads;
using System;
using Foundation;
using UIKit;
namespace Blackjack.iOS.Services
{
public class GoogleAdMobiOSService : IAdService
{
private Interstitial _interstitialAd;
private string _interstitialAdUnitId;
private Action _onInterstitialClosed;
private Action _onInterstitialFailed;
public bool IsInitialized { get; private set; }
public bool AdsEnabled { get; set; } = true;
public void Initialize(string appId)
{
MobileAds.SharedInstance.Start((status) =>
{
IsInitialized = true;
});
// Request tracking permission (iOS 14+)
RequestTrackingPermission();
}
private void RequestTrackingPermission()
{
if (UIDevice.CurrentDevice.CheckSystemVersion(14, 0))
{
AppTrackingTransparency.ATTrackingManager.RequestTrackingAuthorization(
(status) =>
{
// Permission granted or denied
}
);
}
}
public void LoadInterstitial()
{
if (!AdsEnabled) return;
var request = Request.GetDefaultRequest();
Interstitial.Load(_interstitialAdUnitId, request, (ad, error) =>
{
if (error != null)
{
_interstitialAd = null;
return;
}
_interstitialAd = ad;
_interstitialAd.FullScreenContentDelegate = new InterstitialDelegate(this);
});
}
public void ShowInterstitial(Action onClosed, Action onFailed)
{
if (!AdsEnabled || _interstitialAd == null)
{
onClosed?.Invoke();
return;
}
_onInterstitialClosed = onClosed;
_onInterstitialFailed = onFailed;
var rootViewController = UIApplication.SharedApplication.KeyWindow.RootViewController;
_interstitialAd.Present(rootViewController);
}
// Implement other interface methods
public void LoadBanner(AdPlacement placement) { }
public void ShowBanner() { }
public void HideBanner() { }
public void LoadRewardedAd() { }
public void ShowRewardedAd(Action<bool> onResult) { }
private class InterstitialDelegate : FullScreenContentDelegate
{
private readonly GoogleAdMobiOSService _service;
public InterstitialDelegate(GoogleAdMobiOSService service)
{
_service = service;
}
public override void DidDismissFullScreenContent(NSObject ad)
{
_service._interstitialAd = null;
_service._onInterstitialClosed?.Invoke();
_service.LoadInterstitial(); // Preload next ad
}
public override void DidFailToPresentFullScreenContent(NSObject ad, NSError error)
{
_service._interstitialAd = null;
_service._onInterstitialFailed?.Invoke();
}
}
}
}
File: 3-Games/Blackjack/Platforms/iOS/Program.cs
using Blackjack.iOS.Services;
using Blackjack.Core.Services;
static void Main(string[] args)
{
UIApplication.Main(args, null, typeof(AppDelegate));
}
public class AppDelegate : UIApplicationDelegate
{
private BlackjackGame _game;
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
// Initialize ad service
var adService = new GoogleAdMobiOSService();
adService.Initialize("ca-app-pub-YOUR_ID~YOUR_APP_ID");
// Create game
_game = new BlackjackGame();
AdManager.Initialize(_game, adService);
// Check if user purchased ad removal
var settings = GameSettings.LoadSettings();
adService.AdsEnabled = !settings.AdFreeVersion;
// Preload first interstitial
if (adService.AdsEnabled)
adService.LoadInterstitial();
_game.Run();
return true;
}
}
File: 3-Games/Blackjack/Platforms/Desktop/Services/NoAdService.cs
using CardsFramework.Services.Ads;
using System;
namespace Blackjack.Desktop.Services
{
public class NoAdService : IAdService
{
public bool IsInitialized => true;
public bool AdsEnabled { get; set; } = false;
public void Initialize(string appId) { }
public void LoadBanner(AdPlacement placement) { }
public void ShowBanner() { }
public void HideBanner() { }
public void LoadInterstitial() { }
public void ShowInterstitial(Action onClosed, Action onFailed)
{
onClosed?.Invoke(); // Immediately callback
}
public void LoadRewardedAd() { }
public void ShowRewardedAd(Action<bool> onResult)
{
onResult?.Invoke(false); // No reward
}
}
}
File: 3-Games/Blackjack/Platforms/Desktop/Program.cs
using Blackjack.Desktop.Services;
using Blackjack.Core.Services;
static void Main()
{
var adService = new NoAdService();
using (var game = new BlackjackGame())
{
AdManager.Initialize(game, adService);
game.Run();
}
}
File: 1-Framework/Services/Purchase/IPurchaseService.cs
using System;
using System.Threading.Tasks;
namespace CardsFramework.Services.Purchase
{
public interface IPurchaseService
{
Task<bool> InitializeAsync();
Task<PurchaseResult> PurchaseAdRemovalAsync(string productId);
Task<bool> RestorePurchasesAsync();
bool IsAdRemovalPurchased();
}
public class PurchaseResult
{
public bool Success { get; set; }
public string Error { get; set; }
public string TransactionId { get; set; }
}
}
File: 3-Games/Blackjack/Core/Services/PurchaseManager.cs
using System.Threading.Tasks;
using CardsFramework.Services.Purchase;
namespace Blackjack.Core.Services
{
public class PurchaseManager
{
private static PurchaseManager _instance;
private readonly IPurchaseService _purchaseService;
public static PurchaseManager Instance => _instance;
private PurchaseManager(IPurchaseService purchaseService)
{
_purchaseService = purchaseService;
}
public static void Initialize(IPurchaseService purchaseService)
{
_instance = new PurchaseManager(purchaseService);
}
public async Task<bool> PurchaseAdRemovalAsync()
{
var result = await _purchaseService.PurchaseAdRemovalAsync("com.yourcompany.blackjack.removeads");
if (result.Success)
{
// Save to settings
var settings = GameSettings.LoadSettings();
settings.AdFreeVersion = true;
settings.SaveSettings();
// Disable ads
if (AdManager.Instance != null)
{
AdManager.Instance.HideBanner();
}
return true;
}
return false;
}
public async Task<bool> RestorePurchasesAsync()
{
var restored = await _purchaseService.RestorePurchasesAsync();
if (restored)
{
var settings = GameSettings.LoadSettings();
settings.AdFreeVersion = true;
settings.SaveSettings();
if (AdManager.Instance != null)
{
AdManager.Instance.HideBanner();
}
}
return restored;
}
}
}
NuGet Package: Add Plugin.InAppBilling to Android project
File: 3-Games/Blackjack/Platforms/Android/Services/GooglePlayPurchaseService.cs
using System;
using System.Threading.Tasks;
using Plugin.InAppBilling;
using CardsFramework.Services.Purchase;
namespace Blackjack.Android.Services
{
public class GooglePlayPurchaseService : IPurchaseService
{
private const string AD_REMOVAL_PRODUCT_ID = "com.yourcompany.blackjack.removeads";
public async Task<bool> InitializeAsync()
{
return await CrossInAppBilling.Current.ConnectAsync();
}
public async Task<PurchaseResult> PurchaseAdRemovalAsync(string productId)
{
try
{
var billing = CrossInAppBilling.Current;
var purchase = await billing.PurchaseAsync(productId, ItemType.InAppPurchase);
if (purchase != null)
{
return new PurchaseResult
{
Success = true,
TransactionId = purchase.Id
};
}
return new PurchaseResult { Success = false, Error = "Purchase cancelled" };
}
catch (Exception ex)
{
return new PurchaseResult { Success = false, Error = ex.Message };
}
}
public async Task<bool> RestorePurchasesAsync()
{
try
{
var billing = CrossInAppBilling.Current;
var purchases = await billing.GetPurchasesAsync(ItemType.InAppPurchase);
foreach (var purchase in purchases)
{
if (purchase.ProductId == AD_REMOVAL_PRODUCT_ID)
return true;
}
return false;
}
catch
{
return false;
}
}
public bool IsAdRemovalPurchased()
{
var settings = GameSettings.LoadSettings();
return settings.AdFreeVersion;
}
}
}
File: 3-Games/Blackjack/Platforms/iOS/Services/AppleStorePurchaseService.cs
using System;
using System.Threading.Tasks;
using Plugin.InAppBilling;
using CardsFramework.Services.Purchase;
namespace Blackjack.iOS.Services
{
public class AppleStorePurchaseService : IPurchaseService
{
private const string AD_REMOVAL_PRODUCT_ID = "com.yourcompany.blackjack.removeads";
public async Task<bool> InitializeAsync()
{
return await CrossInAppBilling.Current.ConnectAsync();
}
public async Task<PurchaseResult> PurchaseAdRemovalAsync(string productId)
{
try
{
var billing = CrossInAppBilling.Current;
var purchase = await billing.PurchaseAsync(productId, ItemType.InAppPurchase);
if (purchase != null)
{
return new PurchaseResult
{
Success = true,
TransactionId = purchase.Id
};
}
return new PurchaseResult { Success = false, Error = "Purchase cancelled" };
}
catch (Exception ex)
{
return new PurchaseResult { Success = false, Error = ex.Message };
}
}
public async Task<bool> RestorePurchasesAsync()
{
try
{
var billing = CrossInAppBilling.Current;
var purchases = await billing.GetPurchasesAsync(ItemType.InAppPurchase);
foreach (var purchase in purchases)
{
if (purchase.ProductId == AD_REMOVAL_PRODUCT_ID)
return true;
}
return false;
}
catch
{
return false;
}
}
public bool IsAdRemovalPurchased()
{
var settings = GameSettings.LoadSettings();
return settings.AdFreeVersion;
}
}
}
File: 3-Games/Blackjack/Core/BlackjackCardGame.cs
Add interstitial ads between game rounds:
private int _gamesPlayedSinceAd = 0;
private const int GAMES_BETWEEN_ADS = 3; // Show ad every 3 games
private void OnGameEnded()
{
_gamesPlayedSinceAd++;
// Show interstitial ad every N games
if (_gamesPlayedSinceAd >= GAMES_BETWEEN_ADS)
{
_gamesPlayedSinceAd = 0;
AdManager.Instance?.ShowInterstitialAd(
onClosed: () =>
{
// Continue to next game
StartNewGame();
},
onFailed: () =>
{
// Ad failed, continue anyway
StartNewGame();
}
);
}
else
{
StartNewGame();
}
}
public override void Update(GameTime gameTime)
{
// Don't update game logic if ad is showing
if (AdManager.Instance?.IsShowingAd ?? false)
return;
base.Update(gameTime);
// ... rest of update logic
}
DO:
DON'T:
Use Test Ad Units during development:
ca-app-pub-3940256099942544/1033173712ca-app-pub-3940256099942544/4411468910Test Purchase Flow:
Test Scenarios:
Pricing Strategy:
Ad Frequency:
IAdService interface in 1-FrameworkAdManager.cs in Blackjack/CoreGameSettings.cs to include AdFreeVersion propertyBlackjackGame.cs constructor to initialize AdManagerGoogleAdMobService.csMainActivity.cs to initialize adsAndroidManifest.xml with AdMob app IDGoogleAdMobiOSService.csInfo.plist with AdMob app ID and ATTProgram.cs to initialize adsNoAdService.cs stubProgram.csIPurchaseService interfacePurchaseManager.csGooglePlayPurchaseService (Android)AppleStorePurchaseService (iOS)BlackjackCardGame.csTotal Estimated Time: 23-31 hours
ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXXcom.yourcompany.blackjack.removeadscom.yourcompany.blackjack.removeadsAd callbacks happen on UI thread, not game thread. Always queue callbacks:
private ConcurrentQueue<Action> _pendingCallbacks = new();
// In ad callback
_pendingCallbacks.Enqueue(() => onClosed?.Invoke());
// In Update()
while (_pendingCallbacks.TryDequeue(out var callback))
callback?.Invoke();
GameSettings.cs simple; it already handles persistence correctlyLast Updated: 2026-01-05 MonoGame Version: 3.8+ Target Framework: .NET 9.0