苹果在 2019年推出了一个名为 Combine 的声明性异步事件处理框架,通过采用Combine 框架,可以集成事件处理代码并消除嵌套闭包和基于约定的回调,使代码更易于阅读和维护。Combine 框架 API都是使用类型安全的泛型实现,可以无缝接人已有工程,用于处理各类事件。在 Combine 框架中,最重要的3个组成部分是:Publisher (发布者)、Subscriber(订阅者)和 Operator (操作符)。Combine 事件处理机制是典型的观察者模式(与 RxSwift 中的 Observable、Observer 几平完全一致),Reality Kit 中的事件处理机制完全借用了 Combine,也包括 Publisher、Subscriber、Operator 3个组成部分,并且所有 Publisher 都遵循 Event 协议。目前,RealityKit 所有的事件如表 20所示。
表20 RealityKit 中的所有事件
所属事件类型 | 事件 | 描述 |
场景 | SceneEvents.AnchoredStateChanged | 当场景中的 ARAnchor(包括所有遵循 HasAnchoring 协议的实体)状态发生改变时触发 |
SceneEvents.Update | 该事件每帧都会触发,因此可以执行自定义帧更新逻辑 | |
动画 | AnimationEvents. PlaybackCompleted | 当动画播放完毕时触发 |
AnimationEvents. PlaybackLooped | 当动画循环播放时触发 | |
AnimationEvents. Playback Terminated | 当动画被中止时触发,包括播放完毕或者被中断 | |
音频 | AudioEvents. PlaybackCompleted | 当音频播放完毕时触发 |
碰撞 | CollisionEvents. Began | 当两个带碰撞器的对象碰撞开始时触发,每次碰撞只触发一次 |
CollisionEvents. Updated | 当两个带碰撞器的对象碰撞接触后每帧都会触发 | |
CollisionEvents. Ended | 当两个带碰撞器的对象发生碰撞后脱离接触时触发,每次碰撞只触发一次 | |
网络同步 | SynchronizationEvents. OwnershipChanged | 当一个实体对象的所有权属性发生改变时触发 |
SynchronizationEvents. OwnershipRequest | 当一个网络参与者申请对某个实体对象的所有权时触发 |
RealityKit 中的所有事件都可以通过 subscribe(to:on:_:)方法订阅监听,并且所有的事件处理遵循相司的步骤与流程,因此只要理解并掌握一种事件处理方法就可以推广到所有其他类型事件的处理中。
下面以碰撞事件(Collision Events)为例讲解 RealityKit 事件的一般处理方法。通常,在需要处理某类耳件之前需先订阅(subscribe)该事件,在RealityKit 中,我们使用 scene完成订阅操作,典型代码如下
import SwiftUI
import RealityKit
import ARKit
import Combine
struct RealityKitEventView : View {
var body: some View {
return ARViewContainer8().edgesIgnoringSafeArea(.all)
}
}
struct ARViewContainer8: UIViewRepresentable {
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
let config = ARWorldTrackingConfiguration()
config.planeDetection = .horizontal
arView.session.run(config, options: [])
arView.setupGestures()
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {}
}
extension ARView{
func setupGestures() {
let tap = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
self.addGestureRecognizer(tap)
}
@objc func handleTap(_ sender: UITapGestureRecognizer? = nil) {
guard let touchInView = sender?.location(in: self) else {
return
}
guard let raycastQuery = self.makeRaycastQuery(from: touchInView, allowing: .existingPlaneInfinite,alignment: .horizontal) else {
return
}
guard let result = self.session.raycast(raycastQuery).first else {return}
let transformation = Transform(matrix: result.worldTransform)
let box = CustomEntity(color: .yellow,position: transformation.translation)
self.installGestures(.all, for: box)
box.addCollisions(scene: self)
self.scene.addAnchor(box)
}
}
//自定义实体类
class CustomEntity: Entity, HasModel, HasAnchoring, HasCollision {
var subscribes: [Cancellable] = []
required init(color: UIColor) {
super.init()
//设置碰撞组件,可以和其他实体发生碰撞
self.components.set(CollisionComponent(
shapes: [.generateBox(size: [0.1,0.1,0.1])],
mode: .default,
filter: CollisionFilter(group: CollisionGroup(rawValue: 1), mask: CollisionGroup(rawValue: 1))
))
//添加组件,定义实体的模型资源
self.components.set( ModelComponent(
mesh: .generateBox(size: [0.1,0.1,0.1]),
materials: [SimpleMaterial(color: color,isMetallic: false)]
))
}
convenience init(color: UIColor, position: SIMD3<Float>) {
self.init(color: color)
self.position = position
}
required init() {
fatalError("init()没有执行,初始化不成功")
}
func addCollisions(scene: ARView) {
subscribes.append(scene.scene.subscribe(to: CollisionEvents.Began.self, on: self) { event in
guard let box = event.entityA as? CustomEntity else {
return
}
//发生碰撞时把实体变成红色
box.model?.materials = [SimpleMaterial(color: .red, isMetallic: false)]
})
subscribes.append(scene.scene.subscribe(to: CollisionEvents.Ended.self, on: self) { event in
guard let box = event.entityA as? CustomEntity else {
return
}
box.model?.materials = [SimpleMaterial(color: .yellow, isMetallic: false)]
})
}
}
在代码中,我们自定义了一个 CustomEntity 实体类,该实体类继承了 Entity类并遵循了HasModel、HasAnchoring、HasCollision 协议,因此能正常渲染显示、发生碰撞,并可以直接添加到 scene.anchors 集合中。需要注意的是在 CustomEntity 类中,我们定义了 subscribes 数组,专用于保存事件订阅引用,addCollisions()方法用于订阅碰撞事件,并根据碰撞事件开始与结束修改立方体的颜色。在主逻辑中,我们添加了屏幕单击手势,用于在检测到的平面上放置自定义立方体对象,因为绝大多数的工作在 CustomEntity 类中处理,主逻辑变得很清晰,代码也更清爽。运行本示例,在检测到平面时通过单击犀幕生成立方体,使用屏幕手势操作立方体,当两个立方体发生碰撞时会同时改变颜色。