unreal について
unreal は、Unreal Engine プロジェクトで SpacetimeDB を利用するための Skill です。C++ と Blueprint での接続、テーブル購読、キャッシュデータ、コールバック、reducer 呼び出しを扱います。
主な機能
- 01C++ モジュールバインディングの生成
- 02接続の構築とトークン管理
- 03テーブルと SQL クエリの購読
- 04キャッシュテーブルの参照と行イベント処理
- 05reducer の呼び出しと結果受信
- 06Blueprint 対応ワークフロー
活用シーン
- 01Unreal プロジェクトから SpacetimeDB に接続
- 02購読したテーブルデータや行変更の受信
- 03ゲームクライアントから reducer を呼び出す
- 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 をプラグインとして追加します。
- Unreal プロジェクトのルートに
Pluginsフォルダーがない場合は、作成します。 SpacetimeDbSdkフォルダーをPlugins/にコピーします。.uprojectファイルを右クリックし、Generate Visual Studio project files を選択します。- モジュールの
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() を使用します。