Physics Joints In 2D

Understanding joints in a 2D physics enabled Quorum game

Overview

In this tutorial, we will cover how to create joints: a way to link items in a game so that they act more like objects in the real-world. Specifically, we will cover distance joints, which can also be used as springs, weld joints, and rope joints. To see these concepts in action, we will create two games. The first game will have a slinky that can go down some stairs. The second game will have two boxes connected by a rope, where one box can be moved by the player.

Linear Damping

Linear damping, in the physics engine, causes moving items to slow down over time regardless of any other factor. Note that linear damping has a different meaning in real life. Here, it is similar to friction, but it does not require contact with another item or surface. We might want to review friction in the Mass, Friction, and More Tutorial In 2D. While the effect will look similar to using friction, linear damping usually cannot be used to replace friction, because it will affect movement at times when friction would not. Instead, linear damping could be used to simulate air or wind resistance. It can be set like so:

number linearDamping = 0.3
item:SetLinearDamping(linearDamping)

linearDamping is a positive number. Zero means no linear damping. The effect increases with larger numbers.

Angular Damping

The difference between linear damping and angular damping is similar to the difference between linear and angular velocity, which was discussed in the Force and Velocity Tutorial In 2D. Angular damping will cause angular movement to slow down over time. This effect occurs regardless of whether or not an item is in contact with other collidable objects in a game. Angular damping can be set like this:

number angularDamping = 0.3
item:SetAngularDamping(angularDamping)

angularDamping is a positive number. Zero means no angular damping. The effect increases with larger numbers.

Joints

Joints are used to create a connection between two items, with different kinds of joints causing different effects. We can use multiple joints to connect many items as well. There are three different types of joints available at this time: the distance joint, weld joint, and rope joint. First we will discover the distance joint.

Distance Joints

The distance joint forces the two items it connects to maintain a set distance between them. The items will not be able to get closer to or farther from each other. Imagine that they are connected by an invisible rod that allows them to spin about the connection points. Since the items are allowed to spin around their connection points, torque can be introduced. To create a distance joint, the first step is to add this use statement to the top:

use Libraries.Game.Physics.Joints.DistanceJoint2D

Then we need to create the two Item2D objects to be joined and give them positions. Note that Drawable objects are Item2D objects. We also need to create two Vector2 objects and set them to the point on each item that we want the joint to connect to. It may be helpful to review the Force and Velocity Tutorial In 2D’s section on vectors.

Drawable itemA
Drawable itemB
itemA:LoadFilledRectangle(10, 10)
itemB:LoadFilledRectangle(10, 10)
itemA:SetPosition(100, 0)
itemB:SetPosition(150, 0)

Vector2 connectionPointA
Vector2 connectionPointB
connectionPointA:Set(110, 0)
connectionPointB:Set(150, 0)

A distance joint can then be created using this code:

DistanceJoint2D joint
joint:Initialize(itemA, itemB, connectionPointA, connectionPointB)
GetCurrentLayer2D():AddJoint(joint)

A distance joint object is made in the first line. We will need one for each joint we want to make. The next line is the initialization of the joint. This line is where the joint is set-up. The parameters are the two items followed by the vectors containing the points that they will connect at. The parameters should match up so that the first item corresponds to the first vector. In the example above, itemA connects at connectionPointA. The final line of code above adds the joint to the current layer.

Note that if we forget to set the connection points before initializing the joint, the connection points and distance will not be what we want. The same thing will happen if we forget to set the positions of the Item2D objects before initializing the joint. If one or more of the points is set so that it is not on the item it corresponds to, the distance between the two items will not be maintained in the way we expect. If the joint is initialized after the joint is added to the layer, it will not connect the items.

When we initialize the distance joint, it automatically sets the distance that the items must maintain to be the distance between the two connection points. We can change this at anytime using:

number distance = 50
joint:SetDistance(distance)

distance is a number that is more than the distance between the items when they are colliding. If we accidentally set the distance to be too small, the items may go flying off in opposite directions.The distance should not be less than the distance when the items are colliding.

Springing into Action

The distance joint can be modified to work like a spring. There are two properties that need to be set to achieve this: frequency and damping ratio. Frequency means the number of events that occur in a set amount of time, usually a second. This joint uses frequency to calculate the stiffness of the spring, or how easily it allows movement away from the set distance. When a spring is pulled apart or pushed in, it then snaps back to its original form, oscillating from a little too stretched to a little too contracted. The frequency is related to how much the spring oscillates and how far the oscillations stretch or shrink. The damping ratio is similar to linear and angular damping, but it only causes the oscillations to slow down. It causes the oscillations of the spring to decrease and eventually stop. These properties can be set anytime after the joint object has been created like so:

number frequency = 0.1
number dampingRatio = 0.3
joint:SetFrequency(X)
joint:SetDampingRatio(Y)

Both frequency and dampingRatio are positive numbers with 0 as the default value for both. Zero for both means no frequency or damping, meaning we just get a distance joint. While frequency can be any positive number, it works well to pick one between zero and half of the frame rate. For more information on frames, see the Getting Started With Games Tutorial. Usually this means that frequency should be less than or equal to 30. If we pick a frequency greater than half of the frame rate, we may find that it is similar to a picking a value less than half the frequency. Note that smaller values for frequency produce a less stiff spring for the joint. For damping ratio, values between 0 and 1 work well, though the damping ratio can be any positive number. The difference in effect between different values above 1 may not be as easily observable as the difference in effect between values between 0 and 1. In other words, if we slightly increase or decrease a damping ratio value between 0 and 1, there will be more of an observable difference than if we tried doing the same with a value above 1.

Weld Joints

The weld joint connects two items at one point. It can be used to glue them together at the point, or can be adjusted to allow the items some movement around the connection point. The initial setup is similar to the other joints. First a use statement needs to be added to the top of the program.

use Libraries.Game.Physics.Joints.WeldJoint2D

Then the joint must be created and initialized with the items and the connection point.

Drawable itemA
Drawable itemB
itemA:LoadFilledRectangle(10, 10)
itemB:LoadFilledRectangle(10, 10)
itemA:SetPosition(100, 0)
itemB:SetPosition(110, 0)

Vector2 connectionPoint
connectionPoint:Set(110, 0)

WeldJoint2D joint
joint:Initialize(itemA, itemB, connectionPoint)
GetCurrentLayer2D():AddJoint(joint)

Note that we are using a Vector2 object to store the connection point. If you want to review vectors, see the Force and Velocity In 2D Tutorial. With this setup, the connection between the two items will be like glue. They should not be able to move much around their connection. If we want to allow them some more movement around their connection point, we can do that using frequency and damping ratio, like the spring joint.

number frequency = 0.1
number dampingRatio = 0.3
joint:SetFrequency(frequency)
joint:SetDampingRatio(dampingRatio)

frequency and dampingRatio are positive numbers with 0 as the default value for both. Similar to the spring version of the distance joint, as frequency gets smaller, more movement is allowed around the weld point, but a frequency of 0 allows almost no movement. Damping ratio here is similar to the distance joint as well. Values between 0 and 1 have a noticeable effect on slowing down movement around the joint while values over 1 will cause the damping effect to be very pronounced.

Slinky Game Code

This game is a demo of a slinky on top of some stairs. The arrow keys can be used to push the slinky in the regular four directions. Depending on how the slinky is pushed, it may walk down the steps.

Here is a zip file with the sounds used in this game. Read through, then use the following code to discover distance and weld joints.

use Libraries.Game.Game
use Libraries.Game.Graphics.Drawable
use Libraries.Interface.Events.KeyboardListener
use Libraries.Interface.Events.KeyboardEvent
use Libraries.Compute.Vector2
use Libraries.Game.Graphics.Color
use Libraries.Game.Physics.Joints.DistanceJoint2D
use Libraries.Game.Physics.Joints.WeldJoint2D
use Libraries.Sound.Audio
use Libraries.Interface.Events.CollisionEvent2D
use Libraries.Interface.Events.CollisionListener2D

class Main is Game, KeyboardListener, CollisionListener2D
   Drawable slinkyBottom
   Drawable slinkyTop
   integer groundHeight = 550
   integer startLocation = 20
   integer slinkyHeight = 5
   integer slinkyLength = 90
   integer numberOfSteps = 6
   Audio slinkySound
   number collisionCount = 0

   action Main
       StartGame()
   end

   action CreateGame
       EnablePhysics2D(true)
       AddKeyboardListener(me)
       AddCollisionListener(me)
       Color color

  /*
       The ground will be a staircase, so we need steps that decrease in height.
       This code creates the steps from left to right and by decreasing height.
  */
       integer i = 1
       repeat while i <= numberOfSteps
            Drawable ground
            ground:LoadFilledRectangle(GetScreenWidth() / numberOfSteps, groundHeight/numberOfSteps * (numberOfSteps-i))
            ground:SetPosition((i - 1) * (GetScreenWidth() / numberOfSteps), 0)
            if (i mod 2) = 1
                ground:SetColor(color:Red())
            else
                ground:SetColor(color:Yellow())
            end
            ground:EnablePhysics(true)
            ground:SetUnmovable()
            ground:SetFriction(0.8)
            Add(ground)
            i = i + 1
        end

        //  We save the slinky top and bottom so that we can move them with key presses.
        slinkyTop = CreateSlinky(18, slinkyBottom)
        Vector2 gravity
        gravity:Set(0, -170)
        SetGravity2D(gravity)
        slinkySound:Load("Sound/Clang.ogg")
        slinkySound:SetVolume(0.4)
    end

  /*
    This action creates a slinky by creating boxes that connect one to another
    with a weld joint and a spring (distance) joint. The boxes are stacked and
    each connects to the box above and below it. The joints are setup so that 
    there is a weld joint on one edge and a spring joint on the other. Which side
    each kind of joint is alternates so that the slinky is alternatingly pushed up 
    on one side by the spring joint, and held together on the other by the weld
    joint.
  */
    action CreateSlinky(integer sizeOfSlinky, Drawable bottom) returns Drawable
        Drawable previousLink = bottom
        Color color

        integer i = 0
        repeat while i < sizeOfSlinky
            Drawable link
            WeldJoint2D weldJoint
            DistanceJoint2D springJoint
            Vector2 anchor

            //  First another box is added to the slinky stack.
            if i = 0
                link = bottom
            end
            link:LoadFilledRectangle(slinkyLength, slinkyHeight)
            link:SetPosition(startLocation, groundHeight + i * slinkyHeight - groundHeight/numberOfSteps)
            if (i mod 2 ) = 0
                link:SetColor(color:Blue())
            else
                link:SetColor(color:Purple())
            end
            link:EnablePhysics(true)
            link:SetResponsive()
            link:SetMass(5)
            link:SetFriction(0.8)
            Add(link)
            if i > 0
                // Then the weld and spring joints are added to opposite touching edges.
                anchor:Set(startLocation + slinkyLength * ((i + 1) mod 2),
                groundHeight + i * slinkyHeight - groundHeight/numberOfSteps)
                weldJoint:Initialize(previousLink, link, anchor)
                weldJoint:SetFrequency(0.1)
                weldJoint:SetDampingRatio(0.2)
                anchor:SetX(startLocation + slinkyLength * (i  mod 2))
                springJoint:Initialize(previousLink, link, anchor, anchor)
                springJoint:SetDistance(slinkyHeight)
                springJoint:SetFrequency(1)
                //  We make sure to add the joints to the layer so they can work.
                GetCurrentLayer2D():AddJoint(springJoint)
                GetCurrentLayer2D():AddJoint(weldJoint)
            end
            previousLink = link
            i = i + 1
        end
        return previousLink
    end

    //  A sound plays when slinky collides with the steps or itself half of the time. 
    action BeginCollision(CollisionEvent2D event)
        collisionCount = collisionCount + 1
        if (collisionCount > 2)
            slinkySound:SetX(event:GetItemB():GetX())
            slinkySound:SetBalance(event:GetItemB():GetX())
            slinkySound:SetY(event:GetItemB():GetY())
            slinkySound:SetPitch(event:GetItemB():GetY()/120)
            slinkySound:Play() 
            collisionCount = 0
        end
   end

  /*
    We set the linear velocity of both the top and bottom pieces of the slinky,
    so no matter which side is up, the top side is moved.
  */
    action PressedKey(KeyboardEvent event)
        Vector2 velocity
        if event:keyCode = event:LEFT
            velocity:Set(-220, 0)
            slinkyTop:SetLinearVelocity(velocity)
            slinkyBottom:SetLinearVelocity(velocity)
        end

        if event:keyCode = event:RIGHT
            velocity:Set(220, 0)
            slinkyTop:SetLinearVelocity(velocity)
            slinkyBottom:SetLinearVelocity(velocity)
        end

        if event:keyCode = event:UP
            velocity:Set(0, 220)
            slinkyTop:SetLinearVelocity(velocity)
            slinkyBottom:SetLinearVelocity(velocity)
        end

        if event:keyCode = event:DOWN
            velocity:Set(0, -220)
            slinkyTop:SetLinearVelocity(velocity)
            slinkyBottom:SetLinearVelocity(velocity)
        end
    end
end

Try it Yourself: Slinky

If we run this game, we should have a slinky on the top of a set of steps. Use the arrow keys to push the slinky.

Rope Joint

The rope joint is similar to the distance joint. It maintains a maximum distance between two items, but the rope joint allows the items to be closer to each other than that distance. It acts as if an imaginary rope is keeping the items from getting farther apart than the length of the rope. The setup for the rope joint is very similar to the distance joint. First we need to add this use statement to the top:

use Libraries.Game.Physics.Joints.RopeJoint2D

Then we need to create the joint, initialize it, and add it to the layer we want it to live in. Reviewing the Layers Tutorial may help here.

Drawable itemA
Drawable itemB
itemA:LoadFilledRectangle(10, 10)
itemB:LoadFilledRectangle(10, 10)
itemA:SetPosition(100, 0)
itemB:SetPosition(150, 0)

Vector2 connectionPointA
Vector2 connectionPointB
connectionPointA:Set(110, 0)
connectionPointB:Set(150, 0)

RopeJoint2D joint
joint:Initialize(itemA, itemB, connectionPointA, connectionPointB, 50)
GetCurrentLayer2D():AddJoint(joint)

The joint is created in the third to last line and then the joint is then initialized. The parameters are: the two items that the joint will connect, the two points they will be connected at, and the maximum length of the rope. The connection points are ordered in the parameter list so that itemA connects at connectionPointA.

Just like with the distance joint, it is important the length of the rope not be less than the distance between the connection points when the two items are colliding. This will cause unpredictable behavior. If one or more of the connection points is not actually on the item it is connecting, unexpected behavior around that point and item will result. With the distance joint, it is possible to change the distance while the game is running, but changing the length of the rope while the game is running with the rope joint can cause unexpected behavior.

Rope Game Code

This game is a demo of a rope connecting two boxes. One box is in the center of the screen and is unmovable. It is connected to a responsive box that can be pushed in the regular four directions using the arrow keys. For a review on the difference between unmovable and responsive objects, check out the Gravity In 2D Tutorial. The rope will keep the boxes from getting too far apart, and influence the movement of the responsive box.

Here is a zip file containing a Quorum project using the code below. Read through, then use the following code to discover distance and weld joints.

use Libraries.Game.Game
use Libraries.Game.Graphics.Drawable
use Libraries.Interface.Events.KeyboardListener
use Libraries.Interface.Events.KeyboardEvent
use Libraries.Compute.Vector2
use Libraries.Game.Graphics.Color
use Libraries.Game.Physics.Joints.WeldJoint2D
use Libraries.Game.Physics.Joints.RopeJoint2D
use Libraries.Sound.Audio

class Main is Game, KeyboardListener

   Drawable unmovableBox
   Drawable movingBox
   Vector2 from
   Vector2 to
   RopeJoint2D ropeJoint
   Audio bingSound
   number timePassed = 0

   //  We want to create two boxes, one that is fixed and one that can move, and connect them with a rope.
   action Main
       StartGame()
   end

   action CreateGame
       EnablePhysics2D(true)
       AddKeyboardListener(me)
       Color color

       //  First we setup the box that is responsive.
       movingBox:LoadFilledRectangle(40,40)
       movingBox:SetPosition(150, 300)
       movingBox:SetColor(color:Purple())
       movingBox:EnablePhysics(true)
       movingBox:SetResponsive()
       movingBox:SetMass(1)
       movingBox:SetFriction(0.2)
       movingBox:SetLinearDamping(0.9)
       Add(movingBox)

       //  Then we need to setup the non-responsive box.
       unmovableBox:LoadFilledRectangle(40,40)
       unmovableBox:SetPosition(370,300)
       unmovableBox:SetColor(color:Blue())
       unmovableBox:EnablePhysics(true)
       unmovableBox:SetMass(1)
       Add(unmovableBox)

  /*
       We will use this CreateRope action to connect the two boxes with a rope.
       The code for this is below CreateGame. The parameters are the two items
       to be connected and the two points that they connect at. 
  */
       from:Set(movingBox:GetX() + 40, movingBox:GetY())
       to:Set(unmovableBox:GetX(), unmovableBox:GetY())
       CreateRope(movingBox, unmovableBox, from, to)
  /*
       The rope joint helps stabilize the rope. Setting the maximum length of
       the rope joint to be less than the length of the rope makes means that
       even with some stretch, the rope should not extend to the breaking point
       without a very large force.
  */
       ropeJoint:Initialize(movingBox, unmovableBox, from, to, 180)
       GetCurrentLayer2D():AddJoint(ropeJoint)

       Vector2 gravity
       gravity:Set(0, -130)
       SetGravity2D(gravity)
       bingSound:Load("media/Bing.ogg")
    end

    action Update(number seconds)
        timePassed = timePassed + seconds
        if timePassed > 0.5
            bingSound:SetPitch(movingBox:GetY()/100)
            bingSound:SetBalance(movingBox:GetX())
            bingSound:Play()
            timePassed = 0
        end
    end

  /*
    This CreateRope action makes a "rope" between two items by making many small
    boxes and connecting them with weld joints. The weld joints have frequency so
    that movement is allowed around the connection point. The boxes and weld joints
    together make a sort of chain which acts like a rope on screen.
  */
    action CreateRope(Drawable itemA, Drawable itemB, Vector2 anchorA, Vector2 anchorB)
       Drawable previousLink = itemA
       Color color
       Vector2 anchor = anchorA
  /*
       The distance is rounded up when it is cast as an integer so that the
       rope does not end up too short.
  */
       integer length = cast(integer, anchorA:Distance(anchorB)+1)

       integer i = 0
       repeat while i <= length/5
           //  Here we setup one of the individual boxes making up the rope.
            Drawable link
            WeldJoint2D weldJoint

            if i not= length/5
                link:LoadFilledRectangle(5, 2)
                link:SetPosition(anchor:GetX(), anchor:GetY())
                if (i mod 2 ) = 0
                    link:SetColor(color:Blue())
                else
                    link:SetColor(color:Purple())
                end
                link:EnablePhysics(true)
                link:SetResponsive()
                link:SetMass(0.1)
                link:SetFriction(0.2)
                //  Linear Damping is set to model a little air resistance.
                link:SetLinearDamping(0.9)
                Add(link)
            else
                link = itemB
            end

            //  A weld joint is created between two of the items.
            weldJoint:Initialize(previousLink, link, anchor)
            weldJoint:SetFrequency(0.1)
            weldJoint:SetDampingRatio(0.2)
            GetCurrentLayer2D():AddJoint(weldJoint)

  /*
            The anchor is moved to the next position. The rope is being created
            horizontally here, so only the x coordinate is updated.
  */
            anchor:SetX(anchor:GetX() + 5)
            previousLink = link
            i = i + 1
        end
    end

    action PressedKey(KeyboardEvent event)
        Vector2 velocity
        if event:keyCode = event:LEFT
            velocity:Set(-220, 0)
            movingBox:SetLinearVelocity(velocity)
        end

        if event:keyCode = event:RIGHT
            velocity:Set(220, 0)
            movingBox:SetLinearVelocity(velocity)
        end

        if event:keyCode = event:UP
            velocity:Set(0, 220)
            movingBox:SetLinearVelocity(velocity)
        end

        if event:keyCode = event:DOWN
            velocity:Set(0, -220)
            movingBox:SetLinearVelocity(velocity)
        end
    end
end

Try it Yourself: Rope

If we run this game, we should have a rope connecting two boxes. One box is in the center of the screen and is unmovable. It is connected to a responsive box that can be pushed in the regular four directions using the arrow keys. The rope will keep the boxes from getting too far apart, and influence the movement of the responsive box.

Next Tutorial

In the next tutorial, we will discuss Gravity In 3D, which describes how to add gravity to a 3D game.