tabletopkit
dpearson2699/swift-ios-skills
Build multiplayer spatial board games on visionOS with TabletopKit, handling boards, pieces, turns, and FaceTime synchronization.
What is tabletopkit?
TabletopKit is a visionOS-exclusive framework for creating multiplayer tabletop games with spatial rendering, equipment interaction, player seating, and turn management. Use it when building board games, card games, or dice-based experiences that synchronize across players via FaceTime Group Activities.
- Define game tables (round or rectangular) with RealityKit rendering
- Create interactive equipment (pawns, cards, dice) with state management and control restrictions
- Manage player seats, turn order, and seat claiming/releasing
- Implement game actions and turn flows with observer callbacks
- Handle gesture-driven interactions (snapping, tosses, physics) via TabletopInteraction
- Synchronize multiplayer gameplay through FaceTime Group Activities
How to install tabletopkit
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill tabletopkit- visionOS 2.0+ (core features); visionOS 2.2+ for TabletopInteraction.Configuration; visionOS 26.0+ for CustomAction and CustomEquipmentState
- Swift 6.3
- Group Activities capability enabled in Signing & Capabilities for multiplayer
- 3D assets (USDZ files) in a RealityKit content bundle for tables, pieces, cards, and dice
How to use tabletopkit
- 1.Import TabletopKit and RealityKit in your source files
- 2.Define a table by conforming to EntityTabletop with a shape and RealityKit entity
- 3.Create equipment (pawns, cards, dice) conforming to EntityEquipment with appropriate state types (BaseEquipmentState, CardState, DieState)
- 4.Define player seats conforming to EntityTableSeat with poses around the table
- 5.Build a TableSetup, add seats and equipment, then instantiate TabletopGame
- 6.Claim a seat with game.claimAnySeat() or game.claimSeat(matching:)
- 7.Call game.update(deltaTime:) each frame or use the .tabletopGame(_:parent:automaticUpdate:) modifier
- 8.Read game state safely with game.withCurrentSnapshot(_:) and observe changes via TabletopGame.Observer
Use cases
- Building a multiplayer chess or checkers game with spatial board and piece interaction
- Creating a card game where players draw, play, and manage hands across a virtual table
- Developing a dice-rolling game with physics simulation and turn-based gameplay
- Implementing a cooperative board game with shared table state and player-specific equipment control
- Making a tabletop RPG companion app with character tokens and interactive map pieces
- visionOS game developers
- Multiplayer game designers targeting Apple Vision Pro
- Developers building tabletop/board game digital adaptations
- Teams creating spatial computing experiences with RealityKit
tabletopkit FAQ
Use BaseEquipmentState for generic pieces/pawns, CardState for cards (tracks faceUp/faceDown), DieState for dice (tracks integer value), RawValueState for custom data as UInt64, or CustomEquipmentState (visionOS 26.0+) for complex state with game data.
Set the seatControl property: .any (all players), .restricted([seatIDs]) (specific seats), .restrictedCurrent([seatIDs]) (specific seats during their turn), .current (only active turn player), or .inherited (from parent equipment).
Yes, the Simulator supports single-player layout testing, but Group Activities multiplayer synchronization is not supported in the Simulator.
Enable the Group Activities capability in Signing & Capabilities and use TabletopGame's built-in Group Activities integration to coordinate state and actions across FaceTime participants.
Use RealityKit entities with EntityTabletop and EntityEquipment protocols. Implement a RenderDelegate (or EntityRenderDelegate for RealityKit) to receive visual state updates and animate entities accordingly.
Full instructions (SKILL.md)
Source of truth, from dpearson2699/swift-ios-skills.
name: tabletopkit description: "Builds multiplayer spatial board games using TabletopKit on visionOS. Use when creating tabletop game experiences with boards, pieces, cards, or dice; managing seats, turns, equipment state, TabletopAction flows, or TabletopInteraction delegates; synchronizing gameplay through FaceTime Group Activities; rendering with RealityKit; or implementing snapping, tosses, and physics on a virtual table surface."
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. TabletopKit is visionOS-only. Core APIs are visionOS 2.0+; availability-sensitive APIs are called out below. Targets Swift 6.3.
Contents
- Setup
- Game Configuration
- Table and Board
- Equipment (Pieces, Cards, Dice)
- Player Seats
- Game Actions and Turns
- Interactions
- RealityKit Rendering
- Group Activities Integration
- Common Mistakes
- Review Checklist
- References
Setup
Platform Requirement
TabletopKit is exclusive to visionOS. Core gameplay, equipment, seating,
actions, interactions, rendering, and Group Activities coordination are
visionOS 2.0+. TabletopInteraction.Configuration is visionOS 2.2+.
CustomAction, CustomEquipmentState, TableSetup.register(action:), and
the direct TabletopGame.addAction(_ action: some CustomAction) overload are
visionOS 26.0+. The Simulator supports single-player layout testing but not
Group Activities multiplayer.
Project Configuration
import TabletopKitin source files that define game logic.import RealityKitfor entity-based rendering.- For multiplayer, add the Group Activities capability in Signing & Capabilities.
- Provide 3D assets (USDZ) in a RealityKit content bundle for tables, pieces, cards, and dice.
Key Types Overview
| Type | Role |
|---|---|
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 |
Game Configuration
Build a game in three steps: define the table, configure the setup, create the
TabletopGame instance.
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)))
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(_:).
Table and Board
Tabletop Protocol
Conform to EntityTabletop to define the playing surface. Provide a shape
(round or rectangular) and a RealityKit Entity for visual representation.
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:
// 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
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 Type | Use 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 |
CustomEquipmentState | visionOS 26.0+ custom state with a BaseEquipmentState plus game data |
Defining Equipment
// 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.restrictedCurrent([seatID1, seatID2])-- specific seats only while they are in turn.current-- only the seat whose turn it is.inherited-- inherits from parent equipment
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.
Player Seats
Conform to EntityTableSeat and provide a pose around the table:
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.
Game Actions and Turns
Built-in Actions
Use TabletopAction factory methods to modify game state:
// 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 on visionOS 26.0+, conform to CustomAction.
Custom action application and validation must depend only on the action data and
the supplied TableState / TableSnapshot so every peer resolves the same
result. Register custom action types during setup before dispatching them:
setup.register(action: CollectCoin.self)
game.addAction(CollectCoin(coinID: coinID, playerID: playerID))
CustomAction, CustomEquipmentState, setup.register(action:), and
.customAction(_:context:) are visionOS 26.0+. Register each custom action type
before dispatching it. See references/tabletopkit-patterns.md
for full custom action and custom state examples.
Score Counters
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:
game.addAction(.createBookmark(id: StateBookmarkIdentifier(1)))
game.jumpToBookmark(matching: StateBookmarkIdentifier(1))
Interactions
TabletopInteraction.Delegate
Return an interaction delegate from the .tabletopGame modifier to handle
player gestures on equipment:
.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)
}
Use interaction.value.gesture for gesture-specific state. Avoid deprecated
gesturePhase. For destination control, prefer
interaction.setConfiguration(.init(allowedDestinations: ...)) on visionOS
2.2+ rather than deprecated setAllowedDestinations(_:) or
value.allowedDestinations.
Handling Gestures and Tossing Dice
Basic toss(equipmentID:as:) is core TabletopKit. onTossStart, TabletopInteraction.TossOutcome, and TossableRepresentation.face(for:) are visionOS 26.0+.
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:.
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.
RealityKit Rendering
Conform to EntityRenderDelegate to bridge state to RealityKit. Provide a
root entity. TabletopKit automatically positions EntityEquipment entities.
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:
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])
Group Activities Integration
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.
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.
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()orclaimSeat(_:)before interacting with equipment. Without a seat, actions are rejected. - Mutating state outside actions. All state changes must go through
TabletopActionorCustomAction. Directly modifying equipment properties bypasses synchronization. - Missing custom action registration. Custom actions must be registered with
setup.register(action:)before use. Custom actions are visionOS 26.0+. - 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. - Ignoring discarded actions on visionOS 26. Implement
actionWasDiscarded(_:)when local action queue pressure matters; it is called for local actions that cannot be enqueued. - Using wrong parent ID. Equipment
parentIDin 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.
Review Checklist
-
import TabletopKitpresent; answer explicitly states TabletopKit is visionOS-only and target is visionOS 2.0+ - Availability checked for visionOS 2.2+
TabletopInteraction.Configurationand visionOS 26.0+ custom action/custom state APIs -
TableSetupcreated with aTabletop/EntityTabletopconforming type - All equipment conforms to
EquipmentorEntityEquipmentwith correct state type - Seats added and
claimAnySeat()/claimSeat(_:)called at game start - All custom actions registered with
setup.register(action:) -
TabletopGame.Observerimplemented for reacting to confirmed actions and, on visionOS 26.0+, discarded local actions when relevant -
EntityRenderDelegateorRenderDelegateconnected -
.tabletopGame(_:parent:automaticUpdate:)modifier onRealityView -
GroupActivitydefined andcoordinateWithSession(_:)called; multiplayer described as Group Activities/SharePlay synchronization - Group Activities capability added in Xcode for multiplayer builds
- Debug visualization (
debugDraw) disabled before release - Device notes state Simulator is single-player only; multiplayer requires 2+ Apple Vision Pro units on FaceTime
References
- references/tabletopkit-patterns.md -- extended patterns for observer implementation, custom actions, dice simulation, card overlap, and network coordination
- Apple Documentation: TabletopKit
- Creating tabletop games (sample code; visionOS 2.2+, Xcode 16.2+)
- Synchronizing group gameplay with TabletopKit (sample code; visionOS 26.0+, Xcode 26.0+)
- Simulating dice rolls as a component for your game (sample code; visionOS 26.0+, Xcode 26.0+)
- Implementing playing card overlap and physical characteristics (sample code; visionOS 26.0+, Xcode 26.0+)
- WWDC24 session 10091: Build a spatial board game
Related skills
More from dpearson2699/swift-ios-skills and the wider catalog.

tipkit
Implement Apple TipKit feature-discovery UI for iOS 17+ apps with inline tips, popovers, rules, and CloudKit sync.

vision-framework
Detect text, faces, barcodes, and objects in iOS images and video using on-device Vision framework.

weatherkit
Fetch WeatherKit forecasts, alerts, and attribution for iOS 18+ apps using Swift 6.3.

widgetkit
Build Home Screen, Lock Screen, Control Center, and StandBy widgets for iOS with timeline providers and interactive controls.

tech-news-digest
Generate tech news digests with unified source model, quality scoring, and multi-format output. Six-source data collection from RSS feeds, Twitter/X KOLs, GitHub releases, GitHub Trending, Reddit, and web search. Pipeline-based scripts with retry mechanisms and deduplication. Supports Discord, email, and markdown templates.

pdf-to-markdown
[Document Processing] Use when you need to convert PDF files to Markdown with support for native text PDFs and scanned documents (OCR).