visionOS示例代码:Happy Beam

利用完整空间使用ARKit创建一个有趣的游戏。

下载地址

visionOS 1.0+ Xcode 15.0+

概述

在visionOS中,您可以使用多种不同的框架创建有趣的动态游戏和应用程序,以创建新的空间体验:RealityKit、ARKit、SwiftUI和Group Activities。这个示例介绍了Happy Beam,一个游戏,在这个游戏中,您和您的朋友可以在FaceTime通话中一起玩耍。

您将学习游戏的机制,其中脾气暴躁的云在空间中漂浮,人们通过用手做一个心形来投射光束。人们将光束对准云朵,使它们高兴起来,计分器会记录每个玩家为云朵打气的情况。

使用SwiftUI设计游戏界面

visionOS中的大多数应用程序都以窗口的形式启动,根据应用程序的需求打开不同类型的场景。

在这里,您可以看到Happy Beam如何使用多个SwiftUI视图呈现有趣的界面,显示欢迎屏幕、给出说明的教练屏幕、记分板和游戏结束屏幕。

欢迎窗口

说明

记分板

结束窗口

以下是应用程序中显示游戏每个阶段的主要视图:

less 复制代码
struct HappyBeam: View {
    @Environment(.openImmersiveSpace) private var openImmersiveSpace
    @EnvironmentObject var gameModel: GameModel
    
    @State private var session: GroupSession<HeartProjection>? = nil
    @State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    @State private var subscriptions = Set<AnyCancellable>()
    
    var body: some View {
        let gameState = GameScreen.from(state: gameModel)
        VStack {
            Spacer()
            Group {
                switch gameState {
                case .start:
                    Start()
                case .soloPlay:
                    SoloPlay()
                case .lobby:
                    Lobby()
                case .soloScore:
                    SoloScore()
                case .multiPlay:
                    MultiPlay()
                case .multiScore:
                    MultiScore()
                }
            }
            .glassBackgroundEffect(
                in: RoundedRectangle(
                    cornerRadius: 32,
                    style: .continuous
                )
            )
        }
    }
}

当3D内容开始出现时,游戏会打开一个沉浸式空间,以在主窗口之外和人的周围呈现内容。

less 复制代码
@main
struct HappyBeamApp: App {
    @StateObject private var gameModel = GameModel()
    @State private var immersionState: ImmersionStyle = .mixed
    
    var body: some SwiftUI.Scene {
        WindowGroup("HappyBeam", id: "happyBeamApp") {
            HappyBeam()
                .environmentObject(gameModel)
        }
        .windowStyle(.plain)
        
        ImmersiveSpace(id: "happyBeam") {
            HappyBeamSpace(gestureModel: HeartGestureModelContainer.heartGestureModel)
                .environmentObject(gameModel)
        }
        .immersionStyle(selection: $immersionState, in: .mixed)
    }
}

HappyBeam容器视图声明对openImmersiveSpace的依赖:

java 复制代码
@Environment(.openImmersiveSpace) private var openImmersiveSpace

它稍后在应用程序的声明中使用该依赖,当开始显示3D内容时,它会打开空间:

python 复制代码
if gameModel.countDown == 0 {
    Task {
        await openImmersiveSpace(id: "happyBeam")
    }
}

使用ARKit检测心形手势

Happy Beam应用程序使用ARKit在visionOS中支持的3D手部跟踪功能来识别中央的心形手势。使用手部跟踪需要运行会话并得到佩戴者的授权。它使用NSHandsTrackingUsageDescription用户信息键来向玩家解释应用程序请求手部跟踪权限的原因。

显示某人用手做心形手势的截图。从玩家的手中心射出一束光,延伸到一个客厅,愁闷的云朵朝着玩家漂浮。

swift 复制代码
Task {
    do {
        try await session.run([handTrackingProvider])
    } catch {
        print("ARKitSession error:", error)
    }
}

当您的应用程序仅显示窗口或体积时,无法获取手部跟踪数据。相反,当您呈现沉浸式空间时,才能获取这些数据,就像前面的示例中一样。

您可以根据您的用例和预期体验的精度要求来检测手势。例如,Happy Beam可以要求严格的手指关节定位,以紧密地呈现心形。然而,它提示人们做一个心形手势,并使用启发式方法来指示何时手势足够接近。

以下检查一个人的拇指和食指是否几乎接触:

scss 复制代码
func computeTransformOfUserPerformedHeartGesture() -> simd_float4x4? {
    // 获取最新的手部锚点,如果它们中的任何一个没有被跟踪,则返回false。
    guard let leftHandAnchor = latestHandTracking.left,
          let rightHandAnchor = latestHandTracking.right,
          leftHandAnchor.isTracked, rightHandAnchor.isTracked else {
        return nil
    }
    
    // 获取所有所需的关节并检查它们是否被跟踪。
    let leftHandThumbKnuckle = leftHandAnchor.skeleton.joint(named: .handThumbKnuckle)
    let leftHandThumbTipPosition = leftHandAnchor.skeleton.joint(named: .handThumbTip)
    let leftHandIndexFingerTip = leftHandAnchor.skeleton.joint(named: .handIndexFingerTip)
    let rightHandThumbKnuckle = rightHandAnchor.skeleton.joint(named: .handThumbKnuckle)
    let rightHandThumbTipPosition = rightHandAnchor.skeleton.joint(named: .handThumbTip)
    let rightHandIndexFingerTip = rightHandAnchor.skeleton.joint(named: .handIndexFingerTip)
    
    guard leftHandIndexFingerTip.isTracked && leftHandThumbTipPosition.isTracked &&
            rightHandIndexFingerTip.isTracked && rightHandThumbTipPosition.isTracked &&
            leftHandThumbKnuckle.isTracked &&  rightHandThumbKnuckle.isTracked else {
        return nil
    }
    
    // 获取所有关节的世界坐标位置。
    let leftHandThumbKnuckleWorldPosition = matrix_multiply(leftHandAnchor.transform, leftHandThumbKnuckle.rootTransform).columns.3.xyz
    let leftHandThumbTipWorldPosition = matrix_multiply(leftHandAnchor.transform, leftHandThumbTipPosition.rootTransform).columns.3.xyz
    let leftHandIndexFingerTipWorldPosition = matrix_multiply(leftHandAnchor.transform, leftHandIndexFingerTip.rootTransform).columns.3.xyz
    let rightHandThumbKnuckleWorldPosition = matrix_multiply(rightHandAnchor.transform, rightHandThumbKnuckle.rootTransform).columns.3.xyz
    let rightHandThumbTipWorldPosition = matrix_multiply(rightHandAnchor.transform, rightHandThumbTipPosition.rootTransform).columns.3.xyz
    let rightHandIndexFingerTipWorldPosition = matrix_multiply(rightHandAnchor.transform, rightHandIndexFingerTip.rootTransform).columns.3.xyz
    
    let indexFingersDistance = distance(leftHandIndexFingerTipWorldPosition, rightHandIndexFingerTipWorldPosition)
    let thumbsDistance = distance(leftHandThumbTipWorldPosition, rightHandThumbTipWorldPosition)
    
    // 当食指中心之间的距离和拇指尖之间的距离都小于四厘米时,心形手势检测为true。
    let isHeartShapeGesture = indexFingersDistance < 0.04 && thumbsDistance < 0.04
    if !isHeartShapeGesture {
        return nil
    }
    
    // 计算心形手势中点的位置。
    let halfway = (rightHandIndexFingerTipWorldPosition - leftHandThumbTipWorldPosition)/2
    let heartMidpoint = rightHandIndexFingerTipWorldPosition - halfway
    
    // 计算从左拇指关节到右拇指关节的向量并进行归一化(x轴)。
    let xAxis = normalize(rightHandThumbKnuckleWorldPosition - leftHandThumbKnuckleWorldPosition)
    
    // 计算从右拇指尖到右食指尖的向量并进行归一化(y轴)。
    let yAxis = normalize(rightHandIndexFingerTipWorldPosition - rightHandThumbTipWorldPosition)
    
    let zAxis = normalize(cross(xAxis, yAxis))
    
    // 从三个轴和中点向量创建心形手势的最终变换。
    let heartMidpointWorldTransform = simd_matrix(SIMD4(xAxis.x, xAxis.y, xAxis.z, 0), SIMD4(yAxis.x, yAxis.y, yAxis.z, 0), SIMD4(zAxis.x, zAxis.y, zAxis.z, 0), SIMD4(heartMidpoint.x, heartMidpoint.y, heartMidpoint.z, 1))
    return heartMidpointWorldTransform
}

为了支持辅助功能和一般用户偏好,将多种输入方式包含在使用手部跟踪作为一种输入形式的应用程序中。

Happy Beam支持以下几种输入方式:

  1. 一个显示某人用手做心形手势的截图。从玩家的手中心射出一束光,延伸到一个客厅,愁闷的云朵朝着玩家漂浮。 使用具有自定义心形手势的ARKit交互式手部输入。

  2. 一个显示一个3D心形悬停在一个塔顶上的截图。 使用拖动手势输入,将固定的光束在其平台上旋转。

  3. 一个显示某人使用VoiceOver玩Happy Beam的截图。画中画显示某人双手放在膝盖上,进行VoiceOver手势以激活元素。在游戏中,一个云朵显示鼓励动画,VoiceOver用一个矩形突出显示该云朵,以显示它是焦点元素。 使用RealityKit的辅助功能组件来支持自定义的鼓励云朵操作。

  4. 一个显示某人使用游戏控制器玩Happy Beam的截图。 游戏控制器支持,通过Switch Control使光束的控制更加交互。

使用RealityKit显示3D内容

应用程序中的3D内容以从Reality Composer Pro导出的资源形式呈现。您将每个资源放置在表示您的沉浸式空间的RealityView中。

以下显示了Happy Beam如何在游戏开始时生成云朵,以及用于地面光束投影仪的材质。因为游戏使用碰撞检测来计分------当光束与愁闷的云朵碰撞时,它们会变得开心------所以您为可能涉及的每个模型创建碰撞形状。

scss 复制代码
@MainActor
func placeCloud(start: Point3D, end: Point3D, speed: Double) async throws -> Entity {
    let cloud = await loadFromRealityComposerPro(
        named: BundleAssets.cloudEntity,
        fromSceneNamed: BundleAssets.cloudScene
    )!
        .clone(recursive: true)
    
    cloud.generateCollisionShapes(recursive: true)
    cloud.components[PhysicsBodyComponent.self] = PhysicsBodyComponent()
    
    var accessibilityComponent = AccessibilityComponent()
    accessibilityComponent.label = "Cloud"
    accessibilityComponent.value = "Grumpy"
    accessibilityComponent.isAccessibilityElement = true
    accessibilityComponent.traits = [.button, .playsSound]
    accessibilityComponent.systemActions = [.activate]
    cloud.components[AccessibilityComponent.self] = accessibilityComponent
    
    let animation = cloudMovementAnimations[cloudPathsIndex]
    
    cloud.playAnimation(animation, transitionDuration: 1.0, startsPaused: false)
    cloudAnimate(cloud, kind: .sadBlink, shouldRepeat: false)
    spaceOrigin.addChild(cloud)
    
    return cloud
}

为多人游戏体验添加SharePlay支持

您可以在visionOS中使用Group Activities框架来支持FaceTime通话期间的SharePlay。Happy Beam使用Group Activities来同步分数、活跃玩家列表以及每个玩家投射光束的位置。

注意

使用Apple Vision Pro developer kit的开发人员可以通过安装Persona Preview Profile在设备上测试空间SharePlay体验。

使用可靠的通道发送重要的信息,即使由于延迟可能会稍微滞后。以下显示了Happy Beam如何在收到分数消息时更新游戏模型的分数状态:

ini 复制代码
sessionInfo.reliableMessenger = GroupSessionMessenger(session: newSession, deliveryMode: .reliable)

Task {
    for await (message, sender) in sessionInfo!.reliableMessenger!.messages(of: ScoreMessage.self) {
        gameModel.clouds[message.cloudID].isHappy = true
        gameModel
            .players
            .filter { $0.name == sender.source.id.asPlayerName }
            .first!
            .score += 1
    }
}

对于低延迟需求的数据发送使用不可靠的信使。由于传递模式是不可靠的,一些消息可能无法传递。Happy Beam使用不可靠的模式来向FaceTime通话中的每个参与者发送光束位置的实时更新。

ini 复制代码
sessionInfo.messenger = GroupSessionMessenger(session: newSession, deliveryMode: .unreliable)

以下显示了Happy Beam如何为每条消息序列化光束数据:

javascript 复制代码
// 在玩家选择FaceTime中的Spatial选项时,向每个玩家发送光束数据。
func sendBeamPositionUpdate(_ pose: Pose3D) {
    if let sessionInfo = sessionInfo, let session = sessionInfo.session, let messenger = sessionInfo.messenger {
        let everyoneElse = session.activeParticipants.subtracting([session.localParticipant])
        
        if isShowingBeam, gameModel.isSpatial {
            messenger.send(BeamMessage(pose: pose), to: .only(everyoneElse)) { error in
                if let error = error { print("Message failure:", error) }
            }
        }
    }
}
相关推荐
海云安6 分钟前
金融领域先锋!海云安成功入选2024年人工智能先锋案例集
人工智能·金融
小王毕业啦11 分钟前
省级金融发展水平数据(2000-2022年)
大数据·人工智能·金融·数据挖掘·数据分析·社科数据
催催1212 分钟前
领夹麦克风哪个品牌好,手机领夹麦克风哪个牌子好,选购推荐
网络·人工智能·经验分享·其他·5g·智能手机
解压专家66620 分钟前
7z 解压器手机版与解压专家:安卓解压工具对决
ios·智能手机·winrar·7-zip
城市数据研习社23 分钟前
【论文分享】基于街景图像识别和深度学习的针对不同移动能力老年人的街道步行可达性研究——以南京成贤街社区为例
人工智能·深度学习·数据分析
梓羽玩Python31 分钟前
AI全自动开发神器 Windsurf!Cursor 的强力替代方案!GPT-4o和Claude模型免费用!
人工智能·python·程序员
逐星ing34 分钟前
[AIGC]使用阿里云Paraformer语音识别录音识别 API 进行音频处理 —— 完整流程及代码示例
人工智能·spring·阿里云·aigc·语音识别
智通财经快讯1 小时前
FBX福币交易所多只高位股重挫,聚星科技首日高开348%
大数据·人工智能·科技
企业通用软件开发1 小时前
大语言模型提示词工程学习--写小说系列(文心一言&豆包&通义千问):三种大模型的比较
人工智能·学习·语言模型
葡萄皮Apple1 小时前
人工智能技术的应用前景及其对生活和工作方式的影响
人工智能·生活