ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
visionOS向けTabletopKitについて
TabletopKitを使用してvisionOS向けのボードゲームをゼロから構築しましょう。ゲームをセットアップする方法、RealityKitを活用して強力なレンダリングを追加する方法や、FaceTimeの空間Personaを使用したマルチプレイヤーでのプレイをコードを数行追加するだけで実現する方法をご紹介します。
関連する章
- 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チームの Juliaです 今日は 新しいTabletopKitと Apple Vision Proでのゲーム制作について ご紹介したいと思います まず TabletopKitで テーブルゲームを作る際の 基本的な要素についてご説明します
ゲームの仕組みとしては プレイヤーがサイコロを振って盤上を移動し カードを集めていきます ゲーム盤の形状は超現実的なもので 現実世界の可能性を超えた アニメーションが駆使され 後片付けも必要ありません サンプルコードもご用意しています TabletopKitのフレームワークを使えば Apple Vision Pro用の 空間的なマルチプレイヤーの テーブルゲームを制作できます カードゲームやサイコロゲーム より複雑なボードゲームも可能です ジェスチャやレイアウトは TabletopKitで処理できるので デベロッパは楽しく革新的なユーザー体験を デザインすることに集中できます TabletopKitはGroupActivitiesや RealityKitなどのフレームワークとも 自然に統合できるので スムーズなネットワーキングと 魅力的なレンダリングを 数行のコードで追加できます
スマートなデフォルト設定や 多くの便利な機能を提供することで 機能的なゲームを素早く立ち上げると同時に 自由なカスタマイズを行って 真にユニークなゲームを 作り上げることができます
テーブルゲームはすでに Vision Proで人気があります その良い例が App Storeの Game Roomのソリティアです
ただし ゲームを開発する時は 様々な要素について 考慮する必要があります 表のレイアウト アニメや物理シミュレーション 高性能なマルチプレイヤーなどです
TabletopKitは そのような障壁を取り除き ゲーム制作の楽しさと 創造性を引き出すように設計されています 今日ご紹介するゲームは 古典的なボードゲームをアレンジしたもので どこか懐かしく楽しいものです まず最初に ゲームの初期状態を設定します プレイ中に目にしたり操作したりする すべてのものが対象です
その後 得点のつけ方や無効な動作の禁止など ゲームの論理的な仕組みを決定します
次に RealityKitを使って 視覚効果やオーディオ効果を加え ゲームに命を吹き込みます 最後に GroupActivitiesを使って コードを数行だけ追加し マルチプレイヤー機能を有効にします
ではまず ゲームの設定から見ていきましょう
テーブルゲームについて考えるには 盤上を見るのが一番です
まず テーブルを用意して全員の席を決めます そこにマス目を設定した盤を置き 具体的なオブジェクトを追加します プレイの駒 カード サイコロなどの要素で ゲームに命を吹き込みましょう
ゲームで最初に記述すべきものは テーブルです 設定もプレイも すべて盤上で行われるからです テーブルは半径のある円形でも 幅と奥行きのある長方形でも構いません テーブルは各ゲームに必須です 少人数のゲームなら 小さい円形のテーブルがよいかもしれません 例えば 友達とのチェッカーなどです 広々とした長方形のテーブルは 大型のボードや数多くの要素を使用する 複雑なボードゲームに最適です
テーブルができたら 発生しうるすべての位置や向きを テーブルの原点と座標系を基準に記述します
テーブルを記述するのは簡単です ここでの形状と寸法は これから作成するゲームの プレイ可能領域を表します この形状は多くの場合 レンダリングする テーブルエンティティと一致しますが そうである必要はありません
ここでは 長方形のテーブルを作成し フレームワークでエンティティの バウンディングボックスを指定します テーブルを定義した後 テーブルの周りに座席を配置します
座席の位置は テーブルの原点を基準に決定します 1つのシートに割り当てられるのは 一度に1名のプレイヤーのみで 各プレイヤーはゲームに参加するために 座席を確保する必要があります ゲームの観戦者は着席できません
プレイヤーはゲーム全体を通じて 様々な席を獲得できます
このゲームの参加者は3人までなので 固有のIDを割り当てた3つの席を テーブル周りに等間隔に配置し 皆がテーブルの中央を向くようにします
マルチプレイヤーセッションでは 他のメンバーも観戦できますが 着席はできないため テーブル上のオブジェクトを 操作することはできません テーブルと座席を用意したら あとはゲーム自体の設定をするだけです テーブル上にあるものはすべて要素です
この例では ボード マス目 駒 カード サイコロはすべて 種類の異なる要素になります
では それぞれの要素を使ってどのように ゲームのコンポーネントを 表現するのかをご紹介します
まず 駒の記述方法を説明します 駒は各プレイヤーがボード上で動かすもので ゲーム内の物理的なオブジェクトであり RealityKitエンティティとして- 表示されるため 固有のサイズがあり プレイヤーが操作できます
駒はそれぞれの対応する座席の前から出発し その座席の所有物なので その座席のプレイヤーだけが 動かすことができます
これがサンプルの駒のコードです 駒はEntityEquipmentプロトコルに 準拠しているため RealityKitエンティティがアタッチされており ゲーム内の有形オブジェクトになります
初期状態では 駒の主要なプロパティを設定します seatControlは対応する 座席のみに設定したので 自分の駒を動かせるのは その席のプレイヤーだけです
駒の開始位置は テーブルを基準に設定したため 駒はまず 各プレイヤーの目の前に配置されます
また エンティティを引き渡せば 駒のバウンディングボックスが フレームワークによって決定されます
このゲームの要素のもう1つの例は マス目です 高架のベルトコンベヤが ゲーム盤の役割を果たし 駒はその上にあるマス目に沿って移動します
個々のマス目は盤上の特定の地点を表します
ゲームの中では プレイヤーが自分の駒を 様々なマス目に移動させます
2人のプレイヤーが同じマスに到達した場合 1つのマスに複数の駒が 同時に入ることができます
各マスにはカテゴリもあり それがゲームプレイや 表示されるアニメーションに影響します
これがコンベヤのマスで Equipmentプロトコルに準拠しています
駒と同じように 初期状態の マスの様々なプロパティを記述します
まず ボードを親に設定します これは マス目がボードのバウンディング ボックス上にあることを意味します
次に 位置を設定します マス目はボードの子なので 位置はすべてボードの座標系を 基準としたものになります
最後に マスのエンティティは レンダリングしないので バウンディングボックスを明示します
同様のパターンでカード1組 サイコロ そしてカードを受け取る 各プレイヤーの手を追加します
これでゲームを始めるために必要なものが すべて揃いましたので ゲームの仕組みを構成していきましょう ゲームは通常 様々なプレイヤーがオブジェクトを 操作することで進んでいきます ゲームをどの程度自動化し どの程度をユーザーが自分で 操作できるようにするかは 設定によって変更できます 例えば カードの配布が退屈なら 自動的に全員にカードを配る ボタンを追加できます それでもプレイヤーが手で山札から カードを引けるようにしておけば より臨場感を演出できます この例では プレイヤーがサイコロを振り 駒を動かしてカードを集めます TabletopKitでは システムジェスチャが監視されるので SwiftUIのシーンを利用して アプリをビルドする時と同じジェスチャが 処理されたうえで インタラクションの形で戻されます
インタラクションには アクションを追加して ゲームの状態を変えることができます 具体的な例を見てみましょう プレイヤーが山札から カードをつまみ出してドラッグし いずれかの山の上に置くとします そして システムのピンチ動作を監視し それをTabletopKitの インタラクションにして そのカードを新しい山に移動する アクションを追加します
システムジェスチャによって TabletopKitの インタラクションが生成されます ジェスチャが変わるたびに TabletopKitはインタラクションの コールバックを呼び出します コールバックでは対象となる要素と 現在のフェーズが指定されます ジェスチャフェーズは システムジェスチャのフェーズを示すので ユーザーがピンチすると同時に開始され ピンチ動作をやめると終了します インタラクションフェーズは TabletopKitの インタラクションのフェーズなので 例えば ユーザーがサイコロを拾うと開始され 投げられたサイコロがテーブルに落ちると 終了します
ジェスチャは開始されると同時に 開始フェーズに入り 持続中はずっと更新フェーズになります ジェスチャは実行中に いつでもキャンセルできます 例えば 手でドラッグしている最中に その手を背中の後ろへ回したりした場合です キャンセルは例えば ピンチをやめてオブジェクトを 落とすような意図的な終了とは異なります
ゲームをRealityViewにアタッチする時は TabletopInteractionオブジェクトの 実装を指定します
更新のコールバックは ジェスチャの更新のたびに呼び出されます コンテキストには対象となる要素や その配置場所といった 書き込み可能なプロパティがいくつかあり インタラクションのキャンセルや終了などの 変更を行う関数もあります
値にはジェスチャやインタラクションの フェーズ 提案される移動先 関連するポーズなど 読み取り可能な情報が含まれます インタラクションが更新される時が ゲーム状態を調整する機会です
アクションはゲーム状態に適用される 個別の操作で 駒を移動したり カードをめくったりすることです
アクションは指示されるとキューに入り 1つずつ実行されます
一般的な例は 親同士の間でのオブジェクト移動などです このコードスニペットでは 操作可能なオブジェクトを あらゆる有効な親要素に ドロップできるようにしています
ジェスチャが終了したという コールバックを受信したら 提示された有効な親があるかどうかを 確認します
ある場合は 操作のコンテキストにアクションを追加して その要素を提示された親要素に移動します
これでプレイヤーは盤上で駒を移動したり カードを引いたり サイコロを振ったりできるようになります
ゲーム状態の変化については ユーザーが完全にコントロールできます TabletopKitは 何が動かされたかについての情報を示し 何が起こるべきかについては ユーザーが選択できます 例えば チェスのゲームを作るとしたら まず 1つのモードでゲームのルールを紹介し 可能な動きを紹介して実演します そしてもう1つの練習モードで ルールを強制することなく 少しゲームを楽しめるようにします
ゲームメカニクスは 楽しいゲーム体験の重要な要素ですが ソーシャルダイナミクスも同じです それらをゲームごとに組み合わせるのです この時点でサンプルゲームは 技術的にプレイ可能です でも Apple Vision Proのゲームは 単にプレイ可能なだけではありません RealityKitはすでに ほとんどの作業を行って ゲームに魔法のような 視覚効果を与えてくれています 影と光を使った非常にリアルなモデルや 風変わりな定型アニメーションなど 制作できるものはみな RealityKitでレンダリングできます このサンプルアプリでは 駒がいい位置に到達すると ロボットが喜んではしゃぎ 悪い位置に着くと落ち込みます とても可愛いです
さっき見たように インタラクティブな要素は ゲームの進行に合わせて 自動的に動き回ります エンティティは手動でロードするので 様々な特殊効果を自由に追加し ユーザーの操作に合わせて トリガすることができます RealityKitで効果音を再生するのは 非常に簡単なので サイコロを転がす時に音を鳴らしてみます これが先ほど説明した インタラクションの更新コールバックです
ここで プレイヤーがサイコロを離すという ジェスチャの終了を監視します
オーディオライブラリコンポーネントで 好きな音を探します
音響リソースが見つかったら それをサイコロで再生するよう RealityKitに伝えるだけです RealityKitは 空間オーディオに対応しているため 音はどこでもサイコロがある場所で 鳴らすことができます
では 実際にサイコロを投げてみましょう
テーブルゲームは 他の人と 一緒にプレイする時が一番楽しいものです FaceTimeの空間Personaはすばらしい機能で 非常にリアルなので これを利用して新しい作品を生み出せば 家族や友人が同じ部屋にいない時でも 一緒にゲームを楽しむことができます
デフォルト設定がある ネットワークゲームの場合は 数行のコードで GroupActivitiesセッションを開始し それをフレームワークに渡します そこから優れた新機能や カスタムの空間テンプレートを活用して ゲーム体験をカスタマイズし プレイヤーや観戦者を室内の好きな場所に 配置することができます TabletopKitはマルチプレイヤー ネットワークに対応しており とても簡単に設定できます このフレームワークでは アクションが同期され 全プレイヤーのゲーム状態が一致します
プレイヤーがカードを拾うなどの アクションを送信すると それが検証され 決定論的な順序で ゲーム状態に追加されます
アニメーションや物理シミュレーションなど 処理負荷の高い機能の一部は 各自のローカルで処理されるため マルチプレイヤー画面は 素早くスムーズに動作し続けます
この例では プレイヤーがいつでも好きな時に SharePlayを開始できるよう ツールバーにボタンを用意してみます このコードは単純な SharePlayボタンを示しています SharePlayシンボルを使った SwiftUIボタンです
このボタンが押されたら 新しいGroupActivitiesセッションを 開始します
セッションが有効になったら それをTabletopKitに伝え そのGroupActivitiesセッションと調整します これで基本的な設定は完了です ゲームの状態は すべてのプレイヤー間で同期されます ゲームで独自の空間レイアウトを使用するなら 空間Personaの カスタムテンプレートAPIを利用できます
デフォルトでは 最初の設定時に記述した座席を使って GroupActivitiesセッションの デフォルトのテンプレートが定義されます
そのため このサンプルでは それぞれの人物が テーブル上の座席の脇に配置され 中央を向いて回転します
別の空間設定を使用したい場合は Custom Spatial Template APIを 利用して 任意のテンプレートを設定すれば デフォルトのテンプレートが オーバーライドされます 詳しくはビデオをご覧ください
TabletopKitを使えば ゲームに命を吹き込むことができます 魅力的な体験を かつてなく簡単に作り上げ FaceTimeの空間ペルソナで 社会的な交流をサポートすることができます
私たちはゲーム開発の際によくある 複雑な問題のいくつかを解決していますが ゲームの見た目や雰囲気 動作などは 皆さんが自由に決定できます
また 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) } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。