Unity SDK Setup Guide (C#)
---
sidebar_label: Unity (C#) sidebar_category: Gaming SDK Guides
Unity SDK Setup Guide (C#)
This guide covers integrating Gaming Intelligence into a Unity game using C#. There is no prebuilt Unity package — integration is done by adding a single C# wrapper class to your project that uses UnityWebRequest to call the Gaming Intelligence REST API directly. HMAC request signing uses System.Security.Cryptography.HMACSHA256, which is available in every Unity scripting backend including IL2CPP.
Prerequisites
Before you begin, have the following ready:
| Requirement | Where to Find It |
|---|---|
| SDK Key | Linkzly Console → Gaming → [Your Game] → Settings → SDK Configuration → SDK Key |
| Organization ID | Linkzly Console → Organization Settings |
| Game ID | Linkzly Console → Gaming → [Your Game] → Settings → General Settings |
| Unity Version | Unity 2020.3 LTS or later |
| Scripting Backend | Mono or IL2CPP (both supported) |
Your SDK key is masked by default in the console. Use the eye icon to reveal it or the copy button to copy it directly.
Installation
There is no package to install from the Unity Package Manager or Asset Store. Add the integration file manually.
Create a folder at Assets/Linkzly/ and place the following two C# files inside it. The files have no external dependencies — they use only Unity's built-in UnityEngine.Networking and the .NET System.Security.Cryptography namespace.
The Client Class
Create Assets/Linkzly/LinkzlyGaming.cs:
// Assets/Linkzly/LinkzlyGaming.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
namespace Linkzly
{
public class GamingOptions
{
public string ApiKey;
public string OrganizationId;
public string GameId;
public string GameVersion;
public string BaseUrl = "https://gaming.linkzly.com";
public string SigningSecret; // Required when HMAC Signing is enabled
public int MaxBatchSize = 100;
public float FlushIntervalSeconds = 5f;
public bool AutoSessionTracking = true;
public bool Debug = false;
}
public class LinkzlyGaming : MonoBehaviour
{
private static LinkzlyGaming _instance;
/// <summary>
/// The singleton instance. Only accessible after Configure() has been called.
/// </summary>
public static LinkzlyGaming Instance
{
get
{
if (_instance == null)
UnityEngine.Debug.LogWarning(
"[LinkzlyGaming] Not initialized. Call LinkzlyGaming.Configure() first.");
return _instance;
}
}
private GamingOptions _options;
private string _currentPlayerId;
private string _currentSessionId;
private readonly List<Dictionary<string, object>> _queue = new();
private bool _sessionActive;
// ── Initialization ────────────────────────────────────────────────────
/// <summary>
/// Initialize the SDK. Call once before tracking any events.
/// The recommended location is a Bootstrap MonoBehaviour that runs first.
/// Subsequent calls after initialization are silently ignored.
/// </summary>
public static void Configure(GamingOptions options)
{
if (_instance != null)
{
UnityEngine.Debug.LogWarning(
"[LinkzlyGaming] Already initialized. Configure() ignored.");
return;
}
var go = new GameObject("LinkzlyGaming");
DontDestroyOnLoad(go);
_instance = go.AddComponent<LinkzlyGaming>();
_instance._options = options;
_instance._currentSessionId = Guid.NewGuid().ToString();
if (options.AutoSessionTracking)
{
_instance.EnqueueInternal("session_start", null);
_instance._sessionActive = true;
}
_instance.StartCoroutine(_instance.FlushLoop());
if (options.Debug)
UnityEngine.Debug.Log("[LinkzlyGaming] SDK initialized.");
}
// ── Public API ────────────────────────────────────────────────────────
/// <summary>
/// Associate all subsequent events with a player identity.
/// Call after the player logs in or completes authentication.
/// </summary>
public void Identify(string playerId)
{
_currentPlayerId = playerId;
if (_options.Debug)
UnityEngine.Debug.Log($"[LinkzlyGaming] Player identified: {playerId}");
}
/// <summary>
/// Enqueue a game event. Events are sent automatically on the next flush cycle
/// or immediately when the queue reaches MaxBatchSize.
/// </summary>
public void Track(string eventType, Dictionary<string, object> data = null)
{
EnqueueInternal(eventType, data);
}
/// <summary>
/// Force-send all queued events immediately. Call before scene transitions
/// or any operation that may suspend coroutines.
/// </summary>
public void Flush()
{
if (_queue.Count == 0) return;
var batch = new List<Dictionary<string, object>>(_queue);
_queue.Clear();
StartCoroutine(SendBatch(batch));
}
/// <summary>
/// End the current session, flush all queued events, and clear the player identity.
/// Call on logout.
/// </summary>
public void Reset()
{
if (_options.AutoSessionTracking && _sessionActive)
EnqueueInternal("session_end", null);
Flush();
_currentPlayerId = null;
_currentSessionId = Guid.NewGuid().ToString();
_sessionActive = false;
}
// ── Unity lifecycle hooks ─────────────────────────────────────────────
private void OnApplicationPause(bool isPaused)
{
if (!_options.AutoSessionTracking) return;
if (isPaused)
{
EnqueueInternal("session_end", null);
Flush();
_sessionActive = false;
}
else
{
_currentSessionId = Guid.NewGuid().ToString();
EnqueueInternal("session_start", null);
_sessionActive = true;
}
}
private void OnApplicationQuit()
{
if (_options.AutoSessionTracking && _sessionActive)
EnqueueInternal("session_end", null);
Flush();
// Note: coroutines may not complete after OnApplicationQuit.
// For a guaranteed flush, call Reset() explicitly from your quit UI flow.
}
// ── Internal helpers ──────────────────────────────────────────────────
private void EnqueueInternal(string eventType, Dictionary<string, object> data)
{
var evt = new Dictionary<string, object>
{
["event_id"] = Guid.NewGuid().ToString(),
["event_type"] = eventType,
["timestamp"] = DateTime.UtcNow.ToString("o"),
["platform"] = "unity",
["player_id"] = _currentPlayerId ?? "",
["session_id"] = _currentSessionId,
["game_version"] = _options.GameVersion ?? "",
["data"] = data ?? new Dictionary<string, object>()
};
_queue.Add(evt);
if (_options.Debug)
UnityEngine.Debug.Log(
$"[LinkzlyGaming] Queued: {eventType} (queue={_queue.Count})");
if (_queue.Count >= _options.MaxBatchSize)
Flush();
}
private IEnumerator FlushLoop()
{
while (true)
{
yield return new WaitForSeconds(_options.FlushIntervalSeconds);
if (_queue.Count > 0)
Flush();
}
}
private IEnumerator SendBatch(List<Dictionary<string, object>> events)
{
var payload = new Dictionary<string, object>
{
["batch_id"] = Guid.NewGuid().ToString(),
["batch_timestamp"] = DateTime.UtcNow.ToString("o"),
["events"] = events
};
string bodyJson = MiniJson.Serialize(payload);
byte[] bodyBytes = Encoding.UTF8.GetBytes(bodyJson);
string url = $"{_options.BaseUrl}/api/v1/gaming/events";
var req = new UnityWebRequest(url, "POST")
{
uploadHandler = new UploadHandlerRaw(bodyBytes),
downloadHandler = new DownloadHandlerBuffer()
};
req.SetRequestHeader("Content-Type", "application/json");
req.SetRequestHeader("Authorization", $"Bearer {_options.ApiKey}");
req.SetRequestHeader("X-Organization-ID", _options.OrganizationId);
req.SetRequestHeader("X-Game-ID", _options.GameId);
if (!string.IsNullOrEmpty(_options.SigningSecret))
{
string ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
string nonce = Guid.NewGuid().ToString();
string signature = ComputeHmac(_options.SigningSecret, ts, nonce, bodyJson);
req.SetRequestHeader("X-Timestamp", ts);
req.SetRequestHeader("X-Nonce", nonce);
req.SetRequestHeader("X-Signature-256", signature);
}
yield return req.SendWebRequest();
if (req.result == UnityWebRequest.Result.Success)
{
if (_options.Debug)
UnityEngine.Debug.Log(
$"[LinkzlyGaming] Batch accepted: {req.downloadHandler.text}");
}
else
{
UnityEngine.Debug.LogWarning(
$"[LinkzlyGaming] Batch send failed ({req.responseCode}): " +
$"{req.downloadHandler.text}");
}
}
private static string ComputeHmac(
string secret, string timestamp, string nonce, string body)
{
string signingString = $"{timestamp}.{nonce}.{body}";
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signingString));
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
}
}
MiniJson Helper
UnityWebRequest requires serializing Dictionary<string, object> to JSON, but Unity's built-in JsonUtility does not support dictionaries. Add a second file, Assets/Linkzly/MiniJson.cs, that handles this:
// Assets/Linkzly/MiniJson.cs
// Minimal JSON serializer for Dictionary<string, object> and List<object>.
// If your project already has Newtonsoft.Json (com.unity.nuget.newtonsoft-json),
// replace MiniJson.Serialize() calls with JsonConvert.SerializeObject().
using System;
using System.Collections;
using System.Globalization;
using System.Text;
namespace Linkzly
{
internal static class MiniJson
{
public static string Serialize(object obj)
{
var sb = new StringBuilder();
SerializeValue(obj, sb);
return sb.ToString();
}
private static void SerializeValue(object value, StringBuilder sb)
{
switch (value)
{
case null:
sb.Append("null");
break;
case bool b:
sb.Append(b ? "true" : "false");
break;
case string s:
sb.Append('"');
sb.Append(s
.Replace("\\", "\\\\")
.Replace("\"", "\\\"")
.Replace("\n", "\\n")
.Replace("\r", "\\r")
.Replace("\t", "\\t"));
sb.Append('"');
break;
case IDictionary dict:
sb.Append('{');
bool firstDict = true;
foreach (DictionaryEntry entry in dict)
{
if (!firstDict) sb.Append(',');
sb.Append('"');
sb.Append(entry.Key);
sb.Append("\":");
SerializeValue(entry.Value, sb);
firstDict = false;
}
sb.Append('}');
break;
case IList list:
sb.Append('[');
bool firstList = true;
foreach (var item in list)
{
if (!firstList) sb.Append(',');
SerializeValue(item, sb);
firstList = false;
}
sb.Append(']');
break;
default:
sb.Append(Convert.ToString(value, CultureInfo.InvariantCulture));
break;
}
}
}
}
If your project already has Newtonsoft.Json installed via the Unity Package Manager (com.unity.nuget.newtonsoft-json), you can delete MiniJson.cs and replace the MiniJson.Serialize(payload) call in LinkzlyGaming.cs with Newtonsoft.Json.JsonConvert.SerializeObject(payload).
Initialization
Call LinkzlyGaming.Configure() once before any events are tracked. The recommended pattern is a GameBootstrap MonoBehaviour attached to a persistent GameObject in your first-loaded scene:
using Linkzly;
using UnityEngine;
public class GameBootstrap : MonoBehaviour
{
private void Awake()
{
LinkzlyGaming.Configure(new GamingOptions
{
ApiKey = "YOUR_SDK_KEY",
OrganizationId = "YOUR_ORG_ID",
GameId = "YOUR_GAME_ID",
GameVersion = Application.version,
SigningSecret = "YOUR_SIGNING_SECRET", // Required if HMAC is enabled
MaxBatchSize = 50,
FlushIntervalSeconds = 5f,
AutoSessionTracking = true,
Debug = Debug.isDebugBuild
});
}
}
Configure() creates a new GameObject named LinkzlyGaming marked with DontDestroyOnLoad. It persists across scene loads automatically. Calling Configure() more than once after initialization is silently ignored.
Note: Store credentials outside version-controlled files. Use Unity's
ScriptableObject-based configuration assets loaded at runtime, Gradle build scripts, or Xcode build settings to injectApiKeyandSigningSecretat build time rather than committing them to source code.
Configuration Options
| Parameter | Type | Default | Required | Description |
|---|---|---|---|---|
ApiKey |
string |
— | Yes | Your game's SDK key from the Linkzly console |
OrganizationId |
string |
— | Yes | Your organization ID |
GameId |
string |
— | Yes | Your game ID |
GameVersion |
string |
— | Yes | Your game's version string. Use Application.version to read from Player Settings |
BaseUrl |
string |
https://gaming.linkzly.com |
No | API base URL. Override for development or custom deployments |
SigningSecret |
string |
"" |
Conditionally | HMAC signing secret. Required when HMAC Signing is enabled in Game Settings |
MaxBatchSize |
int |
100 |
No | Maximum number of events per outgoing batch. Range: 1–1000 |
FlushIntervalSeconds |
float |
5 |
No | How often the SDK sends queued events, in seconds |
AutoSessionTracking |
bool |
true |
No | When true, sends session_start and session_end events automatically via OnApplicationPause and OnApplicationQuit |
Debug |
bool |
false |
No | Enables verbose logging in the Unity console under the [LinkzlyGaming] prefix |
Tracking Events
Identifying the Player
Call Identify() after the player logs in. All events queued after this call will carry the player ID:
LinkzlyGaming.Instance.Identify("player-001");
Tracking a Game Event
Use Track() to send any supported event type. Pass an optional dictionary of key-value data:
// Level completed
LinkzlyGaming.Instance.Track("level_complete", new Dictionary<string, object>
{
{ "level_id", "level-5" },
{ "score", 12400 },
{ "duration_seconds", 182 },
{ "stars", 3 }
});
// In-app purchase
LinkzlyGaming.Instance.Track("purchase", new Dictionary<string, object>
{
{ "item_id", "gem-pack-500" },
{ "amount", 4.99f },
{ "currency", "USD" },
{ "store", "apple" }
});
// Achievement unlocked
LinkzlyGaming.Instance.Track("achievement_unlocked", new Dictionary<string, object>
{
{ "achievement_id", "first-win" },
{ "achievement_name", "First Victory" },
{ "points", 50 }
});
// Custom event
LinkzlyGaming.Instance.Track("custom", new Dictionary<string, object>
{
{ "custom_event_name", "boss_defeated" },
{ "boss_id", "dragon-king" },
{ "difficulty", "hard" },
{ "attempts", 3 }
});
For a complete list of supported event types, see Section 17.1 of the SDKs documentation.
Currency Events
// Player earns in-game currency
LinkzlyGaming.Instance.Track("currency_earned", new Dictionary<string, object>
{
{ "amount", 500 },
{ "currency_type", "gold" },
{ "source", "level_reward" }
});
// Player spends in-game currency
LinkzlyGaming.Instance.Track("currency_spent", new Dictionary<string, object>
{
{ "amount", 200 },
{ "currency_type", "gold" },
{ "reason", "item_buy" }
});
Session Management
Automatic Session Tracking (Default)
When AutoSessionTracking = true, the SDK manages sessions for you using Unity's OnApplicationPause and OnApplicationQuit callbacks:
- A
session_startevent is sent whenConfigure()is called. - A
session_endevent is sent and queued events are flushed when the app is paused (goes to background). - A new
session_idUUID is generated and a newsession_startevent is sent when the app resumes from pause (returns to foreground).
You do not need to call any session methods manually when using automatic tracking.
Manual Session Tracking
To control sessions yourself, set AutoSessionTracking = false and call Track() with session event types explicitly:
// On gameplay start (e.g., after the player taps "Play")
LinkzlyGaming.Instance.Track("session_start");
// On gameplay end or intentional exit
LinkzlyGaming.Instance.Track("session_end");
LinkzlyGaming.Instance.Flush();
Flushing Before Exit
Call Reset() from your quit button handler to send a session_end event and flush before Application.Quit():
public void OnQuitButtonPressed()
{
LinkzlyGaming.Instance.Reset();
Application.Quit();
}
Note on
OnApplicationQuit: Unity does not guarantee coroutines complete before the process exits. For the final flush, callReset()from your own quit UI flow rather than relying solely onOnApplicationQuit. On iOS and Android,OnApplicationPause(true)fires reliably when the OS suspends your app and is the primary flush trigger for those platforms.
HMAC Request Signing
HMAC signing is enabled by default for all new games. When enabled, every request must include a valid signature computed from your signing secret.
Pass SigningSecret in GamingOptions — the SDK computes the signature and attaches X-Signature-256, X-Timestamp, and X-Nonce to every outgoing batch automatically:
LinkzlyGaming.Configure(new GamingOptions
{
ApiKey = "YOUR_SDK_KEY",
OrganizationId = "YOUR_ORG_ID",
GameId = "YOUR_GAME_ID",
GameVersion = Application.version,
SigningSecret = "YOUR_SIGNING_SECRET"
});
How the signature is computed:
- Build the signing string:
"{timestamp}.{nonce}.{rawBodyString}" - Compute
HMAC-SHA256of the signing string using your signing secret as the key (System.Security.Cryptography.HMACSHA256). - Hex-encode the resulting bytes to lowercase.
- Send the result in
X-Signature-256, along withX-Timestamp(Unix milliseconds) andX-Nonce(UUID v4).
The replay window is fixed at 300 seconds. Requests timestamped more than 5 minutes out of sync with the server are rejected with 401 Unauthorized. The SDK uses DateTimeOffset.UtcNow for timestamps — ensure the device clock is synchronized.
Your signing secret is available in the console under Game Settings → SDK Configuration. It is separate from the SDK Key.
Security note: Do not hardcode your signing secret in source files committed to version control. Use a
ScriptableObjectconfiguration asset loaded at runtime, Unity Cloud Build environment variables, or Xcode/Gradle build settings to supply the value at build time without embedding it in source code.
To disable HMAC for local development, toggle off HMAC Signing Required in Game Settings → Security Settings and omit SigningSecret from GamingOptions. Re-enable it before releasing to production.
Batch Limits
The ingestion endpoint enforces the following limits per request:
| Limit | Value |
|---|---|
| Maximum events per batch | 1,000 |
| Maximum body size | 1 MB |
| Maximum size per event | 10 KB |
| Maximum event age | 24 hours |
The MaxBatchSize option (default: 100) is a client-side limit that triggers an early flush when the queue reaches that size. Setting it lower reduces memory usage in games with high event volumes. Do not set it above 1,000.
Events that violate the per-event or timestamp limits are dropped by the server and counted in the events_dropped field of the batch response. Enable Debug = true to log dropped event details to the Unity console.
IL2CPP and WebGL Compatibility
System.Security.Cryptography.HMACSHA256 is supported on all platforms under IL2CPP. If you see code stripping errors at runtime, add the following to Assets/link.xml:
<linker>
<assembly fullname="System.Core">
<type fullname="System.Security.Cryptography.HMACSHA256" preserve="all" />
</assembly>
</linker>
For WebGL builds, HMACSHA256 is available when using the IL2CPP backend with .NET Standard 2.1 or higher. UnityWebRequest works in WebGL — requests are made via XMLHttpRequest under the hood. The Linkzly ingestion endpoint permits cross-origin requests.
Troubleshooting
Events are not appearing in the Linkzly dashboard
- Set
Debug = trueinGamingOptionsand check the Unity console for[LinkzlyGaming]entries. - Verify
ApiKey,OrganizationId, andGameIdare correct (no trailing spaces or newlines). - Confirm
LinkzlyGaming.Configure()is called before anyTrack()calls. - On Android, confirm
android.permission.INTERNETis present inAndroidManifest.xml. On iOS, confirm the app is not blocked by App Transport Security forgaming.linkzly.com.
HMAC errors (401 Unauthorized)
- Confirm
SigningSecretinGamingOptionsmatches the secret shown in Game Settings → SDK Configuration exactly. - Verify the device clock is synchronized. The replay window is fixed at 300 seconds — requests with timestamps more than 5 minutes out of sync are rejected.
- Check the Unity console for
[LinkzlyGaming] Batch send failed (401)entries and inspect the response body for a specific error message.
High events_dropped count in batch responses
Events are dropped if they exceed the 10 KB per-event limit, have a timestamp older than 24 hours, or carry a future timestamp more than 5 minutes ahead. Enable Debug = true to log the server response for each batch, which includes drop reasons.
NullReferenceException when calling LinkzlyGaming.Instance
Instance is null if Configure() has not been called. Ensure your GameBootstrap script runs before any script that calls Track(). Use Unity's Edit → Project Settings → Script Execution Order to explicitly set GameBootstrap to a lower order number (earlier execution).
Events lost on application quit
Unity does not guarantee coroutines complete after OnApplicationQuit. Call Reset() explicitly from your quit button handler before Application.Quit() to ensure the final session_end event and any queued events are flushed.
Was this helpful?
Help us improve our documentation