关于 unity
介绍如何在 Unity 中使用 SpacetimeDB C# SDK 连接游戏,重点涵盖 Unity 生命周期、主线程更新、游戏状态回调和重连处理。
主要功能
- 01通过 Unity Package Manager 安装 SDK
- 02生成 C# 模块绑定
- 03使用 MonoBehaviour 管理连接生命周期
- 04在 Update() 中推进连接
- 05将行事件映射到游戏对象
- 06配置重连和令牌持久化
适用场景
- 01将 Unity 游戏连接到 SpacetimeDB
- 02在表行变化时更新游戏对象
- 03在场景切换期间维持连接
- 04在中断后恢复游戏客户端连接
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ clockworklabs/ spacetimedb --skill unity安装后在 Claude Code 中输入/unity确认已加载。
详细介绍
SpacetimeDB Unity 集成
本技能介绍连接 SpacetimeDB 的 Unity 特定模式。有关服务器端模块开发,请参阅 csharp-server 技能。
安装
通过 Unity Package Manager 使用 Git URL 添加:
https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.git
窗口 > Package Manager > + > 从 Git URL 添加软件包
生成模块绑定
spacetime generate --lang csharp --out-dir Assets/SpacetimeDB/module_bindings --module-path PATH_TO_MODULE
将生成的文件放在 Assets 文件夹中,以便 Unity 编译它们。
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(重要)
必须在 Update() 中每帧调用 FrameTick()。 SDK 会将所有网络消息加入队列,并且只有在调用 FrameTick() 时才会处理它们。如果不调用它,就不会触发任何回调,客户端看起来会像是卡住了。请参阅上方 SpacetimeManager 中的 Update() 方法。
线程安全:FrameTick() 会在调用线程(Unity 中为主线程)上处理消息。切勿从后台线程调用它。切勿从后台线程访问 conn.Db。
自动重连
在构建器中添加 .WithAutomaticReconnect(),以便在连接中断后恢复已建立的连接。在上面的单例中,仅在首次 OnConnected 时创建订阅,或者将其移到 Build() 之后:每次成功重连后,都会再次运行 OnConnected。连接中断期间仍需持续调用 FrameTick(),或者使用 SpacetimeDBNetworkManager,它也会对正在重连的连接执行 tick。不要根据 IsActive 来决定是否执行 tick。
SDK 会保留身份、表句柄、回调和订阅,并将订阅合并为一批重新播放。中断期间缓存的行仍可读取;恢复后,行回调会报告净变化。OnApplied 会再次运行,因此应将一次性对象设置与标记数据就绪的操作分开。每次重连尝试都会使用新的 ConnectionId。
IsReconnecting 会在下次成功握手之前报告恢复状态。OnDisconnect((conn, error, next) => ...) 和 OnConnectError((error, next) => ...) 重载会提供 NextReconnect?,其中包含即将进行的 Attempt 和 Delay;若为终止性失败,则为 null。初始连接失败不会重试。重试使用指数退避和抖动,延迟从 MinDelay(默认 1 s)逐步增加到 MaxDelay(默认 30 s),可通过 .WithAutomaticReconnect(new AutomaticReconnectOptions { MinDelay = ..., MaxDelay = ... }) 调整;低于 500 ms 和 1 s 下限的值会被提高,并发出警告,以免重试中的客户端压垮数据库。Disconnect() 会停止重试。
断开连接时发出的调用会立即失败。待处理的 reducer 调用可能会报告 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 调用 Reducer
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 回调
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 访问、reducer 调用)都必须在主线程上进行。如果需要向后台线程传递数据,请先在回调中复制数据。
加载场景
在 SpacetimeManager 上使用 DontDestroyOnLoad(gameObject),防止场景切换时连接被销毁。否则,每次加载新场景时连接都会断开。
IL2CPP / AOT
SpacetimeDB SDK 使用代码生成。如果 IL2CPP 构建遇到问题:
- 确保生成的绑定是最新的
- 如果使用程序集剥离,请检查
link.xml是否保留了 SpacetimeDB 类型
Token 持久化
上方的 SpacetimeManager 单例演示了通过 PlayerPrefs 保存和加载 Token。持久化保存服务器签发的 Token,并在重新连接时将其传回,可保持相同的身份;如果未保存 Token,服务器会在 OnConnect 回调中签发新身份。此 Token 不会过期,丢失后也无法找回,因此自签发身份仅用于开发。用于生产环境时,请通过 SpacetimeAuth 等 OIDC 提供商进行身份验证,由其处理 Token 生命周期。
启用自动重新连接后,SDK 会保留身份验证 Token,并将其传给 OnConnect,包括在 WebGL 中。未启用自动重新连接时,使用已保存 Token 的 WebGL 连接可能会返回一个短期有效的 WebSocket Token;此时请保留原先保存的 Token,不要在上方的回调中覆盖它。
对于会过期的凭据,请配置 .WithToken(initialToken)、.WithAutomaticReconnect() 和 .WithTokenProvider(() => RefreshTokenAsync())。提供程序会返回用于同一身份的 Task<string>。当过期时间无法读取、剩余有效期不超过 30 秒或原始有效期的 5%,或者复用的 Token 被拒绝时,系统会在尝试重新连接前调用该提供程序。首次连接时不会调用,也不会在连接期间定期调用。提供程序调用失败时会重试;如果新提供的 Token 被拒绝,则会停止恢复。Disconnect() 会忽略待处理的提供程序结果,但不会取消提供程序自身的异步操作。