Code Repo: github.com/xuchi16/vis…
Project Path: github.com/xuchi16/vis…
本文首要包括以下内容:
- 加载实体的根本操作和暗影设置
- 根本的小车移动逻辑
- UI 和手柄操控物体移动
方针及规划
这个运用咱们希望完成如下根本功用
-
主页面:操控翻开和封闭 Immsersive Space,在翻开的情况下操控小车移动
-
游戏场景:包括地板和小车,完成根本的光影。小车能够在地板上移动,超越边界后会掉落
-
手柄操控:经过手柄操控小车移动
小车是否移动依赖于用户输入,而 ECS 系统中,由 System 操控对应的 Component 移动。因而需求让不同的操控器、页面、组件等经过一个 model 同享状况,输入源(UI/手柄)更改状况,而 System 依据当时的状况操控物体移动:
-
数据传递:界说一个 ViewModel 用于存储当时用户的输入状况,在 App 中初始化,并将其传递给各页面和组件(蓝色部分)
-
操控流:
ContentView
和GameController
作为输入源,将用户动作(图中绿色部分,如按下了前进/后退键)更新到 ViewModel 中,这样担任操控的MoveSystem
就能够读取到,并相应地操控小车移动
根本完成
ViewModel
ViewModel 是这个运用同步状况的中心数据结构,需求记载当时用户按住了哪些按键。根本的按键包括上下左右,此外,还有左上、左下、右上、右下几个方向。但只需求界说上下左右四个方向,当用户按下左上这类复合按键时,一起将左和上置为 true 即可。
@Observable
class ViewModel {
var forward = false
var backward = false
var left = false
var right = false
}
CarControlApp
CarControlApp 作为入口,初始化 model 并传递给下一级组件。
- 经过环境变量传递给
ContentView
和ImmersiveView
- 初始化时经过显式
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)
}
}
作用:
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 中界说的速度,这样才能决议小车的移动
-
当用户操控小车左右移动时,其实并非是线性的左右移动,而是操控的小车的转向
前后移动:
- 依据当时用户输入是向前仍是向后决议移动向量
forward(0, 0, 1)
仍是backward(0, 0, -1)
- 获取小车当时的方向角度将上述向量转向,然后决议移动方向。这儿运用的是 act(_:)办法。
- 将方向向量乘以标量速度,然后得到终究的移动向量
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()))
}
}
}
}
终究作用