Linkzly

Unreal Engine SDK Setup Guide

This guide covers integrating Gaming Intelligence into Unreal Engine games using C++ and Blueprint.

7 min read

Unreal Engine SDK Setup Guide

This guide covers integrating Gaming Intelligence into Unreal Engine games using C++ and Blueprint.


Prerequisites

Before you begin, make sure you have the following:

Requirement Details
SDK Key Found in Console → Gaming → [Game Name] → Settings → SDK Configuration → SDK Key
Organization ID Found in your Linkzly organization settings
Game ID Found in Console → Gaming → [Game Name] → Settings
Unreal Engine Version 5.0 or later
Project type C++ project (not Blueprint-only)

Step 1: Configure Build.cs

Open your game's Source/[GameName]/[GameName].Build.cs file and add "HTTP" and "Json" to PublicDependencyModuleNames:

// [GameName].Build.cs
public class MyGame : ModuleRules
{
    public MyGame(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[]
        {
            "Core",
            "CoreUObject",
            "Engine",
            "InputCore",
            "HTTP",       // Required for FHttpModule
            "Json",       // Required for JSON serialization
            "JsonUtilities"
        });
    }
}

Step 2: Create the Linkzly Gaming Subsystem

Create two files: LinkzlyGamingSubsystem.h and LinkzlyGamingSubsystem.cpp in your Source/[GameName]/ directory.

LinkzlyGamingSubsystem.h

#pragma once

#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "HttpModule.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"
#include "LinkzlyGamingSubsystem.generated.h"

USTRUCT(BlueprintType)
struct FLinkzlyEvent
{
    GENERATED_BODY()

    UPROPERTY(BlueprintReadWrite)
    FString EventId;

    UPROPERTY(BlueprintReadWrite)
    FString EventType;

    UPROPERTY(BlueprintReadWrite)
    FString Timestamp; // ISO 8601

    UPROPERTY(BlueprintReadWrite)
    FString PlayerId;

    UPROPERTY(BlueprintReadWrite)
    FString SessionId;

    UPROPERTY(BlueprintReadWrite)
    FString Platform = TEXT("unreal");

    UPROPERTY(BlueprintReadWrite)
    FString SdkVersion = TEXT("1.0.0");

    UPROPERTY(BlueprintReadWrite)
    FString GameVersion;
};

UCLASS()
class MYGAME_API ULinkzlyGamingSubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()

public:
    // Called automatically by the engine when the GameInstance starts
    virtual void Initialize(FSubsystemCollectionBase& Collection) override;
    virtual void Deinitialize() override;

    // Configuration — call this before sending any events
    UFUNCTION(BlueprintCallable, Category = "Linkzly")
    void Configure(const FString& InSDKKey, const FString& InOrganizationId, const FString& InGameId);

    // Track a single event
    UFUNCTION(BlueprintCallable, Category = "Linkzly")
    void Track(const FLinkzlyEvent& Event);

    // Track multiple events in a single batch
    UFUNCTION(BlueprintCallable, Category = "Linkzly")
    void TrackBatch(const TArray<FLinkzlyEvent>& Events);

    // Convenience: start a session and set PlayerId/SessionId on all subsequent events
    UFUNCTION(BlueprintCallable, Category = "Linkzly")
    void Identify(const FString& InPlayerId, const FString& InSessionId);

private:
    FString SDKKey;
    FString OrganizationId;
    FString GameId;
    FString CurrentPlayerId;
    FString CurrentSessionId;

    void SendEvents(const TArray<FLinkzlyEvent>& Events);
    FString BuildJsonPayload(const TArray<FLinkzlyEvent>& Events);
    FString ComputeHmac(const FString& Timestamp, const FString& Nonce, const FString& Body);
    FString GenerateUUID();
    FString GetCurrentISOTimestamp();

    void OnResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bSuccess);
};

LinkzlyGamingSubsystem.cpp

#include "LinkzlyGamingSubsystem.h"
#include "HttpModule.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"
#include "Misc/Guid.h"
#include "Misc/DateTime.h"

// OpenSSL is bundled with Unreal Engine via ThirdParty/OpenSSL
#include "openssl/hmac.h"
#include "openssl/sha.h"

void ULinkzlyGamingSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    Super::Initialize(Collection);
    UE_LOG(LogTemp, Log, TEXT("LinkzlyGamingSubsystem initialized"));
}

void ULinkzlyGamingSubsystem::Deinitialize()
{
    Super::Deinitialize();
}

void ULinkzlyGamingSubsystem::Configure(
    const FString& InSDKKey,
    const FString& InOrganizationId,
    const FString& InGameId)
{
    SDKKey = InSDKKey;
    OrganizationId = InOrganizationId;
    GameId = InGameId;
}

void ULinkzlyGamingSubsystem::Identify(const FString& InPlayerId, const FString& InSessionId)
{
    CurrentPlayerId = InPlayerId;
    CurrentSessionId = InSessionId;
}

void ULinkzlyGamingSubsystem::Track(const FLinkzlyEvent& Event)
{
    TArray<FLinkzlyEvent> Batch;
    Batch.Add(Event);
    SendEvents(Batch);
}

void ULinkzlyGamingSubsystem::TrackBatch(const TArray<FLinkzlyEvent>& Events)
{
    SendEvents(Events);
}

void ULinkzlyGamingSubsystem::SendEvents(const TArray<FLinkzlyEvent>& Events)
{
    if (SDKKey.IsEmpty() || OrganizationId.IsEmpty() || GameId.IsEmpty())
    {
        UE_LOG(LogTemp, Warning, TEXT("Linkzly: SDK not configured. Call Configure() first."));
        return;
    }

    FString JsonPayload = BuildJsonPayload(Events);

    // Generate HMAC signing headers
    FString Timestamp = FString::FromInt(
        (int64)(FDateTime::UtcNow() - FDateTime(1970, 1, 1)).GetTotalMilliseconds());
    FString Nonce = GenerateUUID();
    FString Signature = ComputeHmac(Timestamp, Nonce, JsonPayload);

    TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request =
        FHttpModule::Get().CreateRequest();

    Request->SetURL(TEXT("https://gaming.linkzly.com/api/v1/gaming/events"));
    Request->SetVerb(TEXT("POST"));
    Request->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
    Request->SetHeader(TEXT("Authorization"),
        FString::Printf(TEXT("Bearer %s"), *SDKKey));
    Request->SetHeader(TEXT("X-Organization-ID"), OrganizationId);
    Request->SetHeader(TEXT("X-Game-ID"), GameId);
    Request->SetHeader(TEXT("X-Timestamp"), Timestamp);
    Request->SetHeader(TEXT("X-Nonce"), Nonce);
    Request->SetHeader(TEXT("X-Signature-256"), Signature);
    Request->SetContentAsString(JsonPayload);

    Request->OnProcessRequestComplete().BindUObject(
        this, &ULinkzlyGamingSubsystem::OnResponse);

    Request->ProcessRequest();
}

FString ULinkzlyGamingSubsystem::BuildJsonPayload(const TArray<FLinkzlyEvent>& Events)
{
    TArray<TSharedPtr<FJsonValue>> EventArray;

    for (const FLinkzlyEvent& Evt : Events)
    {
        TSharedPtr<FJsonObject> EventObj = MakeShared<FJsonObject>();
        EventObj->SetStringField(TEXT("event_id"),   Evt.EventId.IsEmpty() ? GenerateUUID() : Evt.EventId);
        EventObj->SetStringField(TEXT("event_type"), Evt.EventType);
        EventObj->SetStringField(TEXT("timestamp"),  Evt.Timestamp.IsEmpty() ? GetCurrentISOTimestamp() : Evt.Timestamp);
        EventObj->SetStringField(TEXT("platform"),   TEXT("unreal"));
        EventObj->SetStringField(TEXT("player_id"),  Evt.PlayerId.IsEmpty() ? CurrentPlayerId : Evt.PlayerId);
        EventObj->SetStringField(TEXT("session_id"), Evt.SessionId.IsEmpty() ? CurrentSessionId : Evt.SessionId);

        if (!Evt.SdkVersion.IsEmpty())
            EventObj->SetStringField(TEXT("sdk_version"), Evt.SdkVersion);
        if (!Evt.GameVersion.IsEmpty())
            EventObj->SetStringField(TEXT("game_version"), Evt.GameVersion);

        EventArray.Add(MakeShared<FJsonValueObject>(EventObj));
    }

    TSharedPtr<FJsonObject> RootObj = MakeShared<FJsonObject>();
    RootObj->SetArrayField(TEXT("events"), EventArray);

    FString OutputString;
    TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString);
    FJsonSerializer::Serialize(RootObj.ToSharedRef(), Writer);

    return OutputString;
}

FString ULinkzlyGamingSubsystem::ComputeHmac(
    const FString& Timestamp, const FString& Nonce, const FString& Body)
{
    // Signing string format: "{timestamp}.{nonce}.{body}"
    FString SigningString = FString::Printf(TEXT("%s.%s.%s"), *Timestamp, *Nonce, *Body);
    std::string SigningStr(TCHAR_TO_UTF8(*SigningString));
    std::string KeyStr(TCHAR_TO_UTF8(*SDKKey));

    unsigned char Digest[EVP_MAX_MD_SIZE];
    unsigned int DigestLen = 0;

    HMAC(EVP_sha256(),
         KeyStr.c_str(), (int)KeyStr.size(),
         (const unsigned char*)SigningStr.c_str(), SigningStr.size(),
         Digest, &DigestLen);

    FString HexResult;
    for (unsigned int i = 0; i < DigestLen; i++)
    {
        HexResult += FString::Printf(TEXT("%02x"), Digest[i]);
    }
    return HexResult;
}

FString ULinkzlyGamingSubsystem::GenerateUUID()
{
    return FGuid::NewGuid().ToString(EGuidFormats::DigitsWithHyphens).ToLower();
}

FString ULinkzlyGamingSubsystem::GetCurrentISOTimestamp()
{
    FDateTime Now = FDateTime::UtcNow();
    return Now.ToIso8601();
}

void ULinkzlyGamingSubsystem::OnResponse(
    FHttpRequestPtr Request,
    FHttpResponsePtr Response,
    bool bSuccess)
{
    if (!bSuccess || !Response.IsValid())
    {
        UE_LOG(LogTemp, Warning, TEXT("Linkzly: HTTP request failed."));
        return;
    }

    int32 StatusCode = Response->GetResponseCode();
    if (StatusCode == 202)
    {
        UE_LOG(LogTemp, Log, TEXT("Linkzly: Events accepted. Response: %s"),
            *Response->GetContentAsString());
    }
    else
    {
        UE_LOG(LogTemp, Warning, TEXT("Linkzly: Unexpected status %d. Body: %s"),
            StatusCode, *Response->GetContentAsString());
    }
}

Step 3: Track Events

From C++

Call Configure() once during game startup (e.g., in your AGameMode::BeginPlay() or UGameInstance::StartGameInstance()), then call Track() wherever events occur.

// In UMyGameInstance::StartGameInstance()
void UMyGameInstance::StartGameInstance()
{
    Super::StartGameInstance();

    ULinkzlyGamingSubsystem* Linkzly =
        GetSubsystem<ULinkzlyGamingSubsystem>();

    Linkzly->Configure(
        TEXT("lnkz_sk_your_sdk_key_here"),
        TEXT("org_your_org_id_here"),
        TEXT("game_your_game_id_here")
    );

    // Set player identity after authentication
    Linkzly->Identify(TEXT("player_123"), TEXT("session_abc"));
}
// Tracking an in-game event
void AMyPlayerController::OnItemPurchased(const FString& ItemId, float Amount)
{
    ULinkzlyGamingSubsystem* Linkzly =
        GetGameInstance()->GetSubsystem<ULinkzlyGamingSubsystem>();

    FLinkzlyEvent Event;
    Event.EventType    = TEXT("purchase");
    Event.GameVersion  = TEXT("1.0.0");
    // EventId and Timestamp are auto-filled if left empty

    Linkzly->Track(Event);
}

From Blueprint

All public methods are exposed via BlueprintCallable. Add a Get Game Instance Subsystem node typed to Linkzly Gaming Subsystem, then call Configure, Identify, and Track directly in your Blueprint graph.


Step 4: Session Management

Tie sessions to UGameInstance lifecycle events so sessions start and end cleanly:

// UMyGameInstance.cpp

void UMyGameInstance::StartGameInstance()
{
    Super::StartGameInstance();

    ULinkzlyGamingSubsystem* Linkzly = GetSubsystem<ULinkzlyGamingSubsystem>();
    Linkzly->Configure(TEXT("lnkz_sk_..."), TEXT("org_..."), TEXT("game_..."));

    // Generate a fresh session ID each launch
    FString SessionId = FGuid::NewGuid().ToString(EGuidFormats::DigitsWithHyphens).ToLower();
    Linkzly->Identify(TEXT(""), SessionId); // PlayerId set after login
}

void UMyGameInstance::Shutdown()
{
    ULinkzlyGamingSubsystem* Linkzly = GetSubsystem<ULinkzlyGamingSubsystem>();

    FLinkzlyEvent EndEvent;
    EndEvent.EventType = TEXT("session_end");
    Linkzly->Track(EndEvent);

    Super::Shutdown();
}

Step 5: HMAC Signing

HMAC signing is enabled by default in the Linkzly console. The subsystem implementation above computes and attaches the required headers automatically.

If you need to disable HMAC during development: Console → Gaming → [Game Name] → Settings → Security Settings → HMAC Signing Required → Off.

Signing details:

Header Value
X-Timestamp Unix timestamp in milliseconds
X-Nonce UUID v4
X-Signature-256 HMAC-SHA256 hex of {timestamp}.{nonce}.{body}

The replay protection window is 300 seconds. Requests with a timestamp older than 5 minutes are rejected.


Standard Event Types

Event Type Description
session_start Player begins a game session
session_end Player ends a game session
level_start Player enters a level
level_complete Player completes a level
purchase In-game purchase
ad_impression Ad shown to player
custom Any custom event

Troubleshooting

Issue Resolution
Events not appearing in dashboard Verify SDK Key, Org ID, and Game ID are correct
401 Unauthorized SDK Key is invalid or revoked; regenerate in Console
400 Bad Request Check that event_type, player_id, and session_id are not empty
HMAC signature mismatch Ensure system clock is synced (NTP); replay window is 300s
No HTTP module found Confirm "HTTP" and "Json" are in PublicDependencyModuleNames
OpenSSL not found UE bundles OpenSSL under Engine/Source/ThirdParty/OpenSSL; add it to your Build.cs if needed

Next Steps

Was this helpful?

Help us improve our documentation