Using Object-Oriented Programming using Python to Simulate Bouncing Balls

Bouncing Balls Using Object-Oriented Programming in Python (Bouncing Ball Series #2)

In this week’s article, I’ll discuss an example of using object-oriented programming in Python to create a real-world simulation. I’ll build on the code from the first article in the Bouncing Ball Series, in which I looked at the simulation of a single bouncing ball in Python. This article will extend this simulation to many bouncing balls using object-oriented programming in Python.

Here’s the output of the simulation you’ll work on:

Before I talk about using object-oriented programming, let’s start with a short recap of the single ball simulation.

Recap of The Single Ball Simulation

If you want to work through the whole first article and code, you can read the post about a single bouncing ball in Python and skip the rest of this section. If you’d rather jump straight into using object-oriented programming in Python, you can read this brief recap first. The code in this article will build on this.

Here’s the final code from the first article in this series:

import turtle

# Set key parameters
gravity = -0.005  # pixels/(time of iteration)^2
y_velocity = 1  # pixels/(time of iteration)
x_velocity = 0.25  # pixels/(time of iteration)
energy_loss = 0.95

width = 600
height = 800

# Set window and ball
window = turtle.Screen()
window.setup(width, height)
window.tracer(0)

ball = turtle.Turtle()

ball.penup()
ball.color("green")
ball.shape("circle")

# Main loop
while True:
    # Move ball
    ball.sety(ball.ycor() + y_velocity)
    ball.setx(ball.xcor() + x_velocity)

    # Acceleration due to gravity
    y_velocity += gravity

    # Bounce off the ground
    if ball.ycor() < -height / 2:
        y_velocity = -y_velocity * energy_loss
        # Set ball to ground level to avoid it getting "stuck"
        ball.sety(-height / 2)

    # Bounce off the walls (left and right)
    if ball.xcor() > width / 2 or ball.xcor() < -width / 2:
        x_velocity = -x_velocity

    window.update()

The highlights of this code are:

  1. You’re using the turtle module, which allows you to create basic graphics-based applications without too much fuss. This means the focus is on the rest of the code and not on the display of graphics
  2. The ball is a Turtle object
  3. You move the ball by changing its x– and y-values using different speeds along the two axes. Each iteration of the while loop will move the ball by a number of steps horizontally and a number of steps vertically
  4. Since there’s gravity pulling the ball down, you change the y-speed in each iteration to take into account the acceleration due to gravity
  5. The ball bounces off the walls and off the ground, and the code achieves this by detecting when the ball’s position has reached these barriers and changing the ball’s direction when this happens. However, there’s also some energy being lost each time the ball bounces off the ground, which means the ball reaches a lower height each time it bounces on the ground

Let’s move on to using object-oriented programming in Python to “package” the ball’s characteristics and actions into a class.

Using Object-Oriented Programming in Python

This article is not a detailed, comprehensive tutorial about using object-oriented programming. You can read Chapter 7 of The Python Coding Book about object-oriented programming for a more detailed text.

The fundamental principle in object-oriented programming is to think of the objects that represent your real-life situation and create a template or a blueprint to create such objects in your code. The philosophy is to think from a human-first perspective rather than a computer-first one. The characteristics of the object and the actions it can perform are then included in this template through a class definition.

In this case, the object in the real world is a ball. The ball has a shape, a size, and a colour, and it can move and bounce. Therefore, the class you define will need to take care of all these attributes of the ball.

Creating The Ball Class

To make this article and the code I’ll present more readable, I’ll include the class definition and the code creating the simulation in a single script in this post. However, you can separate the class definition into one module and the code running the simulation into another one if you prefer, as long as you import the class into your simulation script.

Let’s start by defining a class called Ball:

import turtle

class Ball(turtle.Turtle):
    def __init__(self):
        super().__init__()

The class Ball inherits from the Turtle class in the turtle module. Therefore, the __init__() method calls super().__init__() to initialise this object as a Turtle first.

Adding data attributes

You’ll first deal with the velocity of the ball and its starting position, and as was the case for the single ball example, the velocity is represented by the two components along the x– and y– axes:

import turtle
import random

class Ball(turtle.Turtle):
    def __init__(self, x=0, y=0):
        super().__init__()
        self.y_velocity = random.randint(-10, 50) / 10
        self.x_velocity = random.randint(-30, 30) / 10
        self.setposition(x, y)

The __init__() method now includes the parameters x and y, which both have a default value of 0. These represent the ball’s initial coordinates and are used as arguments in setposition(). setposition() is a method of the Turtle class and, therefore, also of the Ball class, since Ball inherits from Turtle.

The x– and y-velocities are set as data attributes. I’m using randint() from the random module to create random integers and then dividing by 10 to give floats with one value after the decimal point as this is sufficient for this simulation.

Another data attribute you’ll need is the size of the ball. You can also assign a random size to each ball, and you can choose whichever random distribution you prefer for this. I’ll use the gamma distribution to make sure most balls are within a certain range of sizes:

import turtle
import random

class Ball(turtle.Turtle):
    def __init__(self, x=0, y=0):
        super().__init__()
        self.penup()
        self.hideturtle()
        self.y_velocity = random.randint(-10, 50) / 10
        self.x_velocity = random.randint(-30, 30) / 10
        self.setposition(x, y)
        self.size = int(random.gammavariate(25, 0.8))
        self.color((random.random(),
                    random.random(),
                    random.random())
                   )

In addition to using gammavariate() from the random module to determine the size of the ball, you’re also setting the colour as a random RGB value using the Turtle method color. You use two more Turtle methods to initialise the ball. penup() makes sure the ball doesn’t draw any lines when it moves and you’ll need to call this method before you call setposition() or move the ball in any other way. hideturtle() ensures the Turtle object itself is not visible as you don’t need this.

Drawing the ball

Let’s add a method for the Ball class that will allow you to draw the ball on the screen when you need to:

import turtle
import random

class Ball(turtle.Turtle):
    def __init__(self, x=0, y=0):
        super().__init__()
        self.penup()
        self.hideturtle()
        self.y_velocity = random.randint(-10, 50) / 10
        self.x_velocity = random.randint(-30, 30) / 10
        self.setposition(x, y)
        self.size = int(random.gammavariate(25, 0.8))
        self.color((random.random(),
                    random.random(),
                    random.random())
                   )
    def draw(self):
        self.clear()
        self.dot(self.size)

ball = Ball()
ball.draw()

turtle.done()

The method draw() you’ve defined uses two Turtle methods to draw a dot of the required size and clear the previously drawn dot. You’ll need to clear the previous drawings when the ball starts moving. Otherwise, the ball will leave a trail as it moves!

This is a good point to test the class so far by creating an instance of the class Ball and using its draw() method. You use Ball() with no arguments, and therefore, the values used are the default values x=0 and y=0 you defined in the __init__() signature. The code creates a ball at the centre of the screen.

As mentioned earlier, I’m using a single script for defining the class and running the simulation in this article. However, you can separate these into two modules if you prefer.

The call to turtle.done() keeps the window open at the end of the code, but you’ll only need this line temporarily. It’s required here for now so that you can view the output from this script. However, once you introduce an infinite loop, you’ll be able to remove this line. Each time you run this code, a ball will be displayed in the middle of the window, each time having a different colour and size.

Moving the ball

You’ll need another method to move the ball:

import turtle
import random

class Ball(turtle.Turtle):
    def __init__(self, x=0, y=0):
        super().__init__()
        self.penup()
        self.hideturtle()
        self.y_velocity = random.randint(-10, 50) / 10
        self.x_velocity = random.randint(-30, 30) / 10
        self.setposition(x, y)
        self.size = int(random.gammavariate(25, 0.8))
        self.color((random.random(),
                    random.random(),
                    random.random())
                   )
    def draw(self):
        self.clear()
        self.dot(self.size)

    def move(self):
        self.sety(self.ycor() + self.y_velocity)
        self.setx(self.xcor() + self.x_velocity)

# Simulation code
window = turtle.Screen()
window.tracer(0)

ball = Ball()

while True:
    ball.draw()
    ball.move()

    window.update()

You’re changing the x– and y-positions using the two velocity attributes of the Ball object. This is a good time to introduce a while loop in the simulation code and to control the animation better using the tracer() and update() methods on the Screen object (technically, this is the _Screen object, but this is not too relevant here!)

This code now shows a ball that shoots off in a random direction from the centre:

You can adjust the range of velocity values to slow down the ball if needed. However, you also need to account for gravity which pulls the ball down. This is reflected by changing the y-velocity of the ball in each iteration, as you did in the example in the first post of the Bouncing Ball Series. The gravity parameter can be included as a class attribute:

import turtle
import random

class Ball(turtle.Turtle):
    gravity = -0.05  # pixels/(time of iteration)^2

    def __init__(self, x=0, y=0):
        super().__init__()
        self.penup()
        self.hideturtle()
        self.y_velocity = random.randint(-10, 50) / 10
        self.x_velocity = random.randint(-30, 30) / 10
        self.setposition(x, y)
        self.size = int(random.gammavariate(25, 0.8))
        self.color((random.random(),
                    random.random(),
                    random.random())
                   )
    def draw(self):
        self.clear()
        self.dot(self.size)

    def move(self):
        self.y_velocity += self.gravity
        self.sety(self.ycor() + self.y_velocity)
        self.setx(self.xcor() + self.x_velocity)

# Simulation code
window = turtle.Screen()
window.tracer(0)

ball = Ball()

while True:
    ball.draw()
    ball.move()

    window.update()

The ball no longer shoots off in one direction now as it’s pulled down by gravity, and its trajectory changes to show the ball falling to the ground:

The last thing you need to do is to make the ball bounce when it hits the ground or the walls.

Bouncing the ball

I’ve chosen to separate the bouncing into two methods. One method deals with bouncing off the ground, and the other takes care of bouncing off the walls. Let’s start with bouncing off the ground:

import turtle
import random

class Ball(turtle.Turtle):
    gravity = -0.05  # pixels/(time of iteration)^2

    def __init__(self, x=0, y=0):
        super().__init__()
        self.penup()
        self.hideturtle()
        self.y_velocity = random.randint(-10, 50) / 10
        self.x_velocity = random.randint(-30, 30) / 10
        self.setposition(x, y)
        self.size = int(random.gammavariate(25, 0.8))
        self.color((random.random(),
                    random.random(),
                    random.random())
                   )
    def draw(self):
        self.clear()
        self.dot(self.size)

    def move(self):
        self.y_velocity += self.gravity
        self.sety(self.ycor() + self.y_velocity)
        self.setx(self.xcor() + self.x_velocity)

    def bounce_floor(self, floor_y):
        if self.ycor() < floor_y:
            self.y_velocity = -self.y_velocity
            self.sety(floor_y)

# Simulation code
width = 1200
height = 800

window = turtle.Screen()
window.setup(width, height)
window.tracer(0)

ball = Ball()

while True:
    ball.draw()
    ball.move()
    ball.bounce_floor(-height/2)

    window.update()

The method bounce_floor() you’ve just added needs the y-coordinate of the floor. This could be the bottom of your window or any other horizontal line in your animation. I’ve added values for the screen’s width and height, and the screen’s dimensions are set using the setup() method from the turtle module. The ball will now bounce on the ground:

From the first article in this series, you’ll recall that there’s one problem with this type of bouncing. The ball will always bounce up to the same height. You can see this by commenting out the line that sets the x-position in the move() method to disable the horizontal movement of the ball temporarily. The code now gives the following animation:

The maximum height of the ball does not change with each bounce. However, this is not what happens in real life, as energy is lost each time the ball bounces on the ground. You can account for this energy loss with every bounce:

import turtle
import random

class Ball(turtle.Turtle):
    gravity = -0.05  # pixels/(time of iteration)^2
    energy_loss_ground = 0.95

    def __init__(self, x=0, y=0):
        super().__init__()
        self.penup()
        self.hideturtle()
        self.y_velocity = random.randint(-10, 50) / 10
        self.x_velocity = random.randint(-30, 30) / 10
        self.setposition(x, y)
        self.size = int(random.gammavariate(25, 0.8))
        self.color((random.random(),
                    random.random(),
                    random.random())
                   )
    def draw(self):
        self.clear()
        self.dot(self.size)

    def move(self):
        self.y_velocity += self.gravity
        self.sety(self.ycor() + self.y_velocity)
        self.setx(self.xcor() + self.x_velocity)

    def bounce_floor(self, floor_y):
        if self.ycor() < floor_y:
            self.y_velocity = -self.y_velocity * self.energy_loss_ground
            self.sety(floor_y)

# Simulation code
width = 1200
height = 800

window = turtle.Screen()
window.setup(width, height)
window.tracer(0)

ball = Ball()

while True:
    ball.draw()
    ball.move()
    ball.bounce_floor(-height/2)

    window.update()

The amount of energy lost with each bounce is another class attribute, and you reduce the velocity by this factor each time the ball bounces on the ground.

Let’s add the bouncing off the walls. You can have a different energy loss parameter for the walls, too:

import turtle
import random

class Ball(turtle.Turtle):
    gravity = -0.05  # pixels/(time of iteration)^2
    energy_loss_ground = 0.95
    energy_loss_walls = 0.8

    def __init__(self, x=0, y=0):
        super().__init__()
        self.penup()
        self.hideturtle()
        self.y_velocity = random.randint(-10, 50) / 10
        self.x_velocity = random.randint(-30, 30) / 10
        self.setposition(x, y)
        self.size = int(random.gammavariate(25, 0.8))
        self.color((random.random(),
                    random.random(),
                    random.random())
                   )
    def draw(self):
        self.clear()
        self.dot(self.size)

    def move(self):
        self.y_velocity += self.gravity
        self.sety(self.ycor() + self.y_velocity)
        self.setx(self.xcor() + self.x_velocity)

    def bounce_floor(self, floor_y):
        if self.ycor() < floor_y:
            self.y_velocity = -self.y_velocity * self.energy_loss_ground
            self.sety(floor_y)

    def bounce_walls(self, wall_x):
        if abs(self.xcor()) > wall_x:
            self.x_velocity = -self.x_velocity * self.energy_loss_walls
            sign = self.xcor() / abs(self.xcor())
            self.setx(wall_x * sign)

# Simulation code
width = 1200
height = 800

window = turtle.Screen()
window.setup(width, height)
window.tracer(0)

ball = Ball()

while True:
    ball.draw()
    ball.move()
    ball.bounce_floor(-height/2)
    ball.bounce_walls(width/2)

    window.update()

And this gives a reasonably realistic simulation of a ball bouncing around the room:

It’s now time to add lots more bouncing balls.

Using Object-Oriented Programming in Python To Simulate Many Bouncing Balls

One of the main reasons you may choose to use an object-oriented programming approach for a problem is to easily create many items of that object. The hard work goes into defining the class, and creating many instances of that class then becomes relatively straightforward.

Let’s make a couple of small changes to the code so far to move from a single bouncing ball to many bouncing balls. You’ll start by creating six bouncing balls:

import turtle
import random

class Ball(turtle.Turtle):
    gravity = -0.05  # pixels/(time of iteration)^2
    energy_loss_ground = 0.95
    energy_loss_walls = 0.8

    def __init__(self, x=0, y=0):
        super().__init__()
        self.penup()
        self.hideturtle()
        self.y_velocity = random.randint(-10, 50) / 10
        self.x_velocity = random.randint(-30, 30) / 10
        self.setposition(x, y)
        self.size = int(random.gammavariate(25, 0.8))
        self.color((random.random(),
                    random.random(),
                    random.random())
                   )
    def draw(self):
        self.clear()
        self.dot(self.size)

    def move(self):
        self.y_velocity += self.gravity
        self.sety(self.ycor() + self.y_velocity)
        self.setx(self.xcor() + self.x_velocity)

    def bounce_floor(self, floor_y):
        if self.ycor() < floor_y:
            self.y_velocity = -self.y_velocity * self.energy_loss_ground
            self.sety(floor_y)

    def bounce_walls(self, wall_x):
        if abs(self.xcor()) > wall_x:
            self.x_velocity = -self.x_velocity * self.energy_loss_walls
            sign = self.xcor() / abs(self.xcor())
            self.setx(wall_x * sign)

# Simulation code
width = 1200
height = 800

window = turtle.Screen()
window.setup(width, height)
window.tracer(0)

balls = [Ball() for _ in range(6)]

while True:
    for ball in balls:
        ball.draw()
        ball.move()
        ball.bounce_floor(-height/2)
        ball.bounce_walls(width/2)

    window.update()

You’re creating the six balls using a Python list comprehension. Since you’re not using any arguments in Ball(), all the balls are created at the centre of the screen. The other change is in the while loop. The calls to the various Ball methods are now within a for loop since you need to iterate through the list of balls to consider all the balls.

This code gives the following output:

Each ball that the program creates has a different size, direction of travel, speed, and colour. They all move and bounce based on their own characteristics. However, they’re all following the rules defined in the template that’s used to create all the balls. This template is the class Ball.

Adding more balls while the simulation is running

Let’s make one final addition to this simulation. You can link a button click with a function that creates a new ball and adds it to the list using the onclick() method in the turtle module:

import turtle
import random

class Ball(turtle.Turtle):
    gravity = -0.05  # pixels/(time of iteration)^2
    energy_loss_ground = 0.95
    energy_loss_walls = 0.8

    def __init__(self, x=0, y=0):
        super().__init__()
        self.penup()
        self.hideturtle()
        self.y_velocity = random.randint(-10, 50) / 10
        self.x_velocity = random.randint(-30, 30) / 10
        self.setposition(x, y)
        self.size = int(random.gammavariate(25, 0.8))
        self.color((random.random(),
                    random.random(),
                    random.random())
                   )
    def draw(self):
        self.clear()
        self.dot(self.size)

    def move(self):
        self.y_velocity += self.gravity
        self.sety(self.ycor() + self.y_velocity)
        self.setx(self.xcor() + self.x_velocity)

    def bounce_floor(self, floor_y):
        if self.ycor() < floor_y:
            self.y_velocity = -self.y_velocity * self.energy_loss_ground
            self.sety(floor_y)

    def bounce_walls(self, wall_x):
        if abs(self.xcor()) > wall_x:
            self.x_velocity = -self.x_velocity * self.energy_loss_walls
            sign = self.xcor() / abs(self.xcor())
            self.setx(wall_x * sign)

# Simulation code
width = 1200
height = 800

window = turtle.Screen()
window.setup(width, height)
window.tracer(0)

balls = [Ball() for _ in range(6)]

def add_ball(x, y):
    balls.append(Ball(x, y))

window.onclick(add_ball)

while True:
    for ball in balls:
        ball.draw()
        ball.move()
        ball.bounce_floor(-height/2)
        ball.bounce_walls(width/2)

    window.update()

The function name you use as an argument for onclick() is add_ball. This is a function you define in the code, and this function needs to accept two arguments. These arguments represent the mouse click’s x– and y– coordinates, and you use them in the function to create a new instance of Ball using these coordinates. The function also adds this new ball to the list of balls.

You can now add more balls to the simulation by clicking anywhere on the window to create a new ball:

The definition of a class makes it straightforward to add more bouncing balls to the code.

Final Words

This simulation is very realistic. But it’s not perfect, of course. When creating real-world simulations, you’ll often want to start by making some simplifications, and then you can add complexity as required to make the simulation closer to reality.

Using object-oriented programming in Python, you’ve been able to create a template to create a ball. This template:

  • Defines the colour and size of the ball
  • Determines the starting position, direction of travel, and speed of the ball
  • Defines how the ball moves
  • Works out when and how the ball bounces off the ground or walls

When you create a ball using this class, all of these actions and characteristics will be automatically there as part of the ball. In the post about Python instance variables, I use the backpack analogy to describe how an object carries everything it needs with it wherever it goes!

In the third and final post in the Bouncing Ball Series, I’ll take into account balls hitting each other and bouncing off each other, too.

Have fun using object-oriented programming in Python!

Further Reading


Get the latest blog updates

No spam promise. You’ll get an email when a new blog post is published


Leave a Reply