Loading...
Loading...
Compare original and translation side by side
import TabletopKitimport RealityKitimport TabletopKitimport RealityKit| Type | Role |
|---|---|
| Central game manager; owns setup, actions, observers, rendering |
| Configuration object passed to |
| Protocol for the table surface |
| Protocol for interactive game pieces |
| Protocol for player seat positions |
| Commands that modify game state |
| Gesture-driven player interactions with equipment |
| Callback protocol for reacting to confirmed actions |
| Callback protocol for visual updates |
| RealityKit-specific render delegate |
| Type | Role |
|---|---|
| 核心游戏管理器,负责配置、动作、观察者、渲染逻辑 |
| 传入 |
| 桌面的协议定义 |
| 可交互游戏道具的协议定义 |
| 玩家座位位置的协议定义 |
| 修改游戏状态的指令 |
| 玩家对道具的手势驱动交互 |
| 响应已确认动作的回调协议 |
| 处理视觉更新的回调协议 |
| 专属RealityKit的渲染委托 |
TabletopGameimport 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()update(deltaTime:).tabletopGame(_:parent:automaticUpdate:)withCurrentSnapshot(_:)TabletopGameimport 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(_:)EntityTabletopshapeEntitystruct 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)
}
}EntityTabletopshapeEntitystruct 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)
}
}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)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)EquipmentEntityEquipmentidEquipmentIdentifierinitialState| State Type | Use Case |
|---|---|
| Generic pieces, pawns, tokens |
| Playing cards (tracks |
| Dice with an integer |
| Custom data encoded as |
EquipmentEntityEquipmentidEquipmentIdentifierinitialState| State Type | Use Case |
|---|---|
| 通用道具、棋子、代币 |
| 扑克牌类道具(可跟踪 |
| 带有整数 |
| 编码为 |
// 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
)
}
}// 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
)
}
}seatControl.any.restricted([seatID1, seatID2]).current.inheritedseatControl.any.restricted([seatID1, seatID2]).current.inheritedlayoutChildren(for:visualState:).planarStacked(layout:animationDuration:).planarOverlapping(layout:animationDuration:).volumetric(layout:animationDuration:)layoutChildren(for:visualState:).planarStacked(layout:animationDuration:).planarOverlapping(layout:animationDuration:).volumetric(layout:animationDuration:)EntityTableSeatstruct 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.playerChangedSeatsEntityTableSeatstruct 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.playerChangedSeatsTabletopAction// 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// 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)))CustomActionstruct 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
}
}setup.register(action: CollectCoin.self)CustomActionstruct 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
}
}setup.register(action: CollectCoin.self)setup.add(counter: ScoreCounter(id: .init(0), value: 0))
// Update: game.addAction(.updateCounter(matching: .init(0), value: 42))
// Read: snapshot.counter(matching: .init(0))?.valuesetup.add(counter: ScoreCounter(id: .init(0), value: 0))
// Update: game.addAction(.updateCounter(matching: .init(0), value: 42))
// Read: snapshot.counter(matching: .init(0))?.valuegame.addAction(.createBookmark(id: StateBookmarkIdentifier(1)))
game.jumpToBookmark(matching: StateBookmarkIdentifier(1))game.addAction(.createBookmark(id: StateBookmarkIdentifier(1)))
game.jumpToBookmark(matching: StateBookmarkIdentifier(1)).tabletopGame.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.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)
}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
))
}
}
}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
))
}
}
}.cube.tetrahedron.octahedron.decahedron.dodecahedron.icosahedron.sphereheight:in:radius:in:restitution:.cube.tetrahedron.octahedron.decahedron.dodecahedron.icosahedron.sphereheight:in:radius:in:restitution:game.startInteraction(onEquipmentID: pieceID)game.startInteraction(onEquipmentID: pieceID)EntityRenderDelegaterootEntityEquipmentclass GameRenderer: EntityRenderDelegate {
let root = Entity()
func onUpdate(timeInterval: Double, snapshot: TableSnapshot,
visualState: TableVisualState) {
// Custom visual updates beyond automatic positioning
}
}.tabletopGame(_:parent:automaticUpdate:)RealityViewstruct 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])EntityRenderDelegaterootEntityEquipmentclass GameRenderer: EntityRenderDelegate {
let root = Entity()
func onUpdate(timeInterval: Double, snapshot: TableSnapshot,
visualState: TableVisualState) {
// Custom visual updates beyond automatic positioning
}
}RealityView.tabletopGame(_:parent:automaticUpdate:)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])GroupActivitycoordinateWithSession(_:)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.MultiplayerDelegatejoinAccepted()playerJoined(_:)didRejectPlayer(_:reason:)multiplayerSessionFailed(reason:)GroupActivitycoordinateWithSession(_:)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.MultiplayerDelegatejoinAccepted()playerJoined(_:)didRejectPlayer(_:reason:)multiplayerSessionFailed(reason:)claimAnySeat()claimSeat(_:)TabletopActionCustomActionsetup.register(action:)TabletopGameactionWasRolledBack(_:snapshot:)parentIDoutcome.tossableRepresentation.face(for: outcome.restingOrientation)claimAnySeat()claimSeat(_:)TabletopActionCustomActionTabletopGamesetup.register(action:)actionWasRolledBack(_:snapshot:)parentIDoutcome.tossableRepresentation.face(for: outcome.restingOrientation)import TabletopKitTableSetupTabletopEntityTabletopEquipmentEntityEquipmentclaimAnySeat()claimSeat(_:)setup.register(action:)TabletopGame.ObserverEntityRenderDelegateRenderDelegate.tabletopGame(_:parent:automaticUpdate:)RealityViewGroupActivitycoordinateWithSession(_:)debugDrawimport TabletopKitTabletopEntityTabletopTableSetupEquipmentEntityEquipmentclaimAnySeat()claimSeat(_:)setup.register(action:)TabletopGame.ObserverEntityRenderDelegateRenderDelegateRealityView.tabletopGame(_:parent:automaticUpdate:)GroupActivitycoordinateWithSession(_:)debugDraw