【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组件的最基本使用方法。

相关推荐
tangweiguo030519871 小时前
SwiftUI布局完全指南:从入门到精通
ios·swift
T1an-15 小时前
最右IOS岗一面
ios
坏小虎7 小时前
Expo 快速创建 Android/iOS 应用开发指南
android·ios·rn·expo
光影少年9 小时前
Android和iOS原生开发的基础知识对RN开发的重要性,RN打包发布时原生端需要做哪些配置?
android·前端·react native·react.js·ios
北京自在科技9 小时前
Find My 修复定位 BUG,AirTag 安全再升级
ios·findmy·airtag
Digitally10 小时前
如何不用 USB 线将 iPhone 照片传到电脑?
ios·电脑·iphone
Sim14801 天前
iPhone将内置本地大模型,手机端AI实现0 token成本时代来临?
人工智能·ios·智能手机·iphone
Digitally1 天前
如何将 iPad 上的照片传输到 U 盘(4 种解决方案)
ios·ipad
报错小能手1 天前
ios开发方向——swift并发进阶核心 @MainActor 与 DispatchQueue.main 解析
开发语言·ios·swift
LcGero1 天前
Cocos Creator 业务与原生通信详解
android·ios·cocos creator·游戏开发·jsb