Skip to content

Instantly share code, notes, and snippets.

@FleshMobProductions
Last active September 9, 2024 09:35
Show Gist options
  • Save FleshMobProductions/7b523b81d7595e685410be11b24aac3f to your computer and use it in GitHub Desktop.
Save FleshMobProductions/7b523b81d7595e685410be11b24aac3f to your computer and use it in GitHub Desktop.
Ryan Juckett's Code for Damped Springs (https://www.ryanjuckett.com/damped-springs/) implemented in C# using UnityEngine Mathf methods
using UnityEngine;
namespace FMPUtils.Extensions
{
public static class SpringMotion
{
// Reference video:
// https://www.youtube.com/watch?v=bFOAipGJGA0
// Instant "Game Feel" Tutorial - Secrets of Springs Explained (by Toyful Games)
// The channel LlamAcademy also made an adaption of the concept for Unity,
// "Add JUICE to Your Game with Springs | Unity Tutorial" - you can watch it here:
// https://www.youtube.com/watch?v=6mR7NSsi91Y
// Copyright notice of the original source:
/******************************************************************************
Copyright (c) 2008-2012 Ryan Juckett
http://www.ryanjuckett.com/
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source
distribution.
******************************************************************************/
// This file uses altered parts from the source: Comments are kept from the
// original article (for the most part) while the code has been translated from
// c++ to C# using UnityEngine Mathf methods. The original source can be found
// in this article: https://www.ryanjuckett.com/damped-springs/
// Permissions and conditions from the original license are retained.
//******************************************************************************
// Cached set of motion parameters that can be used to efficiently update
// multiple springs using the same time step, angular frequency and damping
// ratio.
//******************************************************************************
public struct DampedSpringMotionParams
{
// newPos = posPosCoef*oldPos + posVelCoef*oldVel
public float posPosCoef, posVelCoef;
// newVel = velPosCoef*oldPos + velVelCoef*oldVel
public float velPosCoef, velVelCoef;
};
//******************************************************************************
// This function will compute the parameters needed to simulate a damped spring
// over a given period of time.
// - An angular frequency is given to control how fast the spring oscillates.
// - A damping ratio is given to control how fast the motion decays.
// damping ratio > 1: over damped
// damping ratio = 1: critically damped
// damping ratio < 1: under damped
//******************************************************************************
private static DampedSpringMotionParams CalcDampedSpringMotionParams(
float deltaTime, // time step to advance
float angularFrequency, // angular frequency of motion
float dampingRatio) // damping ratio of motion
{
const float epsilon = 0.0001f;
DampedSpringMotionParams pOutParams;
// force values into legal range
if (dampingRatio < 0.0f) dampingRatio = 0.0f;
if (angularFrequency < 0.0f) angularFrequency = 0.0f;
// if there is no angular frequency, the spring will not move and we can
// return identity
if (angularFrequency < epsilon)
{
pOutParams.posPosCoef = 1.0f; pOutParams.posVelCoef = 0.0f;
pOutParams.velPosCoef = 0.0f; pOutParams.velVelCoef = 1.0f;
return pOutParams;
}
if (dampingRatio > 1.0f + epsilon)
{
// over-damped
float za = -angularFrequency * dampingRatio;
float zb = angularFrequency * Mathf.Sqrt(dampingRatio * dampingRatio - 1.0f);
float z1 = za - zb;
float z2 = za + zb;
// Value e (2.7) raised to a specific power
float e1 = Mathf.Exp(z1 * deltaTime);
float e2 = Mathf.Exp(z2 * deltaTime);
float invTwoZb = 1.0f / (2.0f * zb); // = 1 / (z2 - z1)
float e1_Over_TwoZb = e1 * invTwoZb;
float e2_Over_TwoZb = e2 * invTwoZb;
float z1e1_Over_TwoZb = z1 * e1_Over_TwoZb;
float z2e2_Over_TwoZb = z2 * e2_Over_TwoZb;
pOutParams.posPosCoef = e1_Over_TwoZb * z2 - z2e2_Over_TwoZb + e2;
pOutParams.posVelCoef = -e1_Over_TwoZb + e2_Over_TwoZb;
pOutParams.velPosCoef = (z1e1_Over_TwoZb - z2e2_Over_TwoZb + e2) * z2;
pOutParams.velVelCoef = -z1e1_Over_TwoZb + z2e2_Over_TwoZb;
}
else if (dampingRatio < 1.0f - epsilon)
{
// under-damped
float omegaZeta = angularFrequency * dampingRatio;
float alpha = angularFrequency * Mathf.Sqrt(1.0f - dampingRatio * dampingRatio);
float expTerm = Mathf.Exp(-omegaZeta * deltaTime);
float cosTerm = Mathf.Cos(alpha * deltaTime);
float sinTerm = Mathf.Sin(alpha * deltaTime);
float invAlpha = 1.0f / alpha;
float expSin = expTerm * sinTerm;
float expCos = expTerm * cosTerm;
float expOmegaZetaSin_Over_Alpha = expTerm * omegaZeta * sinTerm * invAlpha;
pOutParams.posPosCoef = expCos + expOmegaZetaSin_Over_Alpha;
pOutParams.posVelCoef = expSin * invAlpha;
pOutParams.velPosCoef = -expSin * alpha - omegaZeta * expOmegaZetaSin_Over_Alpha;
pOutParams.velVelCoef = expCos - expOmegaZetaSin_Over_Alpha;
}
else
{
// critically damped
float expTerm = Mathf.Exp(-angularFrequency * deltaTime);
float timeExp = deltaTime * expTerm;
float timeExpFreq = timeExp * angularFrequency;
pOutParams.posPosCoef = timeExpFreq + expTerm;
pOutParams.posVelCoef = timeExp;
pOutParams.velPosCoef = -angularFrequency * timeExpFreq;
pOutParams.velVelCoef = -timeExpFreq + expTerm;
}
return pOutParams;
}
//******************************************************************************
// This function will update the supplied position and velocity values over
// according to the motion parameters.
//******************************************************************************
private static void UpdateDampedSpringMotion(
ref float pPos, // position value to update
ref float pVel, // velocity value to update
float equilibriumPos, // position to approach
DampedSpringMotionParams parameters) // motion parameters to use
{
float oldPos = pPos - equilibriumPos; // update in equilibrium relative space
float oldVel = pVel;
pPos = oldPos * parameters.posPosCoef + oldVel * parameters.posVelCoef + equilibriumPos;
pVel = oldPos * parameters.velPosCoef + oldVel * parameters.velVelCoef;
}
/// <summary>
/// Calculate a spring motion development for a given deltaTime
/// </summary>
/// <param name="position">"Live" position value</param>
/// <param name="velocity">"Live" velocity value</param>
/// <param name="equilibriumPosition">Goal (or rest) position</param>
/// <param name="deltaTime">Time to update over</param>
/// <param name="angularFrequency">Angular frequency of motion</param>
/// <param name="dampingRatio">Damping ratio of motion</param>
public static void CalcDampedSimpleHarmonicMotion(ref float position, ref float velocity,
float equilibriumPosition, float deltaTime, float angularFrequency, float dampingRatio)
{
var motionParams = CalcDampedSpringMotionParams(deltaTime, angularFrequency, dampingRatio);
UpdateDampedSpringMotion(ref position, ref velocity, equilibriumPosition, motionParams);
}
/// <summary>
/// Calculate a spring motion development for a given deltaTime
/// </summary>
/// <param name="position">"Live" position value</param>
/// <param name="velocity">"Live" velocity value</param>
/// <param name="equilibriumPosition">Goal (or rest) position</param>
/// <param name="deltaTime">Time to update over</param>
/// <param name="angularFrequency">Angular frequency of motion</param>
/// <param name="dampingRatio">Damping ratio of motion</param>
public static void CalcDampedSimpleHarmonicMotion(ref Vector2 position, ref Vector2 velocity,
Vector2 equilibriumPosition, float deltaTime, float angularFrequency, float dampingRatio)
{
var motionParams = CalcDampedSpringMotionParams(deltaTime, angularFrequency, dampingRatio);
UpdateDampedSpringMotion(ref position.x, ref velocity.x, equilibriumPosition.x, motionParams);
UpdateDampedSpringMotion(ref position.y, ref velocity.y, equilibriumPosition.y, motionParams);
}
/// <summary>
/// Calculate a spring motion development for a given deltaTime
/// </summary>
/// <param name="position">"Live" position value</param>
/// <param name="velocity">"Live" velocity value</param>
/// <param name="equilibriumPosition">Goal (or rest) position</param>
/// <param name="deltaTime">Time to update over</param>
/// <param name="angularFrequency">Angular frequency of motion</param>
/// <param name="dampingRatio">Damping ratio of motion</param>
public static void CalcDampedSimpleHarmonicMotion(ref Vector3 position, ref Vector3 velocity,
Vector3 equilibriumPosition, float deltaTime, float angularFrequency, float dampingRatio)
{
var motionParams = CalcDampedSpringMotionParams(deltaTime, angularFrequency, dampingRatio);
UpdateDampedSpringMotion(ref position.x, ref velocity.x, equilibriumPosition.x, motionParams);
UpdateDampedSpringMotion(ref position.y, ref velocity.y, equilibriumPosition.y, motionParams);
UpdateDampedSpringMotion(ref position.z, ref velocity.z, equilibriumPosition.z, motionParams);
}
/// <summary>
/// Calculate a spring motion development for a given deltaTime quickly without
/// considering corner cases for dampingRatio or angularFrequency
/// </summary>
/// <param name="position">"Live" position value</param>
/// <param name="velocity">"Live" velocity value</param>
/// <param name="equilibriumPosition">Goal (or rest) position</param>
/// <param name="deltaTime">Time to update over</param>
/// <param name="angularFrequency">Angular frequency of motion</param>
/// <param name="dampingRatio">Damping ratio of motion</param>
public static void CalcDampedSimpleHarmonicMotionFast(ref float position, ref float velocity,
float equilibriumPosition, float deltaTime, float angularFrequency, float dampingRatio)
{
float x = position - equilibriumPosition;
velocity += (-dampingRatio * velocity) - (angularFrequency * x);
position += velocity * deltaTime;
}
/// <summary>
/// Calculate a spring motion development for a given deltaTime quickly without
/// considering corner cases for dampingRatio or angularFrequency
/// </summary>
/// <param name="position">"Live" position value</param>
/// <param name="velocity">"Live" velocity value</param>
/// <param name="equilibriumPosition">Goal (or rest) position</param>
/// <param name="deltaTime">Time to update over</param>
/// <param name="angularFrequency">Angular frequency of motion</param>
/// <param name="dampingRatio">Damping ratio of motion</param>
public static void CalcDampedSimpleHarmonicMotionFast(ref Vector2 position, ref Vector2 velocity,
Vector2 equilibriumPosition, float deltaTime, float angularFrequency, float dampingRatio)
{
Vector2 x = position - equilibriumPosition;
velocity += (-dampingRatio * velocity) - (angularFrequency * x);
position += velocity * deltaTime;
}
/// <summary>
/// Calculate a spring motion development for a given deltaTime quickly without
/// considering corner cases for dampingRatio or angularFrequency
/// </summary>
/// <param name="position">"Live" position value</param>
/// <param name="velocity">"Live" velocity value</param>
/// <param name="equilibriumPosition">Goal (or rest) position</param>
/// <param name="deltaTime">Time to update over</param>
/// <param name="angularFrequency">Angular frequency of motion</param>
/// <param name="dampingRatio">Damping ratio of motion</param>
public static void CalcDampedSimpleHarmonicMotionFast(ref Vector3 position, ref Vector3 velocity,
Vector3 equilibriumPosition, float deltaTime, float angularFrequency, float dampingRatio)
{
Vector3 x = position - equilibriumPosition;
velocity += (-dampingRatio * velocity) - (angularFrequency * x);
position += velocity * deltaTime;
}
}
}
@Chuniqus
Copy link

Seems to be working on my end. Thx! :)

@FleshMobProductions
Copy link
Author

Good to know, thanks for trying it out!

@SwathingSoap
Copy link

Why don't we use something simple like this?

  public static void CalcDampedSimpleHarmonicMotion (
            ref float value, 
            ref float velocity, 
            float equilibriumPosition, 
            float deltaTime, 
            float angularFrequency, 
            float dampingRatio
        )
        {

            float x = value - equilibriumPosition;
            velocity += (-dampingRatio * velocity) - (angularFrequency * x);
            value += velocity * deltaTime ;
        }

It's quite suitable for similar animations

Unity_HRLjrO2fDz.mp4

@FleshMobProductions
Copy link
Author

I initially just ported the original code, but this looks pretty efficient, thanks for sharing and including an example video! I'll add your method to the script above

@Kofiro
Copy link

Kofiro commented Aug 14, 2022

Hello, sorry if this seems like a trivial question to ask, but could you kindly share an example on how to use this? I can't seem to be able to get it to work. Thanks.

@Kofiro
Copy link

Kofiro commented Aug 14, 2022

void Update()
   {
       if (Input.GetMouseButton(0))
       {
          
           Vector3 localPos = new Vector3(2f, 2f, 2f);
           
           SpringMotion.CalcDampedSimpleHarmonicMotionFast(ref localPos, ref velocity, originalPos, Time.deltaTime * 20f, frequency, damping);
       }
   }

@Kofiro
Copy link

Kofiro commented Aug 14, 2022

The code above is my attempt at using it. Kindly let me know if I am doing anything wrong. Thanks.

@SwathingSoap
Copy link

Hello, sorry if this seems like a trivial question to ask, but could you kindly share an example on how to use this? I can't seem to be able to get it to work. Thanks.

    protected float value;
    protected float velocity;

    public float targetValue; //Set in inspector


  private void Update()
    {
        float deltaTime = Time.deltaTime;
        
        CodeExtensions.SpringMotion.CalcDampedSimpleHarmonicMotion(ref value, 
            ref velocity,
            targetValue,
            deltaTime,
            angularFrequency,
            dampingRatio);
        
        SetValue(value);
    }


 protected override void SetValue(float value)
    {
        text.fontSize = value;
    }

Ignore the access modifiers, I have an abstract class for different types of Sprint motion

@Kofiro
Copy link

Kofiro commented Aug 21, 2022

Hello, sorry if this seems like a trivial question to ask, but could you kindly share an example on how to use this? I can't seem to be able to get it to work. Thanks.

    protected float value;
    protected float velocity;

    public float targetValue; //Set in inspector


  private void Update()
    {
        float deltaTime = Time.deltaTime;
        
        CodeExtensions.SpringMotion.CalcDampedSimpleHarmonicMotion(ref value, 
            ref velocity,
            targetValue,
            deltaTime,
            angularFrequency,
            dampingRatio);
        
        SetValue(value);
    }


 protected override void SetValue(float value)
    {
        text.fontSize = value;
    }

Ignore the access modifiers, I have an abstract class for different types of Sprint motion

Thanks a lot. It works now! I truly appreciate your help. Have a nice day!!

@Zain-ul-din
Copy link

Can anyone show me an example by this I can move my player smoothly in the x direction
some like this transform.position = new Vector3 (transform.position.x + 4f,transform.position.y,transform.position.z);
Thanks in advance!

@SwathingSoap
Copy link

some like this transform.position = new Vector3 (transform.position.x + 4f,transform.position.y,transform.p

If you want to move your character more smoothly, you don't have to use SpringMotion, personally I would use it for movement last

First of all you don't use Time.deltaTime in your implementation;
transform.position = new Vector3 (transform.position.x + 4f * Time.deltaTime,transform.position.y,transform.position.z);

@Zain-ul-din
Copy link

But i wana to switch line on single btn click here is the code please see

#region ChangeLines
        
        
        [Space(30)] [Header("Change Lines")] 
        [SerializeField]       private    List<Vector3>    lines;
        [Tooltip("Input Delay in Second")]                                      [Range(0,1f)]
        [SerializeField]       private    float            inputDelay            =  0.2f;
        [SerializeField]       private    int              initialLine           =     0;
        [SerializeField]       private    float            lineChangeSpeed       =    3f;
        [SerializeField]       private    float            lineChangeSpeedSmooth =  0.2f;
        [SerializeField]       private    AnimationCurve   lineChangeCurve;
        [SerializeField]       private    AnimationCurve   lineChangeSmoothCurve;
        
        private     float           _inputDelay;
        private     Vector3         _currentTargetedLine;
        public      int             CurrentLineIdx {get; private set;}
        private     bool            _canMoveLR;
        private     Transform       mesh;
        private     bool            _overRideQuaternion;
        private     bool            _isPressingBtn;

        /// <summary>
        /// Jump Initial SetUp on Awake
        /// </summary>
        private void InitChangeLines() 
        { 
          Utilities.QuickSort(0, lines.Count - 1, ref lines, (lhs, rhs) => lhs.x < rhs.x  );
          CurrentLineIdx      =    initialLine;
          _inputDelay         =    inputDelay;
          _canMoveLR          =    true;
          mesh                =    transform.GetChild(0);
          _overRideQuaternion =    false;
        }

        private void ChangeLines () 
        {
            int flag;
            
            #if UNITY_EDITOR
             if (Input.GetKey(KeyCode.LeftArrow)) { flag = -1; }
             else if (Input.GetKey(KeyCode.RightArrow)) { flag = 1; }
             else flag = 0;
            #else
             if (leftBtn.isPressing) { flag = -1; }
             else if (rightBtn.isPressing) { flag = 1; }
             else flag = 0;
            #endif

            _isPressingBtn = flag != 0;
            
            if (flag == 0 || CurrentLineIdx == 0 || CurrentLineIdx == (lines.Count - 1)) Idle();
            MoveX(flag);
            MoveTowardsCurrentLine(flag);
        }

        /// <summary>
        /// -1 left & 1 right
        /// </summary>
        /// <param name="flag"></param>
        public void MoveX (int flag) 
        {
            if (!_canMoveLR) return;
            if (inputDelay >= 0) inputDelay -= Time.deltaTime;
            if (inputDelay > 0 || flag == 0) return;
            inputDelay = _inputDelay;

            if (flag == 1) MoveRight(flag);
            else if (flag == -1) MoveLeft(flag);
        } 
        
        // Helper Functions
        private void MoveLeft(int flag) 
        {
            CurrentLineIdx = Mathf.Clamp((CurrentLineIdx - 1), 0, lines.Count - 1);
            SetCurrentLine(lines[CurrentLineIdx]);
            if (CurrentLineIdx == 0) 
            {
                Idle(); 
            }
            else 
            {
                // mesh.DORotate(new Vector3(0, -45, 0), 0.3f);
                // .OnComplete(() => {
                //     if (flag == 0) mesh.DORotate(new Vector3(0, 0, 0), 1f);
                // });   
            }
        }
        
        private void MoveRight(int flag) 
        {
            CurrentLineIdx = Mathf.Clamp((CurrentLineIdx + 1), 0, lines.Count - 1);
            SetCurrentLine(lines[CurrentLineIdx]);
            if (CurrentLineIdx == (lines.Count - 1)) 
            {
                Idle(); 
            }
            else {
                // mesh.DORotate(new Vector3(0, 45, 0), 0.3f);
                // .OnComplete(() => {
                //     if (flag == 0) mesh.DORotate(new Vector3(0, 0, 0), 1f);
                // });
            }
        }

        private void Idle() 
        {
            if (_overRideQuaternion) return;
            mesh.rotation = Quaternion.Lerp(mesh.rotation, Quaternion.Euler(0,0,0), Time.deltaTime * 8f);
            // mesh.DORotate(new Vector3(0, 0, 0), 1f);
        }
        
        private void SetCurrentLine (Vector3 line) 
        {
            inputDelay = _inputDelay;
            OnLineChange?.Invoke(lines, CurrentLineIdx);
            _currentTargetedLine = line;
        }
        
        private void MoveTowardsCurrentLine (int moveFlag) 
        {
            var targetPos = Utilities.VectorX(transform, _currentTargetedLine.x);
            if (!_isPressingBtn) 
            { 
                transform.position = Vector3.Lerp(transform.position, targetPos, lineChangeCurve
                    .Evaluate(Time.deltaTime * lineChangeSpeed));     
            }
            else 
            {
                transform.position = Vector3.Lerp(transform.position, targetPos,
                    lineChangeSmoothCurve.Evaluate(Time.smoothDeltaTime * lineChangeSpeedSmooth));
                // transform.position = Vector3.MoveTowards (transform.position, targetPos, lineChangeSpeedSmooth * Time.deltaTime);
            }
        }

        #endregion

MoveTowardsCurrentLine is function where i have to do this logic

@Zain-ul-din
Copy link

Hey how can I check spring is on rest let's suppose I want to animate the button and also wanna to open a panel using this button who can i dot this ?

@Zain-ul-din
Copy link

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using Randoms.Motions.Spring;
using UnityEngine.Events;
using UnityEngine.UI;

public class BtnController : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
{
    [HideInInspector] public bool isPressing;
    
    [SerializeField] private Vector2Spring spring;
    [SerializeField]   Vector2  targetValue;
    [SerializeField] private RectTransform obj;
    [SerializeField] private float getDowndelay = 1f;
    
    bool isInitized = true;
    internal Vector2 _targetValue;
    internal Vector2 initialValue;
    
    internal float delay = 1f;

    public UnityEvent onClick;  
    void Start ()
    {
        initialValue = obj.localScale;
        _targetValue = targetValue;
        spring.GetSpringValues (targetValue, Time.deltaTime);
    }
    
    void SetValue (Vector2 value)
    {
        obj.localScale = value;
    }
    
    public bool wasPressed = false;

    void Update ()
    {
        _targetValue = isPressing ? targetValue : initialValue;
        
        Vector2 springValue = spring.GetSpringValues (_targetValue, Time.deltaTime);

        if (isPressing)
        {
            wasPressed = true;
        }
        else 
        {
            // calculate delay
            if (wasPressed && springValue == initialValue)
            {
                wasPressed = false;
                onClick?.Invoke ();
                // getDowndelay -= Time.deltaTime;
                // if (getDowndelay < 0)
                // {
                //     onClick?.Invoke ();
                //     wasPressed = false;
                // }
            }
        }


        if (delay > 0)
        {
            delay -= Time.deltaTime;
        } 
        else
        {
            SetValue (springValue);
        }
        
    }
    
    public void OnPointerDown (PointerEventData eventData)
    {
       isPressing = true;
    }

    public void OnPointerUp (PointerEventData eventData)
    {
       isPressing = false;
    }
}

@Zain-ul-din
Copy link

here is the code
Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment