스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
visionOS용 TabletopKit 소개
TabletopKit을 사용하여 visionOS용 보드게임의 빌드를 백지부터 시작해 보세요. 게임을 설정하는 방법, RealityKit으로 강력한 렌더링을 추가하는 방법, 간단한 코드를 더해 FaceTime의 공간 페르소나를 사용한 멀티플레이어 모드를 활성화하는 방법을 소개합니다.
챕터
- 0:00 - Introduction
- 2:37 - Set up the play surface
- 7:45 - Implement rules
- 12:01 - Integrate RealityKit effects
- 13:30 - Configure multiplayer
리소스
관련 비디오
WWDC24
WWDC23
-
다운로드
안녕하세요 저는 TabletopKit 팀의 줄리아입니다 TabletopKit을 소개하고 Apple Vision Pro용 게임 제작에 대해 이야기하게 되어 기쁩니다 오늘은 TabletopKit을 사용한 테이블 게임 만들기의 기본을 알려 드리겠습니다
게임의 방법은 플레이어가 주사위를 굴려 보드 위를 이동하고 카드를 모으는 것입니다 게임 보드를 보면 현실에서 불가능한 초현실적인 기하학적 구조와 애니메이션이 적용되어 있고 따로 정리할 필요가 없습니다 이 비디오와 함께 샘플 코드가 제공됩니다 TabletopKit은 Apple Vision Pro용 테이블탑 멀티플레이어 공간 경험 제작을 위한 프레임워크이며 카드 게임이나 주사위 게임, 더 복잡한 보드 게임 제작도 지원합니다 TabletopKit이 제스처와 일반 레이아웃을 처리하므로 사용자를 위한 즐겁고 혁신적인 경험을 제작하는 작업에 집중할 수 있습니다 TabletopKit은 잘 알려진 프레임워크에 자연스럽게 연동됩니다 이를테면 GroupActivities나 RealityKit에 말이죠 따라서 코드 몇 줄로도 원활한 네트워킹과 탁월한 렌더링을 추가할 수 있습니다
스마트한 기본값과 여러 계층의 편의 함수를 제공하므로 기능이 작동하는 게임을 실행할 수 있는 동시에 자유롭게 원하는 대로 커스터마이징할 수 있어 그야말로 특별한 경험을 제작할 수 있습니다
테이블 게임은 Vision Pro에서 이미 인기를 끌고 있습니다 App Store에 출시된 Game Room의 Solitaire가 좋은 예입니다
하지만 게임을 제작하려면 상당히 많은 요소를 고려해야만 합니다 테이블 레이아웃이나 애니메이션, 물리 시뮬레이션 멀티플레이어 성능 등을 고려해야 하죠
TabletopKit을 사용하면 이러한 진입 장벽을 낮추고 즐겁게 창의성을 발휘하며 게임을 제작할 수 있습니다 오늘 보여 드릴 게임은 고전 보드 게임을 변형한 것으로 향수와 재미를 느낄 수 있는 게임입니다 먼저 게임의 초기 상태를 설정할 것입니다 플레이하면서 보고 상호작용할 모든 요소를 설정한 다음
게임의 논리적 메커니즘을 구현합니다 이를테면 점수를 관리하거나 유효하지 않은 움직임을 방지합니다
다음에는 RealityKit으로 시각 및 음향 효과를 추가해 게임에 생동감을 불어넣겠습니다 마지막으로 GroupActivities를 사용해 단 몇 줄의 코드만 더해 멀티플레이어 기능을 추가하겠습니다
그럼 먼저 게임 설정부터 살펴보겠습니다
테이블 게임을 제작할 때는 테이블부터 시작하는 게 가장 쉽습니다
따라서 테이블부터 시작하고 사용자가 앉을 자리를 결정합니다 보드 위에 게임 타일을 배치하고 그 위에 사물을 추가합니다 즉, 플레이어 말과 카드, 주사위를 배치해 게임을 구현합니다
게임에서 가장 먼저 기술할 대상은 테이블입니다 모든 설정과 게임플레이가 테이블 위에서 이루어집니다 Table은 반경이 정의된 원형 또는 너비와 깊이가 있는 직사각형입니다 각 게임에는 테이블이 필요합니다 참여자 간 거리가 가까운 게임에는 작은 원형 테이블이 좋습니다 친구와 즐기는 체커처럼 말이죠 방사형의 사각형 테이블은 보드가 크고 사물이 많은 복잡한 보드 게임에 적합합니다
테이블이 설정되면 이후의 모든 위치와 방향은 테이블의 원점과 좌표계를 기준으로 기술됩니다
테이블은 간단하게 정의할 수 있습니다 테이블의 형태 및 크기는 제작하려는 게임의 플레이 가능한 공간을 나타냅니다 대부분 이 형태는 렌더링하려는 테이블 엔티티와 일치하지만 반드시 그럴 필요는 없습니다
지금은 직사각형 테이블을 만들고 있으며 프레임워크가 엔티티의 바운딩 박스를 결정합니다 테이블을 정의한 다음에는 테이블 주변에 시트를 배치합니다
Seat는 테이블의 원점을 기준으로 배치됩니다 한 시트에는 한 번에 한 플레이어만 할당될 수 있습니다 플레이어는 시트에 앉아야만 게임과 상호작용할 수 있습니다 게임의 관전자는 시트에 앉지 않습니다
게임을 진행하며 플레이어는 다른 시트를 차지할 수도 있습니다
이 게임은 최대 3명이 플레이하므로 고유 ID를 가진 시트 3개를 테이블 주변에 같은 간격으로 배치하고 시트가 테이블 중앙을 향하도록 설정하겠습니다
멀티플레이어 세션에서는 추가 인원이 관전할 수 있지만 관전자는 시트에 앉지 못하므로 테이블 위 사물과 상호작용할 수 없습니다 테이블과 플레이어 시트를 만들었으니 이제 게임 자체를 만들 차례입니다 테이블 위에 있는 것은 모두 장비(equipment)입니다
이 샘플에서는 보드, 타일, 말, 카드 그리고 주사위가 모두 다양한 유형의 장비에 해당되죠
몇 가지 예시를 통해 장비를 이용하여 게임의 구성요소를 나타내 보겠습니다
먼저 말을 어떻게 기술하는지 보여 드리겠습니다 말은 플레이어가 보드 위에서 움직이는 물체입니다 말은 게임의 물리적인 물체이며 RealityKit 엔티티로 렌더링됩니다 따라서 고유한 크기를 가지고 플레이어가 상호작용할 수 있습니다
각 말은 처음에 각 플레이어 시트 앞에 배치되며 해당 시트에서 소유하므로 해당 시트에 앉은 플레이어만 말을 이동할 수 있습니다
샘플의 말을 구현하는 코드는 이렇습니다 말은 EntityEquipment 프로토콜을 따르고 이는 연결된 RealityKit 엔티티가 있음을 의미하므로 게임에서 형태를 가진 물체입니다
초기 상태에서 말의 주요 속성을 설정했습니다 seatControl을 해당 시트로 제한하여 해당 플레이어만 말을 움직일 수 있습니다
말의 시작 위치를 테이블을 기준으로 설정합니다 따라서 말은 처음에 각 플레이어 앞에 배치됩니다
또한 엔티티를 전달하므로 프레임워크가 엔티티의 바운딩 박스를 결정할 수 있습니다
타일은 이 게임에 있는 또 다른 장비의 예시입니다 떠 있는 컨베이어 벨트가 게임 보드의 역할을 하고 보드 위의 타일에서 말들이 움직입니다
각 타일은 보드 위의 특정 위치입니다
게임을 진행하며 플레이어는 여러 타일 위로 말을 움직입니다
두 플레이어의 말이 같은 타일에 배치되면 타일 하나에 둘 이상의 말이 존재할 수 있습니다
각 타일에는 카테고리가 있어 트리거할 게임플레이와 애니메이션을 결정합니다
이것은 컨베이어 타일이며 Equipment 프로토콜을 채택합니다
말과 마찬가지로 초기 상태에서 타일의 다양한 속성을 기술합니다
먼저 부모를 보드로 설정합니다 이렇게 하면 타일이 보드의 바운딩 박스 위에 위치합니다
그런 다음 위치를 설정합니다 타일은 보드의 자식이므로 타일의 위치는 모두 보드의 좌표계를 기준으로 합니다
끝으로 타일의 엔티티는 렌더링하지 않을 것이므로 바운딩 박스를 명시해 정의합니다
비슷한 패턴으로 카드 덱과 주사위 각 플레이어가 카드를 모으는 플레이어의 손을 추가했습니다
게임에 필요한 모든 요소를 올바른 위치에 배치했으니 이제 게임 역학을 구축할 수 있습니다 일반적으로 이 게임은 여러 플레이어들이 필요한 사물과 상호작용하며 진행됩니다 게임을 얼마나 자동화할지 플레이어가 조작 가능한 범위는 어느 정도로 할지 결정할 수 있습니다 예를 들어 딜러 역할은 지루할 수 있으므로 딜러 기능을 자동으로 실행하는 버튼을 추가할 수도 있을 겁니다 하지만 카드는 계속 직접 가져오도록 하겠습니다 직접 참여하는 느낌을 줄 수 있으니까요 샘플에서는 직접 주사위를 굴리고 말을 이동하며 카드를 가져옵니다 TabletopKit은 시스템 제스처를 모니터링하므로 SwiftUI 장면을 사용해 앱을 제작할 때와 동일한 제스처를 볼 수 있습니다 이러한 제스처는 처리를 거쳐 상호작용의 형태로 다시 돌아옵니다
상호작용마다 게임 상태를 변경하는 동작을 추가할 수 있습니다 구체적인 예를 들어 보겠습니다 플레이어가 핀치하고 드래그하여 덱에서 카드를 뽑아 기존 더미 중 하나에 놓습니다 시스템 핀치를 모니터링하고 TabletopKit 상호작용으로 변환하여 카드를 새 더미로 옮기는 동작을 추가할 수 있습니다
시스템 제스처는 TabletopKit Interaction을 생성합니다 제스처가 변할 때마다 TabletopKit이 상호작용 콜백을 호출합니다 콜백에서는 대상 장비와 현재 단계를 지정합니다 제스처 단계는 시스템 제스처의 단계를 제공하므로 사용자가 핀치하면 시작되고 손가락을 놓으면 종료됩니다 상호작용 단계는 TabletopKit 상호작용의 단계를 제공합니다 예를 들어 사용자가 주사위를 집으면 시작되고 사용자가 던진 주사위가 테이블에 놓이면 종료됩니다
제스처는 시작될 때만 Started 단계에서 한 번 시작되고 지속되는 동안 Update 단계를 유지합니다 제스처는 진행되는 도중 언제든지 취소될 수 있습니다 예를 들어 손으로 드래그하다가 손을 등 뒤로 옮기면 취소됩니다 취소는 의도적인 종료와 다릅니다 사물을 놓으려고 핀치를 해제하면 의도적으로 종료한 겁니다
게임을 RealityView에 연결할 때 TabletopInteraction 객체를 구현한 결과물을 전달합니다
제스처가 업데이트될 때마다 업데이트 콜백이 호출됩니다 컨텍스트에는 관련된 구성요소나 배치할 수 있는 위치 등 수정 가능한 속성이 있습니다 또한 상호작용을 취소하거나 종료하는 등의 동작을 수행하는 함수가 있습니다
값은 읽을 수 있는 정보이며 제스처나 상호작용 단계 요청된 목적지와 관련된 자세 등입니다 상호작용 업데이트마다 게임 상태를 변경할 수 있습니다
Action은 게임 상태에 적용되는 개별 동작으로 사물을 새 그룹으로 이동하거나 카드를 뒤집는 동작 등입니다
동작은 요청될 때마다 큐에 추가되며 하나씩 적용됩니다
흔한 예시는 부모 간 객체 이동입니다 이 코드 스니펫에서는 상호작용할 수 있는 모든 객체가 유효한 모든 부모 구성요소에 할당될 수 있도록 했습니다
따라서 제스처가 종료되었다는 콜백을 수신하면 요청된 부모가 유효한 값 중에 있는지 확인한 다음
존재한다면 상호작용 컨텍스트에 동작을 추가하여 해당 장비를 요청된 부모 장비로 이동합니다
따라서 플레이어가 게임 보드에서 말을 움직이고 카드를 손으로 가져오고 주사위를 던질 수 있습니다
게임이 진행되며 게임 상태가 어떻게 변하는지 완전히 제어할 수 있습니다 TabletopKit은 움직인 모든 요소에 대한 정보를 제공하므로 올바른 동작과 발생하지 않아야 할 동작을 파악할 수 있습니다 체스 게임을 제작한다고 해 보겠습니다 게임의 규칙을 배우기 위한 게임 모드를 만들어 게임에서 가능한 움직임을 강조해 표시하고 따르도록 할 수 있습니다 다른 자유 플레이 모드를 구현하여 이동을 강제하는 규칙을 없애고 자유롭게 즐기도록 할 수도 있죠
게임의 재미를 더하려면 게임 역학도 중요한 요소지만 플레이어 간의 역학관계도 그만큼 중요합니다 각 게임마다 특별한 조합이 필요하죠! 이제 플레이할 수 있는 샘플 게임을 완성했습니다 하지만 Apple Vision Pro용 게임은 여기에서 그치지 않습니다 RealityKit이 이미 마법 같은 게임 비주얼 구현을 위해 대부분의 작업을 완료했습니다 그림자와 조명이 있는 초현실적인 모델이나 탁월한 스타일의 애니메이션이죠 여러분이 제작한 모든 것을 RealityKit은 렌더링할 수 있습니다 샘플 앱에서 로봇 말은 유리한 위치에 멈추면 기쁨을 표시하고 불리한 위치라면 실망감을 드러냅니다 꽤 귀엽습니다
앞에서 본 대로 게임을 진행하며 TabletopKit이 상호작용 가능한 구성요소를 움직입니다 엔티티를 직접 로드하므로 원하는 모든 특수 효과를 여기에 연결하여 상호작용 시 트리거할 수 있습니다 RealityKit에서는 아주 쉽게 효과음을 재생할 수 있습니다 주사위 굴림 효과음을 추가해 보겠습니다 앞에서 살펴본 상호작용 업데이트 콜백입니다
플레이어가 주사위를 놓아 제스처가 종료되는 것을 모니터링합니다
audioLibraryComponent에서 효과음을 찾습니다
soundResource가 있으면 RealityKit이 주사위 엔티티에서 효과음을 재생하도록 지시합니다 RealityKit은 공간 오디오를 처리할 수 있으므로 공간 내 엔티티의 위치에서 효과음이 발생합니다
게임에서 주사위를 던져 어떻게 작동하지는 보겠습니다
솔리테어도 재미있지만 테이블 게임은 다른 사람과 함께 플레이할 때 가장 재밌습니다 FaceTime의 공간 페르소나는 정말 놀랍습니다 아주 사실적이고 무엇이든 쉽게 시작할 수 있습니다 한 방에 있지 않더라도 친구 또는 가족과 함께 게임을 플레이할 수 있죠
기본 시트 설정을 사용하는 네트워크 게임의 경우 단 몇 줄의 코드만으로 GroupActivities 세션을 시작해 프레임워크에 전달할 수 있습니다 그 후에는 멋진 신규 기능과 커스텀 공간 템플릿을 활용해 경험을 커스터마이징하고 플레이어와 관전자를 방에서 원하는 위치에 배치할 수 있습니다 TabletopKit이 멀티플레이어 네트워킹을 처리해 주며 아주 쉽게 설정할 수 있습니다 이 프레임워크는 동작을 동기화해 모든 플레이어의 게임 상태를 일치시킵니다
플레이어가 예를 들어 카드를 집는 동작을 전송하면 동작은 검증을 거친 다음 결정론적인 알고리즘 순서로 게임 상태에 추가됩니다
애니메이션이나 물리 시뮬레이션 등 성능 제약이 많은 일부 동작은 각 플레이어별로 로컬에서 처리되므로 멀티플레이어 환경이 빠르고 원활하게 유지됩니다
샘플에서는 플레이어가 원할 때 SharePlay를 시작할 수 있도록 툴바에 버튼을 추가하겠습니다 간단한 SharePlay 버튼의 코드 스니펫입니다 SharePlay 기호의 SwiftUI 버튼입니다
플레이어가 버튼을 누르면 새 GroupActivities 세션을 활성화합니다
세션이 활성화되면 GroupActivities 세션과 상호작용하도록 TabletopKit에 알립니다 기본 네트워킹은 이것으로 끝입니다! 이제 활성 플레이어 간 게임 상태가 동기화됩니다 게임에 특별한 공간 레이아웃을 사용하려면 커스텀 공간 페르소나 템플릿 API를 사용하면 됩니다
기본적으로 TabletopKit은 설정 시 기술한 시트를 바탕으로 GroupActivities 세션의 기본 공간 페르소나 템플릿을 정의합니다
샘플 게임에서는 각 페르소나가 테이블 옆 각자의 시트에 배치되며 중앙을 향하도록 회전됩니다
다른 공간 설정을 사용하려면 커스텀 공간 페르소나 템플릿 API로 원하는 템플릿으로 설정하면 TabletopKit에서 설정하는 기본 템플릿을 오버라이드합니다 자세한 내용은 비디오를 확인하세요
TabletopKit은 게임을 구현하는 데 큰 도움이 됩니다 FaceTime 공간 페르소나를 통한 연결을 지원하는 멋진 경험을 그 어느 때보다 쉽게 제작할 수 있습니다
TabletopKit이 게임 제작의 공통적이고 복잡한 문제를 해결하면 개발자는 게임의 모습, 느낌, 동작은 원하는 대로 만들 수 있습니다
TabletopKit은 RealityKit이나 GroupActivities와 같은 다른 멋진 Apple 프레임워크와 원활하게 연동되어 제작 프로세스를 더욱 간단하게 만들어 줍니다 자세히 알아보려면 이 비디오들을 참고하세요
TabletopKit을 사용하면 어떤 개발자라도 게임을 개발할 수 있습니다 여러분의 멋진 작품을 기대하겠습니다
-
-
3:52 - Make a rectangular table
// Make a rectangular table. let entity = try! Entity.load(named: "table", in: table_Top_KitBundle) let table: Tabletop = .rectangular(entity: entity)
-
4:25 - Place seats
// Place 3 seats around the table, facing the center. static let seatPoses: [TableVisualState.Pose2D] = [ .init(position: .init(x: 0, y: Double(GameMetrics.tableDimensions.z)), rotation: .degrees(0)), .init(position: .init(x: -Double(GameMetrics.tableDimensions.x), y: 0), rotation: .degrees(-90)), .init(position: .init(x: Double(GameMetrics.tableDimensions.x), y: 0), rotation: .degrees(90)) ]
-
5:40 - Define player pawns
// Define an object that describes a pawn for each player. struct PlayerPawn: EntityEquipment { let id: ID let entity: Entity var initialState: BaseEquipmentState init(id: ID, seat: PlayerSeat, pose: TableVisualState.Pose2D, entity: Entity) { self.id = id self.entity = entity initialState = .init(seatControl: .restricted([seat.id]), pose: pose, entity: entity) } }
-
6:55 - Define an object that describes a tile
// Define an object that describes a tile on the conveyor belt struct ConveyorTile: Equipment { enum Category: String { case red case green case grey } let id: ID let category: ConveyorTile.Category let initialState: BaseEquipmentState init(id: ID, boardID: EquipmentIdentifier, position: TableVisualState.Point2D, category: ConveyorTile.Category) { self.id = id self.category = category initialState = .init(parentID: boardID, pose: .init(position: position, rotation: .init()), boundingBox: .init(center: .zero, size: .init(x: 0.06, y: 0, z: 0.06)))
-
9:53 - Monitor interactions
// The view contains all the content in the game. RealityView { (content: inout RealityViewContent) in content.entities.append(loadedGame.renderer.root) }.tabletopGame(loadedGame.tabletop, parent: loadedGame.renderer.root) { _ in GameInteraction(game: loadedGame) } // Define an object that manages player interactions. struct GameInteraction: TabletopInteraction { func update(context: TabletopKit.TabletopInteractionContext, value: TabletopKit.TabletopInteractionValue) { switch value.phase { //... }
-
10:48 - Respond to interaction updates
// Respond to interaction updates. func update(context: TabletopKit.TabletopInteractionContext, value: TabletopKit.TabletopInteractionValue) { switch value.phase { //... case .ended: { guard let dst = value.proposedDestination.equipmentID else { return } context.addAction(.moveEquipment(matching: value.startingEquipmentID, childOf: dst)) } }
-
12:52 - Add a sound effect to the die roll
// Respond to interaction updates. func update(context: TabletopKit.TabletopInteractionContext, value: TabletopKit.TabletopInteractionValue) { switch value.gesturePhase { //... case .ended: { if let die = game.tabletop.equipment(of: Die.self, matching: value.startingEquipmentID) { if let audioLibraryComponent = die.entity.components[AudioLibraryComponent.self] { if let soundResource = audioLibraryComponent.resources["dieSoundShort.mp3"] { die.entity.playAudio(soundResource) } } } } } }
-
14:44 - Set up multiplayer with SharePlay
// Set up multiplayer using SharePlay. // Provide a button to begin SharePlay. import GroupActivities func shareplayButton() -> some View { Button("SharePlay", systemImage: "shareplay") { Task {try! await Activity().activate() } } } // After joining the SharePlay session, start multiplayer. sessionTask = Task.detached { @MainActor in for await session in Activity.sessions() { tabletopGame.coordinateWithSession(session) } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.