Meet TabletopKit for visionOS
Spatial Computing 进阶 20m

认识 visionOS 的 TabletopKit

Meet TabletopKit for visionOS

2024年6月10日

在 Apple 官方观看视频

一句话判断

TabletopKit 是 Apple 为 visionOS 推出的桌面游戏专用框架,把棋盘布局、手势识别、物理模拟和多人联网四个最难的环节全部封装好了,让开发者可以专注在游戏逻辑和视觉效果上。

这场 Session 讲了什么

Apple 在 visionOS 上推出了一个全新的框架 TabletopKit,专门用于构建桌面游戏(board games、card games、dice games)。这个框架的核心卖点是”处理掉所有无聊的部分”——你描述桌子的形状和尺寸、定义座位、摆放棋子和卡牌,框架自动处理 pinch-drag 手势、碰撞检测、多人同步。

TabletopKit 和 RealityKit 深度集成,3D 渲染和物理效果通过 RealityKit entity 来实现。多人联网基于 GroupActivities 框架(就是 SharePlay 那套),添加一个 TabletopGame activity 类型就能把单机游戏变成多人游戏,不需要自己写任何网络代码。

Session 通过一个完整的示例游戏展示了从桌子和座位的定义、到 Equipment 的摆放、到交互逻辑的编写、到视觉效果和音效的添加、再到多人联网的全过程。

值得深挖的点

Equipment 抽象:用声明式描述代替命令式布局

TabletopKit 的核心抽象是 Equipment——桌上的一切都是 Equipment。棋子是 Equipment,卡牌是 Equipment,骰子是 Equipment,棋盘上的格子也是 Equipment。每个 Equipment 都有初始位置(相对桌子或父 Equipment 的坐标系)、所属关系(哪个座位的玩家能操作它)和物理属性。

这个设计厉害的地方在于它的层级关系。棋盘格子(tile)的 parent 是棋盘(board),格子的位置是在棋盘的坐标系里描述的。当你移动棋盘时,所有格子自动跟着走。这比手动计算每个格子的绝对位置要靠谱得多。

EntityEquipment 协议将 Equipment 和 RealityKit Entity 绑定,框架从 Entity 的 bounding box 自动推断物理尺寸。如果你不需要渲染(比如棋盘格子只是逻辑上的占位),可以用 Equipment 协议并手动提供 bounding box。这种”有实体就用实体,没实体就手动声明”的灵活性很实用。

手势到交互的映射:框架替你做了最脏的活

在 visionOS 上实现拖拽棋子听起来简单——监听 pinch 手势,把手的位置映射到棋子位置——但实际上要处理很多边界情况:棋子不能穿过棋盘、放下时需要对齐到格子、不能把别人的棋子拿走、两个人同时操作不能冲突。

TabletopKit 把这些全部封装成了一个 interaction callback。你只需要在回调里判断”这个手势是不是合法的”并追加 action,框架负责把手势轨迹转换成 3D 空间中的平滑移动、碰撞处理和最终落点。

手势阶段分两层:gesturePhase 跟踪用户的 pinch 动作,interactionPhase 跟踪 TabletopKit 层面的交互生命周期。一个 toss 骰子的手势可能在用户松手后还要等骰子落地才算结束——这种”手势结束不等于交互结束”的场景,框架处理得很好。

代码片段

定义桌子和座位

场景:创建一个三人桌游的桌面和座位布局。

// 矩形桌子,框架从 entity 自动推断尺寸
let table = TableShapeRectangle(entity: tableEntity)

// 三个座位均匀分布在桌子周围,面向桌子中心
let seats = (0..<3).map { i in
    SeatInfo(
        id: i,
        position: // 均匀分布在桌子边缘,
        orientation: .lookAt(tableCenter)
    )
}

坑:座位方向用 .lookAt 比手动算 rotation 简单得多,别自己造轮子。

定义棋子 Equipment

场景:每个玩家有一个棋子,初始放在自己面前。

struct Pawn: EntityEquipment {
    let id: PawnID
    let seatID: SeatID
    let entity: Entity

    var initialState: EquipmentState<PawnID> {
        .init(
            // 只有对应座位的玩家能移动这个棋子
            seatControl: .only([seatID]),
            // 初始位置:对应座位的前方
            location: .init(parent: tableID, position: seatPosition),
            entity: entity
        )
    }
}

坑:seatControl 设置为 .only 可以防止其他玩家移动你的棋子,别忘了设。

添加多人联网

场景:把单机桌游变成 SharePlay 多人游戏。

// 在 GroupActivities activity 里声明 TabletopGame
struct MyGameActivity: TabletopGame {
    var table: Table { myTable }
    var seats: [Seat] { mySeats }
    // ...
}

// 只需要用 GroupSession 的方式启动,
// TabletopKit 自动处理状态同步

坑:多人模式下,未入座的用户是旁观者,不能和桌面上的物体交互。

最佳实践

新项目:TabletopKit 是 visionOS 桌面游戏的事实标准,没有理由不用。先把桌子和 Equipment 的数据模型定义清楚,再写交互逻辑。视觉效果和音效放到最后加——框架提供的默认手势处理已经足够跑通一个可玩的原型。多人联网建议从第一天就纳入设计,用 GroupActivities 的 API 来测试,不要等到单机完成后再加。

已有项目:如果你已经用纯 RealityKit 构建了桌游,迁移到 TabletopKit 的主要收益是免费获得手势处理和多人同步。评估一下你的手势代码有多复杂——如果超过 200 行,迁移到 TabletopKit 大概率是值得的。视觉效果部分可以保留,TabletopKit 不影响你对 RealityKit entity 的渲染控制。

还有什么值得关注

  • 桌面可以是圆形(圆形桌)或矩形(矩形桌),形状影响座位的默认分布和物体的移动边界。
  • 旁观者机制:未入座的人可以看到游戏但无法交互,适合直播或观战场景。
  • Apple 提到了 Game Room 里的 Solitaire 是 TabletopKit 的典型案例,可以作为参考实现。
WWDC 2024