大多数浏览器和
Developer App 均支持流媒体播放。
-
自定支持同播共享的空间自影像模板
了解如何在 visionOS 同播共享体验中使用自定的空间自影像模板来微调自影像相对于 App 的放置方式。我们会介绍如何在示例 App 中采用支持同播共享的空间自影像模板、调换参与者的座位,以及在模拟器中测试你的更改。我们还将介绍自定空间模板设计方面的推荐做法,帮助你让自己的体验大放异彩。
章节
- 0:00 - Introduction
- 1:01 - SharePlay on visionOS
- 4:50 - Build "Guess Together"
- 23:41 - Play Guess Together
- 25:13 - Design for Spatial Personas
资源
相关视频
WWDC23
WWDC22
WWDC21
-
下载
- 大家好 我是 Ethan - 我是 Kevin 我们是 Spatial FaceTime 团队的工程师 很高兴可以为大家介绍 visionOS App 的构建过程 让 App 真正充分展现 空间自影像提供的 临场感 在整个过程中 你将了解 开发同播共享 App 时可以使用的 新的 Xcode 功能 和 GroupActivities API 本场讲座分为三个部分 首先 我将介绍 visionOS 上的 FaceTime 通话和同播共享功能 然后 我将展示 “Guess Together”的构建过程 通过这个示例 App 来展示 用于构建自定 空间自影像模板的新 API 我还将展示如何使用新支持的 模拟 FaceTime 通话功能 在模拟器中测试 visionOS 同播共享 App 最后 Kevin 将介绍如何 为空间自影像设计 出色的同播共享体验 我们先来了解 visionOS 上的 FaceTime 通话和同播共享
visionOS 上的 FaceTime 通话 利用空间自影像 营造真实的临场感 通过空间自影像进行 FaceTime 通话 会让你感觉与参与者共处一室 同播共享 App 例如这个视频中 展示的无边记 App 是这种体验的核心部分 通过采用同播共享 你可以将 FaceTime 通话提供的 共享情境扩展到你的 App
当你在 visionOS 上采用同播共享时 你负责让 App 在视觉上保持一致 App 的 UI 应该在所有 通话参与者之间保持同步 相应地 FaceTime 通话 负责维护共享的空间情境 它会以一致的方式 围绕 App 排列参与者 这种视觉和空间的一致性 共同营造出了一种错觉 让参与者感觉他们正在共享空间中 使用同一个 App 他们应该能够通过指向 App UI 或在 UI 上使用手势来进行沟通 就像所有人在现实共享空间中 聚集在白板周围一样 在使用空间自影像的典型 visionOS FaceTime 通话中 一开始会将参与者排列成一个圆圈 让他们能够轻松地相互交流互动 但是 参与者可以随时 开始同播共享活动 这时 通话会转换为空间模板 重新排列每位参与者的 空间自影像 相对于新共享 App 的位置 重要的是 这个模板只是定义了 每位参与者的起始位置 也就是座位 在任何时候 都可能有 一个或多个参与者 开始在空间内移动 打破初始模板的排列方式 但是 如果参与者使用他们 设备上的数码旋钮重新定位 他们会回到起始座位
GroupActivities 框架提供各种 可供同播共享 App 采用的 内置系统空间模板 例如 视频 App 可能更适合采用 我们目前所用的模板: 并排模板 它会将参与者 沿着一条曲线放置 让 App 位于他们面前 但音乐 App 可能 更适合采用对话模板 它会将参与者排列成 一个有缺口的圆圈 将 App 放置在 圆圈边缘的缺口处 而 3D 建模 App 可能 更适合采用环绕模板 它会让参与者围绕共享空间容器 排列成一个封闭圆圈
所有这些模板都定义了 相对于共享 App 的座位 在共享活动开始时 空间自影像 将被放置在这些座位上 无论他们之前位于 共享空间的什么位置 但如果这些内置系统模板 都不太适合你的活动 该怎么办? 例如 也许你正在开发 一款国际象棋 App 你想让两名玩家面对面坐着 将其余参与者排列在一旁观看 或者 你在开发一款纸牌游戏 庄家应该被放置在 其他玩家的对面 借助 visionOS 2 中的新功能 你可以创建自定空间模板 让你的 App 完全控制 空间自影像相对于 共享场景的放置方式 我非常喜欢自定空间模板 它们解锁了各种可能性 能够 在 visionOS 上提供独特的社交体验
这个讲座的下一部分 将重点介绍更高级的 同播共享概念 如果你刚接触同播共享、 群组活动或 visionOS 建议先观看 “使用群组活动打造个性化体验” 和“构建空间同播共享体验” 以便充分利用好本次讲座 前者介绍了如何 在活动参与者之间 同步 App 状态 后者则概要介绍了 空间同播共享功能
看完这两个讲座后 我们来看看 我一直在开发的示例 App: “Guess Together” “Guess Together”是一款 团队合作型短语猜谜游戏 在使用空间自影像的 FaceTime 通话中运行 它有点像字谜游戏 玩家会分成两队 轮流参与游戏 轮到你时 你会收到一个秘密短语 比如名人的名字或成语 然后尝试使用任何必要的方法 让你的团队猜出这个短语 只要不说出短语本身就行 “Guess Together”分为四个阶段 当你首次启动 App 时 将显示一个欢迎 UI 邀请你使用当前的 FaceTime 通话 创建一个同播共享群组会话 同播共享开始后 “Guess Together” 将进入类别选择阶段 你将在这个阶段 决定想玩的类别 例如 也许你想玩从历史 事件中提取的短语 或者更简单一点 比如不同的水果和蔬菜 接下来该选择团队了 你将决定是加入蓝队还是红队 最后 可以开始游戏了 “Guess Together”将展示一个 包含记分牌和计时器的视图 活跃玩家面前还将 显示第二个视图 其中包含他们的队友 需要猜测的秘密短语 除了在同播共享开始前 显示的欢迎阶段 其余每个阶段 都需要采用空间模板 就像你会慎重考虑如何 为 App 中的每个屏幕 实施用户界面一样 对于 visionOS 上的同播共享 App 也需要慎重考虑 如何相对于你的共享场景 排列空间自影像
类别选择屏幕将显示 一个共享用户界面 所有参与者都应该能够轻松 看到这个界面并进行交互 这时 所有参与者 都没有任何不同 既没有活跃玩家 玩家也没有分组 出于这些原因 这里适合选择 系统提供的并排模板 它是适用于“Guess Together” 这类窗口化 App 的默认模板 这个模板正是为了 这个目的而设计的 它将参与者面向共享 App 排列成一条曲线 让所有人都能轻松访问界面 团队选择屏幕具有类似的要求 但有一个关键区别 这时 参与者将被分成不同的团队 我认为最好能让 App 将你的座位放置在队友旁边 由于需要按团队分配座位 团队选择屏幕应该 使用自定空间模板
一开始 参与者将坐在 这五个面向 App 的座位上 类似于并排模板 但当他们加入团队后 “Guess Together”会将他们 分配到蓝队或红队的座位 FaceTime 通话会将他们 重新安排到新的座位 他们会发现自己坐在 其他团队成员旁边
游戏阶段明显最需要 使用自定空间模板 参与者可以在游戏中 扮演多种角色 包括当前玩家和 对方团队中的玩家等
活跃玩家将坐在 记分牌窗口的左侧 他们前面会放置一个讲台 他们的队友应该 位于他们正对面 也就是共享 App 的右侧 对方团队将直接坐在 记分牌窗口正前方
这就是“Guess Together” 接下来 我们将介绍 如何一起实施这些模板 这需要对同播共享活动本身 进行一些迭代和测试 幸运的是 Xcode 16 提供 绝佳的迭代和测试体验
Xcode 16 的新功能可让你在 visionOS 模拟器中创建模拟 FaceTime 通话 这为针对 visionOS 开发 同播共享 App 带来了颠覆性变化 你无需在设备上进行实际 FaceTime 通话 即可完全构建 App 要开始模拟 FaceTime 通话 首先打开菜单栏中的 “Features”菜单 然后打开“FaceTime”子菜单 在这里选择所需的 远程参与者配置 一定要尝试 App 的各种配置 这样就可以了 我们来试一下
我已经在 Xcode 中打开了 “Guess Together”项目 准备在模拟器中运行这个项目
这就是“Guess Together” 由于我现在还没有 加入同播共享会话 它显示的是欢迎阶段
接下来我将激活 FaceTime 通话 将鼠标移到“Features”菜单 再移到“FaceTime”子菜单 然后选择 “User and 4 Spatial Participants”
就像这样 我现在加入了 模拟 FaceTime 通话 我将使用空间按钮来激活 我的空间自影像
现在 “Play Guess Together” 按钮处于活动状态 我也做好了开始同播共享的准备 点按这个按钮
App 现在位于同播共享会话中 并显示类别选择阶段 如果向左平移
可以看到模拟参与者 已被移入并排空间模板中 我们来看看这在代码中 是如何配置的
“Guess Together”在 SessionController 类中 管理它与当前同播共享 群组会话的交互 我现在来打开这个类
SessionController 负责 在给定同播共享会话期间 跟踪和同步 App 的所有状态 它会存储会话本身 使用 GroupSessionMessenger 与其他参与者同步状态 并使用会话的 SystemCoordinator 来配置空间自影像 然后 它提供在 App 中 更新各种状态的方法 例如进入团队选择阶段、 加入团队和开始游戏 这里已经完成了很多操作 我们来盘点一下还需要做些什么 首先 我们需要为团队 选择阶段设计并配置 自定空间模板 完成后 我们将进入游戏阶段 将模板整合到一起 然后 我们将为游戏阶段 配置一个群组沉浸式空间 在活跃玩家的座位前面 放置一个包含当前 游戏短语的讲台
最后 完成 App 的所有配置后 我们将加入 FaceTime 通话 实际玩一下“Guess Together” 方便你了解整个体验 是如何顺畅进行的
提醒一下 这就是我们的 团队选择模板的预期效果 App 前方有五个观众座位 两边各有一组容纳三人团队的座位
我们先来构建一个只包含 五个起始座位的模板 要定义空间模板 需要创建 符合 SpatialTemplate 协议的类型 我将创建一个名为 TeamSelectionTemplate 的 struct 并让它符合 SpatialTemplate 协议
SpatialTemplate 的主要要求 是一组模板元素
这些元素被定义为 具有给定位置的座位
一个座位可以容纳 一个空间自影像 现在我需要定义座位的位置
座位位置是相对于 共享 App 的位置来定义的 我想将第一个座位 放在 App 的正前方 所以将 X 轴偏移值设为 0 使它与 App 的中心对齐 然后将 Z 轴偏移值设为 4 使它位于 App 前方 4 米处 我会为第二个座位设置 相同的 Z 轴偏移值 但这次将它的 X 轴偏移值设为 1 使它位于第一个 座位的右侧一米处 首先将座位放置在我的 App 中 然后让它在 App 前方 沿着 Z 轴偏移 4 米 对于第二个座位 首先 将它放在相同的位置 然后调整 X 轴偏移值 使它向右偏移 1 米
对第三个座位执行相同的操作 但这次向左偏移 1 米 将剩下的两个座位分别放在 左右两侧再偏移 1 米的位置
现在我们已经通过创建符合 SpatialTemplate 协议的 struct 定义好了团队选择模板 在定义模板中的每个座位时 我们都通过指定 X 轴和 Z 轴值 提供了一个相对于 App 的偏移位置 这些值以米为单位 开局很不错 但我们 还需要为红队和蓝队安排座位 我们需要从一开始的 5 个座位着手 沿着一定角度再设置 6 个座位 然后对它们进行标记 分别预留给蓝队和红队成员 首先创建一个座位 使它 相对于模板最左侧的座位 向左偏移 0.5 米 并且向前偏移 0.5 米
继续使用这种模式来放置 剩下的 2 个蓝队座位 但现在我需要通过某种方式 标记这些座位 将它们预留给蓝队 这时就需要用到 SpatialTemplateRole 通过将角色附加到座位 可以将座位预留给已经为自己 分配了相应角色的参与者 定义角色时 需要创建符合 SpatialTemplateRole 协议的类型 我将创建一个具有 String 原始值 并且符合 SpatialTemplateRole 协议的 Role enum 然后创建 blueTeam 和 redTeam 角色
很好 现在 我可以将 blueTeam 角色分配给蓝队座位 将它们预留给 blueTeam 的成员
对于 blueTeam 座位 我们 创建了一个 blueTeam 角色 它具有符合 SpatialTemplateRole 协议的 enum 然后 我们从模板的起始座位着手 沿一定角度放置了 3 个座位 并为每个座位分配了这个角色 这样可以将座位预留给蓝队的参与者
我们来为红队座位 执行相同的操作
这样一来 模板就完成了 现在我们需要在团队 选择阶段激活这个模板 我将在 SessionController 中 完成操作 “Guess Together”使用这个 updateSpatialTemplatePreference 方法来管理它的空间模板 我将更新 teamSelection case 并访问 SystemCoordinator 配置
我没有设置内置偏好 而是使用新的自定方法并提供 TeamSelectionTemplate
我们现在已经创建 并配置了自定模板 但还需要完成一个重要步骤: 角色分配 当本地参与者的团队发生变化时 我们需要告知 FaceTime 通话 以便系统知道需要 将他们移到新座位
“Guess Together”经过设计 可以使用这个 updateLocalParticipantRole 方法 来处理角色分配 我将首先切换到当前的游戏阶段 因为我需要的角色 依赖于这个阶段
在团队选择阶段 本地参与者空间模板角色 依赖于他们的团队 所以我要切换到团队 现在 我们就准备好 分配蓝队角色了 我将访问 systemCoordinator 并使用 assignRole 方法 然后传入我们刚才 定义的 blueTeam 角色
对红队采用相同的模式
如果我的团队是空值 表示我应该坐在观众席 也就是没有角色 我可以直接在 systemCoordinator 中 使用 resignRole() 方法 现在是时候更新 类别选择阶段的角色了 选择类别使用的是 内置的并排模板 不附加任何角色 我想确保在进入这个 阶段时放弃我的角色 以防我仍然保留着 上一个阶段的角色
很好 我们来试一下
再次运行 App 这一次进入团队选择阶段
当我加入红队时 我会移到红队座位上
加入蓝队时 会移到蓝队座位上
快速回顾一下 向空间模板座位添加角色后 相应座位会预留给 具有这个角色的参与者 FaceTime 通话会将 这个座位保持空座 直到参与者为自己 分配相关角色 要向本地参与者分配角色 需要在 systemCoordinator 中 使用 assignRole 方法 如果有预留给 相应角色的空座位 FaceTime 通话会将本地 参与者的空间自影像 移到那个座位 要让本地参与者回到没有角色的座位 可以在 systemCoordinator 中 调用 resignRole 方法 来放弃本地参与者的角色
好的 第一个任务完成了 后面两个任务应该会快一些 我们来看用于 游戏阶段的空间模板
这里展示了我们 游戏模板的预期效果 我想让活跃玩家 坐在队友的对面 当参与者轮流玩游戏时 他们应该 循环坐入和离开这个活跃玩家座位 对方团队的玩家应该 坐在旁边的观众座位上 到目前为止 我们用来创建 团队选择模板的代码 并没有什么新内容 但我想对这个模板进行 一个额外的自定设置: 座位方向 默认情况下 空间模板座位 面朝共享 App 但实际上 你可以自定座位的方向 使它朝向任意给定点 对于这个模板 我想让 活跃玩家看向活跃团队 同时也让活跃团队 看向活跃玩家 对于观众座位 保留 直接面朝 App 的 默认配置是合理的 我现在打开游戏模板 我已经完成了这个 模板的大部分代码 因此不需要担心这里的细节 只需设置每个座位的方向 这个模板定义了三组座位 玩家座位、活跃团队座位 和观众座位
请注意 在创建这个模板时 我将活跃团队位置 定义成了它自己的变量 这样一来 我可以在设置玩家 座位的方向时重复使用这个位置 我将提供 direction 参数 并使用 lookingAt 方法 来设置玩家座位的方向 使它朝向这个位置
现在 玩家座位将面向 活跃团队座位的中心 接下来设置团队座位的方向 我将再次提供 direction 参数 这一次直接将玩家座位 传递到 lookingAt 方法
好的 这样就完成了模板
现在 我们需要在游戏 阶段激活这个模板 并更新“Guess Together”的 角色分配逻辑 让我们回到 SessionController
首先使用我们用于团队选择的 相同模式来配置游戏模板
现在该进行角色分配了 在这种情况下 角色 取决于谁是活跃玩家 我将创建一个包含 三个分支的条件
第一个分支用于活跃玩家 第二个用于活跃团队 第三个用于观众 再次使用 systemCoordinator 和 assignRole 方法
针对活跃团队采取相同的操作
最后 对于观众成员 直接放弃当前角色
很好 我们来尝试运行游戏阶段
这个阶段开始后 我的对面 是我的队友将要坐的座位 记分牌在我左侧
观众在我右侧 这将为“Guess Together” 玩家打造出色的体验 因为当他们每次 成为活跃玩家时 都不需要自己调整方向 你可能想知道为什么 没有模拟参与者 坐在房间对面的团队座位上 这是因为模拟参与者 永远不会为自己 分配模板中的角色 他们始终都会坐在未添加 角色的第一个空位上
第二个任务就完成了 下面来看看 游戏阶段的沉浸式空间 在游戏阶段 我们需要通过 一种方式来向活跃玩家显示 当前的秘密短语 虽然我可以使用 只对活跃玩家显示的 第二个私密窗口 但我认为打开一个 沉浸式空间会更好 这样可以保持共享空间情境
我可以在活跃玩家面前 放置一个额外的共享 UI
默认情况下 当你在同播共享中 打开沉浸式空间时 它会成为 每个参与者的私密空间 这意味着沉浸式空间中的 参与者将无法看到 彼此的空间自影像 如果要构建一个在参与者 之间共享的沉浸式空间 可以在 systemCoordinator 配置中选择加入群组沉浸式空间 为群组沉浸式空间 打造自定空间模板时 你实际上可以相对于 模板中的座位来放置 UI 元素 因为模板的原点 也是共享空间的原点 我们来试一下
首先选择加入共享 群组沉浸式空间 访问 systemCoordinator 配置 并将 supportsGroupImmersiveSpace 属性设置为 true
现在我们可以添加 秘密短语讲台了 我已经开始添加了 来看一下吧 展开 ImmersiveSpace 群组 然后打开 PhraseDeckPodiumView
PodiumView 被定义为带有 SwiftUI 附件视图的 RealityView 讲台位置是使用这个 updatePodiumPosition 方法设置的 我们将它放置在 活跃玩家座位面前 首先移动讲台 让它 与游戏模板中定义的 玩家座位的位置相匹配 然后 将它偏移到活跃玩家 面前半米左右的位置处 请注意 我可以在这里直接 使用活跃玩家座位的位置 RealityKit 和 GroupActivities 的默认长度单位都是米 因此无需进行任何转换 由于我们选择加入了 群组沉浸式空间 因此 App 沉浸式空间的原点 也是它的空间模板的原点
我们来试一下
现在 活跃玩家一进入 游戏阶段 他们的对面 就是自己的团队成员 并且线索讲台就在他们面前
我迫不及待想要试玩了 这样就可以了
接下来就可以实际玩一下 “Guess Together”了 我将在另一个房间和你 进行 FaceTime 通话
嘿 Kevin 嘿 Ethan 我邀请了 Gabby 和 Mia 嗨 你好 你好 我们来玩“Guess Together”吧 我们要玩什么类别? 我想关掉蔬菜类别 好主意 我要关掉电影和电视类别
好的 我们可以选队了吗? 来选吧
我要加入蓝队
我选红队
我也要加入红队
那我显然就是蓝队了
听起来不错 大家准备好了吗? 我们开始玩吧
看来我第一个上场 祝你好运 开始吧
好的 这是一种水果 通常是红色或绿色的 很甜 很受欢迎 个头不太大 喔 苹果 对 好的 这是一种乐器 它的名称实际上是它全名的 缩写 意思是“非常响亮” 你在说什么? 它有 88 个键 钢琴
没错 哈哈
很有趣 干得漂亮 各位
谢谢 Ethan 打造了 这样一个出色的示例 App 和朋友一起玩“Guess Together” 真的很有趣 我们已经了解了如何构建 使用自定空间模板的 App 下面来了解一下针对 空间自影像设计 同播共享体验的过程 为 visionOS 设计新的 同播共享体验的第一步 是选择你将要使用的 空间自影像模板的类型 具体来说 就是选择使用 自定模板或内置系统模板
你应该问自己的第一个问题是 自定模板可以改善我的体验吗? 在决定创建完全自定的 空间自影像模板之前 你应该确信这样做 确实可以改善你的体验 系统空间自影像模板 经过精心调整 可以利用空间自影像 打造出色的体验 如果你希望所有参与者 都坐在 App 前面 建议使用并排模板 它非常适用于 窗口化 App 和媒体观看活动 对话模板 会让参与者始终关注彼此 并将 App 放置在附近 如果你的 App 是一个空间容器 并且你希望所有参与者 围着它坐成一圈 就非常适合使用环绕模板 与自定模板相比 使用 系统模板有几个好处 首先 它们可以为 App 的用户 提供更加舒适的熟悉体验 就像我们会建议 尽可能在 App 界面中 使用标准 UI 控件一样 使用用户已经熟悉的 空间自影像模板 也是个不错的做法
此外 系统模板会根据通话中的 空间自影像数量 自动进行调整 即使参与者在活动期间 离开或加入通话也是如此 始终有足够的座位 上限是 FaceTime 通话支持的最大数量 确保每个人都可以看到彼此 系统模板还会根据 共享场景的大小 进行动态调整 确保 每个人都能使用 App
假设你已经评估了所有系统模板 仍然认为完全自定模板 可以改善你的体验 那么你应该问自己的 下一个问题是 使用多个模板可以 改善我的体验吗?
建议不要把你的体验 当成一个不可拆分的整体 设计“Guess Together”时 我们没有为整个 App 选择单个自定模板 而是 单独考虑了游戏的每个阶段 混合使用了系统 提供的模板和自定模板 你可能会发现 对于活动的不同阶段 你对这些问题的回答是不一样的 所以我建议在适当的时候 针对体验的不同部分 使用不同的模板 在继续操作之前 你还应该问自己一个问题
我的体验如何支持不使用 空间自影像的参与者?
在为 visionOS 设计同播共享 体验时 你可能会一不小心 只考虑那些在同播共享活动中 使用空间自影像的参与者 但这可能是不够的 如果你要构建多平台 同播共享活动 可能会有参与者使用不支持 空间自影像的平台 即使你的 App 专为 visionOS 设计 参与者还是可以在同播共享 活动期间随时关闭 自己的空间自影像 在设计体验时 这两种情况都要考虑 这就是模板选择的注意事项 现在你已经准备好 定义座位了 定义座位时 你应该始终 遵循几个关键的最佳实践 应该为每个空间自影像 安排一个座位 应该在座位之间 留出足够的间距 并且应该慎重且有目的地 安排座位顺序 假设我要为一个双人棋盘游戏 构建模板 比如国际象棋 我的初始模板草稿 看起来可能是这样的 我放置了两个相对的座位 App 的空间容器位于座位之间 如果只有两个空间自影像 加入我的同播共享活动 这种配置是行得通的 但如果有第三位参与者 加入活动 并且想要使用 空间自影像 体验就会很糟糕 因为我没有为他们创建额外的座位 FaceTime 通话无法 将他们的自影像 与其他参与者放置在一起 作为应变方案 FaceTime 通话会将 第三位参与者单独放在内置模板中 但这意味着下棋的两个人 将无法看到第三个人 而第三个人也看不到前两个人 这就是为什么一定要为每个 空间自影像安排座位的原因 如果我在模板中再增加三个座位 现在就有足够的座位来容纳 FaceTime 单次通话 支持的空间自影像数量上限 也可以为参加活动的每个人 打造更好的用户体验 为每个空间自影像 提供座位很重要 同样重要的是 在每个自影像的座位之间 留出足够的空间 我们建议座位之间 至少相距一米 这样可以留出足够的空间 让参与者稍微调整一下座位 又不会让他们感觉 挤到了隔壁的参与者
另外一定要记住 当自影像彼此靠得太近时 他们会消失 并被替换为 静态联系人照片 通过确保留出 足够的座位间隔 你可以确信活动参与者 始终都能舒适地看到 彼此的空间自影像
最后 有目的地安排 座位顺序也很重要 建议使用每一种可能的空间 自影像组合来检查模板效果 确保它能正确填充座位 例如 如果你在设置自定模板时 有三个空间自影像 那么将按顺序使用 你指定的前三个座位
有新参与者加入时 这个示例 将从中心向外填充座位
如果你将座位顺序定义为从左到右 系统将按这个顺序填充参与者 当座位没有坐满时 模板可能会给人一种 不平衡的感觉
你可以使用模板的 elements 数组中提供的座位顺序 来定义座位填充顺序 这是一个简单的模板 它将座位面向 App 排成一行 “Guess Together”针对团队 选择阶段使用了类似的方法 请注意 elements 列表中的 座位顺序也是参与者 加入通话时填充座位的顺序
如何为自定空间模板 定义座位就介绍到这里 但在继续之前 你还需要 问自己一个非常重要的问题 当参与者首次坐在各个座位上时 你希望他们面朝哪个方向? 换句话说 每个座位的 焦点在哪里? 默认情况下 座位将 面朝 App 的中心 但你可以完全控制座位的方向 当你将 App 的用户放置到 座位上时 你需要帮助他们了解 焦点应该在哪里 他们是否应该面朝 App? 还是面朝另一位参与者? 或者对着完全不同的地方? 我们新推出的 API 提供了许多工具 能帮助你适当调整座位方向 在“Guess Together”的游戏模板中 我们使用 lookingAt 方法 让活跃玩家的座位 面朝他们的队友 如果你想让座位面朝任何 其他具体的方向 也可以使用 lookingAt 方法提供 任何空间模板元素位置 你也可以使用 alignedWith 方法 将座位与 App 的正交轴对齐 例如 X 轴或 Z 轴 例如 如果你想将 一排座位排成一行 全都直接垂直于 App 所在的平面 那么你可能需要与 Z 轴对齐 最后 创建初始座位方向后 你始终可以通过指定 角度值或辐射值来旋转座位 我们已经考虑了每个 座位的方向和位置 接下来应该考虑模板中的 哪些座位是特殊座位了 换句话说 是否需要 预留任何座位?
不要问自己哪些座位 需要分配角色 而是考虑哪些座位是特殊的 或者需要预留 以我们之前看过的 国际象棋模板为例 我想确保在每队的 活跃玩家入座之前 玩家座位始终保持空座 即使一位或两位玩家 没有启用空间自影像 他们的座位仍然应该留空 预留给他们 以便 之后需要时入座 另一方面 观众座位 不是特殊座位 不应该分配角色 你可能会想在这里创建 第三个角色 仅供观众使用 但这可能会带来 更糟糕的用户体验 因为当每个参与者请求 观众角色时 会造成延迟 不如让 FaceTime 通话直接 将他们放置在没有角色的座位 我们来回顾一下 尽可能 依赖没有角色的座位 以便 FaceTime 通话立即 将参与者放置在座位上
使用角色来为需要入座 特定位置的参与者预留座位 请记住 并不是所有 参与者都能获得角色 因为他们可能位于不同的平台 或者停用了空间自影像
现在你已经考虑了 要使用哪种模板类型 如何放置座位和调整座位方向 以及是否应该为特定 参与者预留座位 你应该能够使用出色的自定空间 模板来打造同播共享体验了 但还有最后一个设计要点 需要考虑一下:
避免意外 在针对空间自影像 设计同播共享 App 时 你可以做的最重要的事情之一 就是确保空间自影像模板 永远不会让 App 用户感到意外 使用 App 的自定空间模板时 你有很大的权力 通过模板转换和角色更改 你的 App 可以移动 参与者的空间自影像 这是他们在 FaceTime 通话中的 共享空间里的呈现形式 在设计过程中 一定要考虑 每当你更改模板或使用角色 将参与者分配到新座位时 他们会有怎样的反应 我们来看一些通用准则
尽量减少模板转换
如果通过移动或旋转 座位、交换角色 或者在模板类型之间 切换来更改模板 可能会让参与者晕头转向 因此最好尽可能 减少这样做的频率 在“Guess Together”中 每个阶段都使用单个模板 让座位保持在相同的位置 只在必要时使用角色 让参与者在座位之间移动
如果你确实要转换模板 请在转换后提供视觉上下文线索 帮助参与者自己找到方向 这归根结底可能还是要指定 座位方向 确保每个座位的 视角内都有一个视觉锚点 它可能是 App 本身 也可能是其他参与者
最后 尽量让模板更改 与明确的参与者操作相关联 “Guess Together”中的团队选择 阶段就是一个很好的例子 它将角色更改与参与者按下按钮的 操作相关联 从而让他们加入蓝队
或红队
在设计过程的每个阶段 你都应该 考虑如何最有效地避免糟糕的意外 为参与者带来 真正令人愉悦的惊喜 我们来总结一下 建议下载 “Guess Together”示例代码 和朋友一起试玩一下 它非常有趣 还可以让你充分了解 空间模板带来的可能性 使用 Xcode 16 的 visionOS 模拟器中 新推出的模拟 FaceTime 通话功能 测试你的同播共享体验 这款强大的工具可以 帮助你为 visionOS 开发出色的同播共享 App 无论你的 App 使用哪种空间模板 考虑自定空间模板 是否可以改善你的体验 以及有目的地使用现有系统模板 可以带来哪些好处 最后 当你开始为 visionOS 设计出色的同播共享体验时 记住要避免糟糕的意外 自定空间模板是一款强大的工具 可以在 FaceTime 通话中 实现真正令人兴奋的体验 但需要经过深思熟虑
我们非常期待在 visionOS 上 看到大家用自定空间模板 打造的体验 谢谢观看
-
-
12:32 - Initial team selection template
// Team selection template – custom spatial template import GroupActivities struct TeamSelectionTemplate: SpatialTemplate { let elements: [any SpatialTemplateElement] = [ .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)), ] }
-
13:31 - Completed team selection template with seat roles
import GroupActivities /// The custom spatial template used to arrange Spatial Personas /// during Guess Together's team-selection stage. /// /// The team selection template contains three sets of seats: /// /// 1. Five audience seats that participants are initially placed in. /// 2. Three Blue Team seats that participants are moved to /// when they join team Blue. /// 3. Three Red Team seats. /// /// ``` /// ┌────────────────────┐ /// │ Guess Together │ /// │ app window │ /// └────────────────────┘ /// /// /// % $ /// % $ /// Blue Team % $ Red Team /// * * * * * /// /// Audience /// ``` 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) ] }
-
14:59 - Configuring a custom spatial template
systemCoordinator.configuration.spatialTemplatePreference = .custom(TeamSelectionTemplate())
-
15:39 - Assigning the local participant a spatial template role
systemCoordinator.assignRole(TeamSelectionTemplate.Role.blueTeam)
-
16:00 - Resigning the local participant from a spatial template role
systemCoordinator.resignRole()
-
17:00 - Spatial template roles
// Associating a role with a seat .seat(position: .app.offsetBy(x: -2.5, z: 3.5), role: TeamSelectionTemplate.Role.blueTeam) // Assigning the local participant a role systemCoordinator.assignRole(TeamSelectionTemplate.Role.blueTeam) // Resigning the local participant from their current role systemCoordinator.resignRole()
-
18:42 - Game template with seat direction
import GroupActivities /// The custom spatial template used to arrange spatial Personas /// during Guess Together's game stage. /// /// The team selection template contains three sets of seats: /// /// 1. An seat to the left of the app window for the active player. /// 2. Two seats to the right of the app window for the active player's /// teammates. /// 3. Five seats in front of the app window for the inactive team-members /// and any audience members. /// /// ``` /// ┌────────────────────┐ /// │ Guess Together │ /// │ app window │ /// └────────────────────┘ /// /// /// Active Player % $ Active Team /// $ /// /// * * * * * /// /// Audience /// /// ``` 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 } }
-
21:41 - Configure group immersive space
// Configure group immersive space for await session in GuessingActivity.sessions() { guard let systemCoordinator = await session.systemCoordinator else { continue } systemCoordinator.configuration.supportsGroupImmersiveSpace = true }
-
30:35 - SimpleLine Template
// SimpleLine.swift struct SimpleLine: SpatialTemplate { let elements: [any SpatialTemplateElement] = [ .seat(position: .app.offsetBy(x: 0, z: 2)), .seat(position: .app.offsetBy(x: 1, z: 2)), .seat(position: .app.offsetBy(x: -1, z: 2)), .seat(position: .app.offsetBy(x: 2, z: 2)), .seat(position: .app.offsetBy(x: -2, z: 2)) ] }
-
31:35 - lookingAt Method
// Look at a given position or seat .seat( position: teamSeatPosition, direction: .lookingAt(activePlayerSeat) )
-
31:46 - alignedWith Method
// Look at a given position or seat .seat( position: teamSeatPosition, direction: .lookingAt(activePlayerSeat) ) // Align with a given app axis .seat( position: teamSeatPosition, direction: .alignedWith(appAxis: .z) )
-
32:02 - rotatedBy Method
// Look at a given position or seat .seat( position: teamSeatPosition, direction: .lookingAt(activePlayerSeat) ) // Align with a given app axis .seat( position: teamSeatPosition, direction: .alignedWith(appAxis: .z) ) // Rotate by a given angle .seat( position: teamSeatPosition, direction: .lookingAt(.app).rotatedBy(.degrees(30)) )
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。