18

I have created two lines anchored to a sprite, which are 30˚ apart. I want both lines to swing left and right like a pendulum, always swinging from end to end (in such a way that they are swinging 45˚ to the left and right of their initial position). Please see the image below of what I am trying to achieve:

enter image description here

Below is the code for what I've been able to achieve:

extension Int {
  var degreesToRadians: Double { return Double(self) * .pi / 180 }
}
extension FloatingPoint {
  var degreesToRadians: Self { return self * .pi / 180 }
  var radiansToDegrees: Self { return self * 180 / .pi }
}

class GameScene: SKScene, SKPhysicsContactDelegate {

var anchorSprite = SKSpriteNode(imageNamed: "swingPin")
var armLeft = SKSpriteNode(imageNamed: "swingArm")
var armRight = SKSpriteNode(imageNamed: "swingArm")

override func didMove(to view: SKView) {


    self.physicsWorld.gravity = CGVector(dx: 0, dy: -1.8)
    self.physicsWorld.contactDelegate = self

    var tealBg = SKSpriteNode(imageNamed: "tealBg")
    tealBg.position = CGPoint(x: frame.midX, y: frame.midY)
    tealBg.zPosition = 10
    addChild(tealBg)

    anchorSprite.position = CGPoint(x: frame.midX, y: frame.midY + frame.midY/2)
    anchorSprite.zPosition = 20

    anchorSprite.physicsBody = SKPhysicsBody(rectangleOf: anchorSprite.frame.size)
    anchorSprite.physicsBody?.categoryBitMask = pinCategory
    anchorSprite.physicsBody?.isDynamic = false
    addChild(anchorSprite)

    armRight.anchorPoint = CGPoint(x: 0.5, y: 1)
    armRight.position = anchorSprite.position
    armRight.zPosition = 20
    armRight.physicsBody = SKPhysicsBody(rectangleOf: armRight.frame.size)
    armRight.zRotation = CGFloat(Double(15).degreesToRadians)//CGFloat(Double.pi/6)
    armRight.physicsBody!.isDynamic = true

    addChild(armRight)

    armLeft.anchorPoint = CGPoint(x: 0.5, y: 1)
    armLeft.position = anchorSprite.position
    armLeft.zPosition = 20
    armLeft.physicsBody = SKPhysicsBody(rectangleOf: armRight.frame.size)
    armLeft.zRotation = CGFloat(Double(-15).degreesToRadians)//CGFloat(-Double.pi/6)
    armLeft.physicsBody!.isDynamic = true
    addChild(armLeft)

    // Create joint between two objects
    //Pin joint
    var pinAndRightArmJoint = SKPhysicsJointPin.joint(withBodyA: anchorSprite.physicsBody!, bodyB: armRight.physicsBody!, anchor: CGPoint(x: anchorSprite.position.x, y: self.armRight.frame.maxY))
    self.physicsWorld.add(pinAndRightArmJoint)

    var pinAndLeftArmJoint = SKPhysicsJointPin.joint(withBodyA: anchorSprite.physicsBody!, bodyB: armLeft.physicsBody!, anchor: CGPoint(x: anchorSprite.position.x, y: self.armLeft.frame.maxY))
    self.physicsWorld.add(pinAndLeftArmJoint)

    var fixArms = SKPhysicsJointFixed.joint(withBodyA: armLeft.physicsBody!, bodyB: armRight.physicsBody!, anchor: CGPoint.zero)
    self.physicsWorld.add(fixArms)

    pinAndRightArmJoint.shouldEnableLimits = true
    pinAndRightArmJoint.lowerAngleLimit = CGFloat(Double(-60).degreesToRadians)
    pinAndRightArmJoint.upperAngleLimit = CGFloat(Double(60).degreesToRadians)

 }


override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    //armRight.physicsBody?.angularVelocity = -100.0

     let seq = SKAction.sequence([
        SKAction.rotate(byAngle: CGFloat(Double(45).degreesToRadians), duration: 0.5),
        SKAction.rotate(byAngle: CGFloat(Double(-45).degreesToRadians), duration: 1.0),
        SKAction.rotate(byAngle: CGFloat(Double(45).degreesToRadians), duration: 1.0),
        SKAction.rotate(byAngle: CGFloat(Double(-45).degreesToRadians), duration: 1.0),
        SKAction.rotate(byAngle: CGFloat(Double(45).degreesToRadians), duration: 1.0),
        SKAction.rotate(byAngle: CGFloat(Double(-45).degreesToRadians), duration: 1.0)
])
    armRight.run(seq)
}

From the code above I set lower and upper angle limits and tried running an action, but this just makes the lines inch a bit sideways in a very unrealistic manner. I also tried applying angular velocity on the physics body, but this just made it swing briefly at an inconsistent speed (I need it to swing consistently from one end to the other).

NB

Since I need it to swing from end to end each time, I need the swing cycle to be consistent each time, not necessarily constant. A cycle would generally swing faster as the lines move to the center and slow down a bit when changing from one direction to another. That is the kind of feel I want for movement.

iGetIt
  • 695
  • 5
  • 20
  • Why do you want it to swing at a consistent speed? In a pendulum, angular velocity is not constant – Thijs Steel May 29 '17 at 11:36
  • If you want constant speed, Use .gif file/image of pendulum – iDeveloper May 29 '17 at 11:38
  • 1
    @ThijsSteel Maybe I didn't explain it well enough. Since I need it to swing from end to end each time, I need the swing cycle to be consistent each time, not necessarily constant. A cycle would generally swing faster as the lines move to the center and slow down a bit when changing from one direction to another. That is the kind of feel I want for movement. – iGetIt May 29 '17 at 11:46
  • @iDeveloper a .gif would be inappropriate in my context, I'll also be adding contacts and collisions – iGetIt May 29 '17 at 11:48
  • I assume this drawing is not accurate, but from what I am gathering, you want to rotate the dot in a 100 degree range, I do not know too much about the the physics side of sprite kit to help you with this problem – Knight0fDragon May 30 '17 at 14:26
  • @Knight0fDragon that's okay, thanks. – iGetIt May 30 '17 at 19:29
  • @iGetIt IMHO there is no need to use the physics: you can obtain the correct movements using SKAction timing mode. First of all you can build two circles where each one have a leg, then in total absence of physics you can move repeately your objects changing the timing mode. – Alessandro Ornano May 31 '17 at 06:38
  • @AlessandroOrnano Please explain further why each leg will have a circle. – iGetIt May 31 '17 at 08:13
  • @iGetIt Because if you make two circles you can handle each one individually and so prepare different actions – Alessandro Ornano May 31 '17 at 08:16
  • @AlessandroOrnano I understand iGetIt. Your recommendation is very vague. Should the two circles be at the anchor or at the ends of the legs or at the farthest ends (left and right) of the swing range? Also using SKActions to create the pendulum effect can work, but that involves a lot of try and error to get the SKActions just right. – NSologistic May 31 '17 at 10:15
  • @NSologistic I don't think so.First of all I've express only a suggestion, not an answer: the two circles should be to the top and overlapped.Obviusly if you need something different by a rigid pendulum I agree with you, otherwise you can do it and it's very simple. In case you need a pendulum shaft with oscillations in his body like a rope you need to introduce also [SKConstraints](https://developer.apple.com/reference/spritekit/skconstraint) and this task is more difficult to reproduce.. – Alessandro Ornano May 31 '17 at 10:22
  • Personally I would go with go with `SKAction` and easing, like in the [given answer](https://stackoverflow.com/a/44297857/3402095) so I give it my upvote for an idea. Using physics here seems like an needless move (IMO of course). Also with an `SKActon` you can easily repeat or pause everything. That is a bit more difficult task with physics engine & bodies involved in simulation. – Whirlwind Jun 01 '17 at 11:48
  • I guess it all depends on what he needs it for, if he plans on using the pendulum to apply force to a sprite that hits it, then SKAction would not be the way to go. – Knight0fDragon Jun 02 '17 at 19:03
  • I implemented a physics-based approach that simulates a pendulum's motion. It swings back and forth as you'd expect but doesn't work exactly as described in your question; the pendulum slows slowly over time due to "air resistance" and eventually stops. Is this what you're looking for? – 0x141E Jun 06 '17 at 17:12
  • Setup a physic rig using a pin to bind the two arms together, elevate them to their desired maximum position, drop, let gravity do its work, then reset the simulation at the completion of one cycle, and drop again. This overcomes any issues of "friction" or any other reduction in movement extents in the simulation. This is a common trick/hack to overcome the problems of drift and other issues in simulated motion where it's desired that the results always be the same @0x141E – Confused Jun 07 '17 at 19:08

2 Answers2

8

Here is the practical answer:

Replace addChild(armRight) with anchorSprite.addChild(armRight). Replace addChild(armLeft) with anchorSprite.addChild(armLeft). Delete armRight.position = anchorSprite.position and delete armLeft.position = anchorSprite.position. Also, unless you use the physics joints for other movements in your code, delete all of them, as my solution does not require joints.

Now your arms are children of anchorSprite and subjected to its' coordinate system. If you want to rotate both arms in the same direction at the same time, you can run a rotation action on the anchorSprite. If you want the arms to rotate in different directions you will have to run the rotate action on each arm separately. For either situation, you can use this handy function I made just for the bounty on this question :-P

func runPendulumRotationOnNode(_ node:SKNode, withAngle angle:CGFloat, period:TimeInterval, key:String) {
    let initialRotate = SKAction.rotate(byAngle: angle/2, duration: period/2)
    initialRotate.timingMode = .easeOut
    let rotate = SKAction.rotate(byAngle: angle, duration: period)
    rotate.timingMode = .easeInEaseOut
    let rotateForever = SKAction.repeatForever(SKAction.sequence([rotate.reversed(), rotate]))
    let rotateSequence = SKAction.sequence([initialRotate, rotateForever])
    node.run(rotateSequence, withKey:key)
}

I have tested it, and it works great! You can call it like this to rotate both arms together:

runPendulumRotationOnNode(anchorSprite, withAngle:CGFloat.pi/2, period:0.5, key:"")

Or to rotate the arms in opposite directions, you can use it like this:

runPendulumRotationOnNode(armRight, withAngle:CGFloat.pi/2, period:0.5, key:"")
runPendulumRotationOnNode(armLeft, withAngle:-CGFloat.pi/2, period:0.5, key:"")

Some minor notes, notice how I use CGFloat.pi, an easy constant for π. Also this function assumes the pendulum is starting at its midpoint in the rotation, so π/2 (90 degrees) will rotate the arms π/4 (45 degrees) in either direction.

mogelbuster
  • 1,066
  • 9
  • 19
1

A swift 5 extension version for the good mogelbuster code :

extension SKAction {
    class func pendulum(withAngle angle:CGFloat, period:TimeInterval, key:String) -> SKAction {
        let initialRotate = SKAction.rotate(byAngle: angle/2, duration: period/2)
        initialRotate.timingMode = .easeOut
        let rotate = SKAction.rotate(byAngle: angle, duration: period)
        rotate.timingMode = .easeInEaseOut
        let rotateForever = SKAction.repeatForever(SKAction.sequence([rotate.reversed(), rotate]))
        return SKAction.sequence([initialRotate, rotateForever])
    }
}

Usage:

let pendulum = SKAction.pendulum(withAngle: CGFloat.pi/2, period: 0.5, key: "pendulum")
self.mynode.run(pendulum)
Alessandro Ornano
  • 34,887
  • 11
  • 106
  • 133