skills/unreal/SKILL.md
This skill covers Unreal Engine-specific patterns for connecting to SpacetimeDB. For server-side module development, see the rust-server or csharp-server skills.
Add the SpacetimeDB Unreal SDK as a plugin:
Plugins folder in your Unreal project root if it does not exist.SpacetimeDbSdk folder into Plugins/..uproject file and select Generate Visual Studio project files."SpacetimeDbSdk" to your module's Build.cs:PublicDependencyModuleNames.AddRange(new string[] { "SpacetimeDbSdk" });
spacetime generate --lang unrealcpp \
--uproject-dir <path_to_uproject_directory> \
--module-path <path_to_spacetimedb_module> \
--unreal-module-name <your_unreal_module_name>
This generates C++ bindings in ModuleBindings/ inside your project. Include the generated header:
#include "ModuleBindings/SpacetimeDBClient.g.h"
Regenerate whenever you change module tables, reducers, or types.
The recommended pattern is a singleton Actor that owns the connection. Enable ticking so FrameTick is called every frame.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ModuleBindings/SpacetimeDBClient.g.h"
#include "GameManager.generated.h"
class UDbConnection;
UCLASS()
class AGameManager : public AActor
{
GENERATED_BODY()
public:
AGameManager();
static AGameManager* Instance;
UPROPERTY(EditAnywhere, Category="SpacetimeDB")
FString ServerUri = TEXT("127.0.0.1:3000");
UPROPERTY(EditAnywhere, Category="SpacetimeDB")
FString DatabaseName = TEXT("my-module");
UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB")
UDbConnection* Conn = nullptr;
UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB")
FSpacetimeDBIdentity LocalIdentity;
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
public:
virtual void Tick(float DeltaTime) override;
private:
UFUNCTION() void HandleConnect(UDbConnection* InConn, FSpacetimeDBIdentity Identity, const FString& Token);
UFUNCTION() void HandleConnectError(const FString& Error);
UFUNCTION() void HandleDisconnect(UDbConnection* InConn, const FString& Error);
UFUNCTION() void HandleSubscriptionApplied(FSubscriptionEventContext& Context);
};
#include "GameManager.h"
#include "Connection/Credentials.h"
AGameManager* AGameManager::Instance = nullptr;
AGameManager::AGameManager()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;
}
void AGameManager::BeginPlay()
{
Super::BeginPlay();
Instance = this;
FOnConnectDelegate ConnectDelegate;
BIND_DELEGATE_SAFE(ConnectDelegate, this, AGameManager, HandleConnect);
FOnDisconnectDelegate DisconnectDelegate;
BIND_DELEGATE_SAFE(DisconnectDelegate, this, AGameManager, HandleDisconnect);
FOnConnectErrorDelegate ConnectErrorDelegate;
BIND_DELEGATE_SAFE(ConnectErrorDelegate, this, AGameManager, HandleConnectError);
UCredentials::Init(TEXT(".spacetime_token"));
FString Token = UCredentials::LoadToken();
UDbConnectionBuilder* Builder = UDbConnection::Builder()
->WithUri(ServerUri)
->WithDatabaseName(DatabaseName)
->OnConnect(ConnectDelegate)
->OnDisconnect(DisconnectDelegate)
->OnConnectError(ConnectErrorDelegate);
if (!Token.IsEmpty())
{
Builder->WithToken(Token);
}
Conn = Builder->Build();
}
void AGameManager::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (Conn) { Conn->Disconnect(); Conn = nullptr; }
if (Instance == this) { Instance = nullptr; }
Super::EndPlay(EndPlayReason);
}
void AGameManager::Tick(float DeltaTime)
{
if (Conn && Conn->IsActive())
{
Conn->FrameTick();
}
}
void AGameManager::HandleConnect(UDbConnection* InConn, FSpacetimeDBIdentity Identity, const FString& Token)
{
LocalIdentity = Identity;
UCredentials::SaveToken(Token);
FOnSubscriptionApplied AppliedDelegate;
BIND_DELEGATE_SAFE(AppliedDelegate, this, AGameManager, HandleSubscriptionApplied);
Conn->SubscriptionBuilder()
->OnApplied(AppliedDelegate)
->SubscribeToAllTables();
}
void AGameManager::HandleConnectError(const FString& Error)
{
UE_LOG(LogTemp, Error, TEXT("Connection error: %s"), *Error);
}
void AGameManager::HandleDisconnect(UDbConnection* InConn, const FString& Error)
{
UE_LOG(LogTemp, Warning, TEXT("Disconnected: %s"), *Error);
}
void AGameManager::HandleSubscriptionApplied(FSubscriptionEventContext& Context)
{
UE_LOG(LogTemp, Log, TEXT("Subscription applied - game state loaded"));
}
You must either call Conn->FrameTick() every frame in your Actor's Tick(), or call Conn->SetAutoTicking(true) once at startup. The SDK queues all network messages and only processes them on tick. Without one of these, no callbacks fire and the client appears frozen.
Build a connection with the builder pattern. All builder methods return pointers for chaining with ->.
UDbConnection* Conn = UDbConnection::Builder()
->WithUri(TEXT("127.0.0.1:3000"))
->WithDatabaseName(TEXT("my-module"))
->WithToken(SavedToken) // optional
->WithCompression(ESpacetimeDBCompression::Gzip) // optional
->OnConnect(ConnectDelegate)
->OnConnectError(ErrorDelegate)
->OnDisconnect(DisconnectDelegate)
->Build();
UFUNCTION()
void OnConnected(UDbConnection* Connection, FSpacetimeDBIdentity Identity, const FString& Token);
Save the Token for future reconnection. The Identity is the user's persistent identifier.
After connecting, subscribe to receive table data:
// Subscribe to all public tables
Conn->SubscriptionBuilder()
->OnApplied(AppliedDelegate)
->SubscribeToAllTables();
// Subscribe to specific queries
TArray<FString> Queries = { TEXT("SELECT * FROM player"), TEXT("SELECT * FROM entity") };
Conn->SubscriptionBuilder()
->OnApplied(AppliedDelegate)
->OnError(ErrorDelegate)
->Subscribe(Queries);
Subscribe and SubscribeToAllTables return a USubscriptionHandle*:
USubscriptionHandle* Handle = Conn->SubscriptionBuilder()->...->Subscribe(Queries);
Handle->IsActive(); // true while subscription is live
Handle->Unsubscribe(); // cancel the subscription
Handle->UnsubscribeThen(OnEndDelegate); // cancel with callback
Handle->GetQuerySqls(); // get the SQL queries
Access tables through Conn->Db:
// Find by unique/primary key (returns by value; default-constructed if not found)
FUserType User = Conn->Db->User->Identity->Find(SomeIdentity);
// Filter by BTree index
TArray<FPlayerType> LevelFive = Conn->Db->Player->Level->Filter(5);
// Iterate all rows
TArray<FEntityType> AllEntities = Conn->Db->Entity->Iter();
// Count
int32 Total = Conn->Db->Player->Count();
Register callbacks on table objects. Callbacks use Unreal dynamic multicast delegates.
// OnInsert
Conn->Db->User->OnInsert.AddDynamic(this, &AMyActor::OnUserInsert);
// OnDelete
Conn->Db->User->OnDelete.AddDynamic(this, &AMyActor::OnUserDelete);
// OnUpdate (only fires for rows with a primary key)
Conn->Db->User->OnUpdate.AddDynamic(this, &AMyActor::OnUserUpdate);
UFUNCTION()
void OnUserInsert(const FEventContext& Context, const FUserType& NewRow);
UFUNCTION()
void OnUserDelete(const FEventContext& Context, const FUserType& DeletedRow);
UFUNCTION()
void OnUserUpdate(const FEventContext& Context, const FUserType& OldRow, const FUserType& NewRow);
Register callbacks before connecting or in HandleSubscriptionApplied.
Invoke reducers through Conn->Reducers:
Conn->Reducers->SendMessage(TEXT("Hello!"));
Conn->Reducers->SetName(TEXT("Alice"));
Conn->Reducers->MovePlayer(1.0f, 0.0f);
Observe when a reducer you called completes:
Conn->Reducers->OnSendMessage.AddDynamic(this, &AMyActor::OnSendMessageResult);
UFUNCTION()
void OnSendMessageResult(const FReducerEventContext& Context, const FString& Text)
{
UE_LOG(LogTemp, Log, TEXT("SendMessage result for: %s"), *Text);
}
These delegates fire only for reducer calls made by this connection, not for other clients' calls.
Use the BIND_DELEGATE_SAFE macro to safely bind delegates to member functions:
FOnConnectDelegate ConnectDelegate;
BIND_DELEGATE_SAFE(ConnectDelegate, this, AMyActor, HandleConnect);
This is the recommended pattern for all SpacetimeDB delegate bindings in C++.
// FSpacetimeDBIdentity -- 256-bit unique user identifier, persists across connections
FSpacetimeDBIdentity Identity;
Identity.ToHex();
// FSpacetimeDBConnectionId -- 128-bit per-session connection identifier
FSpacetimeDBConnectionId ConnId = Conn->GetConnectionId();
// From any context
FSpacetimeDBIdentity Id;
bool Found = Context.TryGetIdentity(Id);
FSpacetimeDBConnectionId CId = Context.GetConnectionId();
Use the built-in UCredentials helper to save and load tokens:
UCredentials::Init(TEXT(".spacetime_token"));
FString Token = UCredentials::LoadToken();
// ... after connect:
UCredentials::SaveToken(Token);
All callbacks receive a context struct that provides access to Db and Reducers:
| Type | Used In |
|---|---|
FEventContext | Table row callbacks (OnInsert, OnDelete, OnUpdate) |
FReducerEventContext | Reducer result callbacks |
FSubscriptionEventContext | Subscription lifecycle callbacks (OnApplied, OnError) |
FErrorContext | Error callbacks |
All inherit from FContextBase which provides:
Context.Db // URemoteTables* -- client cache
Context.Reducers // URemoteReducers* -- invoke reducers
Context.SubscriptionBuilder() // start a new subscription
All core classes are Blueprint-accessible via UFUNCTION(BlueprintCallable) and UPROPERTY(BlueprintReadOnly/BlueprintAssignable):
UDbConnection::Builder() and all builder methods are BlueprintCallable.OnInsert, OnDelete, OnUpdate) are BlueprintAssignable delegates.Conn->Db and Conn->Reducers are BlueprintReadOnly properties.BlueprintType USTRUCTs with BlueprintReadWrite properties.This means you can build the entire connection and callback flow in Blueprints without writing C++.
UDbConnection inherits from FTickableGameObject, but auto ticking is off by default. You have two options:
// Option 1: Call FrameTick() manually in your Actor's Tick() (shown in GameManager above)
void Tick(float DeltaTime) { Conn->FrameTick(); }
// Option 2: Enable auto ticking. The SDK then processes messages every frame automatically
Conn->SetAutoTicking(true);
Pick one. Without either, no callbacks fire.
Builder->WithCompression(ESpacetimeDBCompression::Gzip); // default
Builder->WithCompression(ESpacetimeDBCompression::None); // no compression
Codegen produces USTRUCTs prefixed with F (e.g., FUserType, FEntityType) and table classes prefixed with U (e.g., UUserTable). Row types use GENERATED_BODY() and UPROPERTY() for full reflection support.