Recently, I have discovered a great channel called Ten Minute Physics. In it, creator Matthias Müller provides simple steps to write beginner friendly physics simulations. I'm just a few videos in, and it really looks exciting. In this first video, he builds up a simple cannonball simulation in JavaScript.
I thought re-implementing this myself is a good way of internalizing the knowledge behind. I have decided to use Julia for coding because of my affinity towards the language and my desire to enhance my skills. I will also be using a simple game engine package called GameZero.jl because the focus of this exercise is to learn about simulation logic and not graphics programming. A game engine makes it easier to handle graphics with a higher level abstraction.
doggo dot jl has a good simulation tutorial that uses GameZero.jl as well. His tutorial helped me learn how to use GameZero.jl but for the simulation I followed the guidance of ten minute physics.
A simulation is when you create a program that mimics the actions of a system for a certain period of time so that you can observe and analyze its behavior. If we can describe a system's behaviour in terms of mathematical rules, we can write a program to apply this rules in many iterations to create the entire behaviour over time.
Let's imagine a ball in an empty room. Let's just imagine a portal appeared in the wall of the room and a ball thrown from the portal and then portal has disappeared. What will be the ball doing? It will be falling vertically and moving horizontally (because its thrown) at the same time. We also know, from high school physics how an object like a ball would behave i.e, falling and going forward, and how can we describe such behaviour in terms of equations:
For each small-time step which is our speed changes by and in the same way balls position changes by . To keep error small we need to be as small as possible.
You need to install Julia. We need the following directory structure:
\---simulations
| rungame.jl
|
\---ballsim
ballsim.jl
with the ballsim.jl containing the following code:
using GameZero
rungame("ballsim/ballsim.jl")
This is just how GameZero package seems to work at the time of writing. I didn't inspect why it needs to be this way. You will also need to add GameZero package. See Pkg docs
We will use SIM_WIDTH
and SIM_HEIGHT
as the dimensions of our experiment. WIDTH
and HEIGHT
on the other hand are the size of the window, GameZero.jl will display it. scaling_factor
will be used to convert simulation to view. t_delta
is the difference of time that our simulation will apply in each pass. I have decided t_delta
value via experimentation.
SIM_WIDTH = 100
SIM_HEIGHT = 100
WIDTH = 1000
HEIGHT = 1000
scaling_factor = WIDTH / SIM_WIDTH
BACKGROUND = colorant"antiquewhite"
t_delta = 1/60
Next, we have a struct that represents our Ball objects physical properties. It is a mutable struct because properties like speed and position subject to change as time passes.
mutable struct Ball
x::Float16 # horizontal position of the ball along the x axis
y::Float16 # vertical position of the ball along the y axis
r::Float16 # radius of the ball
vx::Float16 # horizontal speed of the ball
vy::Float16 # vertical speed of the ball
ax::Float16 # horizontal acceleration of the ball
ay::Float16 # vertical acceleration of the ball
end
Let's initialize our ball, you can use different parameters for different outcomes. This object ball_sim
is our physical representation that we will use to calculate balls behaviour. On the other hand, ball_obj
is the graphical representation of our ball. It's what we see in the screen. Circle is an object provided by the GameZero.jl.
ball_sim = Ball(1,50,1,5,0,0,-10) #meters
ball_obj = Circle(ball_sim.x*scaling_factor, ball_sim.y*scaling_factor, ball_sim.r*scaling_factor)
Let's draw a horizontal line to the middle as the vertical starting point, just for making sure ball bounces back to same height each time.
hline = Line(0, 500, 1000, 500)
Finally, we have our draw function: let's get over it line by line:
draw
function is called by GameZero.jl automatically and Game
object called g
is passed as an argument.
function draw(g::Game)
We first draw our balls current position and our line again because all drawing actions needs to be done inside the draw
function.
# draw actors
draw(ball_obj, colorant"blue", fill= true)
draw(hline, colorant"black")
We update the speed of our ball, as described in equation 1.
#update sim speed
ball_sim.vx += ball_sim.ax * t_delta
ball_sim.vy += ball_sim.ay * t_delta
And the position of our ball, as described in equation 2.
# update sim pos
ball_sim.x = ball_sim.vx * t_delta + ball_sim.x
ball_sim.y = ball_sim.vy * t_delta + ball_sim.y
Note that one of the updates uses =
while other uses +=
, it's just a shorthand operator called addition assignment that allows adding onto a variable instead of assigning a value to it
After all the updates we need to make sure the new position is still inside the boundaries of the room as follows:
# check x limits
if ball_sim.x > SIM_WIDTH-ball_sim.r # upper limit
ball_sim.x = SIM_WIDTH-ball_sim.r
ball_sim.vx = -ball_sim.vx
elseif ball_sim.x < ball_sim.r # lower limit
ball_sim.x = ball_sim.r
ball_sim.vx = -ball_sim.vx
end
# check y limits
if ball_sim.y > SIM_HEIGHT-ball_sim.r # upper limit
ball_sim.y = SIM_HEIGHT-ball_sim.r
ball_sim.vy = -ball_sim.vy
elseif ball_sim.y < ball_sim.r # lower limit
ball_sim.y = ball_sim.r
ball_sim.vy = -ball_sim.vy
end
Whenever x or y position pass the limits of the room, we reset the position to the last valid position and change the direction of speed.
Finally, we update our ball's graphical representation:
#update ball_obj
ball_obj.x, ball_obj.y = ball_sim.x*scaling_factor, HEIGHT - ball_sim.y*scaling_factor
Here is the full ballsim.jl
file content:
# initialize screen
SIM_WIDTH = 100
SIM_HEIGHT = 100
WIDTH = 1000
HEIGHT = 1000
scaling_factor = WIDTH / SIM_WIDTH
BACKGROUND = colorant"antiquewhite"
t_delta = 1/60
# define initial state of actors
mutable struct Ball
x::Float16
y::Float16
r::Float16
vx::Float16
vy::Float16
ax::Float16
ay::Float16
end
# ideal representation used for calculation
ball_sim = Ball(1,50,1,5,0,0,-10) #meters
# final object that actually got drawn
ball_obj = Circle(ball_sim.x*scaling_factor, ball_sim.y*scaling_factor, ball_sim.r*scaling_factor)
# starting y axis
hline = Line(0, 500, 1000, 500)
function draw(g::Game)
# draw actors
draw(ball_obj, colorant"blue", fill= true)
draw(hline, colorant"black")
#update sim speed
ball_sim.vx += ball_sim.ax * t_delta
ball_sim.vy += ball_sim.ay * t_delta
# update sim pos
ball_sim.x = ball_sim.vx * t_delta + ball_sim.x
ball_sim.y = ball_sim.vy * t_delta + ball_sim.y
if ball_sim.x > SIM_WIDTH-ball_sim.r
ball_sim.x = SIM_WIDTH-ball_sim.r
ball_sim.vx = -ball_sim.vx
elseif ball_sim.x < ball_sim.r
ball_sim.x = ball_sim.r
ball_sim.vx = -ball_sim.vx
end
if ball_sim.y > SIM_HEIGHT-ball_sim.r
ball_sim.y = SIM_HEIGHT-ball_sim.r
ball_sim.vy = -ball_sim.vy
elseif ball_sim.y < ball_sim.r
ball_sim.y = ball_sim.r
ball_sim.vy = -ball_sim.vy
end
#update ball_obj
ball_obj.x, ball_obj.y = ball_sim.x*scaling_factor, HEIGHT - ball_sim.y*scaling_factor
end