unity
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSpacetimeDB Unity Integration
SpacetimeDB Unity 集成
This skill covers Unity-specific patterns for connecting to SpacetimeDB. For server-side module development, see the skill.
csharp-server本 skill 介绍连接 SpacetimeDB 的 Unity 专用模式。有关服务器端模块开发,请参阅 skill。
csharp-serverInstallation
安装
Add via Unity Package Manager using the git URL:
https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.gitWindow > 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_MODULEPlace 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()Update()FrameTick()Update()Thread safety: processes messages on the calling thread (the main thread in Unity). Do NOT call it from a background thread. Do NOT access from background threads.
FrameTick()conn.Db必须在 中每帧调用 。 SDK 会将所有网络消息放入队列,只有在调用 时才会处理这些消息。如果不调用它,回调将不会触发,客户端也会表现为卡死。请参阅上方 SpacetimeManager 中的 方法。
Update()FrameTick()FrameTick()Update()线程安全: 会在调用线程(Unity 中为主线程)处理消息。请勿从后台线程调用它,也请勿在后台线程中访问 。
FrameTick()conn.DbAutomatic Reconnect
自动重连
Add to the builder to recover an established connection after an outage. In the singleton above, create subscriptions only on the first , or move them after : runs again after each successful reconnect. Keep calling during outages, or use , which also ticks reconnecting connections. Do not gate ticking on .
.WithAutomaticReconnect()OnConnectedBuild()OnConnectedFrameTick()SpacetimeDBNetworkManagerIsActiveThe 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. runs again, so separate one-time object setup from marking data ready. Each reconnect attempt has a fresh .
OnAppliedConnectionIdIsReconnectingOnDisconnect((conn, error, next) => ...)OnConnectError((error, next) => ...)NextReconnect?AttemptDelayMinDelayMaxDelay.WithAutomaticReconnect(new AutomaticReconnectOptions { MinDelay = ..., MaxDelay = ... })Disconnect()Calls made while disconnected fail immediately. Pending reducer calls may report , meaning the server may have executed them; they are not replayed. Pending procedures and one-off queries fail with . Regenerate bindings when upgrading to include unknown-outcome handling.
Status.UnknownResultUnknownResultExceptionFor views with arguments (2.13+), use only raw SQL . 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.
SELECT * FROM view_name(literal_args)在 builder 中添加 ,可以在服务中断后恢复已建立的连接。在上方的单例中,仅应在第一次 时创建订阅,或者将订阅移到 之后:每次成功重连后, 都会再次运行。服务中断期间也应持续调用 ,或者使用 ,后者也会为正在重连的连接执行 tick。不要根据 对 tick 进行条件限制。
.WithAutomaticReconnect()OnConnectedBuild()OnConnectedFrameTick()SpacetimeDBNetworkManagerIsActiveSDK 会保留 identity、table handle、回调和订阅,并批量重放订阅。服务中断期间,缓存行仍可读取;恢复后,行回调会报告净变化。 会再次运行,因此应将一次性对象初始化与“标记数据已就绪”操作分开。每次重连尝试都会获得新的 。
OnAppliedConnectionIdIsReconnectingOnDisconnect((conn, error, next) => ...)OnConnectError((error, next) => ...)NextReconnect?AttemptDelayMinDelayMaxDelay.WithAutomaticReconnect(new AutomaticReconnectOptions { MinDelay = ..., MaxDelay = ... })Disconnect()处于断开状态时进行的调用会立即失败。待处理的 reducer 调用可能返回 ,这意味着服务端可能已经执行了这些调用;它们不会被重放。待处理的 procedure 和一次性查询会抛出 。升级时应重新生成绑定,以包含未知结果的处理逻辑。
Status.UnknownResultUnknownResultException对于带参数的视图(2.13+),仅使用原始 SQL 。对该连接上的每个表使用显式查询,不要订阅所有表。所有参数值共享同一个缓存表;取消订阅已废弃的参数,以避免混合它们的行。将动态值验证并编码为 SQL 字面量,不要拼接不可信输入。
SELECT * FROM view_name(literal_args)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 . Do not register them repeatedly in or when automatic reconnect is enabled.
FrameTick()OnConnectedOnSubscriptionApplied注册回调,以便在表数据发生变化时更新 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()OnConnectedOnSubscriptionAppliedCalling 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 (, 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.
FrameTickconn.Db所有 SpacetimeDB SDK 调用(、 访问和 reducer 调用)都必须在主线程中执行。如果需要将数据传递给后台线程,请先在回调中复制数据。
FrameTickconn.DbScene Loading
场景加载
Use 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.
DontDestroyOnLoad(gameObject)在 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 preserves SpacetimeDB types if you use assembly stripping
link.xml
SpacetimeDB SDK 使用代码生成。如果在 IL2CPP 构建中遇到问题:
- 确保生成的绑定为最新版本
- 如果使用程序集剥离,请检查 是否保留了 SpacetimeDB 类型
link.xml
Token Persistence
令牌持久化
Token save/load via 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 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.
PlayerPrefsOnConnectWith automatic reconnect enabled, the SDK retains the authentication token and passes it to , 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.
OnConnectFor expiring credentials, configure , , and . The provider returns 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. ignores a pending provider result without canceling the provider's own asynchronous work.
.WithToken(initialToken).WithAutomaticReconnect().WithTokenProvider(() => RefreshTokenAsync())Task<string>Disconnect()上面的 SpacetimeManager 单例演示了如何通过 保存和加载 token。持久化服务端签发的 token,并在重连时传回该 token,可保持同一个 identity;如果没有保存 token,服务端会在 回调中签发新的 identity。该 token 不会过期,且一旦丢失便无法恢复,因此由服务端自行签发的 identity 仅适用于开发环境。在生产环境中,应使用 OIDC 提供程序(如 SpacetimeAuth)进行身份验证,由其处理 token 生命周期。
PlayerPrefsOnConnect启用自动重连后,SDK 会保留身份验证 token 并将其传递给 ,在 WebGL 上也是如此。未启用自动重连时,使用已保存 token 的 WebGL 连接可能会返回一个短时效的 WebSocket token;此时应保留原始的已保存 token,而不是覆盖上面回调中的 token。
OnConnect对于会过期的凭据,请配置 、 和 。该 provider 为同一个 identity 返回 。当无法读取过期时间、剩余有效期不超过 30 秒或原始生命周期的 5%,或者复用的 token 被拒绝时,会在重连尝试之前调用它。初始连接期间或保持连接时,它不会被定期调用。provider 调用失败时会进行重试;如果新提供的 token 被拒绝,则会停止恢复。 会忽略待处理的 provider 结果,但不会取消 provider 自身的异步工作。
.WithToken(initialToken).WithAutomaticReconnect().WithTokenProvider(() => RefreshTokenAsync())Task<string>Disconnect()