Analytics¶
OxHeart provides a thin analytics abstraction — IAnalyticsService — that gives your game a single call site for telemetry without coupling it to any specific backend. When the toggle is on but no backend is registered, the engine uses NullAnalyticsService, a silent no-op.
Enabling¶
- Open your ServiceConfiguration asset.
- Enable Analytics Service (under Feature Services).
That's it — NullAnalyticsService is registered automatically. All TrackEvent calls will be silently discarded until you provide a real backend.
Tracking Events¶
var analytics = ServiceLocator.Instance.Get<IAnalyticsService>();
// Simple event
analytics.TrackEvent("game_started");
// Event with parameters
analytics.TrackEvent("level_complete", new Dictionary<string, object>
{
{ "level", 3 },
{ "score", 12500 },
{ "time_seconds", 45.2 }
});
TrackEvent is fire-and-forget. The backend owns batching, buffering, and transmission — your game code doesn't need to care.
Runtime Enable/Disable¶
analytics.SetEnabled(false); // pauses dispatch (e.g. user opted out)
analytics.SetEnabled(true); // resumes
Providing a Backend¶
Create your own IAnalyticsService implementation and register it via IServiceRegistrar. Registrars run after the engine's own services, so yours replaces the default NullAnalyticsService:
public class MyAnalyticsService : IAnalyticsService
{
public bool IsEnabled { get; private set; } = true;
public void SetEnabled(bool enabled) => IsEnabled = enabled;
public void TrackEvent(string eventName, IReadOnlyDictionary<string, object> parameters = null)
{
if (!IsEnabled) return;
// Forward to your telemetry backend
}
public void Initialize() { /* SDK init */ }
// NOTE: the engine does NOT call this on a service you registered yourself.
// Call it from your own teardown - see "Lifecycle" below.
public void Shutdown() { /* flush/teardown */ }
}
[Preserve]
public class MyAnalyticsRegistrar : IServiceRegistrar
{
public int Priority => 0;
public void RegisterServices(IServiceLocator serviceLocator, ServiceConfiguration config)
{
if (!config.EnableAnalyticsService) return;
var service = new MyAnalyticsService();
service.Initialize();
serviceLocator.Register<IAnalyticsService>(service);
}
}
See Samples~/Scripts/13_AnalyticsSample.cs for the TrackEvent call site in isolation, and
Samples~/Analytics/ for a complete interactive scene — buffering backend, opt-out toggle, and a
Simulate Quit button that shows Shutdown() flushing the batch.
What Your Backend Owns¶
The interface is deliberately minimal, so several things are the backend's responsibility rather than the engine's. Worth knowing before you write the wrapper:
- Asynchronous start-up.
Initialize()is synchronous, and the engine has no "backend ready" concept. Telemetry SDKs commonly need an asynchronous start — dependency checks, service initialization — before they will accept an event. Buffer events inside your implementation until your SDK is ready, then flush. TheSamples~/Analytics/backend already has exactly that shape; see Lifecycle below for the flush-on-shutdown half. - Consent. OxHeart neither gathers nor stores consent, and never starts collection on your
behalf. If your backend requires an opt-in before collecting, gate it inside your implementation —
SetEnabled()is the natural place to hang "start/stop collecting". - Event-name and parameter rules. The engine passes
eventNameandparametersthrough unchanged and validates neither. Backends impose their own limits: name character sets and lengths, parameter counts, and accepted value types.IReadOnlyDictionary<string, object>is deliberately wider than most backends allow, so convert — and drop or reject what your backend will not take — in your wrapper. - Batching, retries, and offline queueing. Not provided; they belong in the backend.
Lifecycle¶
A service you register through IServiceRegistrar is yours by default: you construct it, you
initialize it, and the engine does not call LateInitialize() or Shutdown() on it. That matters
more for analytics than for most services — a backend that batches events in memory and relies on
Shutdown() to flush will silently lose the last batch of every session if nothing calls it.
Two ways to handle it. Preferred — hand the service to the engine with RegisterManaged:
public void RegisterServices(IServiceLocator serviceLocator, ServiceConfiguration config)
{
if (!config.EnableAnalyticsService) return;
var service = new MyAnalyticsService();
service.Initialize();
// RegisterManaged also enrols the service in the engine's lifecycle, so it receives
// LateInitialize() (safe to resolve other services) and Shutdown() on engine shutdown.
serviceLocator.RegisterManaged<IAnalyticsService>(service);
}
RegisterManaged registers exactly like Register, and additionally enrols the instance in the
engine's service lifecycle. It never calls Initialize() — registrars run after service creation, so
initialize your service yourself before registering, as above. It also retires the default
NullAnalyticsService it replaces, so nothing is left half-alive.
Or keep ownership and tear down yourself, if you would rather not depend on engine ordering:
serviceLocator.Register<IAnalyticsService>(service);
Application.quitting += service.Shutdown; // the engine will not call it for you
Either is fine — just don't do both, or Shutdown() runs twice. Make your Shutdown() idempotent
regardless; it is cheap insurance and matches how the engine's own services behave. If your SDK
already registers its own quit handler, you may not need either.
Two smaller notes on the replacement:
- Registering over the default logs one
Service of type IAnalyticsService ... is already registered. Overwriting.warning at startup. That is expected and harmless — it is the engine reporting that your backend won. WithRegisterManagedyou will also see a line confirming the defaultNullAnalyticsServicewas shut down. - If you never intend to use the no-op default, you can leave Analytics Service switched off in
ServiceConfigurationand register your backend unconditionally. The toggle exists so that game code can callGet<IAnalyticsService>()safely before a backend is wired up, not because the engine requires it.
Design Notes¶
- No config ScriptableObject. Unlike Audio or Localization, analytics has no engine-level config knobs — the backend owns all configuration. The toggle in
ServiceConfigurationis sufficient. - No batching or session tracking. The engine abstraction is intentionally minimal. Batching, offline queues, and session management belong in the backend implementation, not the interface.
- Null-object default (ADR-018). When enabled without a backend, every
TrackEventcall is a no-op — no exceptions, no warnings.