VisionPro开发 - 物体移动

1,282 阅读5分钟

首页:漫游Apple Vision Pro

Code Repo: github.com/xuchi16/vis…

Project Path: github.com/xuchi16/vis…


基本概念

实体组件系统(Entity Component System, ECS)模式是游戏开发中常见的一种模式。RealityKit 也遵循了这一模式。这种模式将应用程序对象分为三种类型:实体(Entities)组件(Components)系统(Systems)

  1. 实体(Entities)
  • 实体是 RealityKit 中的核心角色,任何可以放入场景中的对象,无论是可见的还是不可见的,都是实体。

  • 实体可以是 3D 模型、形状基元、灯光,甚至是像声音发射器或触发体积这样的不可见对象。

  • 实体通过添加组件来存储与特定功能相关的附加状态。实体本身包含的属性相对较少,几乎所有实体状态都存储在实体的组件上。

  1. 组件(Components)
  • 组件是添加到实体上的模块化构建块,它们标识系统将对哪些实体采取行动,并维护系统依赖的每个实体的状态。

  • 组件可以包含逻辑,但限制组件逻辑在验证其属性值或设置初始状态上。

  • 组件负责存储实体的特定状态,而系统则使用这些组件来查询和操作实体。

  1. 系统(Systems)
  • 系统包含代码,RealityKit 在每一帧调用这些代码来实现特定类型的实体行为或更新特定类型的实体状态。

  • 系统使用组件来存储其实体特定的状态,并查询具有特定组件或组件组合的实体来进行操作。

  • 系统通常与一个或多个组件一起工作,例如,游戏中的伤害系统可能会使用健康组件来跟踪每个实体受到的伤害以及它们在被摧毁之前能承受多少伤害。

遵循 ECS 模式的好处在于,它允许在不同的实体中重用组件中的功能性,即使这些实体在继承链上没有共同的祖先。这种设计避免了传统面向对象设计模式中可能出现的工作重复,因为每个实体的逻辑只需要计算一次,而不是对每个可能受影响的实体重复计算。

在 RealityKit 中,开发者可以通过创建和配置实体、组件和系统来构建复杂的 3D 场景和交互。这种模块化的方法使得开发者可以更加灵活地构建和维护他们的应用程序。

基本实现

预定程序控制物体移动

根据 ECS 模式,如果想要移动某些物体,步骤如下:

  1. 需要先定义 Component,其中包含每个移动物体的初始及状态数据

  2. 定义对应的 System,筛选出需要作用的对象,编写对应的 update 逻辑

  3. 注册上述定义的 Component 和 System

  4. 将 Component 添加到需要移动的物体上

在这里我们希望构建一个红色球体,并且可以绕某一点进行圆周运动。同时有一个 Toggle 用来控制运动开始/暂停。

  1. 按照上述步骤,首先我们先定义 Component,用于记录进行圆周运动的对象所需的初始及状态数据。
import RealityKit

public struct MoveComponent: Component {
    public var movementEnabled: () -> Bool
    public var speed: Float = 2.0 // Movement peed
    public var radius: Float = 0.2 // Radius of the routine
    public var angle: Float = 0.0 // Current angle
    
    public var center: SIMD3<Float>
    
    public init(position: SIMD3<Float>) {
        movementEnabled = {false}
        center = [position.x - radius, position.y, position.z]
    }
}
  1. 定义对应的 System,用于识别作用对象,并且实现圆周运动的逻辑。注意,特定对象的运动状态数据不应该写在 System 逻辑里。
import RealityKit

public struct MoveSystem: System {
    static let moveQuery = EntityQuery(where: .has(MoveComponent.self))
    
    public init(scene: Scene) {
    }
    
    public func update(context: SceneUpdateContext) {
        let entities = context.scene.performQuery(Self.moveQuery)
        
        for entity in entities {
            guard var moveComponent = entity.components[MoveComponent.self] else {
                continue
            }
            
            if moveComponent.movementEnabled() {
                // Update angle
                moveComponent.angle += moveComponent.speed * Float(context.deltaTime)
                
                // Calculate position
                let center = moveComponent.center
                let x = cos(moveComponent.angle) * moveComponent.radius + center.x
                let y = sin(moveComponent.angle) * moveComponent.radius + center.y
                entity.position = SIMD3<Float>(x, y, entity.position.z)
                
                entity.components[MoveComponent.self] = moveComponent
            }
        }
    }
}
  1. 定义一个ViewModel,用于存储需要绑定的变量,如控制物体运动/暂停。
import Foundation

@Observable
class ViewModel {
    var enableMovement = false
}
  1. 注册上述定义的 Component 和 System,通常可以在应用打开时统一进行注册。这里同时还可以初始化ViewModel,并且注入环境。
@main struct ObjectMovementApp: App {         @State var model = ViewModel()         init() {         MoveComponent.registerComponent()         MoveSystem.registerSystem()     }         var body: some Scene {         WindowGroup {             ContentView()                 .environment(model)         }         .defaultSize(CGSize(width: 300, height: 400))          ImmersiveSpace(id: "MovementSpace") {             MovementView()                 .environment(model)         }     } }
  1. 将 Component 添加到需要移动的物体上
struct MovementView: View {         private let radius: Float = 0.2      @State var programmaticEntity = Entity()     @Environment(ViewModel.self) var model;      var body: some View {         RealityView { content in             // Programmatic movement             let initPosition = SIMD3<Float>(x: -0.6, y: 1.5, z: -2)             var moveComponent = MoveComponent(position: initPosition)             moveComponent.movementEnabled = {model.enableMovement}             programmaticEntity = ModelEntity(                 mesh: .generateSphere(radius: radius),                 materials: [SimpleMaterial(color: .red, isMetallic: false)]             )             programmaticEntity.position = initPosition             programmaticEntity.components[MoveComponent.self] = moveComponent             content.add(programmaticEntity)         }     } }

手势控制物体移动

如果想要通过手势控制物体移动,同样也需要通过 ECS 模式,增加系统预设的 Component。

相较于刚刚通过程序直接控制物体位置,如果想要通过手势控制物体移动,需要给对应的实体

  1. 定义实体,在定义 ModelEntity 时,指定碰撞体collisionShape及质量mass,赋予其物理性质。需要注意的是此处物体的质量mass需要设置为0,否则会由于重力导致物体下落。
// Drag movement
dragEntity = ModelEntity(
    mesh: .generateSphere(radius: radius),
    materials: [SimpleMaterial(color: .blue, isMetallic: false)],
    collisionShape: .generateBox(size: SIMD3<Float>(repeating: radius)),
    mass: 0.0
)
  1. 为实体增加官方库定义的InputTargetComponent组件,用于相应用户输入
dragEntity.components.set(InputTargetComponent(allowedInputTypes: .indirect))
  1. 定义拖拽手势,并通过.gestrue(_:)modifier 让对应的物体可以被拖动
var dragGesture: some Gesture {
    DragGesture()
        .targetedToAnyEntity()
        .onChanged{ value in
            dragEntity.position = 
            value.convert(value.location3D, from: .local, to: dragEntity.parent!)
        }
}

RealityView { content in
    // ...
}
.gesture(dragGesture)    

最终效果