3
votes

I have a SCNSphere that is climbing a 45 degree hill.

The node maintains a consistent speed until the same point at every level, at which point it unexpectedly drops in speed, here is a 10 second clip of the issue.

The drop in speed occurs at 8 seconds in this clip.

When the node reaches a z position of -240, it seems as though the entire game speed is cut in half.

I have tested this in the following ways always without success.

  • Tried testing without gravity.
  • Tried testing without colliding with the hill.
  • Tried testing without damping or friction.
  • Tried printing the nodes velocity to notice any changes, although the velocity remains at -5.0 on the z axis for the duration of the level despite the significant drop in speed.
  • Tried printing the physicsWorld speed to notice any changes, although the speed remains at 1.0 for the duration of the level despite the significant drop in speed.
  • Checked for a drop in frame rate although it maintains a frame rate of 60 fps, making only 14 draw calls in total with a poly count under 15k.

The sphere's velocity is updated at every frame in the renderer using the following function.

func updatePositions() {

        if let playerPhysicsBod = playerNode.physicsBody {
            playerPhysicsBod.velocity.x = (lastXPosition - playerNode.position.x) * 8
            playerPhysicsBod.velocity.z = -5
            print("player velocity is \(playerNode.physicsBody!.velocity)")
        }
    }

func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        updatePositions()
        updateSegments()
    }

This project is a fresh one, and so there aren't many lines, I will include the entire code base below.

class GameViewController: UIViewController {


    let scene = SCNScene(named: "art.scnassets/gameScene.scn")!
    let jump = SCNScene(named: "art.scnassets/jump.scn")!.rootNode.childNode(withName: "jump", recursively: true)!
    let box = SCNScene(named: "art.scnassets/box.scn")!.rootNode.childNode(withName: "box", recursively: true)!
    var playerNode = SCNNode()
    var cameraNode = SCNNode()
    var lastXPosition = Float()
    var floorSegments = [SCNNode]()


    override func viewDidLoad() {
        super.viewDidLoad()

        // retrieve the SCNView
        let scnView = self.view as! SCNView
//        scnView.isJitteringEnabled = true
        scnView.scene = scene
        scnView.delegate = self
        scnView.showsStatistics = true

        setupCamera()
        setupPlayer()
        setupSegments()
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {

        guard let tLocationX = touches.first?.location(in: self.view).x else { return }
        let ratio = tLocationX / self.view.frame.maxX
        lastXPosition = Float((5 * ratio) - 2.5)

    }

    func setupCamera() {
        if let camera = scene.rootNode.childNode(withName: "camera", recursively: true) {
            cameraNode = camera
            cameraNode.name = "camera"
        }
    }

    func setupPlayer() {
        if let player = scene.rootNode.childNode(withName: "player", recursively: true) {
            playerNode = player
            playerNode.name = "player"
        }
    }

    func setupSegments() {
        if let segment = scene.rootNode.childNode(withName: "segment", recursively: true) {
            floorSegments.append(segment)
        }
    }




}

extension GameViewController: SCNSceneRendererDelegate {


    func updateSegments() {

        playerNode.position = playerNode.presentation.position
        if let lastSegmentClone = floorSegments.last?.clone() {

            lastSegmentClone.childNodes.forEach { (node) in
                node.removeFromParentNode()
            }


            if abs(playerNode.position.z - lastSegmentClone.position.z) < 30 {
                // set up next segment
                lastSegmentClone.position = SCNVector3(lastSegmentClone.position.x, lastSegmentClone.position.y + 4, lastSegmentClone.position.z - 4)
                floorSegments.append(lastSegmentClone)


                // Add falling blocks to the segment
                for _ in 0...2 {

                    let boxClone = box.clone()
                    let randomX = Int.random(in: -2...2)
                    let randomY = Int.random(in: 1...3)
                    boxClone.eulerAngles.z = Float(GLKMathDegreesToRadians(-45))
                    boxClone.position = SCNVector3(randomX, randomY, -randomY)
                    lastSegmentClone.addChildNode(boxClone)

                }

                // Add falling blocks to the segment
                for (index,segment) in floorSegments.enumerated().reversed() {
                    if segment.position.z > playerNode.position.z + 5 {
                        floorSegments.remove(at: index)
                        segment.childNodes.forEach { (node) in
                            node.removeFromParentNode()
                        }
                        segment.removeFromParentNode()
                    }
                }

                scene.rootNode.addChildNode(lastSegmentClone)
            }
        }
    }

    func updatePositions() {

        if let playerPhysicsBod = playerNode.physicsBody {
            playerPhysicsBod.velocity.x = (lastXPosition - playerNode.position.x) * 8
            playerPhysicsBod.velocity.z = -5
            print("player velocity is \(playerNode.physicsBody!.velocity)")
        }
    }

    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        updatePositions()
        updateSegments()
    }

}
1
Don't remove and add more clone(), which is slow. Just hide those and then unhide at right positions. - E.Coms
Also playerPhysicsBod.allowsResting = false may help. - E.Coms
@E.Coms Thank you for your help, unfortunately I have already tested with allowsResting turned off, and without any adding any clones at all. Very strange stuff - Pat Trudel
One possibility is the location was far from origin. In your scene, you may simulate the other way and make active scene happening around origin. - E.Coms

1 Answers

1
votes

It is difficult to judge w/o seeing scn files.

I hope there is no collision between the player and floor nodes...

Player's Position is a float, It might be inexact comparison for equality. I think you should avoid player's position adjustment, as it should be almost the same:

playerNode.position = playerNode.presentation.position

Why don't you keep reference on an empty segment node?

In the method setupSegments clone the segment and after removing children nodes, keep it. No point to always remove children nodes:

 lastSegmentClone.childNodes.forEach { (node) in
            node.removeFromParentNode()
        }

Plus I think you should "reverse" the order in updateSegments:

if let lastSegment = floorSegments.last { // no cloning here....

            if abs(playerNode.position.z - lastSegment.position.z) < 30 {

               let lastSegmentClone = lastSegment.clone() // better to use emptySegmentNode

                 /* If empty segment then it's not needed to remove children nodes...
                  lastSegmentClone.childNodes.forEach { (node) in
                   node.removeFromParentNode() */

Also if you're removing parent node, children nodes are going to be removed automatically....

// Avoid commented code
/*segment.childNodes.forEach { (node) in
                            node.removeFromParentNode()
                        }*/
                        segment.removeFromParentNode()

No sure, but perhaps for floor segments it is better to use custom action for removal:

// calculate somehow wait duration, based on the player's position, velocity or use pure constant..
   let removeAction = SCNAction.sequence([SCNAction.waitForDuration(2.0), SCNAction.removeFromParent()])

   lastSegmentClone.run(removeAction)

In such case you just need a reference on the last floor node, and empty floor node.