7
\$\begingroup\$

I wonder if someone could provide some guidance.

Im attempting to create a pendulum like behaviour in 2D space in Unity without using a hinge joint.

Essentially I want to affect a falling body to act as though it were restrained at the radius of a point, and to be subject to gravity and friction etc.

Ive tried many modifications of this code, and have come up with some cool 'strange-attractor' like behaviour but i cannot for the life of me create a realistic pendulum like action.

This is what I have so far:

    startingposition = transform.position;          //Get start position
    newposition = startingposition + velocity;      //add old velocity
    newposition.y -= gravity * Time.deltaTime;      //add gravity
    newposition = pivot + Vector2.ClampMagnitude(newposition-pivot,radius); //clamp body at radius???
    velocity = newposition-startingposition;        //Get new velocity
    transform.Translate (velocity * Time.deltaTime, Space.World);   //apply to transform

So im working out the new position based on the old velocity + gravity, then constraining it to a distance from a point, which is the element in the code i cannot get correct. Is this a logical way to go about it?

\$\endgroup\$
  • \$\begingroup\$ Why don't you want to use a hinge joint? \$\endgroup\$ – NauticalMile May 27 '14 at 2:11
  • \$\begingroup\$ Production decision for flexibility, and itd be pretty nice to know how to do it, im experimenting with various techniques for a little physics sandbox for my own education. Im sure its a fairly easy operation to perform its just my understanding of maths/physics is lacking. \$\endgroup\$ – user3447980 May 27 '14 at 8:58
  • \$\begingroup\$ Ok I can see an error now, I think i should be working out gravity and velocity independently from each other and then normalising the results. Not able to test if thats the problem currently. \$\endgroup\$ – user3447980 May 27 '14 at 13:36
  • \$\begingroup\$ I'm afraid your pendulum may lose energy too fast with this approach. Let us know if you have such problem. \$\endgroup\$ – kolenda May 28 '14 at 15:15
19
\$\begingroup\$

I thought this would be a relatively simple problem to solve but I spent a couple days trying to figure out how the heck to simulate pendulum movement. I didn't want to cheat and just change the x,y position based on sin(theta) and cos(theta) curves. Instead I wanted to deal with the two forces that are applied in real life, Gravity and Tension. The main piece I was missing was centripetal force.

The Pendulum (mathematics) wikipedia page has a great animation(below, on left) explaining the pendulum motion. You can see my result(on right) strikingly similar to that diagram

The "bob" is the swinging object and the "pivot" is the origin/root.

Pendulum Motion: velocity and acceleration

I also found this article and diagram(below) pretty helpful:


Theta equals the angle between the rope and the direction of gravity.

When the bob is on the left or right the tension equals:

m*g*cos(theta)

The reason the tension force is greater as the bob approaches equilibrium point(middle) is because of centripetal force:

(m*v^2)/ropeLength

So the overrall tension formula looks like as the bob swings is:

m*g*cos(theta) + (m*v^2)/ropeLength

There are two forces in the pendulum system:

  • Gravity
    • GravityForce = mass * gravity.magnitude
    • GravityDirection = gravity.normalized
  • Tension
    • TensionForce = (mass * gravity * Cos(theta)) + ((mass * velocityTangent^2)/ropeLength)
    • TensionDirection = ropeDirection = bob to pivot

Just apply gravity to your object like you would for a normal object and then apply the tension. When you apply the forces, just multiply the force by the direction and deltaTime.

Below is the Pendulum.cs script(also as a GitHub Gist). It works quite well but there is some rounding error drift if you leave it for a while (doesn't return to exactly same position).

The script works in 3D but of course a pendulum only swings in a 2D plane. It also works with gravity in any direction. So for example, if you invert the gravity the pendulum works upside down. Edit->Project Settings->Physics->Gravity

It is very important to have a consistent relatively small deltaTime when updating the pendulum so that you do not bounce around the curve. I am using the technique found in this article, FIX YOUR TIMESTEP! by Glenn Fiedler to accomplish this. Check the Update() function below to see how I implemented it.

Also as a GitHub Gist

using UnityEngine;
using System.Collections;

// Author: Eric Eastwood (ericeastwood.com)
//
// Description:
//      Written for this gd.se question: http://gamedev.stackexchange.com/a/75748/16587
//      Simulates/Emulates pendulum motion in code
//      Works in any 3D direction and with any force/direciton of gravity
//
// Demonstration: https://i.imgur.com/vOQgFMe.gif
//
// Usage: https://i.imgur.com/BM52dbT.png
public class Pendulum : MonoBehaviour {

    public GameObject Pivot;
    public GameObject Bob;


    public float mass = 1f;

    float ropeLength = 2f;

    Vector3 bobStartingPosition;
    bool bobStartingPositionSet = false;

    // You could define these in the `PendulumUpdate()` loop 
    // But we want them in the class scope so we can draw gizmos `OnDrawGizmos()`
    private Vector3 gravityDirection;
    private Vector3 tensionDirection;

    private Vector3 tangentDirection;
    private Vector3 pendulumSideDirection;

    private float tensionForce = 0f;
    private float gravityForce = 0f;


    // Keep track of the current velocity
    Vector3 currentVelocity = new Vector3();

    // We use these to smooth between values in certain framerate situations in the `Update()` loop
    Vector3 currentStatePosition;
    Vector3 previousStatePosition;

    // Use this for initialization
    void Start () {
        // Set the starting position for later use in the context menu reset methods
        this.bobStartingPosition = this.Bob.transform.position;
        this.bobStartingPositionSet = true;

        this.PendulumInit();
    }


    float t = 0f;
    float dt = 0.01f;
    float currentTime = 0f;
    float accumulator = 0f;

    void Update()
    {
        /* */
        // Fixed deltaTime rendering at any speed with smoothing
        // Technique: http://gafferongames.com/game-physics/fix-your-timestep/
        float frameTime = Time.time - currentTime;
        this.currentTime = Time.time;

        this.accumulator += frameTime;

        while (this.accumulator >= this.dt)
        {
            this.previousStatePosition = this.currentStatePosition;
            this.currentStatePosition = this.PendulumUpdate(this.currentStatePosition, this.dt);
            //integrate(state, this.t, this.dt);
            accumulator -= this.dt;
            this.t += this.dt;
        }

        float alpha = this.accumulator/this.dt;

        Vector3 newPosition = this.currentStatePosition*alpha + this.previousStatePosition*(1f-alpha);

        this.Bob.transform.position = newPosition; //this.currentStatePosition;
        /* */

        //this.Bob.transform.position = this.PendulumUpdate(this.Bob.transform.position, Time.deltaTime);
    }


    // Use this to reset forces and go back to the starting position
    [ContextMenu("Reset Pendulum Position")]
    void ResetPendulumPosition()
    {
        if(this.bobStartingPositionSet)
            this.MoveBob(this.bobStartingPosition);
        else
            this.PendulumInit();
    }

    // Use this to reset any built up forces
    [ContextMenu("Reset Pendulum Forces")]
    void ResetPendulumForces()
    {
        this.currentVelocity = Vector3.zero;

        // Set the transition state
        this.currentStatePosition = this.Bob.transform.position;
    }

    void PendulumInit()
    {
        // Get the initial rope length from how far away the bob is now
        this.ropeLength = Vector3.Distance(Pivot.transform.position, Bob.transform.position);
        this.ResetPendulumForces();
    }

    void MoveBob(Vector3 resetBobPosition)
    {
        // Put the bob back in the place we first saw it at in `Start()`
        this.Bob.transform.position = resetBobPosition;

        // Set the transition state
        this.currentStatePosition = resetBobPosition;
    }


    Vector3 PendulumUpdate(Vector3 currentStatePosition, float deltaTime)
    {
        // Add gravity free fall
        this.gravityForce = this.mass * Physics.gravity.magnitude;
        this.gravityDirection = Physics.gravity.normalized;
        this.currentVelocity += this.gravityDirection * this.gravityForce * deltaTime;

        Vector3 pivot_p = this.Pivot.transform.position;
        Vector3 bob_p = this.currentStatePosition;


        Vector3 auxiliaryMovementDelta = this.currentVelocity * deltaTime;
        float distanceAfterGravity = Vector3.Distance(pivot_p, bob_p + auxiliaryMovementDelta);

        // If at the end of the rope
        if(distanceAfterGravity > this.ropeLength || Mathf.Approximately(distanceAfterGravity, this.ropeLength))
        {

            this.tensionDirection = (pivot_p - bob_p).normalized;

            this.pendulumSideDirection = (Quaternion.Euler(0f, 90f, 0f) * this.tensionDirection);
            this.pendulumSideDirection.Scale(new Vector3(1f, 0f, 1f));
            this.pendulumSideDirection.Normalize();

            this.tangentDirection = (-1f * Vector3.Cross(this.tensionDirection, this.pendulumSideDirection)).normalized;


            float inclinationAngle = Vector3.Angle(bob_p-pivot_p, this.gravityDirection);

            this.tensionForce = this.mass * Physics.gravity.magnitude * Mathf.Cos(Mathf.Deg2Rad * inclinationAngle);
            float centripetalForce = ((this.mass * Mathf.Pow(this.currentVelocity.magnitude, 2))/this.ropeLength);
            this.tensionForce += centripetalForce;

            this.currentVelocity += this.tensionDirection * this.tensionForce * deltaTime;
        }

        // Get the movement delta
        Vector3 movementDelta = Vector3.zero;
        movementDelta += this.currentVelocity * deltaTime;


        //return currentStatePosition + movementDelta;

        float distance = Vector3.Distance(pivot_p, currentStatePosition + movementDelta);
        return this.GetPointOnLine(pivot_p, currentStatePosition + movementDelta, distance <= this.ropeLength ? distance : this.ropeLength);
    }

    Vector3 GetPointOnLine(Vector3 start, Vector3 end, float distanceFromStart)
    {
        return start + (distanceFromStart * Vector3.Normalize(end - start));
    }

    void OnDrawGizmos()
    {
        // purple
        Gizmos.color = new Color(.5f, 0f, .5f);
        Gizmos.DrawWireSphere(this.Pivot.transform.position, this.ropeLength);

        Gizmos.DrawWireCube(this.bobStartingPosition, new Vector3(.5f, .5f, .5f));


        // Blue: Auxilary
        Gizmos.color = new Color(.3f, .3f, 1f); // blue
        Vector3 auxVel = .3f * this.currentVelocity;
        Gizmos.DrawRay(this.Bob.transform.position, auxVel);
        Gizmos.DrawSphere(this.Bob.transform.position + auxVel, .2f);

        // Yellow: Gravity
        Gizmos.color = new Color(1f, 1f, .2f);
        Vector3 gravity = .3f * this.gravityForce*this.gravityDirection;
        Gizmos.DrawRay(this.Bob.transform.position, gravity);
        Gizmos.DrawSphere(this.Bob.transform.position + gravity, .2f);

        // Orange: Tension
        Gizmos.color = new Color(1f, .5f, .2f); // Orange
        Vector3 tension = .3f * this.tensionForce*this.tensionDirection;
        Gizmos.DrawRay(this.Bob.transform.position, tension);
        Gizmos.DrawSphere(this.Bob.transform.position + tension, .2f);

        // Red: Resultant
        Gizmos.color = new Color(1f, .3f, .3f); // red
        Vector3 resultant = gravity + tension;
        Gizmos.DrawRay(this.Bob.transform.position, resultant);
        Gizmos.DrawSphere(this.Bob.transform.position + resultant, .2f);


        /* * /
        // Green: Pendulum side direction
        Gizmos.color = new Color(.3f, 1f, .3f);
        Gizmos.DrawRay(this.Bob.transform.position, 3f*this.pendulumSideDirection);
        Gizmos.DrawSphere(this.Bob.transform.position + 3f*this.pendulumSideDirection, .2f);
        /* */

        /* * /
        // Cyan: tangent direction
        Gizmos.color = new Color(.2f, 1f, 1f); // cyan
        Gizmos.DrawRay(this.Bob.transform.position, 3f*this.tangentDirection);
        Gizmos.DrawSphere(this.Bob.transform.position + 3f*this.tangentDirection, .2f);
        /* */
    }
}

More glamour shots:

\$\endgroup\$
  • \$\begingroup\$ This is utterly phenomenal. Really incredible. Been playing around with this and you can really see just how realistic this when you push it into a turbulent state and see it come apart. My one query would be that it appears to be mildly chaotic in increasing and decreasing amplitude, is there a way to explain what is happening there? Still trying pick this apart and digest it all... \$\endgroup\$ – user3447980 May 28 '14 at 18:57
  • \$\begingroup\$ @user3447980 I get this slightly "chaotic" effect as well. Part of it has to do with floating point imprecision in the decimal range. Increasing dt may help as deltaTime will be smaller making it interpolate less but also means more imprecision in the decimal range. I think the real issue is that since the rope can't stretch, I limit the distance away from the pivot(see the bottom of PendulumUpdate). Also keep in mind that this is a rope and not a rigid rod. So if you put the bob in the middle it will free fall, before hitting the end of the rope and starting to swing. \$\endgroup\$ – MLM May 28 '14 at 19:12
  • \$\begingroup\$ @user3447980 I updated the code with a fix for drift in side to side motion when there shouldn't be any movement. I noticed this some setups. I also tried a double precision version of the Pendulum script but there is no difference. There is still a problem where the pendulum slowly doesn't make it as high as when it started which I will try to figure out. \$\endgroup\$ – MLM May 29 '14 at 2:32
  • \$\begingroup\$ @MLM sorry for resurrecting this, but knowing this problem to be a good benchmark for illustrating the properties of numerical integration methods, I noticed your remark where you stated you did not want to cheat by using the analytical (theta-based) solution. As far as I know, the numerical integration (in your case Symplectic Euler) cannot completely solve the issue of not being able to keep a reasonably constant swing radius. Then I saw this line: return this.GetPointOnLine(pivot_p, currentStatePosition + movementDelta, distance <= this.ropeLength ? distance : this.ropeLength);. \$\endgroup\$ – teodron May 20 '16 at 15:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.