This commit is contained in:
Savya Bikram Shah
2026-06-07 18:41:02 +05:45
parent a15b08df0c
commit 729ddf9277
8 changed files with 1791 additions and 69 deletions

View File

@@ -28,7 +28,12 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
private IDisposable _selectedSub;
private IDisposable _returnToMainMenuSubscription;
private bool _navigatingToGameplay;
private int _selectCount;
// App-session counter, NOT instance state. ColorbookFlowController is scene-scoped: a fresh one
// is built every time the Colorbook scene loads, and selecting a drawing unloads the scene — so
// each visit makes exactly ONE selection. An instance counter would reset to 0 each visit, land
// on 1 (odd) every time, and the "% 2 == 0" show branch would NEVER fire. static persists across
// visits for the whole app run (resets on restart), matching the per-session interstitial cap.
private static int _selectCount;
private CancellationTokenSource _scopeCts;
public ColorbookFlowController(
@@ -65,15 +70,21 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
if (!_navigatingToGameplay) _loadingScreen.Hide();
PrewarmInterstitialAdAsync(ct).Forget();
PrewarmInterstitialAdAsync().Forget();
}
private async UniTaskVoid PrewarmInterstitialAdAsync(CancellationToken ct)
// Ad ops MUST NOT use the Colorbook scope token. This scene unloads inside HandleSelectionAsync —
// the same moment the interstitial should show — which disposes ColorBookLifetimeScope, calls our
// Dispose(), and cancels _scopeCts. A scope-bound load/show then aborts mid-flight (load: never
// ready; show: await throws OCE, handlers unsubscribed). AdMobAdService is the Boot/Root-scoped,
// app-lifetime singleton and self-manages teardown (_lifetimeCts + OnDestroy), so drive ad ops on
// CancellationToken.None — they intentionally outlive this scene.
private async UniTaskVoid PrewarmInterstitialAdAsync()
{
try
{
if (!_ads.IsInitialized) await _ads.InitializeAsync(ct);
if (!_ads.IsReady(AdFormat.Interstitial)) await _ads.LoadAsync(AdFormat.Interstitial, ct);
if (!_ads.IsInitialized) await _ads.InitializeAsync(CancellationToken.None);
if (!_ads.IsReady(AdFormat.Interstitial)) await _ads.LoadAsync(AdFormat.Interstitial, CancellationToken.None);
}
catch (OperationCanceledException) { }
catch (Exception ex)
@@ -106,19 +117,18 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
private async UniTaskVoid HandleSelectionAsync(string templateId)
{
var ct = _scopeCts?.Token ?? CancellationToken.None;
_loadingScreen.Show();
_loadingScreen.SetProgress(0f);
// Frequency cap: show an interstitial on every 2nd level open (2nd, 4th, ...). On skip turns
// just keep one prewarmed so the next show has an ad ready. Fire-and-forget either way — the
// ad overlays the transition while the level loads underneath, so a missed/dropped ad callback
// can't stall the flow at 0% anymore.
// can't stall the flow at 0% anymore. Ad ops run on CancellationToken.None, NOT the scene
// scope token: this scene unloads further down, which would otherwise cancel the show.
if (++_selectCount % 2 == 0)
ShowInterstitialAdAsync(ct).Forget();
ShowInterstitialAdAsync().Forget();
else
PrewarmInterstitialAdAsync(ct).Forget();
PrewarmInterstitialAdAsync().Forget();
try
{
@@ -143,7 +153,8 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
// Fire-and-forget interstitial. Shows only if one is already prewarmed; otherwise it kicks a
// load for next time and returns immediately. Never blocks the level load — by design the
// scene swap below does not depend on the ad's close callback, so the ad can never stall it.
private async UniTaskVoid ShowInterstitialAdAsync(CancellationToken ct)
// CancellationToken.None: the show must survive this scene's unload (see PrewarmInterstitialAdAsync).
private async UniTaskVoid ShowInterstitialAdAsync()
{
try
{
@@ -151,11 +162,11 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
if (!_ads.IsReady(AdFormat.Interstitial))
{
_ads.LoadAsync(AdFormat.Interstitial, ct).Forget();
_ads.LoadAsync(AdFormat.Interstitial, CancellationToken.None).Forget();
return;
}
await _ads.ShowAsync(AdFormat.Interstitial, ct);
await _ads.ShowAsync(AdFormat.Interstitial, CancellationToken.None);
}
catch (OperationCanceledException) { }
catch (Exception ex)