Modules · UI Runtime
UI Runtime
The gamemantra Experience overlay renders AI-generated offers directly inside your Unreal or Unity game. C++ controls show/hide; Dart renders the UI. No WebView. No separate process.
Architecture
Engine Boot Sequence
Phases 0–4 are mandatory and must not be reordered. Call on game launch before any scene loads.
// Unreal: called from GameMantraSubsystem::Initialize()
// Order is MANDATORY — never reorder.
// Phase 0: Code push — apply any pending OTA patch FIRST
FlutterCodePush::CheckAndApply(game_id);
// Phase 1: Init Experience engine dimensions
FlutterUnity_Init(screen_width, screen_height, /*pixel_ratio=*/1.0f);
// Phase 2: Init EGL render loop (captures Unity/Unreal shared context)
FlutterEngineInitRenderLoop();
// Phase 3: Start the engine
FlutterEngineRun(updated_snapshot_path);
// Phase 4: First present → Dart fires 'initSession' automatically
// No manual call needed here.Show / Hide Overlay
The overlay is hidden by default. Call showFlutterUI() when you want the Experience layer to composite over your game frame, and hideFlutterUI() to remove it. Both calls are instant — no re-initialisation needed.
// Show the Experience UI overlay (e.g. when an offer arrives)
showFlutterUI();
// Hide the Experience UI overlay (e.g. on game resume, level start)
hideFlutterUI();Requesting & Responding to Offers
Call GM_GetNextOffer at natural trigger points. The SDK fires the offer callback asynchronously — your game thread never blocks. Dart shows the /offer route automatically viaNavigationService.pushNamed.
// 1. Request an offer at a natural trigger point (level fail, store open, etc.)
GM_GetNextOffer(game_id, R"({
"player_id": "player_001",
"session_id": "sess-uuid",
"trigger": "level_fail"
})");
// 2. C++ receives the async callback → enqueues to Dart
// (GM_Dart_OnOfferReceived fires in sdk_client.dart)
// 3. Player interacts → respond
GM_RespondToOffer(game_id, R"({
"offer_id": "offer-uuid",
"accepted": true
})");Named Routes
Register routes in flutter_app/lib/main.dart. C++ pushes routes via the callback queue — never use overlays or Stack-based listeners (they don't fire in embedded Experience without vsync).
// flutter_app/lib/main.dart — register named routes
MaterialApp(
navigatorKey: NavigationService.navigatorKey,
onGenerateRoute: (settings) {
switch (settings.name) {
case '/offer': return MaterialPageRoute(builder: (_) => const OfferPage());
case '/store': return MaterialPageRoute(builder: (_) => const StorePage());
case '/balance': return MaterialPageRoute(builder: (_) => const BalancePage());
default: return MaterialPageRoute(builder: (_) => const GameView());
}
},
);
// C++ fires a route via EnqueueDartCallback → GM_PumpCallbacks drains:
// NavigationService.navigatorKey.currentState?.pushNamed('/offer');
// NavigationService.navigatorKey.currentState?.pop(); // dismissKill Switch
Polled every 60 seconds automatically. Check the level before requesting offers or showing the overlay.
// Polled every 60s automatically. Check before requesting an offer:
int level = GM_GetKillSwitchLevel();
// 0 = NONE → normal operation
// 1 = OFFERS_KILL → suppress offers, continue telemetry
// 2 = TELEMETRY_KILL → suppress telemetry, continue offers
// 3 = FULL_KILL → suppress everything
if (level == 3) {
// FULL_KILL — hide Experience overlay entirely
hideFlutterUI();
}0 — NONE
Normal. All features active.
1 — OFFERS_KILL
Suppress offers. Telemetry continues.
2 — TELEMETRY_KILL
Suppress telemetry. Offers continue.
3 — FULL_KILL
Suppress everything. Hide overlay.
App Pause / Resume
Always flush events and hide the overlay on pause. On resume, re-check the kill switch before showing. Never call FlutterEngineResetContext from your code — the Unreal plugin calls it internally.
// On app pause (background)
void OnAppPause() {
hideFlutterUI(); // hide overlay
GM_FlushEvents(); // flush buffered telemetry
// Do NOT call FlutterEngineResetContext — this crashes the render thread
}
// On app resume (foreground)
void OnAppResume() {
// Re-check kill switch immediately on resume
int level = GM_GetKillSwitchLevel();
if (level != 3) {
showFlutterUI();
}
}Critical Rules
- ✕ Never call
FlutterEngineResetContext— crashes Unity render thread - ✕ Never call
runApp()beforeinitFlutterUnreal() - ✕ Never wrap an entire Scaffold in
BlockPointer— only wrap tappable widgets - ✕ Never hardcode
pixelRatiofrom device DPI — always pass1.0 - ✕ Never use
addPostFrameCallbackorSchedulerBinding— useTimer.periodic(16ms)for gmPumpCallbacks - ✕ Never apply a code push patch mid-session — apply only on game launch (Phase 0)