tabletopkit

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TabletopKit

TabletopKit

Create multiplayer spatial board games on a virtual table surface using TabletopKit. Handles game layout, equipment interaction, player seating, turn management, state synchronization, and RealityKit rendering. visionOS 2.0+ only. Targets Swift 6.3.
使用TabletopKit在虚拟桌面创建多人空间桌游。它提供游戏布局、设备交互、玩家座位、回合管理、状态同步和RealityKit渲染能力。仅支持visionOS 2.0+ 版本,适配Swift 6.3。

Contents

目录

Setup

配置

Platform Requirement

平台要求

TabletopKit is exclusive to visionOS. It requires visionOS 2.0+. Multiplayer features using Group Activities require visionOS 2.0+ devices on a FaceTime call. The Simulator supports single-player layout testing but not multiplayer.
TabletopKit为visionOS专属框架,需要visionOS 2.0及以上版本。使用Group Activities的多人功能需要运行visionOS 2.0+的设备处于FaceTime通话中。模拟器仅支持单人布局测试,不支持多人功能。

Project Configuration

项目配置

  1. import TabletopKit
    in source files that define game logic.
  2. import RealityKit
    for entity-based rendering.
  3. For multiplayer, add the Group Activities capability in Signing & Capabilities.
  4. Provide 3D assets (USDZ) in a RealityKit content bundle for tables, pieces, cards, and dice.
  1. 在定义游戏逻辑的源文件中
    import TabletopKit
  2. import RealityKit
    以实现基于实体的渲染
  3. 如需多人功能,需在「签名与功能」中添加Group Activities能力
  4. 在RealityKit内容包中提供桌面、棋子、卡牌、骰子的3D资源(USDZ格式)

Key Types Overview

核心类型概览

TypeRole
TabletopGame
Central game manager; owns setup, actions, observers, rendering
TableSetup
Configuration object passed to
TabletopGame
init
Tabletop
/
EntityTabletop
Protocol for the table surface
Equipment
/
EntityEquipment
Protocol for interactive game pieces
TableSeat
/
EntityTableSeat
Protocol for player seat positions
TabletopAction
Commands that modify game state
TabletopInteraction
Gesture-driven player interactions with equipment
TabletopGame.Observer
Callback protocol for reacting to confirmed actions
TabletopGame.RenderDelegate
Callback protocol for visual updates
EntityRenderDelegate
RealityKit-specific render delegate
TypeRole
TabletopGame
核心游戏管理器,负责配置、动作、观察者、渲染逻辑
TableSetup
传入
TabletopGame
初始化方法的配置对象
Tabletop
/
EntityTabletop
桌面的协议定义
Equipment
/
EntityEquipment
可交互游戏道具的协议定义
TableSeat
/
EntityTableSeat
玩家座位位置的协议定义
TabletopAction
修改游戏状态的指令
TabletopInteraction
玩家对道具的手势驱动交互
TabletopGame.Observer
响应已确认动作的回调协议
TabletopGame.RenderDelegate
处理视觉更新的回调协议
EntityRenderDelegate
专属RealityKit的渲染委托

Game Configuration

游戏配置

Build a game in three steps: define the table, configure the setup, create the
TabletopGame
instance.
swift
import TabletopKit
import RealityKit

let table = GameTable()
var setup = TableSetup(tabletop: table)
setup.add(seat: PlayerSeat(index: 0, pose: seatPose0))
setup.add(seat: PlayerSeat(index: 1, pose: seatPose1))
setup.add(equipment: GamePawn(id: .init(1)))
setup.add(equipment: GameDie(id: .init(2)))
setup.register(action: MyCustomAction.self)

let game = TabletopGame(tableSetup: setup)
game.claimAnySeat()
Call
update(deltaTime:)
each frame if automatic updates are not enabled via the
.tabletopGame(_:parent:automaticUpdate:)
modifier. Read state safely with
withCurrentSnapshot(_:)
.
构建游戏分三步:定义桌面、完成配置、创建
TabletopGame
实例。
swift
import TabletopKit
import RealityKit

let table = GameTable()
var setup = TableSetup(tabletop: table)
setup.add(seat: PlayerSeat(index: 0, pose: seatPose0))
setup.add(seat: PlayerSeat(index: 1, pose: seatPose1))
setup.add(equipment: GamePawn(id: .init(1)))
setup.add(equipment: GameDie(id: .init(2)))
setup.register(action: MyCustomAction.self)

let game = TabletopGame(tableSetup: setup)
game.claimAnySeat()
如果未通过
.tabletopGame(_:parent:automaticUpdate:)
修饰器开启自动更新,需要每帧调用
update(deltaTime:)
。可通过
withCurrentSnapshot(_:)
安全读取状态。

Table and Board

桌面与棋盘

Tabletop Protocol

Tabletop协议

Conform to
EntityTabletop
to define the playing surface. Provide a
shape
(round or rectangular) and a RealityKit
Entity
for visual representation.
swift
struct GameTable: EntityTabletop {
    var shape: TabletopShape
    var entity: Entity
    var id: EquipmentIdentifier

    init() {
        entity = try! Entity.load(named: "table/game_table", in: contentBundle)
        shape = .round(entity: entity)
        id = .init(0)
    }
}
遵循
EntityTabletop
协议定义游戏桌面,需要提供
shape
(圆形或矩形)和用于视觉展示的RealityKit
Entity
swift
struct GameTable: EntityTabletop {
    var shape: TabletopShape
    var entity: Entity
    var id: EquipmentIdentifier

    init() {
        entity = try! Entity.load(named: "table/game_table", in: contentBundle)
        shape = .round(entity: entity)
        id = .init(0)
    }
}

Table Shapes

桌面形状

Use factory methods on
TabletopShape
:
swift
// Round table from dimensions
let round = TabletopShape.round(
    center: .init(x: 0, y: 0, z: 0),
    radius: 0.5,
    thickness: 0.05,
    in: .meters
)

// Rectangular table from entity
let rect = TabletopShape.rectangular(entity: tableEntity)
使用
TabletopShape
的工厂方法创建:
swift
// Round table from dimensions
let round = TabletopShape.round(
    center: .init(x: 0, y: 0, z: 0),
    radius: 0.5,
    thickness: 0.05,
    in: .meters
)

// Rectangular table from entity
let rect = TabletopShape.rectangular(entity: tableEntity)

Equipment (Pieces, Cards, Dice)

游戏道具(棋子、卡牌、骰子)

Equipment Protocol

Equipment协议

All interactive game objects conform to
Equipment
(or
EntityEquipment
for RealityKit-rendered pieces). Each piece has an
id
(
EquipmentIdentifier
) and an
initialState
property.
Choose the state type based on the equipment:
State TypeUse Case
BaseEquipmentState
Generic pieces, pawns, tokens
CardState
Playing cards (tracks
faceUp
/ face-down)
DieState
Dice with an integer
value
RawValueState
Custom data encoded as
UInt64
所有可交互游戏对象都遵循
Equipment
协议(RealityKit渲染的道具遵循
EntityEquipment
协议)。每个道具都有
id
EquipmentIdentifier
类型)和
initialState
属性。根据道具类型选择对应的状态类型:
State TypeUse Case
BaseEquipmentState
通用道具、棋子、代币
CardState
扑克牌类道具(可跟踪
faceUp
/面朝下状态)
DieState
带有整数
value
属性的骰子
RawValueState
编码为
UInt64
的自定义数据

Defining Equipment

定义道具

swift
// Pawn -- uses BaseEquipmentState
struct GamePawn: EntityEquipment {
    var id: EquipmentIdentifier
    var initialState: BaseEquipmentState
    var entity: Entity

    init(id: EquipmentIdentifier) {
        self.id = id
        self.entity = try! Entity.load(named: "pieces/pawn", in: contentBundle)
        self.initialState = BaseEquipmentState(
            parentID: .init(0), seatControl: .any,
            pose: .identity, entity: entity
        )
    }
}

// Card -- uses CardState (tracks faceUp)
struct PlayingCard: EntityEquipment {
    var id: EquipmentIdentifier
    var initialState: CardState
    var entity: Entity

    init(id: EquipmentIdentifier) {
        self.id = id
        self.entity = try! Entity.load(named: "cards/card", in: contentBundle)
        self.initialState = .faceDown(
            parentID: .init(0), seatControl: .any,
            pose: .identity, entity: entity
        )
    }
}

// Die -- uses DieState (tracks integer value)
struct GameDie: EntityEquipment {
    var id: EquipmentIdentifier
    var initialState: DieState
    var entity: Entity

    init(id: EquipmentIdentifier) {
        self.id = id
        self.entity = try! Entity.load(named: "dice/d6", in: contentBundle)
        self.initialState = DieState(
            value: 1, parentID: .init(0), seatControl: .any,
            pose: .identity, entity: entity
        )
    }
}
swift
// Pawn -- uses BaseEquipmentState
struct GamePawn: EntityEquipment {
    var id: EquipmentIdentifier
    var initialState: BaseEquipmentState
    var entity: Entity

    init(id: EquipmentIdentifier) {
        self.id = id
        self.entity = try! Entity.load(named: "pieces/pawn", in: contentBundle)
        self.initialState = BaseEquipmentState(
            parentID: .init(0), seatControl: .any,
            pose: .identity, entity: entity
        )
    }
}

// Card -- uses CardState (tracks faceUp)
struct PlayingCard: EntityEquipment {
    var id: EquipmentIdentifier
    var initialState: CardState
    var entity: Entity

    init(id: EquipmentIdentifier) {
        self.id = id
        self.entity = try! Entity.load(named: "cards/card", in: contentBundle)
        self.initialState = .faceDown(
            parentID: .init(0), seatControl: .any,
            pose: .identity, entity: entity
        )
    }
}

// Die -- uses DieState (tracks integer value)
struct GameDie: EntityEquipment {
    var id: EquipmentIdentifier
    var initialState: DieState
    var entity: Entity

    init(id: EquipmentIdentifier) {
        self.id = id
        self.entity = try! Entity.load(named: "dice/d6", in: contentBundle)
        self.initialState = DieState(
            value: 1, parentID: .init(0), seatControl: .any,
            pose: .identity, entity: entity
        )
    }
}

ControllingSeats

座位权限控制

Restrict which players can interact with a piece via
seatControl
:
  • .any
    -- any player
  • .restricted([seatID1, seatID2])
    -- specific seats only
  • .current
    -- only the seat whose turn it is
  • .inherited
    -- inherits from parent equipment
通过
seatControl
限制可交互该道具的玩家:
  • .any
    -- 任意玩家
  • .restricted([seatID1, seatID2])
    -- 仅指定座位的玩家
  • .current
    -- 仅当前回合的玩家
  • .inherited
    -- 继承父道具的权限设置

Equipment Hierarchy and Layout

道具层级与布局

Equipment can be parented to other equipment. Override
layoutChildren(for:visualState:)
to position children. Return one of:
  • .planarStacked(layout:animationDuration:)
    -- cards/tiles stacked vertically
  • .planarOverlapping(layout:animationDuration:)
    -- cards fanned or overlapping
  • .volumetric(layout:animationDuration:)
    -- full 3D layout
See references/tabletopkit-patterns.md for card fan, grid, and overlap layout examples.
道具可以挂载到其他道具作为子节点。重写
layoutChildren(for:visualState:)
方法来定位子节点,可返回以下类型:
  • .planarStacked(layout:animationDuration:)
    -- 垂直堆叠的卡牌/ tile
  • .planarOverlapping(layout:animationDuration:)
    -- 扇形展开或重叠的卡牌
  • .volumetric(layout:animationDuration:)
    -- 全3D布局
查看references/tabletopkit-patterns.md获取扇形卡牌、网格、重叠布局示例。

Player Seats

玩家座位

Conform to
EntityTableSeat
and provide a pose around the table:
swift
struct PlayerSeat: EntityTableSeat {
    var id: TableSeatIdentifier
    var initialState: TableSeatState
    var entity: Entity

    init(index: Int, pose: TableVisualState.Pose2D) {
        self.id = TableSeatIdentifier(index)
        self.entity = Entity()
        self.initialState = TableSeatState(pose: pose, context: 0)
    }
}
Claim a seat before interacting:
game.claimAnySeat()
,
game.claimSeat(matching:)
, or
game.releaseSeat()
. Observe changes via
TabletopGame.Observer.playerChangedSeats
.
遵循
EntityTableSeat
协议并提供围绕桌面的位姿:
swift
struct PlayerSeat: EntityTableSeat {
    var id: TableSeatIdentifier
    var initialState: TableSeatState
    var entity: Entity

    init(index: Int, pose: TableVisualState.Pose2D) {
        self.id = TableSeatIdentifier(index)
        self.entity = Entity()
        self.initialState = TableSeatState(pose: pose, context: 0)
    }
}
交互前需要认领座位:
game.claimAnySeat()
game.claimSeat(matching:)
game.releaseSeat()
。可通过
TabletopGame.Observer.playerChangedSeats
监听座位变化。

Game Actions and Turns

游戏动作与回合

Built-in Actions

内置动作

Use
TabletopAction
factory methods to modify game state:
swift
// Move equipment to a new parent
game.addAction(.moveEquipment(matching: pieceID, childOf: targetID, pose: newPose))

// Flip a card face-up
game.addAction(.updateEquipment(card, faceUp: true))

// Update die value
game.addAction(.updateEquipment(die, value: 6))

// Set whose turn it is
game.addAction(.setTurn(matching: TableSeatIdentifier(1)))

// Update a score counter
game.addAction(.updateCounter(matching: counterID, value: 100))

// Create a state bookmark (for undo/reset)
game.addAction(.createBookmark(id: StateBookmarkIdentifier(1)))
使用
TabletopAction
工厂方法修改游戏状态:
swift
// Move equipment to a new parent
game.addAction(.moveEquipment(matching: pieceID, childOf: targetID, pose: newPose))

// Flip a card face-up
game.addAction(.updateEquipment(card, faceUp: true))

// Update die value
game.addAction(.updateEquipment(die, value: 6))

// Set whose turn it is
game.addAction(.setTurn(matching: TableSeatIdentifier(1)))

// Update a score counter
game.addAction(.updateCounter(matching: counterID, value: 100))

// Create a state bookmark (for undo/reset)
game.addAction(.createBookmark(id: StateBookmarkIdentifier(1)))

Custom Actions

自定义动作

For game-specific logic, conform to
CustomAction
:
swift
struct CollectCoin: CustomAction {
    let coinID: EquipmentIdentifier
    let playerID: EquipmentIdentifier

    init?(from action: some TabletopAction) {
        // Decode from generic action
    }

    func validate(snapshot: TableSnapshot) -> Bool {
        // Return true if action is legal
        true
    }

    func apply(table: inout TableState) {
        // Mutate state directly
    }
}
Register custom actions during setup:
swift
setup.register(action: CollectCoin.self)
如需游戏专属逻辑,遵循
CustomAction
协议:
swift
struct CollectCoin: CustomAction {
    let coinID: EquipmentIdentifier
    let playerID: EquipmentIdentifier

    init?(from action: some TabletopAction) {
        // Decode from generic action
    }

    func validate(snapshot: TableSnapshot) -> Bool {
        // Return true if action is legal
        true
    }

    func apply(table: inout TableState) {
        // Mutate state directly
    }
}
配置阶段注册自定义动作:
swift
setup.register(action: CollectCoin.self)

Score Counters

计分器

swift
setup.add(counter: ScoreCounter(id: .init(0), value: 0))
// Update: game.addAction(.updateCounter(matching: .init(0), value: 42))
// Read:   snapshot.counter(matching: .init(0))?.value
swift
setup.add(counter: ScoreCounter(id: .init(0), value: 0))
// Update: game.addAction(.updateCounter(matching: .init(0), value: 42))
// Read:   snapshot.counter(matching: .init(0))?.value

State Bookmarks

状态书签

Save and restore game state for undo/reset:
swift
game.addAction(.createBookmark(id: StateBookmarkIdentifier(1)))
game.jumpToBookmark(matching: StateBookmarkIdentifier(1))
保存和恢复游戏状态,用于撤销/重置功能:
swift
game.addAction(.createBookmark(id: StateBookmarkIdentifier(1)))
game.jumpToBookmark(matching: StateBookmarkIdentifier(1))

Interactions

交互处理

TabletopInteraction.Delegate

TabletopInteraction.Delegate

Return an interaction delegate from the
.tabletopGame
modifier to handle player gestures on equipment:
swift
.tabletopGame(game.tabletopGame, parent: game.renderer.root) { value in
    if game.tabletopGame.equipment(of: GameDie.self, matching: value.startingEquipmentID) != nil {
        return DieInteraction(game: game)
    }
    return DefaultInteraction(game: game)
}
.tabletopGame
修饰器返回交互委托,处理玩家对道具的手势操作:
swift
.tabletopGame(game.tabletopGame, parent: game.renderer.root) { value in
    if game.tabletopGame.equipment(of: GameDie.self, matching: value.startingEquipmentID) != nil {
        return DieInteraction(game: game)
    }
    return DefaultInteraction(game: game)
}

Handling Gestures and Tossing Dice

处理手势和掷骰子

swift
class DieInteraction: TabletopInteraction.Delegate {
    let game: Game

    func update(interaction: TabletopInteraction) {
        switch interaction.value.phase {
        case .started:
            interaction.setConfiguration(.init(allowedDestinations: .any))
        case .update:
            if interaction.value.gesture?.phase == .ended {
                interaction.toss(
                    equipmentID: interaction.value.controlledEquipmentID,
                    as: .cube(height: 0.02, in: .meters)
                )
            }
        case .ended, .cancelled:
            break
        }
    }

    func onTossStart(interaction: TabletopInteraction,
                     outcomes: [TabletopInteraction.TossOutcome]) {
        for outcome in outcomes {
            let face = outcome.tossableRepresentation.face(for: outcome.restingOrientation)
            interaction.addAction(.updateEquipment(
                die, rawValue: face.rawValue, pose: outcome.pose
            ))
        }
    }
}
swift
class DieInteraction: TabletopInteraction.Delegate {
    let game: Game

    func update(interaction: TabletopInteraction) {
        switch interaction.value.phase {
        case .started:
            interaction.setConfiguration(.init(allowedDestinations: .any))
        case .update:
            if interaction.value.gesture?.phase == .ended {
                interaction.toss(
                    equipmentID: interaction.value.controlledEquipmentID,
                    as: .cube(height: 0.02, in: .meters)
                )
            }
        case .ended, .cancelled:
            break
        }
    }

    func onTossStart(interaction: TabletopInteraction,
                     outcomes: [TabletopInteraction.TossOutcome]) {
        for outcome in outcomes {
            let face = outcome.tossableRepresentation.face(for: outcome.restingOrientation)
            interaction.addAction(.updateEquipment(
                die, rawValue: face.rawValue, pose: outcome.pose
            ))
        }
    }
}

Tossable Representations

可投掷对象表示

Dice physics shapes:
.cube
(d6),
.tetrahedron
(d4),
.octahedron
(d8),
.decahedron
(d10),
.dodecahedron
(d12),
.icosahedron
(d20),
.sphere
. All take
height:in:
(or
radius:in:
for sphere) and optional
restitution:
.
骰子物理形状包括:
.cube
(六面骰)、
.tetrahedron
(四面骰)、
.octahedron
(八面骰)、
.decahedron
(十面骰)、
.dodecahedron
(十二面骰)、
.icosahedron
(二十面骰)、
.sphere
(球形)。所有类型都支持传入
height:in:
(球形传
radius:in:
)和可选的
restitution:
(弹性系数)参数。

Programmatic Interactions

代码触发交互

Start interactions from code:
game.startInteraction(onEquipmentID: pieceID)
.
See references/tabletopkit-patterns.md for group toss, predetermined outcomes, interaction acceptance/rejection, and destination restriction patterns.
通过代码启动交互:
game.startInteraction(onEquipmentID: pieceID)
。查看references/tabletopkit-patterns.md获取群体投掷、预设结果、交互接受/拒绝、目的地限制等模式示例。

RealityKit Rendering

RealityKit渲染

Conform to
EntityRenderDelegate
to bridge state to RealityKit. Provide a
root
entity. TabletopKit automatically positions
EntityEquipment
entities.
swift
class GameRenderer: EntityRenderDelegate {
    let root = Entity()

    func onUpdate(timeInterval: Double, snapshot: TableSnapshot,
                  visualState: TableVisualState) {
        // Custom visual updates beyond automatic positioning
    }
}
Connect to SwiftUI with
.tabletopGame(_:parent:automaticUpdate:)
on a
RealityView
:
swift
struct GameView: View {
    let game: Game

    var body: some View {
        RealityView { content in
            content.entities.append(game.renderer.root)
        }
        .tabletopGame(game.tabletopGame, parent: game.renderer.root) { value in
            GameInteraction(game: game)
        }
    }
}
Debug outlines:
game.tabletopGame.debugDraw(options: [.drawTable, .drawSeats, .drawEquipment])
遵循
EntityRenderDelegate
协议将状态桥接到RealityKit,提供
root
实体,TabletopKit会自动调整
EntityEquipment
实体的位置。
swift
class GameRenderer: EntityRenderDelegate {
    let root = Entity()

    func onUpdate(timeInterval: Double, snapshot: TableSnapshot,
                  visualState: TableVisualState) {
        // Custom visual updates beyond automatic positioning
    }
}
RealityView
上使用
.tabletopGame(_:parent:automaticUpdate:)
修饰器连接到SwiftUI:
swift
struct GameView: View {
    let game: Game

    var body: some View {
        RealityView { content in
            content.entities.append(game.renderer.root)
        }
        .tabletopGame(game.tabletopGame, parent: game.renderer.root) { value in
            GameInteraction(game: game)
        }
    }
}
调试轮廓线:
game.tabletopGame.debugDraw(options: [.drawTable, .drawSeats, .drawEquipment])

Group Activities Integration

Group Activities集成

TabletopKit integrates directly with GroupActivities for FaceTime-based multiplayer. Define a
GroupActivity
, then call
coordinateWithSession(_:)
. TabletopKit automatically synchronizes all equipment state, seat assignments, actions, and interactions. No manual message passing required.
swift
import GroupActivities

struct BoardGameActivity: GroupActivity {
    var metadata: GroupActivityMetadata {
        var meta = GroupActivityMetadata()
        meta.type = .generic
        meta.title = "Board Game"
        return meta
    }
}

@Observable
class GroupActivityManager {
    let tabletopGame: TabletopGame
    private var sessionTask: Task<Void, Never>?

    init(tabletopGame: TabletopGame) {
        self.tabletopGame = tabletopGame
        sessionTask = Task { @MainActor in
            for await session in BoardGameActivity.sessions() {
                tabletopGame.coordinateWithSession(session)
            }
        }
    }

    deinit { tabletopGame.detachNetworkCoordinator() }
}
Implement
TabletopGame.MultiplayerDelegate
for
joinAccepted()
,
playerJoined(_:)
,
didRejectPlayer(_:reason:)
, and
multiplayerSessionFailed(reason:)
. See references/tabletopkit-patterns.md for custom network coordinators and arbiter role management.
TabletopKit可直接集成GroupActivities实现基于FaceTime的多人功能。定义
GroupActivity
后调用
coordinateWithSession(_:)
即可,TabletopKit会自动同步所有道具状态、座位分配、动作和交互,无需手动传递消息。
swift
import GroupActivities

struct BoardGameActivity: GroupActivity {
    var metadata: GroupActivityMetadata {
        var meta = GroupActivityMetadata()
        meta.type = .generic
        meta.title = "Board Game"
        return meta
    }
}

@Observable
class GroupActivityManager {
    let tabletopGame: TabletopGame
    private var sessionTask: Task<Void, Never>?

    init(tabletopGame: TabletopGame) {
        self.tabletopGame = tabletopGame
        sessionTask = Task { @MainActor in
            for await session in BoardGameActivity.sessions() {
                tabletopGame.coordinateWithSession(session)
            }
        }
    }

    deinit { tabletopGame.detachNetworkCoordinator() }
}
实现
TabletopGame.MultiplayerDelegate
协议处理
joinAccepted()
playerJoined(_:)
didRejectPlayer(_:reason:)
multiplayerSessionFailed(reason:)
等回调。查看references/tabletopkit-patterns.md获取自定义网络协调器和仲裁者角色管理示例。

Common Mistakes

常见问题

  • Forgetting platform restriction. TabletopKit is visionOS-only. Do not conditionally compile for iOS/macOS; the framework does not exist there.
  • Skipping seat claim. Players must call
    claimAnySeat()
    or
    claimSeat(_:)
    before interacting with equipment. Without a seat, actions are rejected.
  • Mutating state outside actions. All state changes must go through
    TabletopAction
    or
    CustomAction
    . Directly modifying equipment properties bypasses synchronization.
  • Missing custom action registration. Custom actions must be registered with
    setup.register(action:)
    before creating the
    TabletopGame
    . Unregistered actions are silently dropped.
  • Not handling action rollback. Actions are optimistically applied and can be rolled back if validation fails on the arbiter. Implement
    actionWasRolledBack(_:snapshot:)
    to revert UI state.
  • Using wrong parent ID. Equipment
    parentID
    in state must reference a valid equipment ID (typically the table or a container). An invalid parent causes the piece to disappear.
  • Ignoring TossOutcome faces. After a toss, read the face from
    outcome.tossableRepresentation.face(for: outcome.restingOrientation)
    rather than generating a random value. The physics simulation determines the result.
  • Testing multiplayer in Simulator. Group Activities do not work in Simulator. Multiplayer requires physical Apple Vision Pro devices on a FaceTime call.
  • 忘记平台限制:TabletopKit仅支持visionOS,不要为iOS/macOS条件编译,这些平台不存在该框架。
  • 未认领座位:玩家交互道具前必须调用
    claimAnySeat()
    claimSeat(_:)
    ,没有座位的动作会被拒绝。
  • 在动作外修改状态:所有状态变更必须通过
    TabletopAction
    CustomAction
    ,直接修改道具属性会跳过同步逻辑。
  • 未注册自定义动作:创建
    TabletopGame
    前必须通过
    setup.register(action:)
    注册自定义动作,未注册的动作会被静默丢弃。
  • 未处理动作回滚:动作会被乐观应用,如果在仲裁者端验证失败会被回滚,需要实现
    actionWasRolledBack(_:snapshot:)
    来还原UI状态。
  • 父ID错误:状态中的道具
    parentID
    必须指向有效的道具ID(通常是桌面或容器),无效的父ID会导致道具消失。
  • 忽略TossOutcome的面数:投掷结束后,应该从
    outcome.tossableRepresentation.face(for: outcome.restingOrientation)
    读取面数,而不是生成随机值,结果由物理模拟决定。
  • 在模拟器测试多人功能:模拟器不支持Group Activities,多人功能需要处于FaceTime通话中的物理Apple Vision Pro设备。

Review Checklist

检查清单

  • import TabletopKit
    present; target is visionOS 2.0+
  • TableSetup
    created with a
    Tabletop
    /
    EntityTabletop
    conforming type
  • All equipment conforms to
    Equipment
    or
    EntityEquipment
    with correct state type
  • Seats added and
    claimAnySeat()
    /
    claimSeat(_:)
    called at game start
  • All custom actions registered with
    setup.register(action:)
  • TabletopGame.Observer
    implemented for reacting to confirmed actions
  • EntityRenderDelegate
    or
    RenderDelegate
    connected
  • .tabletopGame(_:parent:automaticUpdate:)
    modifier on
    RealityView
  • GroupActivity
    defined and
    coordinateWithSession(_:)
    called for multiplayer
  • Group Activities capability added in Xcode for multiplayer builds
  • Debug visualization (
    debugDraw
    ) disabled before release
  • Tested on device; multiplayer tested with 2+ Apple Vision Pro units
  • 已引入
    import TabletopKit
    ,目标平台为visionOS 2.0+
  • 已使用遵循
    Tabletop
    /
    EntityTabletop
    协议的类型创建
    TableSetup
  • 所有道具都遵循
    Equipment
    EntityEquipment
    协议,并使用正确的状态类型
  • 已添加座位,且游戏启动时调用了
    claimAnySeat()
    /
    claimSeat(_:)
  • 所有自定义动作都通过
    setup.register(action:)
    注册
  • 已实现
    TabletopGame.Observer
    响应已确认的动作
  • 已连接
    EntityRenderDelegate
    RenderDelegate
  • RealityView
    上已添加
    .tabletopGame(_:parent:automaticUpdate:)
    修饰器
  • 已定义
    GroupActivity
    ,多人模式下调用了
    coordinateWithSession(_:)
  • 多人构建版本已在Xcode中添加Group Activities能力
  • 发布前已禁用调试可视化(
    debugDraw
  • 已在真机测试,多人功能使用2台及以上Apple Vision Pro设备测试通过

References

参考资料