Simple Physics Simulation with Julia

ball bouncing up and down left and right Ball goes up, ball comes down

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.

What is a simulation?

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:

v=v0+aΔt v = v_0 + a \Delta t x=x0+vΔt x = x_0 + v \Delta t

For each small-time step which is Δt\Delta t our speed changes by aΔta \Delta t and in the same way balls position changes by vΔtv \Delta t. To keep error small we need Δt\Delta t to be as small as possible.

Requirements

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

The simulation code

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
CC BY-SA 4.0 Berkay Karlık. Last modified: July 11, 2023. Website built with Franklin.jl and the Julia programming language.