认识 visionOS 的 TabletopKit
Meet TabletopKit for visionOS
2024年6月10日
一句话判断
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 的典型案例,可以作为参考实现。