SpacetimeDB Unity 教程(二):从零创建服务器模块并打通 Unity 客户端连接
【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB
本篇是 SpacetimeDB Unity 多人游戏教程的第二节。在前一篇 Part 1 完成 Unity 客户端工程搭建(导入 SpacetimeDB SDK、创建GameManager脚本并挂载SpacetimeDBNetworkManager)之后,本部分将带领你完成另一半:在 Unity 工程旁用spacetimeCLI 初始化一个服务器模块,用 Rust / C# / C++ 三种语言定义 SpacetimeDB 数据表与 reducer,启动本地 SpacetimeDB 实例并发布模块,最后在 Unity 中通过生成的 C# 客户端绑定建立连接、订阅全表数据。读完本篇,你将掌握 "客户端工程 +spacetimedb模块目录" 的标准项目布局、表与 reducer 的核心写法,以及 Unity 客户端连接数据库的完整链路,为后续游戏玩法开发打好地基。
本教程所属完整游戏的成品源码位于仓库的 demo/Blackholio 目录,其中 server-rust、server-csharp、server-cpp 与 client-unity 分别对应本教程各步骤的最终形态,可随时对照查阅。
项目结构:客户端与服务器模块的目录约定
完成 Part 1 后,你的 Unity 项目blackholio目录大致如下:
blackholio/ # Unity 项目目录 ├── Assembly-CSharp.csproj ├── Assets/ │ └── module_bindings/ # 客户端与模块通信的逻辑(本教程稍后生成) ├── Library/ ├── ... # 其余 Unity 文件 └── blackholio-server/ └── spacetimedb/ # 服务器模块所在位置两条关键约定:
- 无论选择哪种服务器语言,模块代码总是放在客户端目录下的
spacetimedb目录中。这也是spacetimeCLI 各类子命令(dev、publish、generate等)寻找模块的默认位置,例如 crates/cli/src/subcommands/init.rs 中创建配置时会自动写入"module-path": "./spacetimedb"。 module_bindings目录可以放在 Unity 工程Assets/下的任意位置,只要在Assets/内部即可;我们将在后续步骤用spacetime generate生成它。
创建服务器模块
在包含blackholio项目的同一级目录中(即blackholio-server要落位的父目录),运行spacetime init初始化服务器模块。若尚未安装spacetimeCLI,请先参照 Getting Started 完成安装。
spacetime init的核心参数(与 crates/cli/src/subcommands/init.rs 中的 clap 定义一一对应):
| 参数 | 说明 |
|---|---|
--lang <LANG> | 服务器语言:rust、csharp、typescript、cpp(本教程演示前三种) |
--server-only | 仅从模板初始化服务器端(不生成客户端),此时允许目标目录非空(如已有 Unity 工程) |
--project-path <PATH> | 项目创建目录,默认./<PROJECT_NAME> |
-t, --template <TEMPLATE> | 指定模板 ID 或 GitHub 仓库,不指定时配合--lang生成空模块 |
--local | 使用本地部署而非 Maincloud |
--non-interactive | 非交互模式,参数全部走命令行 |
交互模式下 CLI 会依次询问 project name、project path(提示默认./blackholio-server)与 database name(提示默认blackholio-<随机后缀>)。在非交互模式下,则必须显式提供--lang或--template。若目标路径非空且没有--server-only,init会拒绝创建;而在--server-only模式下,只要目录里不存在已初始化的spacetimedb子目录,就会允许在非空目录(例如刚建好的 Unity 工程)内落盘——这正是本教程场景所需的校验逻辑(见 init.rs)。
C#(推荐与 Unity 同语言)
spacetime init --lang csharp --server-only blackholio交互提示中,为 project path 输入blackholio-server,为 database name 输入blackholio。该命令会在 Unity 项目blackholio目录内创建一个名为blackholio-server的新文件夹,其内部包含存放 C# 服务器工程的spacetimedb文件夹。
Rust
spacetime init --lang rust --server-only blackholio同样使用blackholio-server作为 project path、blackholio作为 database name。生成的blackholio-server/spacetimedb内含 Rust 服务器工程(src/lib.rs、Cargo.toml)。
C++
spacetime init --lang cpp --server-only blackholio使用blackholio-server/blackholio。生成的blackholio-server/spacetimedb内含 C++ 服务器工程(src/lib.cpp、CMakeLists.txt)。注意 C++ 模块目前仍属较新支持,请留意仓库中 C++ 绑定(crates/bindings-cpp)与文档对应版本提示。
SpacetimeDB 表:在模块代码中定义数据库
初始化完成后,打开对应的模块入口文件:
- C#:
blackholio-server/spacetimedb/Lib.cs - Rust:
blackholio-server/spacetimedb/src/lib.rs - C++:
blackholio-server/spacetimedb/src/lib.cpp
重要:先清空该文件内容,我们将从头编写。
在 SpacetimeDB 中,表与普通关系型数据库的表类似、用于按行存储数据,但有两个显著差异:其一,它完全驻留内存、访问极快;其二,它直接定义在你的模块代码中,而不是写在 SQL 里。每个表的行为与约束都由一个普通的结构体(struct)加宏/属性声明驱动。
首先在文件顶部加入导入:
using SpacetimeDB; public static partial class Module { }use std::time::Duration; use spacetimedb::{rand::Rng, Identity, SpacetimeType, ReducerContext, ScheduleAt, Table, Timestamp};#include "spacetimedb.h" using namespace SpacetimeDB;定义 Config 单例表
先定义一个Config表,用来保存游戏世界的元数据(如世界尺寸)。因为把它当作单例使用,表中只会有一条id为 0 的记录:
// We're using this table as a singleton, so in this table // there will only be one element where the `id` is 0. [Table(Accessor = "config", Public = true)] public partial struct Config { [PrimaryKey] public int id; public long world_size; }// We're using this table as a singleton, so in this table // there only be one element where the `id` is 0. #[spacetimedb::table(accessor = config, public)] pub struct Config { #[primary_key] pub id: i32, pub world_size: i64, }// We're using this table as a singleton, so in this table // there will only be one element where the `id` is 0. struct Config { int32_t id; int64_t world_size; }; SPACETIMEDB_STRUCT(Config, id, world_size); SPACETIMEDB_TABLE(Config, config, Public); FIELD_PrimaryKey(config, id);逐行解读:
- 这定义了一个普通结构体,含
id与world_size两个字段。C# 通过[Table(Accessor = "config", Public = true)]属性、Rust 通过#[spacetimedb::table(...)]过程宏、C++ 通过SPACETIMEDB_TABLE宏,向 SpacetimeDB 声明"以该类型的字段为行类型创建一张新表"。 Accessor(Rust 中的accessor)是表的名称,也是在 SQL 中查询这张表时使用的名字;C++ 中则是SPACETIMEDB_TABLE的第二个参数。Public = true(Rustpublic,C++Public)表示这张表的行对所有人可见(会同步给订阅的客户端)。[PrimaryKey](Rust#[primary_key],C++FIELD_PrimaryKey)指定id字段作为主键。
本教程列名统一使用lower_snake_case,以便三种语言间保持一致;如果你更偏好camelCase或PascalCase也完全可行。
关于主键的语义:主键定义了行的"身份"。修改一行但不改动主键,视为一次更新(update);一旦主键改变,则等价于删除旧行并插入新行。
更多关于建表的内容(索引、约束、列类型等)参见 Tables 文档。
创建实体:SpacetimeType 与 Entity / Circle / Food 表
接下来定义一个SpacetimeType类型的DbVector2,用于在表中存储二维坐标。类型(Type)与表(Table)的区别在于:表会真正存储数据,而SpacetimeType只是允许你创建一个"以该类型为列"的新列类型——DbVector2因此只作为类型存在,不构成一张表。
// This allows us to store 2D points in tables. [Type] public partial struct DbVector2 { public float x; public float y; public DbVector2(float x, float y) { this.x = x; this.y = y; } }// This allows us to store 2D points in tables. #[derive(SpacetimeType, Clone, Debug)] pub struct DbVector2 { pub x: f32, pub y: f32, }// This allows us to store 2D points in tables. struct DbVector2 { float x; float y; }; SPACETIMEDB_STRUCT(DbVector2, x, y);接下来定义三张表示游戏内实体的表。第一张是entity表:实体代表游戏世界中的一个对象,出于便捷考虑,所有实体共享position与mass这两个公共字段:
[Table(Accessor = "entity", Public = true)] public partial struct Entity { [PrimaryKey, AutoInc] public int entity_id; public DbVector2 position; public int mass; } [Table(Accessor = "circle", Public = true)] public partial struct Circle { [PrimaryKey] public int entity_id; [SpacetimeDB.Index.BTree] public int player_id; public DbVector2 direction; public float speed; public Timestamp last_split_time; } [Table(Accessor = "food", Public = true)] public partial struct Food { [PrimaryKey] public int entity_id; }#[spacetimedb::table(accessor = entity, public)] #[derive(Debug, Clone)] pub struct Entity { // The `auto_inc` attribute indicates to SpacetimeDB that // this value should be determined by SpacetimeDB on insert. #[auto_inc] #[primary_key] pub entity_id: i32, pub position: DbVector2, pub mass: i32, } #[spacetimedb::table(accessor = circle, public)] pub struct Circle { #[primary_key] pub entity_id: i32, #[index(btree)] pub player_id: i32, pub direction: DbVector2, pub speed: f32, pub last_split_time: Timestamp, } #[spacetimedb::table(accessor = food, public)] pub struct Food { #[primary_key] pub entity_id: i32, }struct Entity { // The `FIELD_PrimaryKeyAutoInc` constraint indicates to SpacetimeDB that // this value should be determined by SpacetimeDB on insert. int32_t entity_id; DbVector2 position; int32_t mass; }; SPACETIMEDB_STRUCT(Entity, entity_id, position, mass); SPACETIMEDB_TABLE(Entity, entity, Public); FIELD_PrimaryKeyAutoInc(entity, entity_id); struct Circle { int32_t entity_id; int32_t player_id; DbVector2 direction; float speed; Timestamp last_split_time; }; SPACETIMEDB_STRUCT(Circle, entity_id, player_id, direction, speed, last_split_time); SPACETIMEDB_TABLE(Circle, circle, Public); FIELD_PrimaryKey(circle, entity_id); FIELD_Index(circle, player_id); struct Food { int32_t entity_id; }; SPACETIMEDB_STRUCT(Food, entity_id); SPACETIMEDB_TABLE(Food, food, Public); FIELD_PrimaryKey(food, entity_id);设计要点:
entity表使用[AutoInc](Rust#[auto_inc]、C++FIELD_PrimaryKeyAutoInc)声明:entity_id由 SpacetimeDB 在插入时自动分配,客户端无需自己生成。- 可以通过"新建带额外字段、并以
entity_id引用entity表某一行"的方式,衍生出不同类型的实体。本教程创建了两种:Food与Circle。 Food除entity表的公共字段外没有额外字段,因此food表只是"一组应当被识别为食物的entity_id"的集合。Circle代表由玩家控制的实体,因此额外携带player_id(归属玩家)、direction(朝向)、speed(速度)与last_split_time(上次分裂时间)。player_id列上还挂了[SpacetimeDB.Index.BTree](Rust#[index(btree)]、C++FIELD_Index)索引,为后续按玩家过滤圆环提供高效查询。
表示玩家:Player 表与 Unique / AutoInc
再创建一张Player表存储玩家数据:
[Table(Accessor = "player", Public = true)] public partial struct Player { [PrimaryKey] public Identity identity; [Unique, AutoInc] public int player_id; public string name; }#[spacetimedb::table(accessor = player, public)] #[derive(Debug, Clone)] pub struct Player { #[primary_key] identity: Identity, #[unique] #[auto_inc] player_id: i32, name: String, }struct Player { Identity identity; int32_t player_id; std::string name; }; SPACETIMEDB_STRUCT(Player, identity, player_id, name); SPACETIMEDB_TABLE(Player, player, Public); FIELD_PrimaryKey(player, identity); FIELD_UniqueAutoInc(player, player_id);这里引入两个新概念:
[Unique](Rust#[unique]):为player_id加上唯一约束,保证player表中任意两行的player_id互不相同。[AutoInc](Rust#[auto_inc]):该字段由 SpacetimeDB 在插入时自动赋一个自增的值;C++ 中FIELD_UniqueAutoInc将唯一约束与自增合成一个宏。
此外identity字段使用Identity类型——它是 SpacetimeDB 用来唯一标识并认证用户(客户端连接者)的标识符。它与后续 reducer 中ctx.Sender(Rust 为ctx.sender())对应,是"哪个用户执行了这次操作"的依据。
编写第一个 Reducer
reducer是模块中可以被客户端远程调用的函数。"Reducer" 一词由 Clockwork Labs 提出:一个函数执行时,会把一组 insert / delete 操作"归约"进数据库状态(该概念源自函数式编程,与 React Redux 中的 reducer 理念相近)。reducer 可以通过 CLI、客户端 SDK 远程调用,也可以由其他 reducer 调度在未来的某个时刻执行。
关键特性:所有 reducer 都以事务(transactional)且原子(atomic)的方式执行。从 reducer 内部看,所有变更似乎立即生效;但从外部看,只有 reducer 成功完成后变更才会真正落入数据库。一旦 reducer 返回错误或发生 panic,本次执行的所有数据库变更都会被整体回滚,仿佛函数从未被调用过——对于复杂应用而言,这一保证的价值无可替代。
先写一个最简的调试用 reducer,它不更新任何表,只是打印调用者的Identity:
[Reducer] public static void Debug(ReducerContext ctx) { Log.Info($"This reducer was called by {ctx.Sender}"); }#[spacetimedb::reducer] pub fn debug(ctx: &ReducerContext) -> Result<(), String> { log::debug!("This reducer was called by {}.", ctx.sender()); Ok(()) }SPACETIMEDB_REDUCER(debug, ReducerContext ctx) { LOG_INFO("This reducer was called by " + ctx.sender().to_string()); return Ok(); }ReducerContext(Rust 中为&ReducerContext)是 reducer 的上下文入口,提供Sender(调用者身份)、数据库句柄Db(Rust 为db)、随机数发生器Rng、时间戳等能力,后续教程中会频繁使用。
发布模块并调用 reducer
在新开的终端窗口中启动本地 SpacetimeDB 实例:
spacetime start看到以下日志即表示本地实例已成功运行:
Starting SpacetimeDB listening on 127.0.0.1:3000然后在另一个终端进入blackholio-server/spacetimedb目录:
cd blackholio-server/spacetimedb若尚未登录spacetimeCLI,先执行登录:
spacetime login两种登录方式说明:通过
spacetime login走 GitHub 登录时,拿到的 token 由auth.spacetimedb.com签发,身份可恢复;若执行spacetime login --server-issued-login local,则身份由本地服务器直接签发——这类 token 一旦丢失无法找回,且只被签发它的服务器认可。该参数定义可参见 crates/cli/src/subcommands/login.rs。
登录后发布模块:
spacetime publish --server local blackholio发布成功时日志类似:
Build finished successfully. Uploading to local => http://127.0.0.1:3000 Publishing module... Created new database with name: blackholio, identity: c200d2c69b4524292b91822afac8ab016c15968ac993c28711f68c6bc40b89d5调用刚定义的debugreducer:
spacetime call --server local blackholio debug该命令成功时无输出。查看服务器日志验证 reducer 是否被调用:
spacetime logs --server local blackholio应能看到类似输出(首次发布时还会列出各表的创建记录):
2025-01-09T16:08:38.144299Z INFO: spacetimedb: Creating table `circle` 2025-01-09T16:08:38.144438Z INFO: spacetimedb: Creating table `config` 2025-01-09T16:08:38.144451Z INFO: spacetimedb: Creating table `entity` 2025-01-09T16:08:38.144470Z INFO: spacetimedb: Creating table `food` 2025-01-09T16:08:38.144479Z INFO: spacetimedb: Creating table `player` 2025-01-09T16:08:38.144841Z INFO: spacetimedb: Database initialized 2025-01-09T16:08:47.306823Z INFO: src/lib.rs:68: This reducer was called by c200e1a6494dbeeb0bbf49590b8778abf94fae4ea26faf9769c9a8d69a3ec348.至此,服务器端的"定义表 → 定义 reducer → 发布 → 调用"闭环已经跑通。完整游戏的服务器端在本仓库中有多种语言版本可对照:server-rust、server-csharp、server-cpp 与 server-ts。
连接客户端:把 debug 改成 Connect 事件 reducer
接下来让客户端能够连上数据库。先把debugreducer 重命名并改造为连接事件 reducer:在 reducer 声明后附加事件类型参数ReducerKind.ClientConnected(Rust 为client_connected,C++ 为SPACETIMEDB_CLIENT_CONNECTED宏):
[Reducer(ReducerKind.ClientConnected)] public static void Connect(ReducerContext ctx) { Log.Info($"{ctx.Sender} just connected."); }#[spacetimedb::reducer(client_connected)] pub fn connect(ctx: &ReducerContext) -> Result<(), String> { log::debug!("{} just connected.", ctx.sender()); Ok(()) }SPACETIMEDB_CLIENT_CONNECTED(connect, ReducerContext ctx) { LOG_INFO(ctx.sender().to_string() + " just connected."); return Ok(); }ReducerKind.ClientConnected告诉 SpacetimeDB:这是一个特殊 reducer,只会由 SpacetimeDB 自身在客户端连接到数据库时触发,而不会被客户端直接调用。
SpacetimeDB 支持定义在特定事件发生时自动触发的自定义 reducer,三种事件如下:
| 事件 | C# | Rust | C++ | 触发时机 |
|---|---|---|---|---|
| 初始化 | ReducerKind.Init | init | SPACETIMEDB_INIT | 首次发布模块、以及每次用spacetime publish --server local <name> --delete-data清空数据时 |
| 客户端连接 | ReducerKind.ClientConnected | client_connected | SPACETIMEDB_CLIENT_CONNECTED | 用户连上数据库,其身份位于ReducerContext.Sender(Rustctx.sender()) |
| 客户端断开 | ReducerKind.ClientDisconnected | client_disconnected | SPACETIMEDB_CLIENT_DISCONNECTED | 用户断开与数据库的连接 |
重新发布模块使改动生效:
spacetime publish --server local blackholio生成客户端代码:spacetime generate
spacetimeCLI 内置了代码生成能力,可以根据模块中定义的表、类型与 reducer,生成可在 Unity 客户端直接使用的 C# 类型。在blackholio-server/spacetimedb目录中运行:
spacetime generate --lang csharp --out-dir ../../Assets/module_bindings--lang指定目标语言(csharp),--out-dir指定输出目录(指向 Unity 工程Assets/module_bindings);这些参数在 crates/cli/src/subcommands/generate.rs 的配置 schema 中均有定义,且 CLI 参数优先于配置文件中的generate条目。生成完成后,module_bindings目录内会出现一组与模块定义一一对应的文件:
├── Reducers ├── Tables │ ├── Circle.g.cs │ ├── Config.g.cs │ ├── Entity.g.cs │ ├── Food.g.cs │ └── Player.g.cs ├── Types │ ├── Circle.g.cs │ ├── Config.g.cs │ ├── DbVector2.g.cs │ ├── Entity.g.cs │ ├── Food.g.cs │ └── Player.g.cs └── SpacetimeDBClient.g.cs其中Assets/module_bindings/SpacetimeDBClient.g.cs包含一个类型感知的DbConnection类,我们将用它在 Unity 中建立与数据库的连接。
已知问题提醒:生成后 Unity 工程可能会因为 C# 9 的兼容性问题报错。需要将以下代码片段插入 Unity 工程(例如追加到
GameManager.cs文件末尾),这是 Unity 编译器尚未支持IsExternalInit的临时规避方案:namespace System.Runtime.CompilerServices { internal static class IsExternalInit { } }
在 Unity 中连接数据库
现在在 Unity 侧完成连接。首先替换GameManager.cs顶部的 using:
using System; using System.Collections; using System.Collections.Generic; using SpacetimeDB; using SpacetimeDB.Types; using UnityEngine;再将GameManager类整体替换为以下实现(本仓库 demo/Blackholio/client-unity/Assets/Scripts/GameManager.cs 中的完整版本可作对照):
public class GameManager : MonoBehaviour { const string SERVER_URL = "http://127.0.0.1:3000"; const string DATABASE_NAME = "blackholio"; public static event Action OnConnected; public static event Action OnSubscriptionApplied; public float borderThickness = 2; public Material borderMaterial; public static GameManager Instance { get; private set; } public static Identity LocalIdentity { get; private set; } public static DbConnection Conn { get; private set; } private void Start() { Instance = this; Application.targetFrameRate = 60; // 要建立与 SpacetimeDB 的连接,需要注册回调 // 并指定 SpacetimeDB 服务器 URI 与数据库名。 var builder = DbConnection.Builder() .OnConnect(HandleConnect) .OnConnectError(HandleConnectError) .OnDisconnect(HandleDisconnect) .WithUri(SERVER_URL) .WithDatabaseName(DATABASE_NAME); // 如果 PlayerPrefs 中存有 SpacetimeDB 认证 token, // 用它来认证本次连接。 if (AuthToken.Token != "") { builder = builder.WithToken(AuthToken.Token); } // Build 连接即会建立与 SpacetimeDB 服务器的连接。 Conn = builder.Build(); } // 连上 SpacetimeDB 并拿到客户端身份后回调 void HandleConnect(DbConnection _conn, Identity identity, string token) { Debug.Log("Connected."); // 只有 WebGL 播放器受浏览器 WebSocket 请求头限制。 // Unity Editor 即使构建目标为 WebGL 也走桌面传输, // 因此保持常规行为。 #if UNITY_WEBGL && !UNITY_EDITOR if (AuthToken.Token == "") { // 连接未携带 token,说明这是服务器为新身份签发的长生命周期 token,保存它。 // 若已有 token,本次连接可能用的是短生命周期 WebSocket token,不应覆盖。 AuthToken.SaveToken(token); } #else AuthToken.SaveToken(token); #endif LocalIdentity = identity; OnConnected?.Invoke(); // 订阅全部表 Conn.SubscriptionBuilder() .OnApplied(HandleSubscriptionApplied) .SubscribeToAllTables(); } void HandleConnectError(Exception ex) { Debug.LogError($"Connection error: {ex}"); } void HandleDisconnect(DbConnection _conn, Exception ex) { Debug.Log("Disconnected."); if (ex != null) { Debug.LogException(ex); } } private void HandleSubscriptionApplied(SubscriptionEventContext ctx) { Debug.Log("Subscription applied!"); OnSubscriptionApplied?.Invoke(); } public static bool IsConnected() { return Conn != null && Conn.IsActive; } public void Disconnect() { Conn.Disconnect(); Conn = null; } }这段代码做了四件事:
- 构建连接:通过
DbConnection.Builder()链式注册连接/错误/断开三个回调,并用WithUri(SERVER_URL)与WithDatabaseName(DATABASE_NAME)指定服务器地址(本地实例为http://127.0.0.1:3000)与数据库名(blackholio);若本地已存有认证 token 则通过WithToken附加。最后Build()立即发起连接。 - 处理连接成功:
HandleConnect在连接建立并拿到客户端Identity后被 SDK 调用;它保存 token、记录LocalIdentity、广播OnConnected事件,并构建订阅。 - 订阅全表数据:
SubscriptionBuilder().OnApplied(...).SubscribeToAllTables()让 SpacetimeDB 将当前数据库的全部表状态同步到 Unity 客户端的本地缓存。 - 处理订阅完成:
HandleSubscriptionApplied在初始同步完成、本地缓存与服务器表状态一致后触发,可作为"数据已就绪、可以开始渲染游戏"的信号。
WebGL 注意:浏览器 WebSocket API 无法设置
Authorization请求头,因此用已保存的服务器签发 token 重连时,HandleConnect中拿到的是短生命周期 WebSocket token。上述#if UNITY_WEBGL守卫可以避免重连时用短生命周期 token 覆盖掉原始的长期 token(Unity Editor 不受此限制)。
SDK Client Cache(客户端缓存)
"SDK client cache" 是数据库在客户端侧的视图,由传给Subscribe的查询决定。SpacetimeDB 会保证订阅查询的结果在数据变化时自动更新并推送到客户端缓存,客户端据此进行高效访问,无需反复向服务器发起查询。这正是多人游戏客户端"数据跟随服务器实时变化"的底层机制。
验证连接
在 Unity 中点击 Play。一切正常时 Unity 日志应输出:
SpacetimeDBClient: Connecting to ws://127.0.0.1:3000 blackholio Connected. Subscription applied!其中Subscription applied!表示 SDK 已执行订阅查询,并把本地缓存与数据库各表同步完成。服务器侧同样能观测到本次连接:
spacetime logs --server local blackholio ... 2025-01-10T03:51:02.078700Z DEBUG: src/lib.rs:63: c200fb5be9524bfb8289c351516a1d9ea800f70a17a9a6937f11c0ed3854087d just connected.下一步
至此你已经完成了:在 Unity 工程中搭建 SpacetimeDB SDK 环境(Part 1);编写包含表与 reducer 的 SpacetimeDB 服务器模块;启动并发布本地数据库;通过 CLI 调用 reducer、查看日志;用spacetime generate生成 Unity 可用的客户端绑定;并在 Unity 中建立连接、完成全表订阅。这套"服务器模块 + 客户端缓存同步"的骨架,正是后续一切玩法开发的基础。
在 Part 3 中,我们将在此基础上搭建游戏核心玩法——你将学习如何在 Unity 中读写表数据、调用 reducer,以及如何用事件驱动的方式驱动游戏循环。
【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考