Unreal Engine SDK
Reflect SDK for Unreal Engine 5.8+ — a C++ plugin over the shared ReflectCore engine, callable from C++ and Blueprint. Install attribution, deep linking, event and revenue tracking, and a fail-closed privacy gate for games shipping to Android and iOS.
Reflect measures mobile installs. Unreal ships to far more than mobile, so it is worth being precise about what this plugin does on each target:
- Android — full measurement, over JNI to the published
reflect-androidcore. - iOS — full measurement, over a Swift bridge to
ReflectCorecompiled into the plugin. - Editor, Windows, Mac, Linux, console — the plugin loads and every call succeeds immediately with a "no native core" result. Nothing is collected and nothing is sent.
That last row is deliberate, not unfinished. A Reflect app registration is either an Android app or an iOS app, so a desktop build has nothing valid to report as — the ingestion API would reject its events. Rather than fail slowly, the plugin refuses fast, so pressing Play In Editornever leaves your attribution code waiting on a callback that will not arrive.
Requirements
- Unreal Engine 5.8 or later. Verified against 5.8.1.
- Android — NDK
27.2.12479018, platformandroid-34, build-tools35.0.1. These are the versions UE 5.8 itself specifies inEngine/Config/Android/Android_SDK.json. UE 5.8 defaults its games to min SDK 26, and that engine setting is what governs your build — the Reflect core AAR itself only requires API 21 (the same floor as every other Reflect SDK). - iOS — Xcode 16.2+, deployment target 15.0.
Install
Unreal has no dependency manager equivalent to Gradle or CocoaPods, so the plugin is distributed as source and dropped into your project.
- Download the latest release from github.com/bablu147/reflect-sdk-unreal and copy the
ReflectSDK/directory into your project'sPlugins/folder:YourGame/ Plugins/ ReflectSDK/ ReflectSDK.uplugin Source/… - Enable it in your
.uproject:"Plugins": [ { "Name": "ReflectSDK", "Enabled": true } ] - Add the module to your game module's
Build.cs:PublicDependencyModuleNames.Add("ReflectSDK"); - Regenerate project files and build.
The plugin injects its own Android Gradle dependencies, manifest permissions and ProGuard keep-rules through UPL at package time, and compiles the iOS core from source that ships inside the plugin. You do not edit build.gradle, you do not run pod install, and there is no Podfile to maintain.
Plugins/ — not a symlink, not an external directoryTwo tempting shortcuts break packaging in ways that only surface much later, at cook or launch time:
- A symlink — UnrealBuildTool compiles through the link but then looks for the object files at the unresolved path, and the link step fails with
Module.ReflectSDK.cpp.o not found. AdditionalPluginDirectoriespointing at a parent of your project — the build succeeds, the package succeeds, and then the game dies on launch withFailed to open descriptor file …/YourGame.uproject, because staging cannot resolve the project descriptor.
Quick start
#include "Reflect.h"
void UMyGameInstance::Init()
{
Super::Init();
// 1. Bind the streams BEFORE Initialize — see the warning below.
FReflect::OnDeepLink().AddLambda([](const FReflectDeepLinkData& Link)
{
UE_LOG(LogTemp, Log, TEXT("deep link %s (campaign=%s)"), *Link.Url, *Link.Campaign);
});
FReflect::OnAttribution().AddLambda([](const FReflectAttributionData& A)
{
UE_LOG(LogTemp, Log, TEXT("attributed: %s / %s"), *A.Type, *A.Partner);
});
// 2. Initialize.
FReflectConfig Config;
Config.AppKey = TEXT("ak_prod_…"); // from your Reflect dashboard
FReflect::Initialize(Config);
}OnDeepLink before calling InitializeThe native core buffers exactly one deep-link callback while no listener is attached — and it strips clickId and every query parameter from that buffered copy. A listener attached even one line late does not simply receive the callback later; it permanently loses first-install click attribution for that user. There is no recovery and no error is reported.
Everything else in this SDK is forgiving. This is not.
Initialize is asynchronous
Initialize returns immediately, but the privacy gate stays closed until the core reports the consent posture it has stored on the device. Events recorded before the gate opens are dropped, not queued:
FReflect::Initialize(Config);
FReflect::TrackEvent(TEXT("app_ready")); // DROPPED — the gate is not open yetThat is a privacy decision, not a limitation. Buffering pre-consent events and replaying them after a later grant would collect from a window the user had not yet agreed to.
Record from wherever the milestone naturally happens, or check CanCollect() first. In practice the gate opens in well under a second — but it is never open on the line after Initialize.
Events and revenue
FReflectPropertyBag Props;
Props.SetString(TEXT("level_name"), TEXT("forest_02"));
Props.SetInt(TEXT("attempts"), 3);
Props.SetDouble(TEXT("completion_pct"), 87.5);
FReflect::TrackEvent(TEXT("level_complete"), Props);
FReflect::TrackRevenueEvent(TEXT("iap_purchase"), 4.99, TEXT("USD"), Props);FReflectPropertyBag preserves insertion order and keeps value types intact — an int arrives as a number, not a string.
Privacy
The gate starts closed and fails closed: if the stored posture cannot be read for any reason, it stays closed and nothing is collected.
FReflect::SetConsent(true, FReflectPrivacyCompletion::CreateLambda([](bool bOk)
{
if (!bOk)
{
// The core could not durably persist the decision. Do not assume it stuck.
}
}));
EReflectConsentState State = FReflect::GetConsent(); // Denied | GrantedSetConsent and SetEnabled both report whether the change was durably persisted. Ignoring that result is how a GDPR opt-out silently fails: the user sees a confirmation, the app believes the decision was stored, and the next launch reads the old posture. There are exactly two consent states — Denied (the default) and Granted. There is no "unknown".
Blueprint
Every value type is BlueprintType and fully reflected, so structs survive Blueprint pin copies intact. The same API is available from Blueprint and C++.
Platform behaviour
| Android | iOS | Editor / desktop / console | |
|---|---|---|---|
| Core | published AAR | compiled into the plugin | none |
| Deep links | automatic | automatic | ignored |
| App Tracking Transparency | n/a | supported | n/a |
| Events collected | yes | yes | no |
Troubleshooting
No events arriving. Check in order: the gate is open (CanCollect()), your AppKey is set, and BaseUrl is either left alone or set to a real URL. An explicitly empty BaseUrl selects the core's local-only mode, where it collects normally and never opens a network connection — an SDK that looks completely healthy and sends nothing. Use Config.bLocalOnlyDebugMode if that is what you actually want.
Android release builds stop reporting. R8 stripped the core. The plugin ships its own keep-rules, so this only happens if you override the ProGuard configuration — in which case keep com.reflect.core.** and com.reflect.unreal.**, including the native methods of ReflectUnrealBridge, which JNI resolves by name.
Cook fails with an error mentioning Reflect. Do not call FReflect::Initializefrom a game module's StartupModule without an IsRunningCommandlet() guard — the cook commandlet loads your game module too, and starting measurement during a build is meaningless.
Objective-C ARC errors when building for iOS. Something in your project enabled ARC on a module that uses the engine's shared PCH. The Reflect plugin deliberately does not.
This is an engine issue, not a Reflect one, and it affects every plugin equally: UE 5.8's launcher build makes Apple's crash reporter a hard dependency for iOS but ships no Simulator build of it, so linking fails before your code is involved. Build for a real iOS device.
Source
The SDK is open source under the MIT license at github.com/bablu147/reflect-sdk-unreal. It ships a minimal sample project under Sample/ that demonstrates the full integration, including waiting correctly for the privacy gate.