Compare commits

...

2 Commits

Author SHA1 Message Date
Savya Bikram Shah
b74af8867e Minor Fix 2026-05-29 20:04:53 +05:45
Savya Bikram Shah
b669bcc0e0 ads added 2026-05-29 20:02:46 +05:45
33 changed files with 968 additions and 2 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 683c2f491e8042d0a46bb7b5ad497be3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,26 @@
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using Darkmatter.Core.Data.Dynamic.Services.Ads;
using Darkmatter.Core.Enums.Services.Ads;
namespace Darkmatter.Core.Contracts.Services.Ads
{
public interface IAdService
{
bool IsInitialized { get; }
event Action<AdFormat, AdLoadState> LoadStateChanged;
UniTask InitializeAsync(CancellationToken cancellationToken);
UniTask<bool> LoadAsync(AdFormat format, CancellationToken cancellationToken);
bool IsReady(AdFormat format);
UniTask<AdShowResult> ShowAsync(AdFormat format, CancellationToken cancellationToken);
UniTask<bool> ShowBannerAsync(BannerSize size, BannerPosition position, CancellationToken cancellationToken);
void HideBanner();
void DestroyBanner();
void SetConsent(bool hasUserConsent, bool isChildDirected);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ffaede2f106c45a68197d70ec929c7de
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4dcdb224825b4764a080e5b881b996e4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using Darkmatter.Core.Enums.Services.Ads;
using UnityEngine;
namespace Darkmatter.Core.Data.Static.Services.Ads
{
[CreateAssetMenu(fileName = "AdUnitCatalog", menuName = "Darkmatter/Ads/Ad Unit Catalog")]
public class AdUnitCatalogSO : ScriptableObject
{
[Serializable]
public class Entry
{
public AdFormat Format;
public string AndroidUnitId;
public string IosUnitId;
}
[Header("App IDs")]
[SerializeField] private string androidAppId;
[SerializeField] private string iosAppId;
[Header("Test Mode")]
[Tooltip("If true, returns Google sample ad unit IDs (safe for development).")]
[SerializeField] private bool useTestUnits = true;
[Tooltip("Device IDs to treat as test devices (hashed IDs from logcat).")]
[SerializeField] private List<string> testDeviceIds = new();
[Header("Production Unit IDs")]
[SerializeField] private List<Entry> entries = new();
public string AndroidAppId => androidAppId;
public string IosAppId => iosAppId;
public bool UseTestUnits => useTestUnits;
public IReadOnlyList<string> TestDeviceIds => testDeviceIds;
public string GetUnitId(AdFormat format, RuntimePlatform platform)
{
if (useTestUnits || Application.isEditor)
{
return GetTestUnitId(format, platform);
}
foreach (var e in entries)
{
if (e == null || e.Format != format) continue;
return platform == RuntimePlatform.IPhonePlayer ? e.IosUnitId : e.AndroidUnitId;
}
return null;
}
private static string GetTestUnitId(AdFormat format, RuntimePlatform platform)
{
bool ios = platform == RuntimePlatform.IPhonePlayer;
return format switch
{
AdFormat.Banner => ios ? "ca-app-pub-3940256099942544/2934735716" : "ca-app-pub-3940256099942544/6300978111",
AdFormat.Interstitial => ios ? "ca-app-pub-3940256099942544/4411468910" : "ca-app-pub-3940256099942544/1033173712",
AdFormat.Rewarded => ios ? "ca-app-pub-3940256099942544/1712485313" : "ca-app-pub-3940256099942544/5224354917",
AdFormat.RewardedInterstitial => ios ? "ca-app-pub-3940256099942544/6978759866" : "ca-app-pub-3940256099942544/5354046379",
AdFormat.AppOpen => ios ? "ca-app-pub-3940256099942544/5575463023" : "ca-app-pub-3940256099942544/9257395921",
_ => null
};
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e6352f0162df4adf99a5945e25c6bf40
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 40846ca6e71249d68a99e1d226fd162d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,11 @@
namespace Darkmatter.Core.Enums.Services.Ads
{
public enum AdFormat
{
Banner,
Interstitial,
Rewarded,
RewardedInterstitial,
AppOpen
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ce1e736d19fc4569af80b06274dc3935
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,10 @@
namespace Darkmatter.Core.Enums.Services.Ads
{
public enum AdLoadState
{
Idle,
Loading,
Loaded,
Failed
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 44f09c3955b34814abfa83407118a6a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
namespace Darkmatter.Core.Enums.Services.Ads
{
public enum BannerPosition
{
Top,
Bottom,
TopLeft,
TopRight,
BottomLeft,
BottomRight,
Center
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2223e7f2cedb4f479f7c6722e0ca7fd1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
namespace Darkmatter.Core.Enums.Services.Ads
{
public enum BannerSize
{
Banner,
LargeBanner,
MediumRectangle,
FullBanner,
Leaderboard,
SmartBanner,
AnchoredAdaptive
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 570a6e9b47c5451c8ef78aa7a3923ead
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -8,5 +8,6 @@ namespace Darkmatter.Core.Enums.Services.Audio
ShapeReturn = 102, ShapeReturn = 102,
UiTap = 200, UiTap = 200,
PlayButtonTap = 201, PlayButtonTap = 201,
LevelComplete = 300,
} }
} }

View File

@@ -6,8 +6,10 @@ using Darkmatter.Core;
using Darkmatter.Core.Contracts.Features.DrawingCatalog; using Darkmatter.Core.Contracts.Features.DrawingCatalog;
using Darkmatter.Core.Contracts.Features.Loading; using Darkmatter.Core.Contracts.Features.Loading;
using Darkmatter.Core.Contracts.Features.Progression; using Darkmatter.Core.Contracts.Features.Progression;
using Darkmatter.Core.Contracts.Services.Ads;
using Darkmatter.Core.Contracts.Services.Scenes; using Darkmatter.Core.Contracts.Services.Scenes;
using Darkmatter.Core.Data.Signals.Features.Drawing; using Darkmatter.Core.Data.Signals.Features.Drawing;
using Darkmatter.Core.Enums.Services.Ads;
using Darkmatter.Core.Enums.Services.Scenes; using Darkmatter.Core.Enums.Services.Scenes;
using Darkmatter.Libs.Observer; using Darkmatter.Libs.Observer;
using VContainer.Unity; using VContainer.Unity;
@@ -20,33 +22,54 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
private readonly ILoadingScreen _loadingScreen; private readonly ILoadingScreen _loadingScreen;
private readonly IProgressionSystem _progression; private readonly IProgressionSystem _progression;
private readonly ISceneService _scenes; private readonly ISceneService _scenes;
private readonly IAdService _ads;
private readonly IEventBus _bus; private readonly IEventBus _bus;
private IDisposable _selectedSub; private IDisposable _selectedSub;
private IDisposable _returnToMainMenuSubscription; private IDisposable _returnToMainMenuSubscription;
private bool _navigatingToGameplay; private bool _navigatingToGameplay;
private CancellationTokenSource _scopeCts;
public ColorbookFlowController( public ColorbookFlowController(
IDrawingCatalogController drawingCatalog, IDrawingCatalogController drawingCatalog,
ILoadingScreen loadingScreen, ILoadingScreen loadingScreen,
IProgressionSystem progression, IProgressionSystem progression,
ISceneService scenes, ISceneService scenes,
IAdService ads,
IEventBus bus) IEventBus bus)
{ {
_drawingCatalog = drawingCatalog; _drawingCatalog = drawingCatalog;
_loadingScreen = loadingScreen; _loadingScreen = loadingScreen;
_progression = progression; _progression = progression;
_scenes = scenes; _scenes = scenes;
_ads = ads;
_bus = bus; _bus = bus;
_selectedSub = _bus.Subscribe<DrawingSelectedSignal>(OnDrawingSelected); _selectedSub = _bus.Subscribe<DrawingSelectedSignal>(OnDrawingSelected);
} }
public async UniTask StartAsync(CancellationToken cancellation = new CancellationToken()) public async UniTask StartAsync(CancellationToken cancellation = new CancellationToken())
{ {
_scopeCts = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
_returnToMainMenuSubscription = _bus.Subscribe<ReturnToMainMenuSignal>(OnReturnToMainMenu); _returnToMainMenuSubscription = _bus.Subscribe<ReturnToMainMenuSignal>(OnReturnToMainMenu);
_loadingScreen.SetProgress(1f); _loadingScreen.SetProgress(1f);
await _drawingCatalog.InitializeAsync(cancellation); await _drawingCatalog.InitializeAsync(cancellation);
if (!_navigatingToGameplay) _loadingScreen.Hide(); if (!_navigatingToGameplay) _loadingScreen.Hide();
PrewarmRewardedAdAsync(_scopeCts.Token).Forget();
}
private async UniTaskVoid PrewarmRewardedAdAsync(CancellationToken ct)
{
try
{
if (!_ads.IsInitialized) await _ads.InitializeAsync(ct);
if (!_ads.IsReady(AdFormat.Rewarded)) await _ads.LoadAsync(AdFormat.Rewarded, ct);
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
UnityEngine.Debug.LogWarning($"[ColorbookFlow] Rewarded prewarm failed: {ex.Message}");
}
} }
private void OnReturnToMainMenu(ReturnToMainMenuSignal signal) private void OnReturnToMainMenu(ReturnToMainMenuSignal signal)
@@ -72,6 +95,10 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
private async UniTaskVoid HandleSelectionAsync(string templateId) private async UniTaskVoid HandleSelectionAsync(string templateId)
{ {
var ct = _scopeCts?.Token ?? CancellationToken.None;
await ShowRewardedAdAsync(ct);
_loadingScreen.Show(); _loadingScreen.Show();
var progress = new Progress<float>(p => _loadingScreen.SetProgress(p * 0.5f)); var progress = new Progress<float>(p => _loadingScreen.SetProgress(p * 0.5f));
var mappedProgress = new Progress<float>(p => _loadingScreen.SetProgress(0.5f + p * 0.25f)); var mappedProgress = new Progress<float>(p => _loadingScreen.SetProgress(0.5f + p * 0.25f));
@@ -81,9 +108,25 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
cancellationToken: default); cancellationToken: default);
} }
private async UniTask ShowRewardedAdAsync(CancellationToken ct)
{
try
{
if (!_ads.IsInitialized) await _ads.InitializeAsync(ct);
await _ads.ShowAsync(AdFormat.Rewarded, ct);
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
UnityEngine.Debug.LogWarning($"[ColorbookFlow] Rewarded ad failed: {ex.Message}");
}
}
public void Dispose() public void Dispose()
{ {
_selectedSub?.Dispose(); _selectedSub?.Dispose();
_returnToMainMenuSubscription?.Dispose(); _returnToMainMenuSubscription?.Dispose();
_scopeCts?.Cancel();
_scopeCts?.Dispose();
} }
} }

View File

@@ -9,12 +9,14 @@ using Darkmatter.Core.Contracts.Features.GameplayFlow;
using Darkmatter.Core.Contracts.Features.Loading; using Darkmatter.Core.Contracts.Features.Loading;
using Darkmatter.Core.Contracts.Features.Progression; using Darkmatter.Core.Contracts.Features.Progression;
using Darkmatter.Core.Contracts.Features.ShapeBuilder; using Darkmatter.Core.Contracts.Features.ShapeBuilder;
using Darkmatter.Core.Contracts.Services.Audio;
using Darkmatter.Core.Contracts.Services.Scenes; using Darkmatter.Core.Contracts.Services.Scenes;
using Darkmatter.Core.Data.Dynamic.Features.Progression; using Darkmatter.Core.Data.Dynamic.Features.Progression;
using Darkmatter.Core.Data.Signals.Features.Coloring; using Darkmatter.Core.Data.Signals.Features.Coloring;
using Darkmatter.Core.Data.Signals.Features.Drawing; using Darkmatter.Core.Data.Signals.Features.Drawing;
using Darkmatter.Core.Data.Signals.Features.ShapeBuilder; using Darkmatter.Core.Data.Signals.Features.ShapeBuilder;
using Darkmatter.Core.Enums.Features.Progression; using Darkmatter.Core.Enums.Features.Progression;
using Darkmatter.Core.Enums.Services.Audio;
using Darkmatter.Core.Enums.Services.Scenes; using Darkmatter.Core.Enums.Services.Scenes;
using Darkmatter.Libs.Observer; using Darkmatter.Libs.Observer;
using UnityEngine; using UnityEngine;
@@ -34,6 +36,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
private readonly ICaptureFeature _capture; private readonly ICaptureFeature _capture;
private readonly ILoadingScreen _loadingScreen; private readonly ILoadingScreen _loadingScreen;
private readonly IGameplaySceneRefs _refs; private readonly IGameplaySceneRefs _refs;
private readonly ISfxPlayer _sfx;
private readonly IEventBus _bus; private readonly IEventBus _bus;
private IDrawingTemplate _template; private IDrawingTemplate _template;
@@ -55,6 +58,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
ICaptureFeature capture, ICaptureFeature capture,
ILoadingScreen loadingScreen, ILoadingScreen loadingScreen,
IGameplaySceneRefs refs, IGameplaySceneRefs refs,
ISfxPlayer sfx,
IEventBus bus) IEventBus bus)
{ {
_progression = progression; _progression = progression;
@@ -65,6 +69,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
_capture = capture; _capture = capture;
_loadingScreen = loadingScreen; _loadingScreen = loadingScreen;
_refs = refs; _refs = refs;
_sfx = sfx;
_bus = bus; _bus = bus;
} }
@@ -125,6 +130,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
{ {
await SaveCurrentAsync(ct); await SaveCurrentAsync(ct);
_refs.Confetti.Play(); _refs.Confetti.Play();
_sfx.Play(SfxId.LevelComplete);
await _coloring.PlayCompletionAnimationAsync(ct); await _coloring.PlayCompletionAnimationAsync(ct);
_progression.MarkCompleted(_templateId); _progression.MarkCompleted(_templateId);

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3a320ad839a14f578bb9ff149807ed84
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fd05451b7a0c4e04ae9521e8052ee1e2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,31 @@
using Darkmatter.Core.Contracts.Services.Ads;
using Darkmatter.Core.Data.Static.Services.Ads;
using Darkmatter.Libs.Installers;
using UnityEngine;
using VContainer;
using VContainer.Unity;
namespace Darkmatter.Services.Ads
{
public class AdServiceModule : MonoBehaviour, IModule
{
[SerializeField] private AdUnitCatalogSO adUnitCatalog;
[SerializeField] private AdMobAdService adService;
public void Register(IContainerBuilder builder)
{
if (adUnitCatalog != null)
builder.RegisterComponent(adUnitCatalog);
var resolved = adService;
if (resolved == null) resolved = GetComponent<AdMobAdService>();
if (resolved == null) resolved = GetComponentInChildren<AdMobAdService>(includeInactive: true);
if (resolved == null) resolved = FindFirstObjectByType<AdMobAdService>(FindObjectsInactive.Include);
if (resolved != null)
builder.RegisterComponent<IAdService>(resolved);
else
Debug.LogError("[AdServiceModule] No AdMobAdService component found. Assign 'adService' field or add AdMobAdService MonoBehaviour to scene.");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3a17cb41935543a6b4f67be8f398d774
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
{
"name": "Services.Ads",
"rootNamespace": "Darkmatter.Services.Ads",
"references": [
"GUID:6a0a834eb41764f12ba55c3fb04a40cb",
"GUID:f51ebe6a0ceec4240a699833d6309b23",
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
"GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [
{
"name": "com.google.ads.mobile",
"expression": "1.0.0",
"define": "GOOGLE_MOBILE_ADS"
}
],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 662a8e56a1724c84ad8c10211211f0d5
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6fbb9ad075cb419095482b3d74c4311c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,535 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using Darkmatter.Core.Contracts.Services.Ads;
using Darkmatter.Core.Data.Dynamic.Services.Ads;
using Darkmatter.Core.Data.Static.Services.Ads;
using Darkmatter.Core.Enums.Services.Ads;
using UnityEngine;
using AdFormat = Darkmatter.Core.Enums.Services.Ads.AdFormat;
#if GOOGLE_MOBILE_ADS
using GoogleMobileAds.Api;
#endif
namespace Darkmatter.Services.Ads
{
public class AdMobAdService : MonoBehaviour, IAdService
{
[SerializeField] private AdUnitCatalogSO catalog;
[Tooltip("Auto-reload after dismiss/failure for non-banner formats.")]
[SerializeField] private bool autoReload = true;
[Tooltip("Seconds between auto-reload retries on failure.")]
[SerializeField, Min(1f)] private float reloadDelaySeconds = 5f;
public bool IsInitialized => _initialized;
public event Action<AdFormat, AdLoadState> LoadStateChanged;
private bool _initialized;
private bool _hasUserConsent = true;
private bool _isChildDirected;
private CancellationTokenSource _lifetimeCts;
private readonly Dictionary<AdFormat, AdLoadState> _states = new();
#if GOOGLE_MOBILE_ADS
private InterstitialAd _interstitial;
private RewardedAd _rewarded;
private RewardedInterstitialAd _rewardedInterstitial;
private AppOpenAd _appOpen;
private BannerView _banner;
#endif
private void Awake()
{
_lifetimeCts = new CancellationTokenSource();
foreach (AdFormat fmt in Enum.GetValues(typeof(AdFormat)))
_states[fmt] = AdLoadState.Idle;
}
private void OnDestroy()
{
_lifetimeCts?.Cancel();
_lifetimeCts?.Dispose();
_lifetimeCts = null;
#if GOOGLE_MOBILE_ADS
_interstitial?.Destroy();
_rewarded?.Destroy();
_rewardedInterstitial?.Destroy();
_appOpen?.Destroy();
_banner?.Destroy();
#endif
}
public async UniTask InitializeAsync(CancellationToken cancellationToken)
{
if (_initialized) return;
if (catalog == null)
{
Debug.LogError("[AdMobAdService] No AdUnitCatalogSO assigned.");
return;
}
#if GOOGLE_MOBILE_ADS
ApplyRequestConfiguration();
var tcs = new UniTaskCompletionSource<bool>();
MobileAds.Initialize(_ => tcs.TrySetResult(true));
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
{
await tcs.Task;
}
_initialized = true;
Debug.Log("[AdMobAdService] Initialized.");
#else
await UniTask.CompletedTask;
_initialized = true;
Debug.LogWarning("[AdMobAdService] GOOGLE_MOBILE_ADS not defined. Service will no-op. Add scripting define after importing Google Mobile Ads SDK.");
#endif
}
public async UniTask<bool> LoadAsync(AdFormat format, CancellationToken cancellationToken)
{
if (!_initialized)
{
Debug.LogWarning("[AdMobAdService] Load called before init.");
return false;
}
#if GOOGLE_MOBILE_ADS
string unitId = catalog.GetUnitId(format, Application.platform);
if (string.IsNullOrEmpty(unitId))
{
Debug.LogWarning($"[AdMobAdService] No unit ID for {format} on {Application.platform}.");
return false;
}
SetState(format, AdLoadState.Loading);
var request = new AdRequest();
switch (format)
{
case AdFormat.Interstitial:
return await LoadInterstitialAsync(unitId, request, cancellationToken);
case AdFormat.Rewarded:
return await LoadRewardedAsync(unitId, request, cancellationToken);
case AdFormat.RewardedInterstitial:
return await LoadRewardedInterstitialAsync(unitId, request, cancellationToken);
case AdFormat.AppOpen:
return await LoadAppOpenAsync(unitId, request, cancellationToken);
case AdFormat.Banner:
Debug.LogWarning("[AdMobAdService] Use ShowBannerAsync for banners.");
SetState(format, AdLoadState.Failed);
return false;
}
return false;
#else
await UniTask.CompletedTask;
SetState(format, AdLoadState.Failed);
return false;
#endif
}
public bool IsReady(AdFormat format)
{
if (!_initialized) return false;
#if GOOGLE_MOBILE_ADS
return format switch
{
AdFormat.Interstitial => _interstitial != null && _interstitial.CanShowAd(),
AdFormat.Rewarded => _rewarded != null && _rewarded.CanShowAd(),
AdFormat.RewardedInterstitial => _rewardedInterstitial != null && _rewardedInterstitial.CanShowAd(),
AdFormat.AppOpen => _appOpen != null && _appOpen.CanShowAd(),
_ => false
};
#else
return false;
#endif
}
public async UniTask<AdShowResult> ShowAsync(AdFormat format, CancellationToken cancellationToken)
{
if (!_initialized) return AdShowResult.Failure("Not initialized.");
#if GOOGLE_MOBILE_ADS
if (!IsReady(format))
{
bool loaded = await LoadAsync(format, cancellationToken);
if (!loaded) return AdShowResult.Failure($"{format} failed to load.");
}
switch (format)
{
case AdFormat.Interstitial: return await ShowInterstitialAsync(cancellationToken);
case AdFormat.Rewarded: return await ShowRewardedAsync(cancellationToken);
case AdFormat.RewardedInterstitial: return await ShowRewardedInterstitialAsync(cancellationToken);
case AdFormat.AppOpen: return await ShowAppOpenAsync(cancellationToken);
}
return AdShowResult.Failure($"{format} not supported by ShowAsync.");
#else
await UniTask.CompletedTask;
return AdShowResult.Failure("GOOGLE_MOBILE_ADS not defined.");
#endif
}
public async UniTask<bool> ShowBannerAsync(BannerSize size, BannerPosition position, CancellationToken cancellationToken)
{
if (!_initialized) return false;
#if GOOGLE_MOBILE_ADS
string unitId = catalog.GetUnitId(AdFormat.Banner, Application.platform);
if (string.IsNullOrEmpty(unitId)) return false;
_banner?.Destroy();
_banner = new BannerView(unitId, MapBannerSize(size), MapBannerPosition(position));
var tcs = new UniTaskCompletionSource<bool>();
_banner.OnBannerAdLoaded += () => tcs.TrySetResult(true);
_banner.OnBannerAdLoadFailed += err =>
{
Debug.LogWarning($"[AdMobAdService] Banner load failed: {err}");
tcs.TrySetResult(false);
};
SetState(AdFormat.Banner, AdLoadState.Loading);
_banner.LoadAd(new AdRequest());
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
{
bool ok = await tcs.Task;
SetState(AdFormat.Banner, ok ? AdLoadState.Loaded : AdLoadState.Failed);
return ok;
}
#else
await UniTask.CompletedTask;
return false;
#endif
}
public void HideBanner()
{
#if GOOGLE_MOBILE_ADS
_banner?.Hide();
#endif
}
public void DestroyBanner()
{
#if GOOGLE_MOBILE_ADS
_banner?.Destroy();
_banner = null;
SetState(AdFormat.Banner, AdLoadState.Idle);
#endif
}
public void SetConsent(bool hasUserConsent, bool isChildDirected)
{
_hasUserConsent = hasUserConsent;
_isChildDirected = isChildDirected;
#if GOOGLE_MOBILE_ADS
if (_initialized) ApplyRequestConfiguration();
#endif
}
#if GOOGLE_MOBILE_ADS
private void ApplyRequestConfiguration()
{
var config = new RequestConfiguration
{
TagForChildDirectedTreatment = _isChildDirected
? TagForChildDirectedTreatment.True
: TagForChildDirectedTreatment.Unspecified,
TagForUnderAgeOfConsent = _hasUserConsent
? TagForUnderAgeOfConsent.Unspecified
: TagForUnderAgeOfConsent.True
};
if (catalog.TestDeviceIds is { Count: > 0 })
{
config.TestDeviceIds = new List<string>(catalog.TestDeviceIds);
}
MobileAds.SetRequestConfiguration(config);
}
private async UniTask<bool> LoadInterstitialAsync(string unitId, AdRequest request, CancellationToken cancellationToken)
{
var tcs = new UniTaskCompletionSource<bool>();
InterstitialAd.Load(unitId, request, (ad, error) =>
{
if (error != null || ad == null)
{
Debug.LogWarning($"[AdMobAdService] Interstitial load failed: {error}");
tcs.TrySetResult(false);
return;
}
_interstitial?.Destroy();
_interstitial = ad;
WireFullScreenEvents(ad, AdFormat.Interstitial);
tcs.TrySetResult(true);
});
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
{
bool ok = await tcs.Task;
SetState(AdFormat.Interstitial, ok ? AdLoadState.Loaded : AdLoadState.Failed);
return ok;
}
}
private async UniTask<bool> LoadRewardedAsync(string unitId, AdRequest request, CancellationToken cancellationToken)
{
var tcs = new UniTaskCompletionSource<bool>();
RewardedAd.Load(unitId, request, (ad, error) =>
{
if (error != null || ad == null)
{
Debug.LogWarning($"[AdMobAdService] Rewarded load failed: {error}");
tcs.TrySetResult(false);
return;
}
_rewarded?.Destroy();
_rewarded = ad;
WireFullScreenEvents(ad, AdFormat.Rewarded);
tcs.TrySetResult(true);
});
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
{
bool ok = await tcs.Task;
SetState(AdFormat.Rewarded, ok ? AdLoadState.Loaded : AdLoadState.Failed);
return ok;
}
}
private async UniTask<bool> LoadRewardedInterstitialAsync(string unitId, AdRequest request, CancellationToken cancellationToken)
{
var tcs = new UniTaskCompletionSource<bool>();
RewardedInterstitialAd.Load(unitId, request, (ad, error) =>
{
if (error != null || ad == null)
{
Debug.LogWarning($"[AdMobAdService] RewardedInterstitial load failed: {error}");
tcs.TrySetResult(false);
return;
}
_rewardedInterstitial?.Destroy();
_rewardedInterstitial = ad;
WireFullScreenEvents(ad, AdFormat.RewardedInterstitial);
tcs.TrySetResult(true);
});
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
{
bool ok = await tcs.Task;
SetState(AdFormat.RewardedInterstitial, ok ? AdLoadState.Loaded : AdLoadState.Failed);
return ok;
}
}
private async UniTask<bool> LoadAppOpenAsync(string unitId, AdRequest request, CancellationToken cancellationToken)
{
var tcs = new UniTaskCompletionSource<bool>();
AppOpenAd.Load(unitId, request, (ad, error) =>
{
if (error != null || ad == null)
{
Debug.LogWarning($"[AdMobAdService] AppOpen load failed: {error}");
tcs.TrySetResult(false);
return;
}
_appOpen?.Destroy();
_appOpen = ad;
WireFullScreenEvents(ad, AdFormat.AppOpen);
tcs.TrySetResult(true);
});
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
{
bool ok = await tcs.Task;
SetState(AdFormat.AppOpen, ok ? AdLoadState.Loaded : AdLoadState.Failed);
return ok;
}
}
private async UniTask<AdShowResult> ShowInterstitialAsync(CancellationToken cancellationToken)
{
var tcs = new UniTaskCompletionSource<AdShowResult>();
Action onClosed = () => tcs.TrySetResult(AdShowResult.Success());
Action<AdError> onFailed = err => tcs.TrySetResult(AdShowResult.Failure(err?.GetMessage()));
_interstitial.OnAdFullScreenContentClosed += onClosed;
_interstitial.OnAdFullScreenContentFailed += onFailed;
_interstitial.Show();
try
{
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
return await tcs.Task;
}
finally
{
if (_interstitial != null)
{
_interstitial.OnAdFullScreenContentClosed -= onClosed;
_interstitial.OnAdFullScreenContentFailed -= onFailed;
}
ScheduleReload(AdFormat.Interstitial);
}
}
private async UniTask<AdShowResult> ShowRewardedAsync(CancellationToken cancellationToken)
{
var tcs = new UniTaskCompletionSource<AdShowResult>();
bool earned = false;
AdReward reward = default;
Action onClosed = () => tcs.TrySetResult(earned ? AdShowResult.WithReward(reward) : AdShowResult.Success());
Action<AdError> onFailed = err => tcs.TrySetResult(AdShowResult.Failure(err?.GetMessage()));
_rewarded.OnAdFullScreenContentClosed += onClosed;
_rewarded.OnAdFullScreenContentFailed += onFailed;
_rewarded.Show(r =>
{
earned = true;
reward = new AdReward(r.Type, r.Amount);
});
try
{
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
return await tcs.Task;
}
finally
{
if (_rewarded != null)
{
_rewarded.OnAdFullScreenContentClosed -= onClosed;
_rewarded.OnAdFullScreenContentFailed -= onFailed;
}
ScheduleReload(AdFormat.Rewarded);
}
}
private async UniTask<AdShowResult> ShowRewardedInterstitialAsync(CancellationToken cancellationToken)
{
var tcs = new UniTaskCompletionSource<AdShowResult>();
bool earned = false;
AdReward reward = default;
Action onClosed = () => tcs.TrySetResult(earned ? AdShowResult.WithReward(reward) : AdShowResult.Success());
Action<AdError> onFailed = err => tcs.TrySetResult(AdShowResult.Failure(err?.GetMessage()));
_rewardedInterstitial.OnAdFullScreenContentClosed += onClosed;
_rewardedInterstitial.OnAdFullScreenContentFailed += onFailed;
_rewardedInterstitial.Show(r =>
{
earned = true;
reward = new AdReward(r.Type, r.Amount);
});
try
{
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
return await tcs.Task;
}
finally
{
if (_rewardedInterstitial != null)
{
_rewardedInterstitial.OnAdFullScreenContentClosed -= onClosed;
_rewardedInterstitial.OnAdFullScreenContentFailed -= onFailed;
}
ScheduleReload(AdFormat.RewardedInterstitial);
}
}
private async UniTask<AdShowResult> ShowAppOpenAsync(CancellationToken cancellationToken)
{
var tcs = new UniTaskCompletionSource<AdShowResult>();
Action onClosed = () => tcs.TrySetResult(AdShowResult.Success());
Action<AdError> onFailed = err => tcs.TrySetResult(AdShowResult.Failure(err?.GetMessage()));
_appOpen.OnAdFullScreenContentClosed += onClosed;
_appOpen.OnAdFullScreenContentFailed += onFailed;
_appOpen.Show();
try
{
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
return await tcs.Task;
}
finally
{
if (_appOpen != null)
{
_appOpen.OnAdFullScreenContentClosed -= onClosed;
_appOpen.OnAdFullScreenContentFailed -= onFailed;
}
ScheduleReload(AdFormat.AppOpen);
}
}
private void WireFullScreenEvents(InterstitialAd ad, AdFormat format) =>
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
private void WireFullScreenEvents(RewardedAd ad, AdFormat format) =>
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
private void WireFullScreenEvents(RewardedInterstitialAd ad, AdFormat format) =>
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
private void WireFullScreenEvents(AppOpenAd ad, AdFormat format) =>
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
private void ScheduleReload(AdFormat format)
{
if (!autoReload || _lifetimeCts == null) return;
ReloadAfterDelayAsync(format, _lifetimeCts.Token).Forget();
}
private async UniTaskVoid ReloadAfterDelayAsync(AdFormat format, CancellationToken cancellationToken)
{
try
{
await UniTask.Delay(TimeSpan.FromSeconds(reloadDelaySeconds), cancellationToken: cancellationToken);
await LoadAsync(format, cancellationToken);
}
catch (OperationCanceledException) { }
}
private static AdSize MapBannerSize(BannerSize size) => size switch
{
BannerSize.Banner => AdSize.Banner,
BannerSize.LargeBanner => AdSize.LargeBanner,
BannerSize.MediumRectangle => AdSize.MediumRectangle,
BannerSize.FullBanner => AdSize.IABBanner,
BannerSize.Leaderboard => AdSize.Leaderboard,
BannerSize.SmartBanner => AdSize.SmartBanner,
BannerSize.AnchoredAdaptive => AdSize.GetCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(AdSize.FullWidth),
_ => AdSize.Banner
};
private static AdPosition MapBannerPosition(BannerPosition position) => position switch
{
BannerPosition.Top => AdPosition.Top,
BannerPosition.Bottom => AdPosition.Bottom,
BannerPosition.TopLeft => AdPosition.TopLeft,
BannerPosition.TopRight => AdPosition.TopRight,
BannerPosition.BottomLeft => AdPosition.BottomLeft,
BannerPosition.BottomRight => AdPosition.BottomRight,
BannerPosition.Center => AdPosition.Center,
_ => AdPosition.Bottom
};
#endif
private void SetState(AdFormat format, AdLoadState state)
{
_states[format] = state;
LoadStateChanged?.Invoke(format, state);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 018ba83ad51c4d9a89a904ba3d21ead2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f615a2c0d3b0f44718ef3ac2b3d1a9f6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,19 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e6352f0162df4adf99a5945e25c6bf40, type: 3}
m_Name: AdUnitCatalog
m_EditorClassIdentifier: Core::Darkmatter.Core.Data.Static.Services.Ads.AdUnitCatalogSO
androidAppId: ca-app-pub-3940256099942544~3347511713
iosAppId: ca-app-pub-3940256099942544~1458002511
useTestUnits: 0
testDeviceIds: []
entries: []

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6e2d0c78aa02e4411948adcca14299a5
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -17,5 +17,11 @@ MonoBehaviour:
Clip: {fileID: 8300000, guid: 904c9da8da3f449b997f23438816edba, type: 3} Clip: {fileID: 8300000, guid: 904c9da8da3f449b997f23438816edba, type: 3}
Channel: 0 Channel: 0
Volume: 1 Volume: 1
Pitch: 0 Pitch: 1
Loop: 0
- Id: 300
Clip: {fileID: 8300000, guid: b2d674efc9bbd4fda8e5864b6c91296c, type: 3}
Channel: 0
Volume: 1
Pitch: 1
Loop: 0 Loop: 0

View File

@@ -1629,6 +1629,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 018ba83ad51c4d9a89a904ba3d21ead2, type: 3} m_Script: {fileID: 11500000, guid: 018ba83ad51c4d9a89a904ba3d21ead2, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: Services.Ads::Darkmatter.Services.Ads.AdMobAdService m_EditorClassIdentifier: Services.Ads::Darkmatter.Services.Ads.AdMobAdService
catalog: {fileID: 11400000, guid: 6e2d0c78aa02e4411948adcca14299a5, type: 2}
autoReload: 1 autoReload: 1
reloadDelaySeconds: 5 reloadDelaySeconds: 5
--- !u!1 &1239449674 --- !u!1 &1239449674

View File

@@ -39,7 +39,7 @@ MonoBehaviour:
m_ExplicitNullChecks: 0 m_ExplicitNullChecks: 0
m_ExplicitDivideByZeroChecks: 0 m_ExplicitDivideByZeroChecks: 0
m_ExplicitArrayBoundsChecks: 0 m_ExplicitArrayBoundsChecks: 0
m_CompressionType: 2 m_CompressionType: 3
m_InstallInBuildFolder: 0 m_InstallInBuildFolder: 0
m_InsightsSettingsContainer: m_InsightsSettingsContainer:
m_BuildProfileEngineDiagnosticsState: 2 m_BuildProfileEngineDiagnosticsState: 2