Physics Body là gì?

Nó là các nguyên tắc mô phỏng lại các node di truyền của SceneKit, để dùng các bạn cần gọi đến đối tượng SCNPhysicBody và add nó vào các nodes.

Nó bao gốm các phần tình toàn về trọng lực, ma sát, va chạm với các vật thể khác.

 

Các loại Physic Body

Để tạo 1 physics body bạn cần chỉ rõ ra kiểu của nó(static, dynamic, kinematic)

 

Static

Bạn có thể sử dụng kiểu này với các đối tượng không di chuyển như sàn nhà, tường, địa hình,..

 

Dynamic

Bạn có thể sử dụng kiểu này với các đối tượng di chuyển, và có thể va chạm như viên đạn, ngọn lửa, ….

 

Kinematic

Kiểu này không có tương tác va chạm giữa các vật thể nhưng nó có thể di chuyển bởi vật thể khác, nó không tự di chuyển được, nhưng nó sẽ có tác dụng như khi bạn tạo ra 1 game mà bạn muốn 1 cái hộp đi theo ngón tay của bạn thì bạn có thê dùng kiểu này.

 

Tạo Physics Body

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
}

 

Ở đoạn mã trên chúng ta tạo một đối tượng SCNPhysicsShape, nó có tác dụng khi SceneKit tương tác với SCNPhysicsBody thì nó sẽ sử dụng cái shape mà bạn định dạng thể tính toán.

Chúng ta tạo một đối tượng SCNPhysicsBody với type là kiểu static.

Tiếp theo mình sẽ viết hàm render như sau:

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)
    }

Chúng ta sẽ kiểm tra khi đối tượng plane được cập nhật thông tin về toa độ hình dạng, thì sẽ gọi đến hàm update.

Và sửa lại hàm render(_:didUpdate:for:) như sau:

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)
    }

Thêm đối tược kiểu Dynamic Physics Body

Tiếp theo tôi sẽ tạo ra rocketship node có kiểu dynamic vì tôi muốn nó có thể di chuyển và va chạm được.Trong hàm addRocketShipToSceneView(withGestureRecognizer:), thêm đoạn mã sau:

let  rocketshipNodeName = "rocketship"
let physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
rocketshipNode.physicsBody = physicsBody
rocketshipNode.name = rocketshipNodeName

 

Thêm động cơ cho rocketShip

Trước khi làm vậy chúng ta cần tìm cách theo dõi hành động của người dùng khi nào bắt đầu bắn, ở đây chúng ta có thể sử dụng GestureRecognizer:

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
}

 

Method trên có tác dụng lấy ra rocketShip khi bạn swipe, nó sẽ tìm trong các node với toạ độ được truyền vào để lấy ra chính xác cái rocketShip bạn muốn chọn.

Tiếp theo tôi sẽ viết mã để thêm động cơ cho rocketShip:

@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)Kiểm tra swipe có bật hay không

2)Lấy ra hit test từ cái vị trí mình swipe

3)Kiểm tra xem hiện tại có đang chọn cùng một rocketShip hay không

4)Thực hiện di chuyển rocketShip theo chiều y, ở đây tôi có set impulse là true, các bạn có thể đổi thành false để xem sự khác biệt. Bây giờ bạn chạy demo và swipe để xem kết quả.

 

Thêm động cơ và thay đổi một số các thuộc tính

Ở đây các bạn thêm một “reactor” SceneKit  vào folder particles như sau:

Trong ví dụ này chúng ta sẽ không tìm hiểu các tạo ra SceneKit và sẽ học cách hiển thị nó lên các nodes. Vào file ViewController.swift:

 

Ở trong renderer(_:didAdd:for:) method, thêm đoạn đoạn mã này:

planeNodes.append(planeNode)

Đoạn mã này có ý nghĩa, khi một đối tượng node được nhận diện thì chúng ta sẽ thêm ngọn lửa vào.

Thêm đoạn mã sau:

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 }
}

 

Tiếp theo các bạn thêm đoạn mã sau:

@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)Check trạng thái của swipe

2)Lấy ra physicsBody, reactorParticle, engineNode

3)Ở đây tôi sẽ không để lực mà sát và trọng lực tác động vào rocket nên sẽ set isAffectedByGravity là false và damping = 0

4)Set reactorNode với colliderNodes là planeNodes, điều này có nghĩa là khi lửa cháy nó sẽ phát ra các hạt bay trở lại mặt phẳng thay vì việc bay xuyên qua.

5)Thêm reactor vào engine node.

6)Di chuyển rocketShip 0.3 meters với animation easeInEaseOut

Thêm Swipe Gesture

Các bạn thêm đọan mã sau:

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)
}

 

Swipe up thì sẽ đẩy rocketShip lên còn down thì sẽ cho rocketShip bắt đầu bay.

Oke vậy là xong, giờ các bạn chạy code để xem kết quả.

Mã nguồn: https://github.com/tubeobeotu/Swift-Sample/tree/master/ARKitRocketShip

Cảm ơn các bạn đã đọc!