Making a simple Pendulum Model without physics

Disclaimer: This tutorial assumes you know some trigonometry and basic physics.

Hey fellow Developers! Today, I’m writing this tutorial on a Simple Pendulum Model, which simulates the oscillation of a Pendulum in game.

So, I’m writing this because I want to see how much I’ve learnt while reading up on this topic and see if I can help anyone else to understand it in the simplest terms… I think this thread can help me test my knowledge, help others learn and give other more experienced developers an opportunity to leave feedback and tell me what I can do better etc.

So firstly, what is a pendulum? A pendulum is an object that has a:

Bob: image

An Arm(String, Rope…etc): image

and a Fixed Point/Origin: image

and how this works is

the Bob is suspended/connected to the fixed point by the arm.

so now that we know what a pendulum is and we’ve visualized it in basic terms, how do we simulate it in game?

Well, to do that we need to know what Pendulum Motion is, so we that can calculate it ingame:

So, the internet(yes the interweb, my favorite place for finding meanings) says that:

Pendulum motion basically depicts the motion of an object(Bob) hanging from a string(Arm) that moves back and forth. The variables in pendulum motion are the mass, the length of the string, and the location, which is measured by an angle. Forces acting on the mass in pendulum motion are tension and gravity.”

Alright so we know what pendulum Motion is, how do we calculate it?

Well, the displacement of a Pendulum’s Bob is the length of the arc

as seen here: image

Now we just need to find that Arc and have the bob travel along it, so let’s hop into some code :smiley:

–// I’ll be using CFrames for this tutorial, If you can find the displacement
–// You can derive velocity from it yourself using the method I’m about to display
–// And from there you can use a BodyVelocity and constantly update the values
–// But anyways let’s get into it

First I create my Bob and Pendulum in the workspace
Please make sure both of these objects are anchored, remember we aren’t using
the physics engine, we’re manipulating their positions to recreate the Pendulum Motion
:

Then you create your script ( anywhere you want as long as it’s within a runtime environment)

--//Let's define our Bob and Origin Variables

local Bob = workspace.Bob
local Origin = workspace.Origin

--//Next we get the Arm's length ( in this case the distance from the Origin to the Bob
--//We don't use an actual rope constraint or anything else, we can use a beam for aesthetics

local Length = (Origin.Position - Bob.Position).magnitude

--//Okay alot of people may get confused by this part, but now we want a predefined
--//Angle for the sake of this tutorial, so we'll go with an angle of 45 deg
local theta = 45

--//Value for angular velocity which we will use later
local angVel = 0

--//Next we create our function to compute the Bob's Motion per Oscillation
local function Compute()
      --//Using Trigonometry we want to find the arc of the Bob's Displacement on each axis
     --//The reason we multiply the length by the the Arc is so that we take the Length of the arm
     --//Into account when returning the arc of the angle on the axis.. 
    --//In simple terms, we're offsetting the arc from the origin point by the Length

      local XArc = Length * math.sin(theta)
      local YArc = Length * math.cos(theta)

      --//Okay so now we lerp the Bob's CFrame, WHILE keeping it relative to the Origin Point
    Bob.CFrame = Bob.CFrame:Lerp(CFrame.new(Vector3.new(XArc , YArc, 0) , Vector3.new()) + 
    Origin.CFrame.Position, 0.4)
  
end

--//To constantly update the CFrames and Values
game.RunService.Heartbeat:Connect(Compute)

Okay for visualization purposes, I suggest you test the code ingame and see what happens, as you’ll see the Bob experiences displacement, but we want constant displacement, constant velocity, well how do we do that?
well, within our code we need to find the angular velocity and apply it to our angle, so let’s do that right now

local function Compute()

      local XArc = Length * math.sin(theta)
      local YArc = Length * math.cos(theta)

      Bob.CFrame = Bob.CFrame:Lerp(CFrame.new(Vector3.new(XArc , YArc, 0) , Vector3.new()) + 
      Origin.CFrame.Position, 0.4)
    
    --//Now, what's happening here is we're adding our previous angular velocity to a new arc that     
   --//uses the same angle, but it experiences gravity (substituted by 0.01)
  
    angVel = (angVel + (0.01*math.sin(theta)))

   --//Now we apply our AngVel to our Angle and there we have it.
   theta = theta + angVel
end

here’s our Pendulum in the works:

https://gyazo.com/9f2336e945589d5707856b5616433a6e

I apologize in advance if anyone was left confused by this post, I just learnt this and I wanted to share my knowledge with the community in hopes of helping anyone, I really hope someone learned a thing our two from this post.

If you want the Pendulum to return to Equilibrium, you can dampen it with the 0.99 value

local function Compute()

      local XArc = Length * math.sin(theta)
      local YArc = Length * math.cos(theta)

      Bob.CFrame = Bob.CFrame:Lerp(CFrame.new(Vector3.new(XArc , YArc, 0) , Vector3.new()) + 
      Origin.CFrame.Position, 0.4)
 
    angVel = (angVel + (0.01*math.sin(theta))) * 0.99

   theta = theta + angVel
end

Here is the Pendulum with a cool Spiderweb:
https://gyazo.com/430f3122ac7ea03f2891e5f921189135

Also EgoMoose if you’re reading this, I’m a big fan of your work! :smiley:

40 Likes

Nice tutorial, but your code has some issues.

By multiplying the velocity by 0.99 as a substitute for drag and not taking delta time into account, the acceleration of the pendulum depends on the frame rate. For example, imagine two models where x is multiplied by 0.99 every heartbeat. One is running at 30hz and the other is running at 60hz. After 1 second, x on the 30hz model will be at around 7.4 (10 × 0.99^30) and x on the 60hz model will be at around 5.5 (10 × 0.99^60). This is not desirable as players will expect their physics to be the same no matter the framerate of their client or the server.

For drag, you can fix this by calculating how the amount that should be subtracted from the angular velocity, multiplying that by delta time, and then subtracting the result from the angular velocity. You will need to use a lower coefficient - for example 0.9 (or just use something like 0.1 to calculate how much should be subtracted).

For the acceleration, just multiply it by delta time and increase the constant you are multiplying by.

When you use this method you should add the acceleration after the drag is subtracted, because the drag will be overestimated if you add it before. This is more relevant when you’re doing something more precise - for example calculating the drag coefficient of a car based on initial acceleration and top speed and expecting it to reach and maintain that top speed. The user would expect the car to reach the top speed listed.

Fix:

drag = 0.1
speed = 0.5

angVel = (angVel - angVel * drag * deltaTime) + math.sin(theta) * deltaTime * speed

If you’re calculating the drag coefficient or want it stored in the form that you’re using you can use 1 - coefficient.

Another error you made is that you’re using degrees, when math.sin and math.cos assume you are using radians. And I can’t understand why you are using lerp with an alpha of 0.4 rather than just setting the CFrame.

19 Likes

Do you know the texture for the web lol?

2 Likes

How did you make the web swing? Could you possibly send me a .rbxl of it

The creator of this post, has placed the code is right there to show you how to do it if you look at the code it shows it step by step on how to do it.

I know this is an old topic but I got this

local Bob = workspace.Bob
local Origin = workspace.Origin

--//Next we get the Arm's length ( in this case the distance from the Origin to the Bob
--//We don't use an actual rope constraint or anything else, we can use a beam for aesthetics

local Length = (Origin.Position - Bob.Position).magnitude

--//Okay alot of people may get confused by this part, but now we want a predefined
--//Angle for the sake of this tutorial, so we'll go with an angle of 45 deg
local theta = math.deg(360)
if theta == 360 or 0 then
	theta = 1 -- fail safe
end
--//Value for angular velocity which we will use later
local angVel = 0
local realdrag = 1.8
--//Next we create our function to compute the Bob's Motion per Oscillation
local function Compute(deltaTime)
	--//Using Trigonometry we want to find the arc of the Bob's Displacement on each axis
	--//The reason we multiply the length by the the Arc is so that we take the Length of the arm
	--//Into account when returning the arc of the angle on the axis.. 
	--//In simple terms, we're offsetting the arc from the origin point by the Length

	local XArc = Length * math.sin(theta)
	local YArc = Length * math.cos(theta)
	local function sqrtNegative(value)
		local funny = (Vector3.yAxis*value).Unit.Y
		return math.sqrt(math.abs(value))*funny
	end
	local drag = math.clamp(realdrag*(sqrtNegative(YArc)),realdrag/1.77,realdrag*1.77)*1.77 -- so it cant be too strong
	local speed = 0.75
	if drag ~= drag then
		drag = 1.25 -- prevents part from disappearing
	end

	angVel = (angVel - angVel * drag * deltaTime) + math.sin(theta) * deltaTime * speed
	--//Okay so now we lerp the Bob's CFrame, WHILE keeping it relative to the Origin Point
	local cframeBase = Bob.CFrame:Lerp(CFrame.new(Vector3.new(XArc , YArc, 0) , Vector3.new()) + 
		Origin.CFrame.Position, 0.4)
	Bob.CFrame = cframeBase
	theta += angVel
end

--//To constantly update the CFrames and Values
game:GetService("RunService").Heartbeat:Connect(Compute)

The drag is kinda more accurate? I don’t really know it just feels better lol, adjust stuff if you want I guess I just messed with it until it felt right

I have to fix this because its getting more drag at the top of the pendulum, which isn’t accurate because more velocity is at the bottom of the pendulum woopsie

local Bob = workspace.Bob
local Origin = workspace.Origin

--//Next we get the Arm's length ( in this case the distance from the Origin to the Bob
--//We don't use an actual rope constraint or anything else, we can use a beam for aesthetics

local Length = (Origin.Position - Bob.Position).magnitude

--//Okay alot of people may get confused by this part, but now we want a predefined
--//Angle for the sake of this tutorial, so we'll go with an angle of 45 deg
local theta = math.deg(360)
if theta == 360 or 0 then
	theta = 1 -- fail safe
end
--//Value for angular velocity which we will use later
local angVel = 0
local realdrag = 1
--//Next we create our function to compute the Bob's Motion per Oscillation
local function Compute(deltaTime)
	--//Using Trigonometry we want to find the arc of the Bob's Displacement on each axis
	--//The reason we multiply the length by the the Arc is so that we take the Length of the arm
	--//Into account when returning the arc of the angle on the axis.. 
	--//In simple terms, we're offsetting the arc from the origin point by the Length

	local XArc = Length * math.sin(theta)
	local YArc = Length * math.cos(theta)
	local YDrag = Length * math.cos(360-theta)
	local function sqrtNegative(value)
		local funny = (Vector3.yAxis*value).Unit.Y
		return math.sqrt(math.abs(value))*funny
	end
	local drag = math.clamp(realdrag*(sqrtNegative(YDrag/2)),realdrag/1.77,realdrag*1.77)*math.sqrt(1.77) -- so it cant be too strong
	local speed = 0.75
	print(drag)
	if drag ~= drag then
		drag = 1.25 -- prevents part from disappearing
	end

	angVel = (angVel - angVel * drag * deltaTime) + math.sin(theta) * deltaTime * speed
	--//Okay so now we lerp the Bob's CFrame, WHILE keeping it relative to the Origin Point
	local cframeBase = Bob.CFrame:Lerp(CFrame.new(Vector3.new(XArc , YArc, 0) , Vector3.new()) + 
		Origin.CFrame.Position, 0.4)
	Bob.CFrame = cframeBase
	theta += angVel
end

--//To constantly update the CFrames and Values
game:GetService("RunService").Heartbeat:Connect(Compute)
``` fixed for real now
3 Likes