mcpField
U
unreal

unreal

github.com/clockworklabs開発25.5k42 回インストール今日更新
リポジトリ

unreal について

unreal は、Unreal Engine プロジェクトで SpacetimeDB を利用するための Skill です。C++ と Blueprint での接続、テーブル購読、キャッシュデータ、コールバック、reducer 呼び出しを扱います。

主な機能

  1. 01C++ モジュールバインディングの生成
  2. 02接続の構築とトークン管理
  3. 03テーブルと SQL クエリの購読
  4. 04キャッシュテーブルの参照と行イベント処理
  5. 05reducer の呼び出しと結果受信
  6. 06Blueprint 対応ワークフロー

活用シーン

  1. 01Unreal プロジェクトから SpacetimeDB に接続
  2. 02購読したテーブルデータや行変更の受信
  3. 03ゲームクライアントから reducer を呼び出す
  4. 04Blueprint で接続とコールバックを設定

インストール

コマンドをコピーしてターミナルで実行します。

$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. モジュールの Build.cs に "SpacetimeDbSdk" を追加します。
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"

モジュールのテーブル、リデューサー、または型を変更した場合は、再生成してください。


GameManager アクターパターン

推奨パターンは、接続を所有するシングルトンのアクターです。毎フレーム 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 -- 重要

アクターの Tick() で毎フレーム Conn->FrameTick() を呼び出すか、起動時に一度 Conn->SetAutoTicking(true) を呼び出す必要があります。 SDK はすべてのネットワークメッセージをキューに入れ、ティック時にのみ処理します。どちらも行わない場合、コールバックは実行されず、クライアントがフリーズしたように見えます。


接続ビルダー

ビルダーパターンで接続を構築します。すべてのビルダーメソッドはポインターを返すため、-> でチェーンできます。

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 内でコールバックを登録してください。


リデューサーの呼び出し

Conn->Reducers を通じてリデューサーを呼び出します。

Conn->Reducers->SendMessage(TEXT("Hello!"));
Conn->Reducers->SetName(TEXT("Alice"));
Conn->Reducers->MovePlayer(1.0f, 0.0f);
リデューサー結果コールバック

呼び出したリデューサーの完了を監視します。

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);
}

これらのデリゲートは、この接続から行われたリデューサー呼び出しに対してのみ実行され、他のクライアントからの呼び出しでは実行されません。


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();

トークンの永続化

組み込みの UCredentials ヘルパーを使用して、トークンを保存および読み込みます。保存したサーバー発行のトークンを再接続時に渡すと、同じ ID が維持されます。このトークンに有効期限はなく、紛失した場合は復元できないため、自己発行の ID は開発用です。本番環境では、トークンのライフサイクルを管理する SpacetimeAuth などの OIDC プロバイダーで認証してください。

UCredentials::Init(TEXT(".spacetime_token"));
FString Token = UCredentials::LoadToken();
// ... after connect:
UCredentials::SaveToken(Token);

コンテキスト型

すべてのコールバックは、Db と Reducers にアクセスできるコンテキスト構造体を受け取ります。

型使用箇所
FEventContextテーブル行コールバック(OnInsert、OnDelete、OnUpdate)
FReducerEventContextリデューサー結果コールバック
FSubscriptionEventContextサブスクリプションのライフサイクルコールバック(OnApplied、OnError)
FErrorContextエラーコールバック

すべて FContextBase を継承しており、次の機能を提供します。

Context.Db          // URemoteTables* -- client cache
Context.Reducers    // URemoteReducers* -- invoke reducers
Context.SubscriptionBuilder()  // start a new subscription

Blueprint 統合

すべてのコアクラスは、UFUNCTION(BlueprintCallable) および UPROPERTY(BlueprintReadOnly/BlueprintAssignable) を介して Blueprint から利用できます。

  • UDbConnection::Builder() とすべてのビルダーメソッドは BlueprintCallable です。
  • テーブルコールバック(OnInsert、OnDelete、OnUpdate)は BlueprintAssignable デリゲートです。
  • Reducer の呼び出しメソッドと結果デリゲートは Blueprint から利用できます。
  • Conn->Db と Conn->Reducers は BlueprintReadOnly プロパティです。
  • 生成される行型は、BlueprintReadWrite プロパティを持つ BlueprintType の USTRUCT です。

つまり、C++ を記述せずに、接続とコールバックのフロー全体を Blueprint で構築できます。


Unreal 固有の考慮事項

自動ティックの代替手段

UDbConnection は FTickableGameObject を継承していますが、自動ティックはデフォルトで無効です。次のいずれかを選択できます。

// 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
生成される型

Codegen は F を接頭辞とする USTRUCT(例:FUserType、FEntityType)と、U を接頭辞とするテーブルクラス(例:UUserTable)を生成します。行型では、完全なリフレクションサポートを実現するために GENERATED_BODY() と UPROPERTY() を使用します。

コメント