主页:漫游Apple Vision Pro

Code Repo: github.com/xuchi16/vis…

Project Path: github.com/xuchi16/vis…


本文首要包括以下内容:

  • 加载实体的根本操作和暗影设置
  • 根本的小车移动逻辑
  • UI 和手柄操控物体移动

VisionPro开发 - 经过 UI 和手柄操控轿车移动

方针及规划

这个运用咱们希望完成如下根本功用

  • 主页面:操控翻开和封闭 Immsersive Space,在翻开的情况下操控小车移动

  • 游戏场景:包括地板和小车,完成根本的光影。小车能够在地板上移动,超越边界后会掉落

  • 手柄操控:经过手柄操控小车移动

小车是否移动依赖于用户输入,而 ECS 系统中,由 System 操控对应的 Component 移动。因而需求让不同的操控器、页面、组件等经过一个 model 同享状况,输入源(UI/手柄)更改状况,而 System 依据当时的状况操控物体移动:

  • 数据传递:界说一个 ViewModel 用于存储当时用户的输入状况,在 App 中初始化,并将其传递给各页面和组件(蓝色部分)

  • 操控流:ContentViewGameController作为输入源,将用户动作(图中绿色部分,如按下了前进/后退键)更新到 ViewModel 中,这样担任操控的MoveSystem就能够读取到,并相应地操控小车移动

VisionPro开发 - 经过 UI 和手柄操控轿车移动

根本完成

ViewModel

ViewModel 是这个运用同步状况的中心数据结构,需求记载当时用户按住了哪些按键。根本的按键包括上下左右,此外,还有左上、左下、右上、右下几个方向。但只需求界说上下左右四个方向,当用户按下左上这类复合按键时,一起将左和上置为 true 即可。

@Observable
class ViewModel {
    var forward = false
    var backward = false
    var left = false
    var right = false
}

CarControlApp

CarControlApp 作为入口,初始化 model 并传递给下一级组件。

  • 经过环境变量传递给ContentViewImmersiveView
  • 初始化时经过显式register()传递给手柄操控器
 @main
struct CarControlApp: App {
    @State var model = ViewModel()
    @ObservedObject var gameControllerManager = GameControllerManager()
    init() {
        MoveComponent.registerComponent()
        MoveSystem.registerSystem()
        gameControllerManager.register(model: model)
    }
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(model)
        }
        .defaultSize(CGSize(width: 300, height: 400))
        ImmersiveSpace(id: "ImmersiveSpace") {
            ImmersiveView()
                .environment(model)
        }
    }
}

ContentView

ContentView 首要包括 2 部分功用:

  • 操控翻开和封闭 Immsersive Space:可参阅1. 窗口,空间容器和空间 ,这儿不再赘述
  • 操控小车移动:制作方向按钮,并将用户输入同步到 Model 中

在操控物体移动时,一般的按键习气是长按。比如用户一向按着向前的箭头,那么小车就应该一向前进,直到用户松开,因而这儿运用onLongPressGesture操控。当用户按下/松开按键时,相应地设置 Model 状况。

struct ContentView: View {
    // ...
    @Environment(ViewModel.self) var model
    var body: some View {
            // ...  
            VStack {
                HStack {
                    arrowButton(systemName: "arrow.up.left",  directions: [.up, .left])
                    arrowButton(systemName: "arrow.up",  directions: [.up])
                    arrowButton(systemName: "arrow.up.right",  directions: [.up, .right])
                }
                HStack {
                    arrowButton(systemName: "arrow.down.left",  directions: [.down, .left])
                    arrowButton(systemName: "arrow.down",  directions: [.down])
                    arrowButton(systemName: "arrow.down.right",  directions: [.down, .right])
                }
            }
        }
        // ...
    }
    private func arrowButton(systemName: String, directions: [Direction]) -> some View {
        Button(action: {}) {
            Image(systemName: systemName)
        }
        .onLongPressGesture(minimumDuration: .infinity, pressing: { isPressing in
            print("direction: (directions), pressed: (isPressing)")
            for direction in directions {
                move(direction: direction, press: isPressing)
            }
        }, perform: {})
    }
    func move(direction: Direction, press: Bool) {
        switch direction {
        case .up:
            model.forward = press
        case .down:
            model.backward = press
        case .left:
            model.left = press
        case .right:
            model.right = press
        }
    }
    enum Direction {
        case up, down, left, right
    }
}

ImmersiveView

ImmersiveView 首要功用是:

  • 加载轿车和地板实体,赋予对应的初始位置、材料、物理实体信息等特性

  • 为轿车增加光影作用

  • 为轿车增加MoveComponent,并传递 ViewModel,后续供 MoveSystem 运用

假如仅仅加载轿车和地板实体,并没有实践的重力和磕碰作用,因而需求给这些实体增加对应的物理实体PhysicsBodyComponent,并且赋予其对应的磕碰体形状CollisionComponent,这样才能发生相似实在国际的磕碰、重力等作用。

struct ImmersiveView: View {
    @State var floor = Entity()
    @State var car = Entity()
    @Environment(ViewModel.self) var model
    var body: some View {
        RealityView { content in
            // Car
            car = try! await Entity(named: "toy_car")
            car.transform.rotation = simd_quatf(angle: .pi, axis: [0, 1, 0])
            car.components[CollisionComponent.self] = CollisionComponent(shapes: [.generateBox(size: SIMD3(repeating: 1.0))])
            car.position = SIMD3(x: 0, y: 0.95, z: -2)
            let carBody = PhysicsBodyComponent()
            car.components[PhysicsBodyComponent.self] = carBody
            car.enumerateHierarchy { entity, stop in
                if entity is ModelEntity {
                    entity.components.set(GroundingShadowComponent(castsShadow: true))
                }
            }
            car.components[MoveComponent.self] = MoveComponent(model: model)
            content.add(car)
            // Floor
            let floorMaterial = SimpleMaterial(color: .white, roughness: 1, isMetallic: false)
            floor = ModelEntity(
                mesh: .generateBox(width: 3, height: 0.01, depth: 2),
                materials: [floorMaterial],
                collisionShape: .generateBox(width: 3, height: 0.01, depth: 2),
                mass: 0.0
            )
            floor.position = SIMD3(x: 0.0, y: 0.9, z: -2)
            var floorBody = PhysicsBodyComponent()
            floorBody.isAffectedByGravity = false
            floorBody.mode = .static
            floor.components[PhysicsBodyComponent.self] = floorBody
            content.add(floor)
        }
        .shadow(radius: 12)
    }
}

别的能够注意到,在为小轿车增加光影作用时,并不是简单地为实体增加GroundingShadowComponent。由于小轿车是从 USDZ 文件中加载而来,并非ModelEntity,假如仅仅简单增加暗影会发现并不会发生预期的作用。因而能够给Entity类型扩展一个enumerateHierarchy办法,递归地迭代其间的子结构,并且为每个ModelEntity类型的子结构增加暗影,这样就能取得预期的作用。参阅文档

extension Entity {
    func enumerateHierarchy(_ body: (Entity, UnsafeMutablePointer<Bool>) -> Void) {
        var stop = false
        func enumerate(_ body: (Entity, UnsafeMutablePointer<Bool>) -> Void) {
            guard !stop else {
                return
            }
            body(self, &stop)
            for child in children {
                guard !stop else {
                    break
                }
                child.enumerateHierarchy(body)
            }
        }   
        enumerate(body)
    }
}

作用:

VisionPro开发 - 经过 UI 和手柄操控轿车移动

GameController

手柄操控首要功用:

  • 监控手柄连接和断开
  • 监控手柄输入:这部分上述 UI 操控相似,需求判断用户的输入并且映射到 ViewModel 中
func handleGamepadInput(_ gamepad: GCExtendedGamepad) {         let leftThumbstickX = gamepad.leftThumbstick.xAxis.value         let leftThumbstickY = gamepad.leftThumbstick.yAxis.value                 if model == nil {             return         }                 if leftThumbstickX != 0 || leftThumbstickY != 0 {             print("Left Thumbstick Moved: (leftThumbstickX), (leftThumbstickY)")             if leftThumbstickX < -sensitivity {                 model?.left = true             }             if leftThumbstickX > sensitivity {                 model?.right = true             }             if leftThumbstickY > sensitivity {                 model?.forward = true             }             if leftThumbstickY < -sensitivity {                 model?.backward = true             }                     } else {             model?.reset()             print("Left Thumbstick Released")         }     }

MoveComponent

MoveComponent 作为 Component,首要界说了运动目标相关的一些性质,如速度、转弯速度等。一起为了语义上的明晰,还界说了左和右对应的向量。

public struct MoveComponent: Component {
    let model: ViewModel
    let speed: Float = 0.3
    let turnSpeed: Float = 1.0
    private let left = SIMD3<Float>(0, 1, 0)
    private let right = SIMD3<Float>(0, -1, 0)
    func getDirection() -> SIMD3<Float> {
        if model.left {
            return left
        }
        if model.right {
            return right
        }
        return SIMD3<Float>(0, 0, 0)
    }
}

MoveSystem

MoveSystem 首要用于识别包括了MoveComponent的目标,并操控其移动。

  • 当用户操控小车前后移动时,是向小车的前方/后方而非镜头的前方/后方移动。此外,还需求依据 Component 中界说的速度,这样才能决议小车的移动

  • 当用户操控小车左右移动时,其实并非是线性的左右移动,而是操控的小车的转向

前后移动:

  1. 依据当时用户输入是向前仍是向后决议移动向量forward(0, 0, 1)仍是backward(0, 0, -1)
  2. 获取小车当时的方向角度将上述向量转向,然后决议移动方向。这儿运用的是 act(_:)办法。
  3. 将方向向量乘以标量速度,然后得到终究的移动向量
private let forward = SIMD3<Float>(0, 0, 1)
private let backward = SIMD3<Float>(0, 0, -1)
// ...
let deltaTime = Float(context.deltaTime)
if moveComponent.model.forward {
    let forwardDirection = entity.transform.rotation.act(forward)
    entity.transform.translation += forwardDirection * moveComponent.speed * deltaTime
}
if moveComponent.model.backward {
    let backwardDirection = entity.transform.rotation.act(backward)
    entity.transform.translation += backwardDirection * moveComponent.speed * deltaTime
}

假如用户输入一起还包括了左右移动,则需求

  • 获取希望的转向角度:simd_quatf(angle: moveComponent.turnSpeed * deltaTime, axis: moveComponent.getDirection())
  • 依据物体当时的方向entity.orientation,乘以上述转向角度,取得终究的转向方向
if moveComponent.model.left || moveComponent.model.right {
    entity.orientation = simd_mul(entity.orientation,
                                  simd_quatf(angle: moveComponent.turnSpeed * deltaTime, axis: moveComponent.getDirection()))
}

上述两组移动中还有一个共同点需求注意,当咱们乘以移动或旋转速度时,都一起乘以了context.deltaTime。它指的是上次更新到这次更新之间的距离时间。运用运行时每秒钟会有很多帧,每一帧(frame)都会调用一次 update 办法,两帧之间的距离便是这儿的deltaTime。一般咱们设定的移动速度是物体每秒移动速度,假如不做上述乘法,每一帧之间都会移动咱们本来预期 1 秒钟移动的距离,远超预期,因而需求在计算速度时额外乘以deltaTime来到达预期作用。

或许有同学会有疑问,那咱们是否能够减小速度,将其设置为“每帧速度”呢?这儿存在一个问题,帧和帧之间的距离并不总是均匀的,而对于用户而言“秒”才是绝对的单位,因而为了让用户体感上取得一个较为稳定的速度,需求经过将速度乘以deltaTime然后取得移动距离的方法移动物体。

MoveSystem 完好的代码如下:

public struct MoveSystem: System {
    private let forward = SIMD3<Float>(0, 0, 1)
    private let backward = SIMD3<Float>(0, 0, -1)
    static let moveQuery = EntityQuery(where: .has(MoveComponent.self))
    public init(scene: RealityKit.Scene) {
    }
    public func update(context: SceneUpdateContext) {
        let entities = context.scene.performQuery(Self.moveQuery)
        for entity in entities {
            guard let moveComponent = entity.components[MoveComponent.self] else {
                continue
            }
            let deltaTime = Float(context.deltaTime)
            if moveComponent.model.forward {
                let forwardDirection = entity.transform.rotation.act(forward)
                entity.transform.translation += forwardDirection * moveComponent.speed * deltaTime
            }
            if moveComponent.model.backward {
                let backwardDirection = entity.transform.rotation.act(backward)
                entity.transform.translation += backwardDirection * moveComponent.speed * deltaTime
            }
            if moveComponent.model.left || moveComponent.model.right {
                entity.orientation = simd_mul(entity.orientation,
                                              simd_quatf(angle: moveComponent.turnSpeed * deltaTime, axis: moveComponent.getDirection()))
            }
        }
    }
}

终究作用

VisionPro开发 - 经过 UI 和手柄操控轿车移动