This tutorial covers the basics of simulating physics using Guis. I have been creating and showcasing my physics related resources and cool creations, so might as well just give you a brief idea on how I do it. Thanks!
Simulating Physics using Guis
Making Lifeless Guis feel alive
If you have been following my posts recently, you are going to encounter a lot of posts dedicated to physics simulations using Guis, be it steering behaviors, conservation and transfer of momentum or just applying forces to a gui object. This topic covers almost everything you need to know about simulating physics using guis. We’ll also be scripting our own simulation:
- An apple falling from a tree on Newton’s head
General Misconception
Before we begin, I would like to address a general misconception that beginners generally have. Many jump to the conclusion of using TweenService to simulate physics. It is a bad practice. TweenService isn’t meant to simulate physics. Moreover its highly inflexible in terms of physics simulations. You’ll see why, as we get into scripting it.
Understanding Essential Terms
When scripting these simulations, you are frequently going to encounter these terms, not only in this tutorial but also physics in general, whether it be school or university. You should also be thorough with Newton’s Laws of motion! You can skip this portion if you already have a good understanding of the following:
- Motion
- Velocity
- Acceleration
- Forces
- Mass
motion
- the process of the position or orientation of an object with respect to time.
That is the simplest way we can define what motion is. Here is an example of what is in motion and what is not:
A car in motion:
A car at rest:
velocity
- Velocity is a vector quantity. Meaning it has a magnitude and a direction. It is the rate of change of its position with respect to time. It is the Speed but with a Direction of where the object is moving.
acceleration
- It is the rate of change of velocity. Rate refers to the quantity divided by time. The formula for acceleration is:
v is final velocity, u is the initial velocity and t is time!
force
- A very common and simple definition of force is a push or a pull. The formula for force is force = mass x acceleration
mass
- mass is a physical quantity that every entity has, it is the amount of matter an entity contains.
Creating a Physical Entity
We’ll first start by creating a physical entity in our 2D space. You can do this by multiple ways. But, personally I like to use ModuleScripts for the same.
Here’s a boilerplate example:
local Entity = {}
Entity.__index = Entity
function Entity.new(frame, v, a, m) -- v = velocity, a = acceleration, m = mass
local self = setmetatable({
frame = frame,
velocity = v,
acceleration = a,
mass = m,
position = frame.AbsolutePosition
}, Entity)
return self
end
return Entity
We’ll use these essential physical quantities. For this tutorial, We’ll take the acceleration for each object to be constant and the mass to be 1. The one thing to note is that we’ll be using Vector2 values for velocity, position and acceleration! I’ll be explaining how, further in the tutorial
The next boilerplate functions I like to make is an Update function. This function is responsible for updating the velocity, acceleration and position of the entity! Similar to p5.js’s functions. This function is called every RenderStepped from a client script to simulate continuous and smooth motion.
function Entity:Update()
self.position += self.velocity
self.frame.Position = UDim2.new(0, self.position.x, 0, self.position.y)
end
Our physical entity now has a basic structure, we’ll be using this structure throughout!
You might have noticed I wrote self.position += self.velocity
. The reason to that is, velocity is a vector2 value in our code. Suppose the position of our entity was Vector2.new(5, 5). And we said, the velocity is constant and is Vector2.new(1,1). What this essentially is saying is - Position = Position + Velocity. Which is Position = Vector2.new(5, 5) + Vector2.new(1, 1). Running this every frame will give a smooth simulation of the entity moving diagonally to the bottom right of the screen.
Applying Forces to an Entity
We created our Entity, but, how do we move it? We’ll start by creating a simple function called :ApplyForce()
which is essentially ran to apply certain force to the Entity. If you remember, f = ma. Which is force = mass * acceleration. If we are given a force and a mass, we can easily calculate a by saying a = force/mass. Force here will also be a Vector2 value.
function Entity:ApplyForce(force)
self.acceleration = force/self.mass
end
The above method cancels out any other force being acted upon the apple, so it is recommended to use the method below, though for this tutorial, i’d use the above method:
self.acceleration += force/self.mass
or simply
self.velocity += force
Now we need to apply this acceleration to the velocity. In the update function:
function Entity:Update()
self.velocity += self.acceleration
self.position += self.velocity
self.frame.Position = UDim2.new(0, self.position.x, 0, self.position.y)
end
We say add that acceleration to the velocity. This gradually increases the velocity of the entity every frame to simulate smooth movement.
We have this code so far:
local Entity = {}
Entity.__index = Entity
function Entity.new(frame, v, a, m)
local self = setmetatable({
frame = frame,
velocity = v,
acceleration = a,
mass = m,
position = frame.AbsolutePosition
}, Entity)
return self
end
function Entity:ApplyForce(force)
self.acceleration = force/self.mass
end
function Entity:Update()
self.velocity += self.acceleration
self.position += self.velocity
self.frame.Position = UDim2.new(0, self.position.x, 0, self.position.y)
end
return Entity
Lets actually make an entity move! I have a canvas which has a White ball. We’ll be moving this ball!
In a client script I say:
local EntityModule = require(script.Entity)
local NewEntity = EntityModule.new(script.Parent.Canvas.Ball, Vector2.new(0, 0), Vector2.new(0, 0), 1)
task.wait(5)
game:GetService("RunService").RenderStepped:Connect(function()
NewEntity:ApplyForce(Vector2.new(.2, .1)) -- applying the force
NewEntity:Update()
end)
In the above example, we took velocity and acceleration to be 0, 0 at the start and mass to be 1, You can play around with these values to the see magic! After 5 seconds, the ball starts to move to the bottom right corner infinitely! Essentially, its job is to increment the position of the entity.
Simulating an apple falling on Newton’s head.
You must be waiting patiently for this fictional story we are going to simulate. It is said that Newton discovered what gravity is when an apple fell on his head from a tree he was sitting under.
We’ll make the apple fall on newton’s head, bounce off and fall down on the ground! The bouncing part is a bit complex, but we’ll crack it down.
Firstly, lets make our props, I have this ready with me, you can take your time to create yours:
Excuse the mediocre designing skills.
Jokes aside, lets get serious. Lets code Gravity!
Since we have our previous entity code. All we need to do is apply force to the apple!
local EntityModule = require(Entity.module)
local Apple = EntityModule.new(path.to.apple, Vector2.new(0, 0), Vector2.new(0, 0), 1)
game:GetService("RunService").RenderStepped:Connect(function()
local gravity = Vector2.new(0, .5)
Apple:ApplyForce(gravity)
Apple:Update()
end)
Gravity is the Force (Incrementation vector in our specific case) that is being apply to the apple at all times. But, you’ll notice that when you run this code. The apply falls endlessly. To stop this from happening. We’ll have to prevent the apple from crossing the green colored frame aka the ground.
In the Entity Module, we can create a new function called, :CollideWithGround()
.
function Entity:CollideWithGround(ground)
local BottomRightCorner = self.position + self.frame.AbsoluteSize
if BottomRightCorner.y > ground.AbsolutePosition.y then
self.position = Vector2.new(self.position.x, ground.AbsolutePosition.y - self.frame.AbsoluteSize.y)
self.velocity = Vector2.new(self.velocity.x, -self.velocity.y)
end
end
This checks if the apple goes past the ground, position it correctly and also reverse the direction of the velocity!
The next step would be to damp the velocity. Damping means to deplete a part of something. We’ll multiply velocity by 0.95 which reduces the velocity by around 5% every frame!
To do that, just edit the Update function to say:
function Entity:Update()
self.velocity += self.acceleration
self.velocity = self.velocity * .95
self.position += self.velocity
self.frame.Position = UDim2.new(0, self.position.x, 0, self.position.y + 36)
end
You can adjust the damping value to be what ever you want! The higher it is, the more it will bounce.
Now in our client script, we add the :CollideWithGround()
function that takes in the green colored ground frame as the argument.
local EntityModule = require(Entity.module)
local Apple = EntityModule.new(path.to.Apple, Vector2.new(0, 0), Vector2.new(0, 0), 1)
game:GetService("RunService").RenderStepped:Connect(function()
local gravity = Vector2.new(0, .5)
Apple:CollideWithGround(path.to.ground)
Apple:ApplyForce(gravity)
Apple:Update()
end)
And, lets run this…
Cool! We have got gravity working. Now for the bouncing off of newton’s head part. We’ll create a circular head hitbox for newton.
The white circular frame is the head’s hitbox. Set its BackgroundTransparency to 1! Now we’ll declare this hitbox to be an entity in our 2D World! To do this:
local HitboxEntity = EntityModule.new(path.to.headhitbox, Vector2.new(0, 0), Vector2.new(0, 0), 2)
We’ll not move this entity, so there’s no need to call any functions for this entity.
To check whether the apple and the headhitbox, collide, we can make a function :CheckHeadCollision()
in the Entity module.
function Entity:CheckHeadCollision(head)
local centerOfApple = self.position + self.frame.AbsoluteSize/2
local centerOfHead = head.AbsolutePosition + head.AbsoluteSize/2
local appleRadius = self.frame.AbsoluteSize.x/2
local headRadius = head.AbsoluteSize.x/2
if (centerOfHead - centerOfApple).magnitude < appleRadius + headRadius then
-- they are colliding
end
end
We check if the distance between the apple and the head is less than the sum of radiuses of both the head and the apple, if so, then the apple and the head are colliding! Note, that this only works if the apple and the head are circles! I used circles to make your job easier!
Now we need to adjust the velocity such that, the apple bounces on Newton’s head and then falls in front of him!
function Entity:CheckHeadCollision(head)
local centerOfApple = self.position + self.frame.AbsoluteSize/2
local centerOfHead = head.frame.AbsolutePosition + head.frame.AbsoluteSize/2
local appleRadius = self.frame.AbsoluteSize.x/2
local headRadius = head.frame.AbsoluteSize.x/2
if (centerOfApple - centerOfHead).magnitude < appleRadius + headRadius then
-- they are colliding
self.velocity = Vector2.new(self.velocity.x, -self.velocity.y)
self.velocity += Vector2.new(4, 0)
end
end
To bounce the ball on newton’s head, we reverse the direction of the velocity and then move the apple in front of him by adjusting the velocity!
In our client script, we can say:
local EntityModule = require(Entity.module)
local Apple = EntityModule.new(path.to.Apple, Vector2.new(0, 0), Vector2.new(0, 0), 1)
local HitboxEntity = EntityModule.new(path.to.HeadHitbox, Vector2.new(0, 0), Vector2.new(0, 0), 3)
task.wait(5)
game:GetService("RunService").RenderStepped:Connect(function()
local gravity = Vector2.new(0, .5)
Apple:CollideWithGround(path.to.ground)
Apple:CheckHeadCollision(HitboxEntity)
Apple:ApplyForce(gravity)
Apple:Update()
end)
Here’s the result:
Perfect! You can play around with the velocity to bounce the apple off of his head as you desire!
Conclusion
This short note concludes this tutorial. Bouncing the apple off of his head can also be done using Elastic Collisions! To keep it beginner friendly. I took the easier way! Physics can be simulated using various different ways, I shared the way I use frequently! You do you! I hope you learnt something new, thanks, have fun!