mcpField
U
unity

unity

github.com/clockworklabs开发25.4k52 次安装今天更新
代码仓库

关于 unity

介绍如何在 Unity 中使用 SpacetimeDB C# SDK 连接游戏,重点涵盖 Unity 生命周期、主线程更新、游戏状态回调和重连处理。

主要功能

  1. 01通过 Unity Package Manager 安装 SDK
  2. 02生成 C# 模块绑定
  3. 03使用 MonoBehaviour 管理连接生命周期
  4. 04在 Update() 中推进连接
  5. 05将行事件映射到游戏对象
  6. 06配置重连和令牌持久化

适用场景

  1. 01将 Unity 游戏连接到 SpacetimeDB
  2. 02在表行变化时更新游戏对象
  3. 03在场景切换期间维持连接
  4. 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() 会忽略待处理的提供程序结果,但不会取消提供程序自身的异步操作。

评论

支持 Markdown