unity

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SpacetimeDB Unity Integration

SpacetimeDB Unity 集成

This skill covers Unity-specific patterns for connecting to SpacetimeDB. For server-side module development, see the
csharp-server
skill.

本 skill 介绍连接 SpacetimeDB 的 Unity 专用模式。有关服务器端模块开发,请参阅
csharp-server
skill。

Installation

安装

Add via Unity Package Manager using the git URL:
https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.git
Window > Package Manager > + > Add package from git URL

通过 Git URL 使用 Unity Package Manager 添加:
bash
spacetime generate --lang csharp --out-dir Assets/SpacetimeDB/module_bindings --module-path PATH_TO_MODULE
窗口 > 包管理器 > + > 从 Git URL 添加包

Generate Module Bindings

生成模块绑定

bash
spacetime generate --lang csharp --out-dir Assets/SpacetimeDB/module_bindings --module-path PATH_TO_MODULE
Place generated files in your Assets folder so Unity compiles them.

bash
spacetime generate --lang csharp --out-dir Assets/SpacetimeDB/module_bindings --module-path PATH_TO_MODULE
将生成的文件放入 Assets 文件夹,以便 Unity 对其进行编译。

SpacetimeManager Singleton

SpacetimeManager 单例

The core pattern for Unity integration. This MonoBehaviour manages the connection lifecycle.
csharp
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");
    }
}

这是 Unity 集成的核心模式。该 MonoBehaviour 管理连接的生命周期。
csharp
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 (Critical)

FrameTick(关键)

FrameTick()
must be called every frame in
Update()
.
The SDK queues all network messages and only processes them when you call
FrameTick()
. Without it, no callbacks fire and the client appears frozen. See the
Update()
method in the SpacetimeManager above.
Thread safety:
FrameTick()
processes messages on the calling thread (the main thread in Unity). Do NOT call it from a background thread. Do NOT access
conn.Db
from background threads.
必须在
Update()
中每帧调用
FrameTick()
。
SDK 会将所有网络消息放入队列,只有在调用
FrameTick()
时才会处理这些消息。如果不调用它,回调将不会触发,客户端也会表现为卡死。请参阅上方 SpacetimeManager 中的
Update()
方法。
线程安全:
FrameTick()
会在调用线程(Unity 中为主线程)处理消息。请勿从后台线程调用它,也请勿在后台线程中访问
conn.Db
。

Automatic Reconnect

自动重连

Add
.WithAutomaticReconnect()
to the builder to recover an established connection after an outage. In the singleton above, create subscriptions only on the first
OnConnected
, or move them after
Build()
:
OnConnected
runs again after each successful reconnect. Keep calling
FrameTick()
during outages, or use
SpacetimeDBNetworkManager
, which also ticks reconnecting connections. Do not gate ticking on
IsActive
.
The SDK retains the identity, table handles, callbacks, and subscriptions, and replays subscriptions in one batch. Cached rows stay readable during an outage; row callbacks report net changes after recovery.
OnApplied
runs again, so separate one-time object setup from marking data ready. Each reconnect attempt has a fresh
ConnectionId
.
IsReconnecting
reports recovery before the next successful handshake. The
OnDisconnect((conn, error, next) => ...)
and
OnConnectError((error, next) => ...)
overloads expose
NextReconnect?
, with the upcoming
Attempt
and
Delay
, or null for a terminal failure. Initial connection failures do not retry. Retries use exponential backoff and jitter from
MinDelay
(default 1 s) up to
MaxDelay
(default 30 s), tunable with
.WithAutomaticReconnect(new AutomaticReconnectOptions { MinDelay = ..., MaxDelay = ... })
; values below the 500 ms and 1 s floors are raised with a warning, so that retrying clients cannot overwhelm the database.
Disconnect()
stops them.
Calls made while disconnected fail immediately. Pending reducer calls may report
Status.UnknownResult
, meaning the server may have executed them; they are not replayed. Pending procedures and one-off queries fail with
UnknownResultException
. Regenerate bindings when upgrading to include unknown-outcome handling.

For views with arguments (2.13+), use only raw SQL
SELECT * FROM view_name(literal_args)
. Use explicit queries for every table on that connection, not subscribe-all. All argument values share one cache table; unsubscribe from obsolete arguments to avoid combining their rows. Validate/encode dynamic values as SQL literals rather than concatenating untrusted input.
在 builder 中添加
.WithAutomaticReconnect()
,可以在服务中断后恢复已建立的连接。在上方的单例中,仅应在第一次
OnConnected
时创建订阅,或者将订阅移到
Build()
之后:每次成功重连后,
OnConnected
都会再次运行。服务中断期间也应持续调用
FrameTick()
,或者使用
SpacetimeDBNetworkManager
,后者也会为正在重连的连接执行 tick。不要根据
IsActive
对 tick 进行条件限制。
SDK 会保留 identity、table handle、回调和订阅,并批量重放订阅。服务中断期间,缓存行仍可读取;恢复后,行回调会报告净变化。
OnApplied
会再次运行,因此应将一次性对象初始化与“标记数据已就绪”操作分开。每次重连尝试都会获得新的
ConnectionId
。
IsReconnecting
会在下一次成功握手之前报告正在恢复。
OnDisconnect((conn, error, next) => ...)
和
OnConnectError((error, next) => ...)
重载通过
NextReconnect?
暴露即将进行的
Attempt
和
Delay
;若发生终止性失败,则为 null。初始连接失败不会触发重试。重试采用指数退避和抖动策略,起始延迟为
MinDelay
(默认 1 秒),最大延迟为
MaxDelay
(默认 30 秒);可通过
.WithAutomaticReconnect(new AutomaticReconnectOptions { MinDelay = ..., MaxDelay = ... })
调整。低于 500 毫秒和 1 秒下限的值会被上调并发出警告,从而防止重试客户端压垮数据库。调用
Disconnect()
可停止重试。
处于断开状态时进行的调用会立即失败。待处理的 reducer 调用可能返回
Status.UnknownResult
,这意味着服务端可能已经执行了这些调用;它们不会被重放。待处理的 procedure 和一次性查询会抛出
UnknownResultException
。升级时应重新生成绑定,以包含未知结果的处理逻辑。
对于带参数的视图(2.13+),仅使用原始 SQL
SELECT * FROM view_name(literal_args)
。对该连接上的每个表使用显式查询,不要订阅所有表。所有参数值共享同一个缓存表;取消订阅已废弃的参数,以避免混合它们的行。将动态值验证并编码为 SQL 字面量,不要拼接不可信输入。

Row Callbacks for Game State

游戏状态的行回调

Register callbacks to update Unity GameObjects when table data changes.
csharp
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);
    };
}
Register these once after building the connection, before the first
FrameTick()
. Do not register them repeatedly in
OnConnected
or
OnSubscriptionApplied
when automatic reconnect is enabled.

注册回调,以便在表数据发生变化时更新 Unity GameObject。
csharp
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
中重复注册。

Calling Reducers from UI

从 UI 调用 Reducer

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

Reducer Callbacks

Reducer 回调

csharp
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.");
};

csharp
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.");
};

Reading the Client Cache

读取客户端缓存

csharp
// 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;

csharp
// 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-Specific Considerations

Unity 专属注意事项

Main Thread Only

仅限主线程

All SpacetimeDB SDK calls (
FrameTick
,
conn.Db
access, reducer calls) must happen on the main thread. If you need to pass data to a background thread, copy it first in the callback.
所有 SpacetimeDB SDK 调用(
FrameTick
、
conn.Db
访问和 reducer 调用)都必须在主线程中执行。如果需要将数据传递给后台线程,请先在回调中复制数据。

Scene Loading

场景加载

Use
DontDestroyOnLoad(gameObject)
on the SpacetimeManager to prevent the connection from being destroyed during scene transitions. Without it, the connection drops every time you load a new scene.
在 SpacetimeManager 上使用
DontDestroyOnLoad(gameObject)
,防止连接在场景切换过程中被销毁。否则,每次加载新场景时连接都会断开。

IL2CPP / AOT

IL2CPP / AOT

The SpacetimeDB SDK uses code generation. If you encounter issues with IL2CPP builds:
  • Ensure generated bindings are up to date
  • Check that
    link.xml
    preserves SpacetimeDB types if you use assembly stripping
SpacetimeDB SDK 使用代码生成。如果在 IL2CPP 构建中遇到问题:
  • 确保生成的绑定为最新版本
  • 如果使用程序集剥离,请检查
    link.xml
    是否保留了 SpacetimeDB 类型

Token Persistence

令牌持久化

Token save/load via
PlayerPrefs
is demonstrated in the SpacetimeManager singleton above. Persisting the server-issued token and passing it back on reconnect keeps the same identity; without a saved token the server issues a new identity in the
OnConnect
callback. This token does not expire and a lost one can't be recovered, so self-issued identities are for development. For production, authenticate with an OIDC provider such as SpacetimeAuth, which handles token lifecycle.
With automatic reconnect enabled, the SDK retains the authentication token and passes it to
OnConnect
, including on WebGL. Without automatic reconnect, a WebGL connection using a saved token may return a short-lived WebSocket token instead; in that case, keep the original saved token rather than overwriting it in the callback above.
For expiring credentials, configure
.WithToken(initialToken)
,
.WithAutomaticReconnect()
, and
.WithTokenProvider(() => RefreshTokenAsync())
. The provider returns
Task<string>
for the same identity. It is used before reconnect attempts when expiry is unreadable, remaining validity is at most 30 seconds or 5% of the original lifetime, or a reused token is rejected. It is not called for the initial connection or periodically while connected. Provider failures retry; rejection of a freshly provided token stops recovery.
Disconnect()
ignores a pending provider result without canceling the provider's own asynchronous work.
上面的 SpacetimeManager 单例演示了如何通过
PlayerPrefs
保存和加载 token。持久化服务端签发的 token,并在重连时传回该 token,可保持同一个 identity;如果没有保存 token,服务端会在
OnConnect
回调中签发新的 identity。该 token 不会过期,且一旦丢失便无法恢复,因此由服务端自行签发的 identity 仅适用于开发环境。在生产环境中,应使用 OIDC 提供程序(如 SpacetimeAuth)进行身份验证,由其处理 token 生命周期。
启用自动重连后,SDK 会保留身份验证 token 并将其传递给
OnConnect
,在 WebGL 上也是如此。未启用自动重连时,使用已保存 token 的 WebGL 连接可能会返回一个短时效的 WebSocket token;此时应保留原始的已保存 token,而不是覆盖上面回调中的 token。
对于会过期的凭据,请配置
.WithToken(initialToken)
、
.WithAutomaticReconnect()
和
.WithTokenProvider(() => RefreshTokenAsync())
。该 provider 为同一个 identity 返回
Task<string>
。当无法读取过期时间、剩余有效期不超过 30 秒或原始生命周期的 5%,或者复用的 token 被拒绝时,会在重连尝试之前调用它。初始连接期间或保持连接时,它不会被定期调用。provider 调用失败时会进行重试;如果新提供的 token 被拒绝,则会停止恢复。
Disconnect()
会忽略待处理的 provider 结果,但不会取消 provider 自身的异步工作。