Customize spatial Persona templates in SharePlay
Spatial Computing 进阶 20m

在 SharePlay 中自定义 spatial Persona 模板

Customize spatial Persona templates in SharePlay

2024年6月10日

在 Apple 官方观看视频

一句话判断

visionOS 2 让你可以自定义 FaceTime 中 spatial Persona 的座位布局——如果你的 SharePlay App 需要区分角色(比如棋手和观众、庄家和玩家),这个功能直接决定了用户体验的自然程度。

这场 Session 讲了什么

在 visionOS 的 FaceTime 中,spatial Personas 让参与者感觉像在同一个房间里。当有人启动 SharePlay 活动时,系统会将所有人按照一个 spatial template(空间模板)重新排列——定义每个人相对于共享 App 的初始位置。visionOS 1 提供了三种内置模板:side-by-side(并排弧线排列,适合视频类)、conversational(开放式圆圈,适合音乐类)、surround(封闭圆圈围绕共享内容,适合 3D 建模类)。

visionOS 2 新增了自定义 spatial template 能力。开发者可以精确定义每个”座位”在 3D 空间中的位置和朝向,并为每个座位指定角色标签(如”蓝队”、“红队”、“观众”)。当参与者在 FaceTime 中加入 SharePlay 活动后,系统会根据 template 将他们的 spatial Persona 放到对应位置。

演讲者 Ethan 用一个叫 “Guess Together” 的猜词游戏做完整演示。这个游戏有四个阶段(欢迎、分类选择、组队、游戏),每个阶段使用不同的 template——分类选择用系统默认的 side-by-side,组队和游戏阶段用自定义 template 来区分蓝队/红队/当前玩家/对方队伍的座位。

值得深挖的点

自定义 Template 的设计逻辑

自定义 template 不是随意的 3D 空间布局。它需要考虑 FaceTime 的空间感知机制:当用户按下 Digital Crown 重新定位时,他们会被放回 template 定义的初始座位。所以每个座位的朝向必须让参与者能自然地看到共享内容和彼此。

Guess Together 的游戏阶段 template 是一个很好的设计范例。当前玩家被放在计分板窗口左侧,面前有一个讲台——这个位置让玩家的注意力自然集中在需要猜测的队友身上。队友坐在共享 App 右侧,正对当前玩家。对手坐在计分板正前方,既能看到分数又能观察对方。这种布局在真实桌游场景中也很常见:出题人、猜题人、旁观者各有各的位置,不需要解释就知道谁在做什么。

关键的设计约束是:template 定义的是”起始座位”,不是”固定位置”。参与者随时可以自由移动、调整距离。template 只是确保活动开始时所有人有一个合理的初始排列。

模拟器中的 FaceTime 测试支持

visionOS 2 在 Xcode 模拟器中新增了对模拟 FaceTime 通话的支持。这对 SharePlay 开发来说是重大改进——以前你至少需要两台 Vision Pro 设备才能测试多人场景。现在你可以在模拟器中启动模拟 FaceTime 通话,测试你的 spatial template 是否按预期工作、参与者的位置关系是否合理、UI 是否被 Persona 遮挡等。

测试时需要注意:模拟器中的 Persona 位置和真实设备上可能略有差异,最终布局需要在真实设备上验证。但在开发阶段,模拟器的 FaceTime 支持已经足够让你完成大部分 template 调试工作。

代码片段

定义自定义 Spatial Template

import GroupActivities
import SwiftUI

// 为猜词游戏定义空间座位
struct GuessTogetherSpatialTemplate: SpatialTemplate {
    // 座位的方向:面向共享内容的中心
    static let frontCenter = SpatialTemplate.Element(
        role: .currentPlayer,     // 当前出题人
        position: .init(x: -0.8, y: 0, z: -0.5),
        direction: .init(x: 0.8, y: 0, z: 0.5)
    )
    
    static let teammate = SpatialTemplate.Element(
        role: .sameTeam,          // 同队队友
        position: .init(x: 0.8, y: 0, z: -0.5),
        direction: .init(x: -0.8, y: 0, z: 0.5)
    )
    
    static let opponent = SpatialTemplate.Element(
        role: .opposingTeam,      // 对方队伍
        position: .init(x: 0, y: 0, z: -1.5),
        direction: .init(x: 0, y: 0, z: 1.5)
    )
    
    var body: some SpatialTemplate {
        // 定义座位及其容量
        SpatialTemplateSeat(
            element: frontCenter,
            capacity: 1              // 只有一个人出题
        )
        SpatialTemplateSeat(
            element: teammate,
            capacity: .maximum        // 同队可以多人
        )
        SpatialTemplateSeat(
            element: opponent,
            capacity: .maximum        // 对方可以多人
        )
    }
}

坑点:position 和 direction 使用的是相对于共享 scene 中心的坐标,单位是米。方向向量指示的是座位”看向”的方向。如果你的共享内容是一个 window,z 轴负方向通常朝向内容正面。

根据活动阶段切换 Template

// 在 GroupSession 中根据游戏阶段切换 template
class GameManager: ObservableObject {
    var session: GroupSession<GuessTogether>?
    
    func transitionToPhase(_ phase: GamePhase) {
        switch phase {
        case .categorySelection:
            // 使用系统默认的 side-by-side template
            session?.spatialTemplatePreferences = .systemDefault
            
        case .teamSelection:
            // 切换到组队 template
            session?.spatialTemplatePreferences = .init(
                preferred: TeamSelectionTemplate.self
            )
            
        case .playing:
            // 切换到游戏 template,按角色分配座位
            session?.spatialTemplatePreferences = .init(
                preferred: GuessTogetherSpatialTemplate.self
            )
            // 系统会根据每个参与者的角色重新排列 Persona
        }
    }
}

坑点:template 切换是异步的,参与者从旧位置移动到新位置有一个过渡动画。不要在短时间内频繁切换 template,否则会出现参与者在多个位置间反复跳转的情况。

为参与者分配角色

// 使用 GroupSessionParticipant 的 metadata 传递角色信息
struct PlayerInfo: Codable {
    let team: Team
    let role: Role
}

enum Team: String, Codable {
    case blue, red
}

enum Role: String, Codable {
    case player, spectator
}

// 在 template 中根据角色匹配座位
extension SpatialTemplate.Element.Role {
    static let currentPlayer = SpatialTemplate.Element.Role("currentPlayer")
    static let sameTeam = SpatialTemplate.Element.Role("sameTeam")
    static let opposingTeam = SpatialTemplate.Element.Role("opposingTeam")
}

坑点:角色的分配需要在所有参与者之间同步——使用 GroupSession 的 messaging 机制确保每个人的角色信息一致。如果角色信息不一致,不同设备上的 template 匹配结果会不同。

最佳实践

设计自定义 template 时,先在纸上画出俯视图。标出共享内容的位置,然后根据真实桌游/会议的座位习惯安排参与者的位置。核心原则:每个参与者应该能同时看到共享内容和需要交互的人。如果一个座位需要”转头才能看到关键信息”,说明位置安排有问题。

template 的切换应该和游戏/活动的阶段转换同步。不要在中间状态的过渡期间切换 template——比如玩家正在选择队伍时不要改变座位,等选择完成后一次性切换到新布局。这样避免了参与者在不稳定状态下被移动的困惑。

还有什么值得关注

  • 如果你没有 Apple Watch App,Live Activity 点击后会展开成系统提供的全屏视图,里面有按钮可以直接在 iPhone 上打开你的 App。
  • 自定义 template 需要配合 “Build custom experiences with Group Activities” 和 “Build spatial SharePlay experiences” 两个前置 session 的内容,建议先看那两个。
  • 模拟器中的 FaceTime 测试支持是 visionOS 2 SDK 的新功能,确保你的 Xcode 版本是最新的。
WWDC 2024