unity について
Unity から SpacetimeDB の C# SDK に接続する方法を解説するガイドです。Unity のライフサイクル、メインスレッドでの更新、ゲーム状態のコールバック、再接続を扱います。
主な機能
- 01Unity Package Manager による SDK 導入
- 02C# モジュールバインディングの生成
- 03MonoBehaviour による接続ライフサイクル管理
- 04Update() での接続処理
- 05行イベントとゲームオブジェクトの連携
- 06再接続とトークン永続化の設定
活用シーン
- 01Unity ゲームと SpacetimeDB の接続
- 02テーブル行の変更に応じたゲームオブジェクト更新
- 03シーン読み込み中の接続維持
- 04障害後のゲームクライアント復旧
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ clockworklabs/ spacetimedb --skill unityインストール後、Claude Code で/unityを入力して読み込まれたか確認します。
詳細
SpacetimeDB Unity 連携
このスキルでは、SpacetimeDB に接続するための Unity 固有のパターンを説明します。サーバー側のモジュール開発については、csharp-server スキルを参照してください。
インストール
Git URL を使って Unity Package Manager から追加します。
https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.git
Window > Package Manager > + > Add package from git URL
モジュールバインディングの生成
spacetime generate --lang csharp --out-dir Assets/SpacetimeDB/module_bindings --module-path PATH_TO_MODULE
生成したファイルは、Unity がコンパイルできるように Assets フォルダーに配置します。
SpacetimeManager シングルトン
Unity 連携の基本パターンです。この MonoBehaviour が接続のライフサイクルを管理します。
using UnityEngine;
using SpacetimeDB;
using SpacetimeDB.Types;
public class SpacetimeManager : MonoBehaviour
{
private const string TOKEN_KEY = "SpacetimeAuthToken";
private const string SERVER_URI = "http://localhost:3000";
private const string DATABASE_NAME = "my-game";
public static SpacetimeManager Instance { get; private set; }
public DbConnection Connection { get; private set; }
public Identity LocalIdentity { get; private set; }
void Awake()
{
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
Instance = this;
DontDestroyOnLoad(gameObject);
}
void Start()
{
string savedToken = PlayerPrefs.GetString(TOKEN_KEY, null);
Connection = DbConnection.Builder()
.WithUri(SERVER_URI)
.WithDatabaseName(DATABASE_NAME)
.WithToken(savedToken)
.OnConnect(OnConnected)
.OnConnectError(err => Debug.LogError($"Connection failed: {err}"))
.OnDisconnect((conn, err) => {
if (err != null) Debug.LogError($"Disconnected: {err}");
})
.Build();
}
void Update()
{
Connection?.FrameTick();
}
void OnDestroy()
{
Connection?.Disconnect();
}
private void OnConnected(DbConnection conn, Identity identity, string authToken)
{
LocalIdentity = identity;
PlayerPrefs.SetString(TOKEN_KEY, authToken);
PlayerPrefs.Save();
Debug.Log($"Connected as: {identity}");
conn.SubscriptionBuilder()
.OnApplied(OnSubscriptionApplied)
.SubscribeToAllTables();
}
private void OnSubscriptionApplied(SubscriptionEventContext ctx)
{
Debug.Log("Subscription applied: game state loaded");
}
}
FrameTick(重要)
FrameTick() は Update() 内で毎フレーム呼び出す必要があります。 SDK はすべてのネットワークメッセージをキューに格納し、FrameTick() を呼び出したときにのみ処理します。呼び出さないとコールバックは実行されず、クライアントがフリーズしたように見えます。上記の SpacetimeManager にある Update() メソッドを参照してください。
スレッドセーフティ:FrameTick() は呼び出し元のスレッド(Unity ではメインスレッド)でメッセージを処理します。バックグラウンドスレッドから呼び出さないでください。バックグラウンドスレッドから conn.Db にアクセスしないでください。
自動再接続
ビルダーに .WithAutomaticReconnect() を追加すると、障害発生後に確立済みの接続を復旧できます。上記のシングルトンでは、最初の OnConnected でのみサブスクリプションを作成するか、Build() の後に作成してください。OnConnected は再接続が成功するたびに再度実行されます。障害発生中も FrameTick() の呼び出しを続けるか、再接続中の接続もティックする SpacetimeDBNetworkManager を使用してください。IsActive を条件にティックを止めないでください。
SDK は ID、テーブルハンドル、コールバック、サブスクリプションを保持し、サブスクリプションを 1 つのバッチで再送します。障害発生中もキャッシュされた行は読み取り可能です。行コールバックは復旧後に差分を報告します。OnApplied は再度実行されるため、オブジェクトの初期設定とデータの準備完了の処理を分けてください。再接続の試行ごとに新しい ConnectionId が割り当てられます。
IsReconnecting は、次のハンドシェイクが成功する前に復旧中であることを示します。OnDisconnect((conn, error, next) => ...) および OnConnectError((error, next) => ...) のオーバーロードでは NextReconnect? が公開され、次の Attempt と Delay を確認できます。終端エラーの場合は null になります。初回接続の失敗は再試行されません。再試行では指数バックオフとジッターが使用され、MinDelay(既定値 1 秒)から MaxDelay(既定値 30 秒)までの範囲で待機します。.WithAutomaticReconnect(new AutomaticReconnectOptions { MinDelay = ..., MaxDelay = ... }) で調整できます。データベースに過剰な負荷がかからないよう、500 ms および 1 秒の下限を下回る値は警告とともに引き上げられます。Disconnect() を呼び出すと再試行を停止します。
切断中に行った呼び出しは即座に失敗します。保留中のリデューサー呼び出しは Status.UnknownResult を返す場合があります。これは、サーバーが実行した可能性があることを意味します。再送はされません。保留中のプロシージャおよび一度限りのクエリは UnknownResultException で失敗します。結果が不明な場合の処理を含めるには、アップグレード時にバインディングを再生成してください。
ゲームステート用の行コールバック
テーブルデータが変更されたときに Unity GameObject を更新するコールバックを登録します。
void RegisterCallbacks()
{
Connection.Db.Player.OnInsert += (EventContext ctx, Player player) => {
SpawnPlayerObject(player);
};
Connection.Db.Player.OnDelete += (EventContext ctx, Player player) => {
DestroyPlayerObject(player.Id);
};
Connection.Db.Player.OnUpdate += (EventContext ctx, Player oldPlayer, Player newPlayer) => {
UpdatePlayerObject(newPlayer);
};
}
接続を構築した後、最初の FrameTick() より前に一度だけ登録してください。自動再接続が有効な場合、OnConnected や OnSubscriptionApplied で繰り返し登録しないでください。
UI からのリデューサー呼び出し
public class GameUI : MonoBehaviour
{
public void OnMoveButtonClicked(Vector2 direction)
{
SpacetimeManager.Instance.Connection.Reducers.MovePlayer(direction.x, direction.y);
}
public void OnSendChat(string message)
{
SpacetimeManager.Instance.Connection.Reducers.SendMessage(message);
}
}
リデューサーのコールバック
SpacetimeManager.Instance.Connection.Reducers.OnSendMessage += (ReducerEventContext ctx, string text) => {
if (ctx.Event.Status is Status.Committed)
Debug.Log($"Message sent: {text}");
else if (ctx.Event.Status is Status.Failed(var reason))
Debug.LogError($"Send failed: {reason}");
else if (ctx.Event.Status is Status.UnknownResult)
Debug.LogWarning("Connection lost before the result arrived; the message may have been sent.");
};
クライアントキャッシュの読み取り
// Find by primary key
if (Connection.Db.Player.Id.Find(playerId) is Player player)
{
Debug.Log($"Player: {player.Name}");
}
// Iterate all
foreach (var p in Connection.Db.Player.Iter())
{
Debug.Log(p.Name);
}
// Filter by index
foreach (var p in Connection.Db.Player.Level.Filter(5))
{
Debug.Log($"Level 5: {p.Name}");
}
// Count
int total = Connection.Db.Player.Count;
Unity 固有の考慮事項
メインスレッドのみ
SpacetimeDB SDK の呼び出し(FrameTick、conn.Db へのアクセス、リデューサー呼び出し)はすべてメインスレッドで行う必要があります。バックグラウンドスレッドにデータを渡す必要がある場合は、コールバック内で先にコピーしてください。
シーンの読み込み
シーン遷移中に接続が破棄されないよう、SpacetimeManager で DontDestroyOnLoad(gameObject) を使用してください。これを指定しないと、新しいシーンを読み込むたびに接続が切断されます。
IL2CPP / AOT
SpacetimeDB SDK はコード生成を使用します。IL2CPP ビルドで問題が発生した場合は、次の点を確認してください。
- 生成されたバインディングが最新であること
- アセンブリストリッピングを使用している場合、
link.xmlで SpacetimeDB の型が保持されていること
トークンの永続化
PlayerPrefs を使ったトークンの保存と読み込みは、上記の SpacetimeManager シングルトンで示しています。サーバーから発行されたトークンを永続化し、再接続時に渡すことで、同じ ID を維持できます。保存済みのトークンがない場合、サーバーは OnConnect コールバックで新しい ID を発行します。このトークンは期限切れにならず、紛失した場合は復元できないため、自己発行の ID は開発用です。本番環境では、トークンのライフサイクルを管理する SpacetimeAuth などの OIDC プロバイダーで認証してください。
自動再接続が有効な場合、SDK は認証トークンを保持し、WebGL を含め、OnConnect に渡します。自動再接続が無効な場合、保存済みのトークンを使った WebGL 接続では、代わりに有効期間の短い WebSocket トークンが返されることがあります。その場合は、上記のコールバックで上書きせず、元の保存済みトークンを保持してください。
期限切れになる認証情報の場合は、.WithToken(initialToken)、.WithAutomaticReconnect()、.WithTokenProvider(() => RefreshTokenAsync()) を設定してください。プロバイダーは同じ ID に対する Task<string> を返します。有効期限を読み取れない場合、残りの有効期間が30秒以下、または元の有効期間の5%以下の場合、あるいは再利用したトークンが拒否された場合に、再接続の試行前に使用されます。初回接続時や、接続中に定期的に呼び出されることはありません。プロバイダーの失敗時は再試行されます。新たに提供されたトークンが拒否された場合、復旧は停止します。Disconnect() は、保留中のプロバイダーの結果を無視しますが、プロバイダー自身の非同期処理はキャンセルしません。