利用完好空间运用ARKit创立一个风趣的游戏。
下载地址
visionOS 1.0+ Xcode 15.0+
概述
在visionOS中,您能够运用多种不同的结构创立风趣的动态游戏和应用程序,以创立新的空间体会:RealityKit、ARKit、SwiftUI和Group Activities。这个示例介绍了Happy Beam,一个游戏,在这个游戏中,您和您的朋友能够在FaceTime通话中一起玩耍。
您将学习游戏的机制,其间脾气暴躁的云在空间中漂浮,人们经过用手做一个心形来投射光束。人们将光束对准云朵,使它们高兴起来,计分器会记录每个玩家为云朵打气的状况。
运用SwiftUI规划游戏界面
visionOS中的大多数应用程序都以窗口的方式启动,依据应用程序的需求翻开不同类型的场景。
在这里,您能够看到Happy Beam如何运用多个SwiftUI视图出现风趣的界面,显现欢迎屏幕、给出阐明的教练屏幕、记分板和游戏完毕屏幕。
欢迎窗口
阐明
记分板
完毕窗口
以下是应用程序中显现游戏每个阶段的主要视图:
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内容开端出现时,游戏会翻开一个沉溺式空间,以在主窗口之外和人的周围出现内容。
@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的依靠:
@Environment(.openImmersiveSpace) private var openImmersiveSpace
它稍后在应用程序的声明中运用该依靠,当开端显现3D内容时,它会翻开空间:
if gameModel.countDown == 0 {
Task {
await openImmersiveSpace(id: "happyBeam")
}
}
运用ARKit检测心形手势
Happy Beam应用程序运用ARKit在visionOS中支撑的3D手部盯梢功用来识别中央的心形手势。运用手部盯梢需求运转会话并得到佩带者的授权。它运用NSHandsTrackingUsageDescription用户信息键来向玩家解释应用程序恳求手部盯梢权限的原因。
显现或人用手做心形手势的截图。从玩家的手中心射出一束光,延伸到一个客厅,忧虑烦闷的云朵朝着玩家漂浮。
Task {
do {
try await session.run([handTrackingProvider])
} catch {
print("ARKitSession error:", error)
}
}
当您的应用程序仅显现窗口或体积时,无法获取手部盯梢数据。相反,当您出现沉溺式空间时,才能获取这些数据,就像前面的示例中一样。
您能够依据您的用例和预期体会的精度要求来检测手势。例如,Happy Beam能够要求严厉的手指关节定位,以紧密地出现心形。然而,它提示人们做一个心形手势,并运用启发式方法来指示何时手势足够接近。
以下检查一个人的拇指和食指是否简直接触:
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支撑以下几种输入方法:
-
一个显现或人用手做心形手势的截图。从玩家的手中心射出一束光,延伸到一个客厅,忧虑烦闷的云朵朝着玩家漂浮。 运用具有自定义心形手势的ARKit交互式手部输入。
-
一个显现一个3D心形悬停在一个塔顶上的截图。 运用拖动手势输入,将固定的光束在其渠道上旋转。
-
一个显现或人运用VoiceOver玩Happy Beam的截图。画中画显现或人双手放在膝盖上,进行VoiceOver手势以激活元素。在游戏中,一个云朵显现鼓舞动画,VoiceOver用一个矩形突出显现该云朵,以显现它是焦点元素。 运用RealityKit的辅佐功用组件来支撑自定义的鼓舞云朵操作。
-
一个显现或人运用游戏控制器玩Happy Beam的截图。 游戏控制器支撑,经过Switch Control使光束的控制愈加交互。
运用RealityKit显现3D内容
应用程序中的3D内容以从Reality Composer Pro导出的资源方式出现。您将每个资源放置在表明您的沉溺式空间的RealityView中。
以下显现了Happy Beam如安在游戏开端时生成云朵,以及用于地上光束投影仪的原料。由于游戏运用磕碰检测来计分——当光束与忧虑烦闷的云朵磕碰时,它们会变得高兴——所以您为可能触及的每个模型创立磕碰形状。
@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如安在收到分数音讯时更新游戏模型的分数状况:
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通话中的每个参与者发送光束方位的实时更新。
sessionInfo.messenger = GroupSessionMessenger(session: newSession, deliveryMode: .unreliable)
以下显现了Happy Beam如何为每条音讯序列化光束数据:
// 在玩家选择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) }
}
}
}
}