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

相关推荐
ii_best4 小时前
ios按键精灵自动化的脚本教程:自动点赞功能的实现
运维·ios·自动化
app开发工程师V帅15 小时前
iOS 苹果开发者账号: 查看和添加设备UUID 及设备数量
ios
CodeCreator181815 小时前
iOS AccentColor 和 Color Set
ios
iOS民工16 小时前
iOS keychain
ios
m0_7482389219 小时前
webgis入门实战案例——智慧校园
开发语言·ios·swift
Legendary_0081 天前
LDR6020在iPad一体式键盘的创新应用
ios·计算机外设·ipad
/**书香门第*/1 天前
Laya ios接入goole广告,搭建环境 1
ios
wakangda2 天前
React Native 集成 iOS 原生功能
react native·ios·cocoa
几维安全2 天前
如何保护你的 iOS 应用免受逆向工程攻击
macos·objective-c·cocoa
crasowas2 天前
iOS - 超好用的隐私清单修复脚本(持续更新)
ios·app store