触发器
在上节的示例中,所有可见的物体都参与了物理模拟,但在一些应用中,我们物理模拟,同时又需要了解是否有物体与它们发生了碰撞。如在 AR场景中,当角色靠近一散门时,我们并不希望因为角色与门发生碰撞而导致门移动,但又需要了解是否有角色与门发生了碰撞并以此为依据决定是否打开门。在这种应用场合中,使用触发器是最好的选择。
在 RealityKit 中使用触发器非常简单,具体在使用时,只需要将物体的 physicsBody. mode设置为static,并将 collision. mode 设置为 trigger即可,这样既能防止物体产生运动又能捕获到碰撞相关信息。
修改后,运行应用,操作球体与长方体发生碰撞,在碰撞发生后,可以看到相应的碰撞信息依然会打印出来,但由于长方体 physicsBody. mode 属性设置 static,长方体不会参与物理模拟,也不会发生移动。
触发域
使用触发器的方式适合于对可见物体进行碰撞检测,在实际应用开发中,还有一种情况,对不可见物体的碰撞检测,如在 AR 游戏中,当角色进入某一空间后触发新的机关或者激活 AI Agent(NPC,Non-PlayerCharacter,智能体)。对于这种情况,我们可以建一个 ModeIEntity,但是不渲染相应网格,就像上节代码四周围栏所做的那样。但在 RealityKit 中提供了另一种更简单易用的应对这种情况的实体类,它就是 TriggerVolume。Trigger Volume(触发区域体)实体类包含 Transform component、 Synchronizationcomponent、Collision component3 个组件。
触发域实体包含 Collision component 组件,能够与其他碰撞体发生碰撞,因此,我们可以将触发域实体作为一个 传感器使用,当有其他碰撞体进入或者离开触发域实体所占空间时实时地获取相应消息。与其他带碰撞器的实体一样,当有其他碰撞体进入或者离开触发域实体时也会触发 CollisionEvents,我们可以通过订阅这些事件进行相应处理。
触发域实体也是一个实体,但它非常简单,因为不带有网格信息,因此无法对它的触发域实体进行渲染。触发域实体也不参与物理模拟,但将其作为碰撞检测非常高效。
触发域实体的使用与其他实体的使用一样,我们对代码进行改造,将 boxEntity 换成TriggerVolume,关键代码如下所示。
//
// TriggerVolumeView.swift
// ARKitDeamo
//
// Created by zhaoquan du on 2024/3/18.
//
import SwiftUI
import ARKit
import RealityKit
struct TriggerVolumeView: View {
var body: some View {
TriggerVolumeContentView().navigationTitle("触发器与触发域").edgesIgnoringSafeArea(.all)
}
}
struct TriggerVolumeContentView: UIViewRepresentable{
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: Context) -> some ARView {
let arView = ARView(frame: .zero)
let config = ARWorldTrackingConfiguration()
config.planeDetection = .horizontal
context.coordinator.arView = arView
arView.session.delegate = context.coordinator
arView.session.run(config)
return arView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
class Coordinator: NSObject, ARSessionDelegate{
var sphereEntity : ModelEntity!
var arView:ARView? = nil
let gameController = GameController()
@MainActor func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
guard let anchor = anchors.first as? ARPlaneAnchor,
let arView = arView else{
return
}
let planeAnchor = AnchorEntity(anchor:anchor)
let triggerShape: ShapeResource = .generateBox(size: [01,0.2,0.3])
let triggerVolume = TriggerVolume(shape: triggerShape)
triggerVolume.name = "TriggerVolum"
triggerVolume.transform.translation = [0.2,planeAnchor.transform.translation.y + 0.15,0]
let sphereMaterial = SimpleMaterial(color:.red,isMetallic: true)
sphereEntity = ModelEntity(mesh: .generateSphere(radius: 0.05),materials: [sphereMaterial], collisionShape: .generateSphere(radius: 0.05), mass: 0.04)
sphereEntity.physicsBody?.mode = .dynamic
sphereEntity.name = "Sphere"
sphereEntity.transform.translation = [-0.3,planeAnchor.transform.translation.y+0.15,0]
sphereEntity.physicsBody?.material = .generate(friction: 0.001,restitution: 0.01)
let plane :MeshResource = .generatePlane(width: 1.2, depth: 1.2)
let planeCollider : ShapeResource = .generateBox(width: 1.2, height: 0.01, depth: 1.2)
let planeMaterial = SimpleMaterial(color:.gray,isMetallic: false)
let planeEntity = ModelEntity(mesh: plane, materials: [planeMaterial], collisionShape: planeCollider, mass: 0.01)
planeEntity.physicsBody?.mode = .static//静态平面
planeEntity.physicsBody?.material = .generate(friction: 0.001, restitution: 0.1)
planeAnchor.addChild(planeEntity)
planeAnchor.addChild(triggerVolume)
planeAnchor.addChild(sphereEntity)
let subscription = arView.scene.subscribe(to: CollisionEvents.Began.self,on: triggerVolume) { event in
print("trigger volume发生碰撞")
print("EntityA name : \(event.entityA.name)")
print("EntityB name : \(event.entityB.name)")
print("Force : \(event.impulse)")
print("Collision Position: \(event.position)")
}
gameController.gameAnchor = try! Ball.loadBallGame()
gameController.collisionEventStreams.append(subscription)
arView.scene.addAnchor(planeAnchor)
let gestureRecognizers = arView.installGestures(.translation, for: sphereEntity)
if let gestureRecognizer = gestureRecognizers.first as? EntityTranslationGestureRecognizer {
gameController.gestureRecognizer = gestureRecognizer
gestureRecognizer.removeTarget(nil, action: nil)
gestureRecognizer.addTarget(self, action: #selector(self.handleTranslation))
}
arView.session.delegate = nil
arView.session.run(ARWorldTrackingConfiguration())
}
@objc
func handleTranslation(_ recognizer: EntityTranslationGestureRecognizer) {
guard let ball = sphereEntity else { return }
let settings = gameController.settings
if recognizer.state == .ended || recognizer.state == .cancelled {
gameController.gestureStartLocation = nil
ball.physicsBody?.mode = .dynamic
return
}
guard let gestureCurrentLocation = recognizer.translation(in: nil) else { return }
guard let gestureStartLocation = gameController.gestureStartLocation else {
gameController.gestureStartLocation = gestureCurrentLocation
return
}
let delta = gestureStartLocation - gestureCurrentLocation
let distance = ((delta.x * delta.x) + (delta.y * delta.y) + (delta.z * delta.z)).squareRoot()
if distance > settings.ballPlayDistanceThreshold {
gameController.gestureStartLocation = nil
ball.physicsBody?.mode = .dynamic
return
}
ball.physicsBody?.mode = .kinematic
let realVelocity = recognizer.velocity(in: nil)
let ballParentVelocity = ball.parent!.convert(direction: realVelocity, from: nil)
var clampedX = ballParentVelocity.x
var clampedZ = ballParentVelocity.z
// 夹断
if clampedX > settings.ballVelocityMaxX {
clampedX = settings.ballVelocityMaxX
} else if clampedX < settings.ballVelocityMinX {
clampedX = settings.ballVelocityMinX
}
// 夹断
if clampedZ > settings.ballVelocityMaxZ {
clampedZ = settings.ballVelocityMaxZ
} else if clampedZ < settings.ballVelocityMinZ {
clampedZ = settings.ballVelocityMinZ
}
let clampedVelocity: SIMD3<Float> = [clampedX, 0.0, clampedZ]
ball.physicsMotion?.linearVelocity = clampedVelocity
}
}
}
#Preview {
TriggerVolumeView()
}
运行上述代码,在加载后的场景中无法看到触发域实体对象,使用移动手势操作球体,当球体经过触发域实体所在区域时,碰撞被检测到,CollisionEvents.Began 事件被触发,相应信息也被打印出来。在本章所有演示示例中,我们只对碰撞发生的Began事件进行了处理,CollisionEvents 事件其实包括3个事件,具体如下表。
|--------------------------|-----------------------------------|
| 事件名称 | 描述 |
| CollisionEvents. Began | 结构体,当两个碰撞体开始接触时触发,这个事件在每次碰撞中只触发一次 |
| CollisionEvents. Updated | 结构体,当两个碰撞体保持接触时,这个事件在每一帧都会触发 |
| CollisionEvents. Ended | 结构体,当两个碰撞体脱离接触时触发,这个事件在每次碰撞中只触发一次 |
通过这3个事件,就能方便地处理所有与碰撞相关的事务,关于 RealityKit 中事件的处理,可参阅之前相关章节。
自定义物理实体类
我们通过实体的 PhysicsBodyComponent 组件和 PhysicsMotionComponent 组件实现了物理模拟,在 RealityKit 中,ModelEntity实体类默认带有这两个组件,使用ModelEntity 类创建的实体都可以与物理模拟。在 RealityKit 中,使用物理引擎进行物理模拟的类必须遵循相应的物理协议,根据物理模拟类协议的层级结构,我们可以通过遵循HasPhysicsBody、HasPhysicsMotion协议或直接通过遵循HasPhysics协议,自定义物理实体类。自定义物理实体类后,可以更方便灵活地进行物理模拟,并简化代码。
物理引擎总结
物理引擎突破了按照预定脚本执行物体运动计算的方式,通过设置物体的物理参数来运行。使用物理引擎后,虚拟物体之间、虚拟物体与现实环境之间的相互作用不需要进行硬编码,而是按照牛顿运动定律实时计算模拟,由于牛顿运动定律的客观性,这种模拟出来的效果与真实物体间相互作用效果可以做到完全一致,从而大大增强虚拟物体的可信度。