使用 Physics BodyComponent 组件,通过设置物理参数、物理材质、施加作用力,能完全模拟物体在真实世界中的行为,这种方式的优点是遵循物理学规律、控制精确,但缺点是不直观。使用 PhysicsMotion Component组件则可以通过直接设置速度进行物理模拟,但需要明白的是,对物体施加力与设置物体速度是两种完全不同且不相容的操作,无法混合使用。
下面我们使用 PhysicsMotionComponent组件进行演示。在代上节码中,我们手工构建了模拟环境,这是件枯燥且容易出错的工作,而且很难构建复杂的场景,利用 Reality Composer 工具则可以快速地构建场最模型,本示例我们先使用 Reality Composer 构建基本的场景,然后通过设置速度的方式进行物理模拟。
利用 Reality Composer 工具设置好各实体的大小、物理材质、碰撞属性和位置关系,然后在 Xcode 中导入 Reality 场景,具体代码如下。
//
// PhysicsMotionView.swift
// ARKitDeamo
//
// Created by zhaoquan du on 2024/3/14.
//
import SwiftUI
import RealityKit
import ARKit
struct PhysicsMotionView: View {
var body: some View {
PhysicsMotionViewContainer().navigationTitle("物理模拟2").edgesIgnoringSafeArea(.all)
}
}
struct PhysicsMotionViewContainer: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
context.coordinator.loadModel()
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)
planeAnchor.addChild(gameController.gameAnchor)
arView.scene.anchors.append(planeAnchor)
gameController.gameAnchor.backWall?.visit { entity in
entity.components[ModelComponent.self] = nil
}
gameController.gameAnchor.frontWall?.visit { entity in
entity.components[ModelComponent.self] = nil
}
gameController.Ball13?.physicsBody?.massProperties.centerOfMass = ([0.001,0,0.001],simd_quatf(angle: 0, axis: [0,1,0]))
gameController.Ball4?.physicsBody?.material = PhysicsMaterialResource.generate(friction: 0.3, restitution: 0.3)
gameController.Ball6?.physicsBody?.mode = .kinematic
//gameController.Ball6?.collision?.shapes.removeAll()
arView.session.delegate = nil
arView.session.run(ARWorldTrackingConfiguration())
}
@MainActor func loadModel(){
gameController.gameAnchor = try! Ball.loadBallGame()
if let ball = gameController.gameAnchor.motherBall as? Entity & HasCollision {
let gestureRecognizers = arView?.installGestures(.translation, for: ball)
if let gestureRecognizer = gestureRecognizers?.first as? EntityTranslationGestureRecognizer {
gameController.gestureRecognizer = gestureRecognizer
gestureRecognizer.removeTarget(nil, action: nil)
gestureRecognizer.addTarget(self, action: #selector(self.handleTranslation))
}
}
}
@objc
func handleTranslation(_ recognizer: EntityTranslationGestureRecognizer) {
guard let ball = gameController.motherBall 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
}
}
}
extension Entity {
func visit(using block: (Entity) -> Void) {
block(self)
for child in children {
child.visit(using: block)
}
}
}
#Preview {
PhysicsMotionView()
}
在代码中,实现的功能如下:
(1)加载模拟场景并进行相应的处理。
(2) 通过设置物体速度,对物体运动进行物理模拟。
在功能1中,我们首先使用 loadModel()方法加载 Reality 场景,然后通过 session(- session: ARSesion,didAdd anchors: [ARAnchor])方法对平面检测情况进行监视,当ARKit检测到符合要求的水平平面后,将加载的场景挂载到 ARAnchor 下显示,对不需要显示的四周围栏进行了隐藏处理,然后设置了各球体的物理参数、物理材质并重启了 ARSession(为更好组织代码,方便场景管理,我们使用了 GameController类,具体可以参看本节源码)。
在功能2中为方便控制,我们使用了 RealityKit 中的平移手势EntityTranslationGesture Recognizer,通过计算使用者手指在屏幕上滑动的速度生成物体速度,并将其作为母球的速度(为防止速度过大,我们使用了 GameSettings 结构体并定义了几个边界值,具体可以参github源码),通过直接赋予母球速度值就可以观察母球与场景中其他球体在物理引擎作用下的运动效果。
编译后测试,使用平移手势操作母球,当母球与场景中的其他球体发生碰撞时,会产生相应的物理效果。通过本例可以看到,在 Xcode中也可以修改 Reality Composer 工具中设定的各球体的物理属性,如代码清单中第15 行到第17所示,读者也可以修改不同属性看一看它们如何影响物体的行为,取消碰撞体,看一看还能不能发生撞。