资讯动态

SpacetimeDB Unity 客户端集成指南:MonoBehaviour 生命周期、FrameTick 与身份持久化实战

发布时间:2026/9/11 16:37:03 来源:尧图企业网站定制
SpacetimeDB Unity 客户端集成指南MonoBehaviour 生命周期、FrameTick 与身份持久化实战【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB导读本文以 SpacetimeDB 官方 Unity 集成技能文档codex-plugin/plugins/spacetimedb/skills/unity/SKILL.md为主体系统讲解如何在 Unity 游戏项目中接入 SpacetimeDB 实时后端从 UPM 包安装、模块绑定生成到SpacetimeManager单例的连接生命周期管理、每帧FrameTick()的消息泵、表行回调驱动 GameObject、UI 中调用 Reducer以及身份令牌的PlayerPrefs持久化。读完本文你将能在自己的 Unity 工程中搭建一套完整、可断线重连、可保持玩家身份的实时多人游戏客户端骨架并理解其底层消息处理模型与线程约束。本文面向服务端模块开发场景之外的Unity 客户端侧集成服务端模块的编写请参考同仓库的csharp-server技能文档。文中的所有代码模式均有仓库源码与官方 Demo 佐证可作为直接落地的参考。安装通过 Unity Package Manager 引入 SDKUnity 客户端 SDK 以 UPM 包形式发布安装方式是在 Unity 编辑器中打开Window Package Manager Add package from git URL填入 Git URLhttps://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.git仓库中对应包的装配定义文件 sdks/csharp/src/com.clockworklabs.spacetimedbsdk.asmdef 表明该程序集名称为com.clockworklabs.spacetimedbsdk与 UPM 包名一致。包内还随附sdks/csharp/src/UnityDebugLogger.cs将 SDK 日志桥接到UnityEngine.Debug的实现sdks/csharp/src/csc.rsp编译开关配置文件用于在 Unity 中启用必要的 C# 语言特性sdks/csharp/src/Plugins/WebSocket.jslibWebGL 构建下使用的 WebSocket 桥接实现。注技能文档标注其验证环境为SpacetimeDB 2.0 Unity 2022.3本文代码示例均以该版本线为准。生成模块绑定spacetime generate安装好 SDK 后需要为你的服务端模块生成 C# 客户端绑定代码表结构、Reducer 签名、事件表等使用 CLI 命令spacetime generate --lang csharp --out-dir Assets/SpacetimeDB/module_bindings --module-path PATH_TO_MODULE参数说明参数含义--lang csharp指定生成 C# 客户端绑定--out-dir生成文件的输出目录建议放在Assets/下--module-path指向服务端模块源码所在路径CLI 从模块定义中解析表与 Reducer 签名关键点生成文件必须放置在 Unity 工程的Assets文件夹内Unity 才会将它们编译进程序集。生成后的绑定代码在运行时充当远程表句柄与Reducer 调用入口即下文Connection.Db与Connection.Reducers的实体。官方 Demo 的生成产物可以直接参考demo/Blackholio/client-unity/Assets/Scripts/autogen/内含SpacetimeDBClient.g.cs与按表拆分的Tables/*.g.cs可以看到每个表的句柄类都继承了 SDK 中的RemoteTableHandle基类见下文行回调一节。SpacetimeManager 单例连接生命周期的核心模式Unity 集成最核心的模式是一个MonoBehaviour单例负责管理整个连接生命周期。技能文档给出了完整的SpacetimeManager实现逐段拆解如下。单例与场景驻留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); } ... }三个常量分别代表PlayerPrefs中保存令牌的键名、SpacetimeDB 服务端地址、目标数据库模块名称。Awake()里做两件事防重复实例化若已存在实例则销毁新对象保证全局唯一DontDestroyOnLoad让管理器对象在场景切换时不被销毁从而保住已建立的连接详见下文场景加载。构建连接Builder 模式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(); }WithToken(savedToken)把上次保存的令牌带回连接。第一次连接时没有令牌传入null即可由服务端签发新身份WithUri支持http:///https://写法。在 SDK 源码 sdks/csharp/src/SpacetimeDBClient.cs#L619-L638 中Connect会把http://自动替换为ws://、https://替换为wss://并去除末尾斜杠因此你无需手动转换协议Build()会立即触发连接。源码 sdks/csharp/src/SpacetimeDBClient.cs#L29-L47 显示Build()在uri或数据库名缺失时会抛出InvalidOperationException并在 Unity 平台下把连接注册到SpacetimeDBNetworkManagerWebGL 构建依赖它驱动消息解析协程。DbConnection.Builder()实际返回泛型DbConnectionBuilderDbConnection见 sdks/csharp/src/SpacetimeDBClient.cs#L17-L108除文档用到的链式方法外还提供以下可选配置Builder 方法作用WithCompression(Compression)设置消息压缩方式默认BrotliWithLightMode(bool)是否请求轻量更新减少冗余字段推送WithConfirmedReads(bool)为true时服务端仅在事务确认持久化后推送更新为false时内存提交即推送不设置则由服务端决定连接成功回调与订阅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(); }连接成功后 SDK 会回调OnConnect携带服务端分配的Identity与authToken。这里立即把令牌写入PlayerPrefs并Save()——这是断线重连后保持同一身份的关键详见Token 持久化一节。随后通过SubscriptionBuilder().SubscribeToAllTables()订阅全部表将服务端当前快照同步进客户端缓存OnApplied在初始数据全部落地后触发。订阅请求在底层由 sdks/csharp/src/SpacetimeDBClient.cs#L909-L931 的IDbConnection.Subscribe实现它为订阅分配QuerySetId把 SQL 查询串封装为ClientMessage.Subscribe消息发送到服务端未连接时调用会打印错误并返回null。FrameTick绝不能遗漏的每帧消息泵技能文档将FrameTick()标为Critical关键FrameTick()必须在Update()中每帧调用。SDK 把所有网络消息放入队列只有调用FrameTick()时才处理它们。一旦遗漏所有回调都不会触发客户端会表现为假死。void Update() { Connection?.FrameTick(); }从源码 sdks/csharp/src/SpacetimeDBClient.cs#L1015-L1022 可以看清其内部机制public void FrameTick() { webSocket.Update(); while (_applyQueue.TryTake(out var parsedMessage)) { ApplyMessage(parsedMessage); } }也就是说FrameTick()做两件事驱动底层 WebSocket 的状态机发送排队数据、读取入站消息消费应用队列_applyQueue逐一执行ApplyMessage——这正是行回调、Reducer 回调、订阅回调真正被触发的时刻。结合 sdks/csharp/src/SpacetimeDBClient.cs#L230-L260 可以看到完整的数据流SDK 在连接构造时启动一个名为SpacetimeDB Network Thread的后台线程WebGL 构建除外改用协程专门把收到的字节流解析ParseMessages成结构化的ServerMessage解析结果放入_applyQueue。因此网络收包与解析发生在后台线程消息的应用回调触发、缓存更新被延迟到主线程的FrameTick()。这也是所有回调都只在主线程触发这一约束的根本来源。线程安全约束FrameTick()在处理消息时运行在调用线程Unity 中即主线程。因此不要在后台线程调用FrameTick()不要在后台线程访问conn.Db客户端缓存若需把数据交给后台线程应在回调中先拷贝一份再传出去。类似的每帧驱动模式在仓库其他引擎适配中也能印证Godot 适配层 sdks/csharp/src/STDBUpdateManager.cs#L99-L105 在_Process(double delta)中对所有已注册连接逐一调用conn.FrameTick()与 Unity 的Update()完全对应。行回调用表数据变化驱动 GameObject当服务端表数据变化插入/更新/删除被FrameTick()应用后对应表的OnInsert/OnUpdate/OnDelete事件会触发。技能文档展示了经典的玩家表驱动对象生成/销毁模式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); }; }注册时机有两种选择在OnSubscriptionApplied中注册此时初始快照已加载完毕不会错过任何后续变更且快照本身不会触发回调造成重复生成或在Start()连接建立前注册需自行处理快照与增量更新的边界。技能文档推荐前者。底层实现PreApply / Apply / PostApply 三段式从源码看这些事件并非简单的收到消息即触发而是经过严格的三阶段应用流程sdks/csharp/src/SpacetimeDBClient.cs#L672-L689private void ApplyUpdate(IEventContext eventContext, ParsedDatabaseUpdate dbOps) { foreach (var (table, update) in dbOps.Updates) table.PreApply(eventContext, update); foreach (var (table, update) in dbOps.Updates) table.Apply(eventContext, update); foreach (var (table, _) in dbOps.Updates) table.PostApply(eventContext); }对应 sdks/csharp/src/Table.cs#L445-L574 中IRemoteTableHandle的实现PreApply先触发OnBeforeDelete保证所有表在真正删行之前都能读到待删行的旧值Apply把增量MultiDictionaryDelta真正应用到客户端缓存并同步更新索引但不触发用户回调此时其他表可能尚未更新回调时机不成熟PostApply全部表应用完毕后再统一触发OnInsert/OnUpdate/OnDelete确保回调看到的是全局一致的数据视图。值得注意的细节SDK 的事件系统还区分持久表RemoteTableHandle支持OnInsert/OnDelete/OnBeforeDelete/OnUpdate与事件表RemoteEventTableHandle仅支持OnInsert不持久化行、不触发删除/更新回调见 sdks/csharp/src/Table.cs#L673-L683。事件表适合一次性消息类数据如日志、聊天消息减少客户端缓存占用。另外SDK 在#if UNITY_5_3_OR_NEWER下内置了域重载清理逻辑sdks/csharp/src/Table.cs#L191-L205当 Unity 开启 Enter Play Mode Options 中的 Disable Domain Reloading 时静态序列化器会被注册到RemoteTableHandleStaticReset在[RuntimeInitializeOnLoadMethod]阶段重置避免旧数据跨域残留——这也是 Unity 下调试时值得了解的行为。从 UI 调用 Reducer 并处理结果调用服务端 Reducer 只需一行通过SpacetimeManager.Instance.Connection.Reducers.xxx(...)触发对应方法。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); } }Reducers属性由代码生成器根据模块中的#[spacetimedb::reducer]函数生成每个 Reducer 对应一个强类型方法。底层由 sdks/csharp/src/SpacetimeDBClient.cs#L857-L885 的InternalCallReducer实现分配请求 ID、把参数 BSATN 序列化后封装成ClientMessage.CallReducer发送若连接未建立会打印Cannot call reducer, not connected to server!并直接返回。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}); };ctx.Event.Status反映服务端执行结果Status.CommittedReducer 事务已提交成功Status.Failed(reason)Reducer 执行失败reason为失败原因字符串。服务端返回的失败原因在 sdks/csharp/src/SpacetimeDBClient.cs#L407-L419 中被解码为 BSATN 字符串同时 sdks/csharp/src/SpacetimeDBClient.cs#L505-L548 展示了ReducerResult的处理成功时其内嵌的TransactionUpdate会一并解析应用保证Reducer 副作用导致的行变更与Reducer 回调在同一次FrameTick()内先后生效——这是客户端 UI 能即时反映服务端状态一致性的关键。读取客户端缓存订阅建立后客户端维护一份本地缓存Connection.Db可同步查询无需等待网络往返。技能文档给出的四种查询方式如下// 按主键查找 if (Connection.Db.Player.Id.Find(playerId) is Player player) { Debug.Log($Player: {player.Name}); } // 遍历全部 foreach (var p in Connection.Db.Player.Iter()) { Debug.Log(p.Name); } // 按索引过滤 foreach (var p in Connection.Db.Player.Level.Filter(5)) { Debug.Log($Level 5: {p.Name}); } // 计数 int total Connection.Db.Player.Count;底层实现本地索引缓存这些 API 并非实时查询服务端而是读取客户端缓存的镜像索引。从 sdks/csharp/src/Table.cs#L105-L158 可以看到唯一索引UniqueIndexBase内部维护DictionaryColumn, RowFind(value)即字典查找时间复杂度 O(1)行插入/删除时通过 SDK 内部的OnInternalInsert/OnInternalDelete钩子同步维护缓存sdks/csharp/src/Table.cs#L111-L123B 树索引BTreeIndexBase内部维护DictionaryColumn, HashSetRowFilter(value)返回该键下的行集合sdks/csharp/src/Table.cs#L125-L158Iter()与Count直接取自缓存表sdks/csharp/src/Table.cs#L416-L418。主键查找的命名规则若表有#[primary_key]列如Player.Id则生成Find无主键的表则退化为整行作为键。若你需要查询不在缓存中的服务端数据SDK 还提供RemoteQuerytable.RemoteQuery(WHERE ...)见 sdks/csharp/src/Table.cs#L420-L421它走一次性查询通道且不影响订阅缓存但返回的是TaskT[]异步结果。Unity 特有的注意事项主线程约束所有 SpacetimeDB SDK 调用FrameTick、conn.Db访问、Reducer 调用都必须在主线程执行。原因已在前文说明回调触发与缓存更新都发生在FrameTick()内部而它应当只在主线程被调用。需要把数据交给后台线程时先在回调内拷贝副本再传递。场景加载与连接保持对管理连接的对象使用DontDestroyOnLoad(gameObject)否则每次加载新场景时连接都会被销毁、玩家会被迫重连并可能因令牌未持久化而丢失身份。这是SpacetimeManager.Awake()中该调用的意义所在。IL2CPP / AOT 构建SpacetimeDB SDK 依赖代码生成在 IL2CPP / AOT 构建下需注意确保生成的绑定代码保持最新模块结构变更后重新运行spacetime generate若开启程序集裁剪assembly stripping需在link.xml中保留 SpacetimeDB 相关类型防止运行时反射/序列化失败。此项源于技能文档的实践提示具体裁剪清单请结合你的构建产物报错信息核对。Token 持久化与身份生命周期SpacetimeManager中的令牌读写逻辑是完整的身份保持闭环保存OnConnected回调里PlayerPrefs.SetString(TOKEN_KEY, authToken); PlayerPrefs.Save();复用Start()里PlayerPrefs.GetString(TOKEN_KEY, null)作为WithToken(...)的参数关键语义持久化令牌并在重连时回传服务端会恢复同一个Identity没有令牌时服务端在OnConnect回调中签发全新身份——每次重装/清数据都会变成新玩家该令牌永不过期且丢失后无法找回服务端不会重新发放同一令牌因此自签发身份仅适合开发调试生产环境应接入 OIDC 身份提供商如 SpacetimeAuth由其负责令牌的签发、刷新与生命周期管理。官方 Blackholio Demo 完整展示了这一模式demo/Blackholio/client-unity/Assets/Scripts/GameManager.cs#L32-L88读取AuthToken.Token、连接成功后在HandleConnect中调用AuthToken.SaveToken(token)持久化令牌并注册各表行回调后执行SubscribeToAllTables()。注释中还提到一个实用的调试技巧注释掉令牌复用逻辑后同一客户端每次运行都会获得不同身份便于本地测试多玩家互相吞噬的玩法。参考与延伸阅读本技能文档仓库位置codex-plugin/plugins/spacetimedb/skills/unity/SKILL.mdC# SDK 连接与消息泵核心实现sdks/csharp/src/SpacetimeDBClient.cs远程表句柄、索引缓存与行回调实现sdks/csharp/src/Table.cs全引擎通用的每帧驱动模式Godot 适配参考sdks/csharp/src/STDBUpdateManager.cs官方 Unity 客户端 Demo完整集成样板demo/Blackholio/client-unity/Assets/Scripts/GameManager.cs自动生成绑定产物示例demo/Blackholio/client-unity/Assets/Scripts/autogen/服务端模块开发技能Rust / C# 侧codex-plugin/plugins/spacetimedb/skills/csharp-server/SKILL.md【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价