【iOS ARKit】PhysicsBodyComponent

在学习完 RealityKit 进行物理模拟的相关理论知识后,下面通过使用 PhysicsBodyComponent 组件进行物理模拟演示,主要代码如下所示,稍后对代码进行详细解析。

复制代码
//
//  PhysicsBodyView.swift
//  ARKitDeamo
//
//  Created by zhaoquan du on 2024/3/14.
//

import SwiftUI
import ARKit
import RealityKit

struct PhysicsBodyView: View {
    var body: some View {
        PhysicsBodyViewContainer().navigationTitle("物理模拟").edgesIgnoringSafeArea(.all)
    }
}
struct PhysicsBodyViewContainer: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()
        
        func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
            guard let anchor = anchors.first as? ARPlaneAnchor,
                  let arView = arView else{
                return
            }
            let planeAnchor = AnchorEntity(anchor:anchor)
            
            //sample1
            let boxCollider: ShapeResource = .generateBox(size: [0.1,0.2,1])
            let box: MeshResource = .generateBox(size: [0.1,0.2,1], cornerRadius: 0.02)
            let boxMaterial = SimpleMaterial(color: .yellow,isMetallic: true)
            let boxEntity = ModelEntity(mesh: box, materials: [boxMaterial], collisionShape: boxCollider, mass: 0.05)
            boxEntity.physicsBody?.mode = .dynamic
            boxEntity.name = "Box"
            boxEntity.transform.translation = [0.2,planeAnchor.transform.translation.y+0.15,0]
            
            let sphereCollider : ShapeResource = .generateSphere(radius: 0.05)
            let sphere: MeshResource = .generateSphere(radius: 0.05)
            let sphereMaterial = SimpleMaterial(color:.red,isMetallic: true)
            sphereEntity = ModelEntity(mesh: sphere, materials: [sphereMaterial], collisionShape: sphereCollider, 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(boxEntity)
            planeAnchor.addChild(sphereEntity)
            
            //添加碰撞订阅
            let subscription = arView.scene.subscribe(to: CollisionEvents.Began.self, { event in
                print("box发生碰撞")
                print("entityA.name: \(event.entityA.name)")
                print("entityB.name: \(event.entityB.name)")
                print("Force : \(event.impulse)")
                print("Collision Position: \(event.position)")
            })
            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(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
         
            ball.addForce(clampedVelocity*0.1, relativeTo: nil)
        }
    }
}

#Preview {
    PhysicsBodyView()
}

在代码清单中,实现的功能如下:

(1)构建模拟环境。

(2)通过施加力,对物体运动进行物理模拟。

在功能1中,我们通过 session(_ session:ARSession, didAdd anchors: [ARAnchor])方法对平面检测情况进行监视,当 ARKit 检测到符合要求的水平平面后,手动生成一个长方体、一个球体、一个承载这两个物体的平面,构建了基本的模拟环境,如图所示。由于生成的长方体与球体均是带有质量与碰撞器的实体,在使用物理引擎时,它们会在重力作用下下坠,生成的平面主要用于承载这两个物体。在设置好物理模拟相关属性后,我们还订阅(subscriptions)了长方体的碰撞事件,当长方体与其他物体发生碰撞时会打印出发生碰撞的两个实体对象名称、碰撞时的受力和碰撞位置信息。

在功能2中,为方便控制,我们使用了 RealityKit 中的平移手势(Entity Lranslation GrestureRecogniner),通过计算使用者手指在犀幕上滑动的速度生威作用力,并将该作用力施加在球体上,通过施加作用力就可以观察球体与长方体在物理引擎作用下的运动效果(为防止施加的力过大,我们使用了GameSettings结构体并定义了几个边界值,具体可以参看本节源码)。

编译后测试,使用平移手势操作球体,当球体撞击到长方体后,会发生物理交互并触发长方体的碰撞事件。读者可以修改使用不同的物理参数和碰撞形状,看一看物理参数如何影响物体的运动,以及碰撞形状如何影响碰撞位置。

这个例子综合演示了物理参数和属性的设置、物理事件的处理、物理材质对物理模拟的影响,同时也是最简单的物理引擎使用案例,没有使用 group 和 mask设置碰撞分组,仅演示了 PhysicsBodyComponent组件的最基本使用方法。

相关推荐
黑化旺仔5 小时前
【OC】Runtime:类元数据、加载初始化与消息派发
开发语言·macos·ios·objective-c
songgeb5 小时前
iOS App支持设置RTL语言的实践
ios·swift·ui kit
软泡芙6 小时前
【iOS】swift启动屏显示黑屏
ios
用户69371750013848 小时前
当你向领导申请 AI 编程工具预算时,如果领导追问“能提效多少,有量化数据吗”,你该怎么回答?
android·后端·ios
Layer8 小时前
一些脱离 Mac 依赖的 Agent 操作 iPhone 实现
ios·ai编程·测试
用户69371750013849 小时前
当我说 Claude 搭配 GLM-5.3-Flash 太好用了,同事却提醒我:你可能正在失去对代码的掌控
android·后端·ios
茶底世界之下10 小时前
一键裁方会丢掉什么?用 Awaking 给园林照片做两种画幅
ios
Marco_Marco13511 小时前
定位SDK接入实战:Android/iOS/鸿蒙三端怎么选、怎么接
android·ios·harmonyos
老李IT笔记1 天前
关掉设备上的 AI 扩展:两个键各管一层,配错了等于没配
人工智能·ios·智能手机
2501_915921431 天前
iOS应用耗电测试:使用Energy Log、Keymob和Instruments进行性能监控与能源消耗分析
android·ios·小程序·https·uni-app·iphone·webview