Introduction
Hello there!
In this forum post, I’ll show you the inner workings of a physics-based spring and how to make your own! But hold on, what are the uses of such a thing?
For example, FPS games use springs for gun recoil, sway, walk cycle, and much more. And there are a ton more use-cases that extend beyond FPS games.
The base code in this post is from this tutorial made by The Step Event. All I did was port it into Roblox as a module script (and tweak a few stuff). So, massive credits to him!
So without further ado, let’s begin!
Start
Before we start doing anything, let’s create a module script. Here’s the code:
- Target - spring’s resting position
- Tension - aggresiveness of wobble
- Dampening - fadeout of spring
local spring = {}
spring.__index = spring
function spring.new(target: any)
local self = {}
self.Target = target
self.Position = target
self.Velocity = 0
self.Tension = .025
self.Dampening = .3
return setmetatable(self, spring)
end
function spring:Update()
end
function spring:Impulse(num: number)
end
return spring
And yes, there are empty functions. We’ll get back to them later.
A spring has two states, a resting state and an active (extension/contraction) state. If you stretch a spring upwards, it bounces up and down and slows down until it reaches its resting state. So how will we mimic this behavior? Let’s first start with Hooke’s Law.
Hooke’s Law equates to the force needed to push the spring back into it’s rest state. This is why we negate k
in the equation.
F = -k * x
F = -tension * displacement
And to actually simulate the spring’s movement, we’ll need Newton’s Second Law.
F = m * a
F = mass * acceleration
Combining Hooke’s law with Newton’s second law, we get this.
F = (-k * m) / x
F = (-tension * mass) / displacement
For simplicity’s sake, let’s assume our spring has a mass of 1.
F = -k * x
F = -tension * displacement
Then for the loss of energy (or dampening), we can use this formula.
F = d * v
F = dampening * velocity
And in combining these formulas, we get this.
F = (-k * x) - (d * v)
F = (-tension * displacement) - (dampening * velocity)
Okay, okay, that’s nice. Time to convert it into code. Remember that an object has a position, velocity changes the position, and acceleration changes velocity.
function spring:Update()
self.Velocity += (-self.Tension * (self.Position - self.Target)) - (self.Dampening * self.Velocity)
self.Position += self.Velocity
end
And of course, we can’t forget our impulse function.
function spring:Impulse(num: number)
self.Velocity += num
end
Everything should look like this now:
--[[
Description:
A physics-based spring, useful for many applications like
gun recoil, viewmodel arms sway, and etc..
Explanation:
Basics of Motion:
An object has position,
Velocity changes position,
Acceleration changes velocity.
Hooke's Law (force of spring):
F = -tension * displacement
Newton's Second Law (acceleration of spring):
F = mass * acceleration
(let mass be 1)
F = acceleration
Dampening (damper of spring):
F = dampening * velocity
Combination:
F = (-tension * displacement) - (dampening * velocity)
]]
local spring = {}
spring.__index = spring
function spring.new(target: any)
local self = {}
self.Target = target
self.Position = target
self.Velocity = 0
self.Tension = .025
self.Dampening = .3
return setmetatable(self, spring)
end
function spring:Update()
self.Velocity += (-self.Tension * (self.Position - self.Target)) - (self.Dampening * self.Velocity)
self.Position += self.Velocity
end
function spring:Impulse(num: number)
self.Velocity += num
end
return spring
And that’s practically it, you can now go ham with your new physics-based spring module. Thanks for reading!
Example
Moving Part
local spring = require(script.Spring).new(5)
spring.Dampening = .1
spring.Tension = .1
local random = Random.new()
RNS.Heartbeat:Connect(function()
spring:Update()
workspace.Test.Position = Vector3.new(-8.025, spring.Position, 44.45)
end)
while true do
spring:Impulse(2)
task.wait(2)
spring:Impulse(-2)
task.wait(2)
end
Bouncy Pad
local RNS = game:GetService("RunService")
local part = script.Parent
local spring = require(script.Spring).new(part.Position.Y)
spring.Dampening = .1
spring.Tension = .5
local deb = false
RNS.Heartbeat:Connect(function(delta)
spring:Update(delta)
part.Position = Vector3.new(part.Position.X, spring.Position, part.Position.Z)
end)
part.Touched:Connect(function()
if deb then
return
end
deb = true
part.BrickColor = BrickColor.Red()
spring:Impulse(-1)
task.wait(3)
part.BrickColor = BrickColor.Green()
deb = false
end)