阅读 238

iOS开发 · iOS音视频开发 - ARKit 教学:实作火箭飞船发射,学习 SceneKit 和 Physics

首先,我们从下载初始项目开始。建立并运行它,应该会出现警示窗,要你允许在应用程序中使用相机。

physics-camera-access

点击 OK,如果一切顺利的话,应该可以看到相机视图。

另外,本教程建立在先前教程的知识之上,如果在本文中遇到什麽难题,请随时查阅 ARKit教程系列

理解 Physics Body

首先来介绍 physics body,这是建立相关应用的基本元件之一,为了让 SceneKit 知道如何在应用程式中模拟一个 SceneKit 节点 (node),我们需要附加一个SCNPhysicsBodySCNPhysicsBody是一个将 physics simulation 添加到 node 的物件。

在渲染每一帧 (rendering a frame) 之前,SceneKit 执行 physics calculations 到具有附加 physics bodies 的节点。这些计算包括重力,摩擦以及与其他物体的碰撞,你也可以对 body 施加压力和衝力,在这些计算之后,它更新节点的位置和方向,然后才渲染帧。

基本上,在每次渲染帧之前,都会进行物理计算。

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:1012951431,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

Physics Body 类型

要构建 physics body,首先需要指定 physics body type。一个 physics body type 决定一个 physics body 如何与外在压力和其他物体相互作用。三种 physics body types 分别是静态的 (static),动态的 (dynamic) 和运动的 (kinematic)。

Static (静态)

地板、牆壁和地形等类型的 SceneKit 物件适合使用静态 physics body type。静态类型不受力或碰撞影响,不能移动。

Dynamic (动态)

如果你想制作飞翔的喷火龙、Steph Curry 射篮、或火箭炮的爆破等效果,应该想要使用一个动态 physics body type 的 SceneKit 物件。动态类型是指可以受到压力和碰撞影响的 physics body。

Kinematic (运动)

何时会想要使用 kinematic physics body 呢?例如当你想创建一个游戏,需要用手指推动区块,你就会创建一个由手指移动触发的”隐形”推块。这个“隐形”的推块不受其他区块影响,但是它能在接触时推动其他区块。运动类型是一个不受衝力或碰撞影响的 physics body,但在移动时可碰撞影响其他物体,换言之,它可以主动影响别人,但不能被影响。

创建一个 Physics Body

让我们开始吧,先给 detected horizontal plane (水平检测平台) 一个静态的 physics body,这样就有了稳固的基础,让我们的飞船能够站稳脚跟。

ViewController.swiftrenderer(_:didUpdate:for:)下面添加以下方法:

func update(_ node: inout SCNNode, withGeometry geometry: SCNGeometry, type: SCNPhysicsBodyType) {
    let shape = SCNPhysicsShape(geometry: geometry, options: nil)
    let physicsBody = SCNPhysicsBody(type: type, shape: shape)
    node.physicsBody = physicsBody
}
复制代码

在这个方法中,我们创建了一个SCNPhysicsShape物件。一个SCNPhysicsShape物件代表 physics body 的形状,当 SceneKit 检测到你场景的SCNPhysicsBody物件连结时,它会使用你定义的 physics shapes,而不是 rendered geometry 的可见 (visible) 物件。

接下来,我们将.static传入到类型参数和SCNPhysicsShape物件中,藉此来创建一个SCNPhysicsBody物件。

然后,我们将节点的 physics body 设为刚才创建的 physics body。

附加一个静态 Physics Body

现在,我们将在renderer(_:didAdd:for:)方法内部检测到的平面附加一个 static physics body。在将planeNode添加为子节点之前,请调用以下方法:

update(&planeNode, withGeometry: plane, type: .static)
复制代码

更改后,你的renderer(_:didAdd:for:)方法现在应该如下所示:

 func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
        
        let width = CGFloat(planeAnchor.extent.x)
        let height = CGFloat(planeAnchor.extent.z)
        let plane = SCNPlane(width: width, height: height)
        
        plane.materials.first?.diffuse.contents = UIColor.transparentWhite
        
        var planeNode = SCNNode(geometry: plane)
        
        let x = CGFloat(planeAnchor.center.x)
        let y = CGFloat(planeAnchor.center.y)
        let z = CGFloat(planeAnchor.center.z)
        planeNode.position = SCNVector3(x,y,z)
        planeNode.eulerAngles.x = -.pi / 2
        
        update(&planeNode, withGeometry: plane, type: .static)
        
        node.addChildNode(planeNode)
    }
复制代码

当我们的检测平台接收到新讯息进行更新之后,它可能会改变 geometry。因此,我们需要在render(_:didUpdate:for:)中调用相同的方法:

update(&planeNode, withGeometry: plane, type: .static)
复制代码

现在,修改后的render(_:didUpdate:for:)方法应该看起来像这样:

    func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
        guard let planeAnchor = anchor as?  ARPlaneAnchor,
            var planeNode = node.childNodes.first,
            let plane = planeNode.geometry as? SCNPlane
            else { return }
        
        let width = CGFloat(planeAnchor.extent.x)
        let height = CGFloat(planeAnchor.extent.z)
        plane.width = width
        plane.height = height
        
        let x = CGFloat(planeAnchor.center.x)
        let y = CGFloat(planeAnchor.center.y)
        let z = CGFloat(planeAnchor.center.z)
        
        planeNode.position = SCNVector3(x, y, z)
        
        update(&planeNode, withGeometry: plane, type: .static)
    }
复制代码

做得不错!

附加 Dynamic Physics Body

要让火箭节点能受到压力和碰撞的影响,现在我们要给这个节点一个 dynamic physics body。在ViewController中宣告一个 rocketship 节点名称的常数:

let rocketshipNodeName = "rocketship"
复制代码

然后在addRocketshipToSceneView(withGestureRecognizer:)方法中,在调整 rocketship node position 的程式码后面添加下面的 code:

let physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
rocketshipNode.physicsBody = physicsBody
rocketshipNode.name = rocketshipNodeName
复制代码

我们给了rocketshipNode一个名字和 dynamic physics body,我们待会会使用名字来识别rocketshipNode。建立并运行它,应该能够看到如下图的运行结果:

rocket-landing

施加力量

我们现在要对飞船施加力量。

在这之前,我们需要一个触发动作的方法,UISwipeGestureRecognizer可以帮助我们做到这一点。首先,在addRocketshipToSceneView(withGestureRecognizer:)方法的下面添加以下程式码:

func getRocketshipNode(from swipeLocation: CGPoint) -> SCNNode? {
    let hitTestResults = sceneView.hitTest(swipeLocation)
        
    guard let parentNode = hitTestResults.first?.node.parent,
        parentNode.name == rocketshipNodeName
        else { return nil }
                
    return parentNode
}
复制代码

这个方法可以帮助我们从滑动手势的位置获得火箭飞船节点。你可能想知道为什麽我们需要设下多个条件来解包 parentNode,是因为来自 hitTestResults 的返回节点可能是组成火箭的五个节点中的任何一个。

rocket-ship-3dmodel

在前一个方法的正下方,添加以下函式:

@objc func applyForceToRocketship(withGestureRecognizer recognizer: UIGestureRecognizer) {
    // 1
    guard recognizer.state == .ended else { return }
    // 2
    let swipeLocation = recognizer.location(in: sceneView)
    // 3
    guard let rocketshipNode = getRocketshipNode(from: swipeLocation),
        let physicsBody = rocketshipNode.physicsBody
        else { return }
    // 4
    let direction = SCNVector3(0, 3, 0)
    physicsBody.applyForce(direction, asImpulse: true)
}
复制代码

从上面的程式码中,我们做了以下事情:

  1. 确认滑动手势状态为已结束。
  2. 从滑动位置获取 hit test results。
  3. 查看滑动手势是否在火箭飞船上执行过。
  4. 我们将 y 方向的力施加到父节点 (parent node) 的 physics body。如果你有注意到,我们也把衝力的参数设置为 true,这施用于衝力的瞬时变化,来立即加速 physics body。基本上,这个选项可以在设置为 true 时,模拟物体发射时的瞬间效果。

太好了,建立并运行它吧。请向上滑动飞船,你应该能够在你的飞船上施加一个力量!

rocket-jump

添加 SceneKit Particle System 并更改物理属性

起始专案在 “Particles” 文件夹内一个 reactor SceneKit 的粒子系统 (particle system)。

explode

在本教程中,我们不会介绍如何创建一个 SceneKit 粒子系统。让我们来继续了解如何将 SceneKit 粒子系统,添加到节点及其 physics 的属性。

打开ViewController.swift,在ViewController中宣告以下变数:

var planeNodes = [SCNNode]()
复制代码

renderer(_:didAdd:for:)函式裡面,添加以下这行 code 做为函式内的最后一行程式码:

planeNodes.append(planeNode)
复制代码

简单地说,当检测到一个新的平面时,将其附加到 planeNodes 阵列上。我们稍后会将 planeNodes 赋给 reactorParticleSystem 的 colliderNodes 属性。

renderer(_:didAdd:for:)函式的下方,实作下面的委託方法 (delegate method):

func renderer(_ renderer: SCNSceneRenderer, didRemove node: SCNNode, for anchor: ARAnchor) {
    guard anchor is ARPlaneAnchor,
        let planeNode = node.childNodes.first
        else { return }
    planeNodes = planeNodes.filter { $0 != planeNode }
}
复制代码

当对应于一个已被移除 ARAnchor 的 SceneKit node 从场景中被删除时,就调用这个委託方法。这此同时,我们会在 planeNodes 阵列中过滤已被移除的 plane node。

接下来,在applyForceToRocketship(withGestureRecognizer:)函式的正下方添加以下方法:

@objc func launchRocketship(withGestureRecognizer recognizer: UIGestureRecognizer) {
    // 1
    guard recognizer.state == .ended else { return }
    // 2
    let swipeLocation = recognizer.location(in: sceneView)
    guard let rocketshipNode = getRocketshipNode(from: swipeLocation),
        let physicsBody = rocketshipNode.physicsBody,
        let reactorParticleSystem = SCNParticleSystem(named: "reactor", inDirectory: nil),
        let engineNode = rocketshipNode.childNode(withName: "node2", recursively: false)
        else { return }
    // 3
    physicsBody.isAffectedByGravity = false
    physicsBody.damping = 0
    // 4
    reactorParticleSystem.colliderNodes = planeNodes
    // 5
    engineNode.addParticleSystem(reactorParticleSystem)
    // 6
    let action = SCNAction.moveBy(x: 0, y: 0.3, z: 0, duration: 3)
    action.timingMode = .easeInEaseOut
    rocketshipNode.runAction(action)
}
复制代码

在以上的程式码中,我们做了以下动作:

  1. 确保滑动手势状态已结束。
  2. 像刚才一样安全地解包 (unwrapped) rocketshipNode 及 physicsBody。此外,也安全解包 reactorParticleSystem 和 engineNode。希望将 reactorParticleSystem 添加到飞船的引擎上
  3. 将 physics body 受重力影响的属性设置为 false。重力不会再影响飞船节点,我们还将阻尼 (damping) 属性设置为零,阻尼属性模拟流体摩擦或空气阻力对 body 的影响,将其设置为零,就可以将导致火箭节点的 physics body 上流体摩擦或空气阻力的影响零。
  4. 将 planeNodes 设置为 reactorParticleSystem 的 colliderNodes。这将使粒子系统中的粒子在接触时从检测平面反弹,而不是直接飞行穿过它们。
  5. 将 reactorParticleSystem 添加到 engineNode 上。
  6. 我们将火箭节点向上移动了 0.3 米,并且选择 easeInEaseOut 动画效果。

添加滑动手势

在施加压力发射火箭之前,需要在我们的场景视图上添加 swipe gesture recognizers。请在addTapGestureToSceneView()下方添加这段程式码:

func addSwipeGesturesToSceneView() {
    let swipeUpGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(ViewController.applyForceToRocketship(withGestureRecognizer:)))
    swipeUpGestureRecognizer.direction = .up
    sceneView.addGestureRecognizer(swipeUpGestureRecognizer)
    
    let swipeDownGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(ViewController.launchRocketship(withGestureRecognizer:)))
    swipeDownGestureRecognizer.direction = .down
    sceneView.addGestureRecognizer(swipeDownGestureRecognizer)
}
复制代码

手势向上滑动将对飞船节点施加力量,向下滑动则将启动火箭,Nice!

最后,请在viewDidLoad()调用它:

addSwipeGesturesToSceneView()
复制代码

以上就是本文的教学内容!

Showtime!

恭喜你,现在到了展示成果的时刻,尝试向下滑动,看看得到什麽结果吧!

rocket-init

向下滑动,火箭起飞了!

rocket-launch

结语

希望你喜欢这篇教程,并能从中学到一些宝贵的东西。我们也欢迎读者在社交网络上分享这篇教程,让你的朋友也可以获得这些知识!

以供参考,你可以在GitHub下载专案范例

文末推荐:iOS热门文集

文章分类
iOS