Physics Based Spring Module V2.0

Introduction
I’ve created a module that allows you to create a realistic spring object that acts in one direction. This module creates a general solution to a second order differential equation given some inputs. It’s simple to use and is very useful in creating recoil systems, realistic spring systems, and anything else you can imagine a spring is useful for. This spring module is able to accurately replicate real springs. I included the full explanation of the math in this post, and I also included how to use the Spring module and some examples of things that can be made using the Spring module.

Derivation of the algorithm

Physics Involved
A second order differential equation is made using the following physics equations:

  • Newton’s Second Law: F = ma
  • Hooke’s Law: F = -kx
  • Damping Force: F = -αv

In order for the spring to come to a rest state, the spring must have a spring force that is equal to the force of gravity acting on the spring. We also know that velocity is the first derivative of position with respect to time and acceleration is the second derivative of position with respect to time. Thus, we can refer to acceleration as x’’ and velocity as x’, where x is position.

A spring seeks to zero itself out, meaning that the force of gravity acting on the spring will be equal to the spring force acting on the object hanging from the spring. Thus, the second order differential equation looks like this:

mx’’ + αx’ + kx = F

Dividing the equation by m so that x’’ does not have a coefficient in the front, we get:

x’’ + (α/m)x’ + (k/m)x = F/m

Deriving the General Solution
At this point, we should be able to come up with a general solution to this differential equation since we can see that the equation is linear. However, there is one slight problem. When solving a second order differential equation, there are 3 possible characteristic equations depending on the values of m, a, and k. The characteristic equation can be found by solving its characteristic polynomial using the quadratic formula:

r² + (α/m)r + k/m = 0
r₁, r₂ = (-α/m ± √((α/m)² - 4k/m)) / 2

We can observe that √((α/m)² - 4k/m) can give three important values. For now, let’s create a variable Δ such that

Δ = ((α/m)² - 4k/m)

Thus, Δ can be a positive number, 0, or a negative number.

  1. If Δ happens to be a positive number, then r₁, and r₂ will be real numbers.
  2. If Δ happens to be 0, r₁ and r₂ will both be equal, so we can reference them as one variable r.
  3. If Δ happens to be a negative number , r₁ and r₂ will both be imaginary numbers.

Case 1: Δ is a positive number
Since Δ is a positive number, this means that r₁, and r₂ will be distinct real roots. This being said, the characteristic equation of the second order differential equation given the condition that Δ is positive is:

x(t) = C₁e^(-αt/m + t√((α/m)² - 4k/m) / 2) + C₂e^(-αt/m - t√((α/m)² - 4k/m) / 2)

where C₁ and C₂ are arbitrary constants.

Case 2: Δ is 0
Since Δ is 0, this means that r₁ = r₂ are repeated roots, so we can refer to them as just r. This being said, the characteristic equation of the second order differential equation given the condition that Δ is 0 is:

x(t) = C₁e^(-αt/2m) + C₂te^(-αt/2m) = e^(-αt/2m) * (C₁ + C₂t)

Notice that C₂ is multiplied by a variable t. Once again, this is because r is a repeated root.

Case 3: Δ is a negative number
Since Δ is a negative number, this means that r₁, and r₂ are complex conjugates. This is where things start to get a little complex. Imaginary numbers on their own don’t make any sense, but we can make them make sense by using Euler’s formula.

To do this, we must first turn r₁, and r₂ into their complex forms:

r₁ = (α/m + i√(-Δ)) / 2 and r₂ = (α/m - i√(-Δ)) / 2

Thus, the characteristic equation of the second order differential equation given the condition that Δ is negative is:

x(t) = C₁e^((αt/m + it√(-Δ)) / 2) + C₂e^((αt/m - it√(-Δ)) / 2)

Using e^(iθ) = cos(θ) + isin(θ), we get

x(t) = e^(-αt/2m) * (C₁cos(√(-Δ)t) + C₂sin(√(-Δ)t))

Where C₁ and C₂ are arbitrary constants

External Force
What if you want to add an external force into the system? Let’s say gravity for example. Well, you simply solve for a particular solution and add it to the characteristic equation. Since the external force will be a constant, it should be fairly simple creating a particular solution. The particular solution would have to be a constant since it is not with respect to time, so:

xₚ = A

Now, plugging this into the original differential equation,

(xₚ)‘’ + (α/m)(xₚ)’ + (k/m)xₚ = F/m

We can then simplify this to get the value of A

xₚ = A = (F/m) / (k/m) = F / k

Velocity and Acceleration Solutions
Since we now have an equation that defines the position of the mass of the spring, we can easily get the equations for velocity and acceleration by getting the derivatives and second derivatives of the position equations.

Usage

Explanation
The Spring class takes in 6 inputs. If one of the inputs is missing, it is assumed that the input is 0 (or 1 for mass and spring constant). The external force can be changed even after creating the spring.

local Spring = require(script.SpringModule);
local NewSpring = Spring.new(Mass, DampingConstant, SpringConstant, InitialOffset, InitialVelocity, Goal);
  • Mass: The mass of the object hanging on the spring
  • Damping Constant: A constant acting as a resistance against the velocity
  • Spring Constant: A constant that defines how springy a spring is
  • Initial Offset: The initial offset of a spring
  • Initial Velocity: The initial velocity of a spring
  • Goal: The goal position the spring is trying to reach

To see how these parameters change the graph of the offset of the spring, use this desmos graph.

Properties

print(NewSpring.ExternalForce) -- External force of the Spring
print(NewSpring.Goal) -- Goal of the Spring
pring(NewSpring.Frequency) -- Frequency of the Spring
print(NewSpring.Offset) -- Current Offset of the Spring
print(NewSpring.Velocity) -- Current Velocity of the Spring
print(NewSpring.Acceleration) -- Current Acceleration of the Spring
print(NewSpring.StartTick) -- The time using tick() that the spring was created
print(NewSpring.AdvancedObjectStringEnabled) -- Whether to use an advanced string or basic string format for tostring(NewSpring)

Methods

NewSpring:Reset() -- Resets the spring, giving it an updated DifEqFunction Table
NewSpring:SetExternalForce(force) -- Sets the ExternalForce of the spring to the given force
NewSpring:SetGoal(goal) -- Sets the ExternalForce of the spring such that the spring eventually reaches the given number
NewSpring:SetFrequency(frequency) -- Sets the SpringConstant of the Spring to match the given frequency
NewSpring:SnapToCriticalDamping() -- Snaps the damping of the Spring to put the Spring in critical damping
NewSpring:SetOffset(offset, zeroVelocity) -- Sets the velocity of the Spring to the given velocity. If zeroVelocity is set to true, the velocity of the Spring will be set to 0. zeroVelocity = false by default
NewSpring:AddOffset(offset) -- Adds the given offset to the spring
NewSpring:SetVelocity(velocity) -- Sets the velocity of the Spring to the given velocity
NewSpring:AddVelocity(velocity) -- Adds the given velocity to the spring
NewSpring:Print() -- prints the spring in a nice formatted string to the output
Examples

Realistic Spring
Using the Spring module, it is possible to replicate realistic spring physics in game.

local RunService = game:GetService('RunService')
local Spring = require(script:WaitForChild("Spring"));
local Part = workspace:WaitForChild("Part");
local Holder = workspace:WaitForChild("Holder");
local SpringModel = workspace:WaitForChild("Spring");
local InitCFrame = Part.CFrame;
local NewSpring = Spring.new(15, 10, 100, 0, 50, -100);

spawn(function()
	RunService.Stepped:connect(function()
		Part.CFrame = InitCFrame * CFrame.new(0, NewSpring.Offset, 0);
		SpringModel.Size = Vector3.new(SpringModel.Size.X, SpringModel.Size.Y, (Part.Position - Holder.Position).Magnitude);
		SpringModel.CFrame = CFrame.new((Holder.Position + Part.Position)/2, Part.Position);
	end)
end)

The above code creates a spring with the properties

  • Mass = 15
  • DampingConstant = 10
  • SpringConstant = 100
  • InitialOffset = 0
  • InitialVelocity = 50
  • ExternalForce = -100

When run in studio, the outcome looks like this:

Jump Recoil
Using the Spring module, it is possible to easily add recoil to custom camera systems. For example, using my own custom camera where I can add additional rotation about the relative X axis, I can make the spring add recoil to the camera after jumping:

local RunService = game:GetService("RunService");
local UserInputService = game:GetService("UserInputService");
local ContextActionService = game:GetService("ContextActionService");

local Spring = require(script:WaitForChild("Spring"));
local CameraController = require(script:WaitForChild("Camera"));

local Player = game.Players.LocalPlayer;
local Camera = workspace.CurrentCamera;
local Character = script.Parent;
local Humanoid = Character:WaitForChild("Humanoid");
CameraController = CameraController:ControlCamera(Camera);
CameraController:SetCharacter(Character);

local CurrentSpring;
local SpringSettings = {
	BouncySpring = {8, 10, 100, 0, -100, 0};
	OtherSpring = {2, 10, 75, 0, 50, 0};
}


function ManagePlayerInput(ActionName, InputState, Input)
	CameraController:ManagePlayerInput(ActionName, InputState, Input)
	if ActionName == "RightMouseButton" then
		if InputState == Enum.UserInputState.Begin then
			UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition;
		elseif InputState == Enum.UserInputState.End then
			UserInputService.MouseBehavior = Enum.MouseBehavior.Default;
		end
	end
end


function HumanoidStateChanged()
	if Humanoid:GetState() == Enum.HumanoidStateType.Landed then
		if not CurrentSpring then
			CurrentSpring = Spring.new(unpack(SpringSettings.BouncySpring));
		else
			CurrentSpring:Stop();
			CurrentSpring = Spring.new(unpack(SpringSettings.BouncySpring));
		end
	end
end


function RenderStepped(dt)
	if CurrentSpring then
		CameraController.Properties.AdditionalRotation.Y = CurrentSpring.Offset;
	else
		CameraController.Properties.AdditionalRotation.Y = 0;
	end
	CameraController:Update();
end


Humanoid.StateChanged:Connect(HumanoidStateChanged);
RunService.RenderStepped:Connect(RenderStepped);
ContextActionService:BindAction("ScrollWheel", ManagePlayerInput, false, Enum.UserInputType.MouseWheel);
ContextActionService:BindAction("MouseMovement", ManagePlayerInput, false, Enum.UserInputType.MouseMovement, Enum.UserInputType.Touch);
ContextActionService:BindAction("RightMouseButton", ManagePlayerInput, false, Enum.UserInputType.MouseButton2);

The outcome of the code looks like this:

Realistic Rope Bounce
Using the spring module, you can make very realistic springs of all sorts. For example, look at this rope that bounces up and down in a certain position using bezier curves.

local RunService = game:GetService("RunService");

local Line = require(script:WaitForChild("Line"));
local Spring = require(script:WaitForChild("Spring"));

local SpringSettings = {
	BouncySpring = {4, 4, 500, 0, 300, 0};
	OtherSpring = {2, 10, 75, 0, 50, 0};
}

local Rope = Line.new({workspace.P0, workspace.P1, workspace.P2, workspace.P3, workspace.P4}, BrickColor.new("Really black"), 0.1, 20);
local Spring = Spring.new(unpack(SpringSettings.BouncySpring));

local function Lerp(a, b, t)
	return a + (b - a) * t
end

local InitCF = workspace.P1.CFrame;
local InitCF2 = workspace.P2.CFrame;
local Distance = (workspace.P0.Position - workspace.P3.Position).Magnitude;
RunService.Stepped:connect(function()
	workspace.P1.CFrame = CFrame.new((workspace.P0.Position + workspace.P3.Position) / 2, workspace.P3.Position) * CFrame.new(0, Spring.Offset * 2, 0);
	workspace.P2.CFrame = InitCF2 * CFrame.new(0, Spring.Offset * 2, 0);
end)

The outcome of this code looks like this:

Here’s the place so you can see the complete code:
Spring Camera Test.rbxl (68.6 KB)

Showcases

Attack On Titan ODM Gear Rope System
I created a rope system using bezier curves and use the Spring module to create a springy effect when deploying the grapples in the attack on titan game a friend and I are developing:
See Grapple System Here
Updated Grapple System

Summary
The Spring module is up for grabs, you can get it here:

Here’s the desmos graph so you can play around with the spring module visually and mess with parameters:

I also made a git repository for those who prefer using rojo:

Hopefully this helps you in any of your springy situations!

169 Likes

Thank you for the Helpful Module! Probably gonna use that in the Future

3 Likes

Great work @bhristt ! This is a really cool spring module! :hot_face:

4 Likes

Interesting. Is it performant?

2 Likes

Yes, it should be. Requiring the module and creating a new spring using Spring.new() should create a “Spring” that has changing values for Offset, Velocity, and Acceleration. This module doesn’t create any lag either, unless there is some dividing by 0 nonsense by setting mass equal to 0.

4 Likes

In the Usage section of the post I left a desmos link that you can use to play around with the inputs.

2 Likes

Edit: added a usage comment inside of the spring module, and i also made it a free model so you can go back to it for updates!

This is insane! I will probably use this in the future great work.

2 Likes

Hi,

Anyone playing with the camera script, and if so, do you know what parm needs to be changed to decrease the camera wobble?

Thanks

1 Like

You can use this to change the parameters for Spring.new().

local Spring = require(script.Spring)
local NewSpring = Spring.new(Mass, DampingConstant, SpringConstant, InitialOffset, InitialVelocity, ExternalForce)

You can see more on how to use the spring module in the Usage section.

2 Likes

How do you make the Attack on Titan style grapple?

1 Like

This is similar to how I made the rope work in the last example. I use a Bezier curve to create the rope, then I move one of the parts that controls the curve so that it gives the appearance of it wiggling. In the aot game, I do this but to a further extent and move two of the control points at once, and I give it an inward curve at the end by making the last control point go into the gear and the second to last control point go outwards.

1 Like

I don’t know how to program is there like and source code or easier way to explain

Update

  • Added Spring:Start() method so you can start and stop the spring whenever you want
  • Added type checking so you can see the properties and methods of the Spring after requiring the module for ease of use :smirk_cat:

This is really cool I plan to use this in the future.

2 Likes

This is an amazing module and super useful! Thanks for sharing it :slight_smile:

1 Like

I see many uses for this. Great module, but I have one question.
Would there be any merit to using this over the built in spring constraint if you are doing something purely physical (like the box being dangled by the spring in your example)?

1 Like

There’s no real benefit to using the Spring Module for things like a box being dangled, but it can do a lot of things that a SpringConstraint just can’t do. Unlike the SpringConstraint, the spring module let’s you make things like springy ropes. It’s especially useful for things that need to act like a spring, but can’t have a SpringConstraint do those things for them.

2 Likes

Yea, that’s what I was thinking. Thank you!

Is there any way to delta time lock a spring? I’m trying to make an FPS game and the spring’s offset is inconsistent with framerate.