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.
- If Δ happens to be a positive number, then r₁, and r₂ will be real numbers.
- If Δ happens to be 0, r₁ and r₂ will both be equal, so we can reference them as one variable r.
- 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!