ハイライト
visionOS 2 にカスタム spatial template が追加され、開発者は各座席の 3D 空間上の位置、向き、ロールラベルを精密に定義でき、spatial Personas がアクティビティ段階に応じて自動的にグループ化して着席します。
主要内容
visionOS の FaceTime は spatial Personas で通話者が同じ部屋にいる感覚を実現します。誰かが SharePlay アクティビティを開始すると、システムは spatial template(空間テンプレート)に従い全参加者を再配置——各参加者の共有 App に対する初期位置を定義します。visionOS 1 には 3 つの組み込みテンプレート:side-by-side(弧状配置、動画視聴向け)、conversational(開いた円、音楽向け)、surround(共有コンテンツを囲む閉じた円、3D モデリング向け)。
この 3 つではすべてのシナリオをカバーできません。ボードゲームでは 2 人のプレイヤーが向かい合い、残りは観戦。カードゲームではディーラーとプレイヤーが両側に分かれます。visionOS 2 のカスタム spatial template がこの課題を解決。開発者は各「座席」の 3D 空間上の位置と向きを精密に定義し、座席にロールラベル(「青チーム」「赤チーム」「現在のプレイヤー」など)を指定。参加者が参加すると、システムがロールに基づき spatial Persona を対応する座席へ移動します。
プレゼンター Ethan は「Guess Together」という言葉当てゲームで完全デモ。4 段階(ウェルカム、カテゴリ選択、チーム編成、ゲーム)があり、各段階で異なるテンプレート——カテゴリ選択はシステムデフォルトの side-by-side、チーム編成とゲーム段階は青チーム/赤チーム/現在のプレイヤー/相手チームの座席を分けるカスタム template。Kevin が後半でカスタム template 設計の核心原則を共有。
詳細
カスタム Spatial Template の定義
カスタムテンプレートは SpatialTemplate プロトコルに準拠する構造体。核心は elements 配列で、各要素は位置とオプションのロールを持つ seat(座席)(14:59)。
Guess Together のチーム編成段階テンプレートの例:
import GroupActivities
struct TeamSelectionTemplate: SpatialTemplate {
enum Role: String, SpatialTemplateRole {
case blueTeam
case redTeam
}
let elements: [any SpatialTemplateElement] = [
// Blue team:
.seat(position: .app.offsetBy(x: -2.5, z: 3.5), role: Role.blueTeam),
.seat(position: .app.offsetBy(x: -3.0, z: 3.0), role: Role.blueTeam),
.seat(position: .app.offsetBy(x: -3.5, z: 2.5), role: Role.blueTeam),
// Starting positions:
.seat(position: .app.offsetBy(x: 0, z: 4)),
.seat(position: .app.offsetBy(x: 1, z: 4)),
.seat(position: .app.offsetBy(x: -1, z: 4)),
.seat(position: .app.offsetBy(x: 2, z: 4)),
.seat(position: .app.offsetBy(x: -2, z: 4)),
// Red team:
.seat(position: .app.offsetBy(x: 2.5, z: 3.5), role: Role.redTeam),
.seat(position: .app.offsetBy(x: 3.0, z: 3.0), role: Role.redTeam),
.seat(position: .app.offsetBy(x: 3.5, z: 2.5), role: Role.redTeam)
]
}
キーポイント:
SpatialTemplateプロトコルはelements配列のみを要求(12:32)- 座席位置は
.app.offsetBy(x:z:)で定義。共有 App を原点とし、単位はメートル(12:09) - ロールは
SpatialTemplateRoleプロトコルに準拠する String 列挙で定義。座席に付与すると特定ロールの参加者用に座席を確保(13:49) - ロールのない座席は elements 配列の順序で埋まるため、順序が参加者の入場順序を決定(30:41)
テンプレートの有効化とロール割り当て
カスタムテンプレートの設定は 1 行(14:59):
systemCoordinator.configuration.spatialTemplatePreference = .custom(TeamSelectionTemplate())
参加者がチームに参加したら assignRole を呼び、対応座席へ移動(15:39):
systemCoordinator.assignRole(TeamSelectionTemplate.Role.blueTeam)
ロールなし座席(観客席など)に戻るには resignRole(16:00):
systemCoordinator.resignRole()
キーポイント:
systemCoordinatorはGroupSessionから取得。空間動作設定の核心オブジェクト- ロール割り当て後、FaceTime が参加者の spatial Persona をそのロールの空席へ自動移動
- ロール退出時は
resignRole()。システムがロールなし座席に戻す
座席の向き(Seat Direction)
デフォルトでは全座席が共有 App を向きますが、カスタムテンプレートで各座席の向きを精密制御可能。ゲームでは現在のプレイヤーがチームメイトを向き、チームメイトが現在のプレイヤーを向く必要がある(18:42):
struct GameTemplate: SpatialTemplate {
enum Role: String, SpatialTemplateRole {
case player
case activeTeam
}
var elements: [any SpatialTemplateElement] {
let activeTeamCenterPosition = SpatialTemplateElementPosition.app.offsetBy(x: 2, z: 3)
let playerSeat = SpatialTemplateSeatElement(
position: .app.offsetBy(x: -2, z: 3),
direction: .lookingAt(activeTeamCenterPosition),
role: Role.player
)
let activeTeamSeats: [any SpatialTemplateElement] = [
.seat(
position: activeTeamCenterPosition.offsetBy(x: 0, z: -0.5),
direction: .lookingAt(playerSeat),
role: Role.activeTeam
),
.seat(
position: activeTeamCenterPosition.offsetBy(x: 0, z: 0.5),
direction: .lookingAt(playerSeat),
role: Role.activeTeam
)
]
let audienceSeats: [any SpatialTemplateElement] = [
.seat(position: .app.offsetBy(x: 0, z: 5)),
.seat(position: .app.offsetBy(x: 1, z: 5)),
.seat(position: .app.offsetBy(x: -1, z: 5)),
.seat(position: .app.offsetBy(x: 2, z: 5)),
.seat(position: .app.offsetBy(x: -2, z: 5))
]
return audienceSeats + [playerSeat] + activeTeamSeats
}
}
キーポイント:
direction: .lookingAt(...)で指定位置または別の座席オブジェクトを向く(31:35)direction: .alignedWith(appAxis: .z)で App の座標軸に沿って整列(31:46)direction: .lookingAt(.app).rotatedBy(.degrees(30))で初期方向から指定角度回転(32:02)- 観客席は direction 不要。デフォルトで App を向く
Group Immersive Space
ゲーム段階では現在のプレイヤーに現在のパズルを表示。group immersive space を有効にすると、没入空間の座標原点が spatial template の原点と一致し、座席位置で 3D 要素を直接配置可能(21:41):
systemCoordinator.configuration.supportsGroupImmersiveSpace = true
キーポイント:
- デフォルトでは SharePlay の immersive space はプライベート。参加者は互いの spatial Personas を見られない
supportsGroupImmersiveSpace = trueで空間が共有され、全参加者が同一空間内に- RealityKit と GroupActivities はデフォルトでメートル単位。座席位置を RealityKit エンティティ位置に変換不要で直接使用可能
Xcode 16 で FaceTime 通話をシミュレート
Xcode 16 は visionOS シミュレーターで FaceTime 通話をシミュレート可能(08:28)。Features > FaceTime でリモート参加者数を選択し、実機なしで SharePlay アプリを完全テスト。シミュレート参加者はロールを自動割り当てしないため、ロール付き座席は占有されない。
重要ポイント
-
何をするか:ボード/カードゲーム向けに対戦型座席テンプレートを設計——2 人のプレイヤー座席を向かい合わせ、観客席を側面に。なぜ価値があるか:「対戦相手」と「観戦者」の空間区分が必要で、組み込みテンプレートでは不可能。始め方:2 つのロール付き(
player1、player2など)向かい合わせ座席と、ロールなし観客席を定義。ゲーム開始時にassignRole。 -
何をするか:協力ホワイトボード/デザインツールで
lookingAtを使い、参加者の座席を App ではなく互いに向ける。なぜ価値があるか:協作では画面より議論が重要。向かい合うことでジェスチャーと表情のコミュニケーションが自然に。始め方:.lookingAt()で隣接座席の direction を向かい側の座席位置に向ける。 -
何をするか:多段階アクティビティ(「チーム選択 → ゲーム → 集計」など)で段階ごとに異なるテンプレートに切り替え、UI 状態と同期。なぜ価値があるか:各段階で参加者の関係が異なる——選択時は差なし、ゲーム中はロール分担。マッチするテンプレートで空間配置が現在の関係を反映。始め方:状態切り替えメソッドで
spatialTemplatePreferenceを更新。assignRoleまたはresignRoleを同期呼び出し。遷移中間状態でのテンプレート切り替えを避ける。 -
何をするか:group immersive space で座席位置に基づき 3D ヒント(パズルパネル、小道具)を配置。なぜ価値があるか:共有空間の原点がテンプレート原点と一致。特定座席の前方に UI 要素を精密配置でき、参加者が首を回す必要がない。始め方:
supportsGroupImmersiveSpaceを有効化。座席 position に小さなオフセットを加え RealityKit エンティティを配置。
関連セッション
- Meet TabletopKit for visionOS — TabletopKit で visionOS ボードゲームをゼロから構築。カスタム spatial template と自然に組み合わせ可能
- Dive deep into volumes and immersive spaces — visionOS の volume と immersive space カスタマイズの詳細
- Design great visionOS apps — 没入感とジェスチャー操作を含む visionOS アプリ設計原則
- Build a spatial drawing app with RealityKit — RealityKit で空間描画 App を構築。共有空間での 3D コンテンツ配置
コメント
GitHub Issues · utterances