WWDC Quick Look 💓 By SwiftGGTeam
Customize spatial Persona templates in SharePlay

Customize spatial Persona templates in SharePlay

观看原视频

Highlight

visionOS 2 新增自定义 spatial template,开发者可以精确定义每个座位在 3D 空间中的位置、朝向和角色标签,让 spatial Personas 按活动阶段自动分组就座。


核心内容

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

但这三种模板无法覆盖所有场景。比如棋类游戏需要两个玩家面对面、其余人旁观;卡牌游戏需要庄家和玩家分坐两侧。visionOS 2 的自定义 spatial template 正是为了解决这个痛点。开发者可以精确定义每个”座位”在 3D 空间中的位置和朝向,并为座位指定角色标签(如”蓝队”、“红队”、“当前玩家”),参与者加入后系统会根据角色自动将他们的 spatial Persona 移到对应座位。

演讲者 Ethan 用一个叫 “Guess Together” 的猜词游戏做完整演示。这个游戏有四个阶段(欢迎、分类选择、组队、游戏),每个阶段使用不同的模板——分类选择用系统默认的 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

激活模板和分配角色

配置自定义模板只需要一行代码(14:59):

systemCoordinator.configuration.spatialTemplatePreference = .custom(TeamSelectionTemplate())

当参与者加入某个队伍时,调用 assignRole 让系统把他们移到对应座位(15:39):

systemCoordinator.assignRole(TeamSelectionTemplate.Role.blueTeam)

当参与者需要回到无角色座位(比如观众席),调用 resignRole16: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 应用。注意模拟参与者不会自动分配角色,所以有角色的座位不会被他们占据。

核心启发

  • 做什么:为桌游/棋牌类 App 设计对抗式座位模板——两个玩家座位面对面,观众座位在侧面。为什么值得做:这类 App 天然需要”对手”和”旁观者”的空间区分,内置模板无法做到。怎么开始:定义两个带角色(如 player1player2)的面对面座位,加上若干无角色观众座位,用 assignRole 在游戏开始时分配。

  • 做什么:在协作白板/设计工具中用 lookingAt 让参与者的座位面向彼此而非 App。为什么值得做:协作场景中讨论比看屏幕更重要,面向彼此让手势和表情沟通更自然。怎么开始:用 .lookingAt() 让相邻座位的 direction 指向对面的座位位置。

  • 做什么:为多阶段活动(如”选队 → 游戏 → 结算”)在不同阶段切换不同模板,配合 UI 状态同步。为什么值得做:每个阶段参与者的关系不同——选队时无差异,游戏时有角色分工,用匹配的模板让空间布局反映当前关系。怎么开始:在状态切换方法中更新 spatialTemplatePreference,同步调用 assignRoleresignRole,避免在过渡中间态切换模板。

  • 做什么:在 group immersive space 中根据座位位置放置 3D 提示物(如谜题面板、道具)。为什么值得做:共享空间的原点与模板原点一致,可以精确地把 UI 元素放在特定座位前方,参与者不用转头就能看到关键信息。怎么开始:开启 supportsGroupImmersiveSpace,用座位 position 加小偏移量定位 RealityKit 实体。

关联 Session

评论

GitHub Issues · utterances