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:
- 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 - The ball is a
Turtle
object - 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 - 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
- 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!
Subscribe to
The Python Coding Stack
Regular articles for the intermediate Python programmer or a beginner who wants to “read ahead”
Further Reading
- Read the first article in the Bouncing Ball Series, which discussed the simulation of a single bouncing ball in Python
- Find out more about object-oriented programming in Python in Chapter 7 of The Python Programming Book
- Learn about how to understand Python instance variables with the school trip analogy
- Read a bit more about OOP in the Real Python articles on this topic
Subscribe to
The Python Coding Stack
Regular articles for the intermediate Python programmer or a beginner who wants to “read ahead”
[…] as to what the Python code to create this simulation looks like, you can read the blog post about Using Object-Oriented Programming in Python to Simulate Bouncing Balls. Scroll to the bottom if you just want to glance at the completed […]
[…] Bouncing Balls Using Object-Oriented Programming in Python […]