mcpField
U
unreal

unreal

github.com/clockworklabs开发25.5k43 次安装1 天前更新
代码仓库

关于 unreal

unreal 是面向 Unreal Engine 项目的 SpacetimeDB 集成 Skill,说明如何在 C++ 和 Blueprint 中连接数据库、订阅数据并调用 reducer。

主要功能

  1. 01生成 C++ 模块绑定
  2. 02构建连接并管理令牌
  3. 03订阅表和 SQL 查询
  4. 04读取本地表缓存并监听行事件
  5. 05调用 reducer 并接收结果
  6. 06支持 Blueprint 工作流

适用场景

  1. 01在 Unreal 项目中连接 SpacetimeDB
  2. 02同步订阅的表数据并响应行变化
  3. 03从游戏客户端调用 reducer
  4. 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 添加为插件:

  1. 如果 Unreal 项目根目录中没有 Plugins 文件夹,请创建一个。
  2. 将 SpacetimeDbSdk 文件夹复制到 Plugins/。
  3. 右键点击 .uproject 文件,然后选择 Generate Visual Studio project files。
  4. 将 "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)
FReducerEventContextReducer 结果回调
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 属性。
  • 生成的行类型是 BlueprintType USTRUCT,带有 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(),以提供完整的反射支持。

评论

支持 Markdown