6

My objective is to have secondBody 'orbit' firstBody with a constant velocity (500 in this case).

Based on Constant movement in SpriteKit I have implemented the following:

override func didMoveToView(view: SKView) {

    physicsWorld.gravity = CGVector(dx: 0, dy: 0)

    let firstNode = SKShapeNode(circleOfRadius: 10)
    addChild(firstNode)
    firstNode.position = CGPointMake(CGRectGetWidth(frame) / 2.0, CGRectGetHeight(frame) / 2.0)
    let firstPhysicsBody = SKPhysicsBody(circleOfRadius: 10)
    firstPhysicsBody.dynamic = false
    firstNode.physicsBody = firstPhysicsBody

    let secondNode = SKShapeNode(circleOfRadius: 10)
    addChild(secondNode)
    secondNode.position = CGPointMake(CGRectGetWidth(frame) / 2.0 + 40, CGRectGetHeight(frame) / 2.0 + 40)
    let secondPhysicsBody = SKPhysicsBody(circleOfRadius: 10)
    secondPhysicsBody.friction = 0
    secondPhysicsBody.linearDamping = 0
    secondPhysicsBody.angularDamping = 0
    secondPhysicsBody.affectedByGravity = false
    secondNode.physicsBody = secondPhysicsBody

    let joint = SKPhysicsJointPin.jointWithBodyA(firstPhysicsBody, bodyB: secondPhysicsBody, anchor: firstNode.position)
    joint.frictionTorque = 0
    physicsWorld.addJoint(joint)

    secondPhysicsBody.velocity = CGVector(dx: 0, dy: 500)
}

The problem that I am having is that secondNode is slowing down as time goes on. You will notice that I am setting all manner of things like gravity on SKPhysicsWorld, friction linearDamping & angularDamping on SKPhysicsBody and frictionTorque on SKPhysicsJoint.

So, what am I doing wrong? And how can I keep secondNode's speed constant without doing horrid calculations in -update?

Also - I am aware that I can add an SKAction to follow a circular path but that isn't a reasonable solution in this case.

If there's something simple which I'm missing, could you also advice which of the '0's and 'false's that I'm setting can be removed.

Thanks

Community
  • 1
  • 1
Stephen Groom
  • 3,887
  • 1
  • 15
  • 23
  • 1
    I am not familiar with Swift but why don't you check and adjust, if required, the dy speed in the update method? – sangony Mar 03 '15 at 23:56
  • It seems as though everyone I speak to regarding this is suggesting the same. I felt that this was a bit of a hack and I should rely on the physics engine to do the heavy lifting but it seems I shouldn't trust it as much as I was prepared to... – Stephen Groom Mar 04 '15 at 23:01

1 Answers1

8

The slowing down behavior is often to be expected when dealing with physics engines. They are not perfect, especially in more complex scenarios like constraints. You should not rely on Sprite Kit to provide perfect continuos motion simulation because we have no clue what the physic's engine is doing under-the-hood; there are so many factors involved.

In cases where you need continuous motion, (especially something like constant centripetal motion) it is always best to calculate and preserve these values yourself. There is simply no escaping writing calculations in your update method for behaviors like this.

So I wrote a quick example project that calculates the necessary velocity needed to orbit a particular point. You simply specify a period, orbit position and orbit radius. Note that because I am calculating everything, there is no need for any SKJoints, so that makes this implementation also more lightweight. I highly recommend you do your orbiting this way, as it gives you total control over how you want your nodes to orbit each other (i.e. you could have nodes orbit moving nodes, you could have oval orbiting paths, you could adjust the orbit during key moments in your game, etc.)

import SpriteKit

class GameScene: SKScene {
    var node1: SKShapeNode!
    var node2: SKShapeNode!
    var node2AngularDistance: CGFloat = 0

    override func didMoveToView(view: SKView) {
        physicsWorld.gravity = CGVector(dx: 0, dy: 0)
        node1 = SKShapeNode(circleOfRadius: 10)
        node1.position = CGPoint(x: self.size.width/2.0, y: self.size.height/2.0)
        node1.physicsBody = SKPhysicsBody(circleOfRadius: 10)
        node2 = SKShapeNode(circleOfRadius: 10)
        node2.position = CGPoint(x: self.size.width/2.0+50, y: self.size.height/2.0)
        node2.physicsBody = SKPhysicsBody(circleOfRadius: 10)
        self.addChild(node1)
        self.addChild(node2)
    }
    override func update(currentTime: NSTimeInterval) {
        let dt: CGFloat = 1.0/60.0 //Delta Time
        let period: CGFloat = 3 //Number of seconds it takes to complete 1 orbit.
        let orbitPosition = node1.position //Point to orbit.
        let orbitRadius = CGPoint(x: 50, y: 50) //Radius of orbit.

        let normal = CGVector(dx:orbitPosition.x + CGFloat(cos(self.node2AngularDistance))*orbitRadius.x ,dy:orbitPosition.y + CGFloat(sin(self.node2AngularDistance))*orbitRadius.y);
        self.node2AngularDistance += (CGFloat(M_PI)*2.0)/period*dt;
        if (fabs(self.node2AngularDistance)>CGFloat(M_PI)*2)
        {
            self.node2AngularDistance = 0
        }
        node2.physicsBody!.velocity = CGVector(dx:(normal.dx-node2.position.x)/dt ,dy:(normal.dy-node2.position.y)/dt);
    }
    override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
        node1.position = (touches.first! as! UITouch).locationInNode(self)
    }
}

Below is a gif showing the centripetal motion. Notice that because it is completely dynamic we can actually move the centripetal point as it is orbiting.

enter image description here


If however you really want to use your current SKJoints implementation for some reason then I have another solution for you. Simply keep updating your node's linear velocity so that it never slows down.

override func update(currentTime: NSTimeInterval) {
        let magnitude = sqrt(secondNode.physicsBody!.velocity.dx*secondNode.physicsBody!.velocity.dx+secondNode.physicsBody!.velocity.dy*secondNode.physicsBody!.velocity.dy)
        let angle = Float(atan2(secondNode.physicsBody!.velocity.dy, secondNode.physicsBody!.velocity.dx))
        if magnitude < 500 {
            secondNode.physicsBody!.velocity = CGVector(dx: CGFloat(cosf(angle)*500), dy: CGFloat(sinf(angle)*500))
        }
    }


Regardless of the solution you pick (and once again I highly recommend the first solution!), you can choose to apply the velocity over time rather than instantaneously for a more realistic effect. I talk more about this in my answer here

Hopefully I have provided you with enough information to resolve your issue. Let me know if you have any questions. And best of luck with your game!

Community
  • 1
  • 1
Epic Byte
  • 33,840
  • 12
  • 45
  • 93
  • Pretty much the answer I was expecting TBH but I deliberately left out any notion of this hoping that there was something obvious in the physics simulation that I had missed. Thanks! Do you have any citation or reference to people suggestion or advising not to put too much faith in the physics calculations as this is completely news to me and seems too important to not be properly documented... – Stephen Groom Mar 04 '15 at 23:02
  • 2
    I had some trouble with this, until I realized that the dt, was assuming the frame rate of 60 FPS, if your frame rate changes, these calculations will be off. You can fix this by tracking the current time and using it to calculate the difference: let deltaTime = currentTime - lastUpdateTime; let currentFPS = 1 / deltaTime; lastUpdateTime = currentTime; let dt: CGFloat = 1.0/CGFloat(currentFPS) //Delta Time; – Jbryson Sep 20 '16 at 21:17