关于 unreal
unreal 是面向 Unreal Engine 项目的 SpacetimeDB 集成 Skill,说明如何在 C++ 和 Blueprint 中连接数据库、订阅数据并调用 reducer。
主要功能
- 01生成 C++ 模块绑定
- 02构建连接并管理令牌
- 03订阅表和 SQL 查询
- 04读取本地表缓存并监听行事件
- 05调用 reducer 并接收结果
- 06支持 Blueprint 工作流
适用场景
- 01在 Unreal 项目中连接 SpacetimeDB
- 02同步订阅的表数据并响应行变化
- 03从游戏客户端调用 reducer
- 04使用 Blueprint 配置连接和回调
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ clockworklabs/ spacetimedb --skill unreal安装后在 Claude Code 中输入/unreal确认已加载。
详细介绍
SpacetimeDB Unreal Engine 集成
本技能介绍连接 SpacetimeDB 时适用于 Unreal Engine 的模式。有关服务端模块开发,请参阅 rust-server 或 csharp-server 技能。
安装
将 SpacetimeDB Unreal SDK 添加为插件:
- 如果 Unreal 项目根目录中没有
Plugins文件夹,请创建一个。 - 将
SpacetimeDbSdk文件夹复制到Plugins/。 - 右键点击
.uproject文件,然后选择 Generate Visual Studio project files。 - 将
"SpacetimeDbSdk"添加到模块的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>
此操作会在项目中的 ModuleBindings/ 内生成 C++ 绑定。包含生成的头文件:
#include "ModuleBindings/SpacetimeDBClient.g.h"
每当更改模块表、reducer 或类型时,都要重新生成。
GameManager Actor 模式
推荐使用单例 Actor 来管理连接。启用 Tick,这样每帧都会调用 FrameTick。
头文件(GameManager.h)
#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);
};
源文件(GameManager.cpp)
#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"));
}
FrameTick —— 关键
你必须在 Actor 的 Tick() 中每帧调用 Conn->FrameTick(),或在启动时调用一次 Conn->SetAutoTicking(true)。 SDK 会将所有网络消息放入队列,并且只在 Tick 时处理。如果两者都未执行,就不会触发任何回调,客户端看起来也会像是卡住了。
连接构建器
使用构建器模式建立连接。所有构建器方法都会返回指针,以便使用 -> 链式调用。
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();
OnConnect 回调签名
UFUNCTION()
void OnConnected(UDbConnection* Connection, FSpacetimeDBIdentity Identity, const FString& Token);
保存 Token 以供将来重新连接使用。Identity 是用户的持久标识符。
订阅表
连接后,订阅以接收表数据:
// 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 和 SubscribeToAllTables 会返回 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
读取客户端缓存
通过 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();
行回调
在表对象上注册回调。回调使用 Unreal 动态多播委托。
// 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)
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);
在连接前或 HandleSubscriptionApplied 中注册回调。
调用 Reducer
通过 Conn->Reducers 调用 reducer:
Conn->Reducers->SendMessage(TEXT("Hello!"));
Conn->Reducers->SetName(TEXT("Alice"));
Conn->Reducers->MovePlayer(1.0f, 0.0f);
Reducer 结果回调
观察你调用的 reducer 何时完成:
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);
}
这些委托只会在此连接调用的 reducer 完成时触发,不会因其他客户端的调用而触发。
使用 BIND_DELEGATE_SAFE 绑定委托
使用 BIND_DELEGATE_SAFE 宏,将委托安全地绑定到成员函数:
FOnConnectDelegate ConnectDelegate;
BIND_DELEGATE_SAFE(ConnectDelegate, this, AMyActor, HandleConnect);
这是在 C++ 中绑定 SpacetimeDB 委托的推荐模式。
Identity 和 ConnectionId
// 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();
Token 持久化
使用内置的 UCredentials 助手保存和加载令牌。重新连接时传回已保存的服务端签发令牌,可以保留相同的身份。此令牌不会过期,丢失后也无法恢复,因此自签发身份仅用于开发。生产环境中,请使用 SpacetimeAuth 等 OIDC 提供商进行身份验证,由其处理令牌生命周期。
UCredentials::Init(TEXT(".spacetime_token"));
FString Token = UCredentials::LoadToken();
// ... after connect:
UCredentials::SaveToken(Token);
上下文类型
所有回调都会收到一个上下文结构体,可通过它访问 Db 和 Reducers:
| 类型 | 用途 |
|---|---|
FEventContext | 表行回调(OnInsert、OnDelete、OnUpdate) |
FReducerEventContext | Reducer 结果回调 |
FSubscriptionEventContext | 订阅生命周期回调(OnApplied、OnError) |
FErrorContext | 错误回调 |
所有类型都继承自 FContextBase,后者提供:
Context.Db // URemoteTables* -- client cache
Context.Reducers // URemoteReducers* -- invoke reducers
Context.SubscriptionBuilder() // start a new subscription
Blueprint 集成
所有核心类都可通过 Blueprint 访问,使用 UFUNCTION(BlueprintCallable) 和 UPROPERTY(BlueprintReadOnly/BlueprintAssignable):
UDbConnection::Builder()和所有构建器方法都是BlueprintCallable。- 表回调(
OnInsert、OnDelete、OnUpdate)是BlueprintAssignable委托。 - Reducer 调用方法和结果委托都可通过 Blueprint 访问。
Conn->Db和Conn->Reducers是BlueprintReadOnly属性。- 生成的行类型是
BlueprintTypeUSTRUCT,带有BlueprintReadWrite属性。
这意味着无需编写 C++,即可在 Blueprint 中构建完整的连接和回调流程。
Unreal 特定注意事项
自动 Tick 的替代方案
UDbConnection 继承自 FTickableGameObject,但默认关闭自动 Tick。你有两种选择:
// 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);
任选其一。若两者都不采用,则不会触发任何回调。
压缩
Builder->WithCompression(ESpacetimeDBCompression::Gzip); // default
Builder->WithCompression(ESpacetimeDBCompression::None); // no compression
生成的类型
代码生成会生成带 F 前缀的 USTRUCT(例如,FUserType、FEntityType)以及带 U 前缀的表类(例如,UUserTable)。行类型使用 GENERATED_BODY() 和 UPROPERTY(),以提供完整的反射支持。