Title: Use Pygame to visualize bubble sort in Python, Part 1
My previous post Implement bubble sort in Python explained how bubble sort works. This post uses Pygame to help you visualize how the algorithm works. Click the Go button to launch the animation. As the program runs bubble sort, it highlights the two items it is comparing in yellow at each step. When it swaps two items, the highlighted items move on the window.
This example uses techniques from my book Build Your Own Python Action Arcade! to animate the moving items.
Read that book for all of the details.
There are two main pieces to the animation process: the TextSprite class and the event loop. This post describes the TextSprite class. I'll explain the event loop in my next post.
Constructor
One of the big concepts that you can use to make Pygame animation easy is the sprite class. A sprite is an object that manages the movement and drawing of something on the screen. In Build Your Own Python Action Arcade!, sprites include game objects like the player's ship, rocks, bullets, and alien ships. Sprites also include objects that are not part of the game play such as the player's score, number of remaining ships, and the current playing level.
This example needs to display boxes containing text. Unfortunately, Pygame doesn't have a way to draw text boxes, so we need to draw a box and then draw text inside it.
The program also needs a way to move the boxes. Finally, we need a way to tell when a box stops moving and we need a way to tell when the user clicks on a box (the Go button).
The TextSprite class does all of that. Here's the TextSprite class's constructor.
class TextSprite:
'''Sprite to display text.'''
def __init__(self, text, center, width, height, font, text_color='black',
bg_color='light blue', outline_color='black',
disabled_text_color='light gray',
disabled_bg_color='dark gray',
disabled_outline_color='light gray',
enabled=True):
'''Prepare the text surface.'''
self.text = text
self.center = center
self.width = width
self.height = height
self.font = font
self.text_color = text_color
self.bg_color = bg_color
self.enabled = enabled
self.enabled_outline_color = outline_color
self.enabled_text_color = text_color
self.enabled_bg_color = bg_color
self.disabled_outline_color = disabled_outline_color
self.disabled_text_color = disabled_text_color
self.disabled_bg_color = disabled_bg_color
self.pps = 0
self.target_points = None
This code saves the sprite's various drawing properties: text, size, font, colors, and so forth.
The only properties that really need extra explanation are pps and target_points. While the sprite is moving, these are the sprite's speed in pixels per second and a list of points that the sprite should visit.
update
A sprite class must provide two key methods: update and draw.
The update method updates the sprite. It might move the sprite, add acceleration, change colors, rotate, ... whatever is appropriate for that kind of sprite. Here's the TextSprite class's update method.
def update(self, elapsed_seconds, bounds):
'''If we're moving, move toward the target point.'''
if self.target_points is None:
return
# Get a vector from the current position to the first target point.
direction = self.target_points[0] - self.center
# See how far we can move in the elapsed time.
max_length = self.pps * elapsed_seconds
# See if we can get to the target point.
if direction.length() <= max_length:
# We're there.
self.center = self.target_points[0]
self.target_points.pop(0)
if len(self.target_points) == 0:
self.target_points = None
else:
# Move as far as we can.
direction.scale_to_length(max_length)
self.center += direction
In this program, the sprite only needs to update itself if it is moving. If the target_points list is None, update simply returns.
If target_points is not None, the code gets a direction vector pointing from the sprite's current location to the first point in the target_points list. It uses the sprite's speed (pixels per second as stored in self.pps) and scales that by the elapsed time to get the maximum distance the sprite should move in this call to update.
If the direction vector is no longer than the maximum length, the sprite moves that distance so it lands at the first point in the target_points list. It then removes that point from the list. If the list is then empty, it sets the list to None so it knows it is done moving.
If the direction vector is longer than the maximum length, the code shortens it to that maximum length and moves the sprite along the shorter vector.
Vector operations such as these are described in my book. Look there or search online for "Pygame Vector2" for more details.
draw
The following code shows the TextSprite class's draw method.
def draw(self, surface):
'''Draw the text.'''
# Get our current colors.
if self.enabled:
outline_color = self.enabled_outline_color
text_color = self.enabled_text_color
bg_color = self.enabled_bg_color
else:
outline_color = self.disabled_outline_color
text_color = self.disabled_text_color
bg_color = self.disabled_bg_color
# Draw the text onto its own surface.
text_surface = self.font.render(self.text, True, text_color, bg_color)
# Draw the text surface onto a properly sized surface.
self.surface = pygame.Surface((self.width, self.height))
self.surface.fill(bg_color)
text_width, text_height = text_surface.get_size()
x = (self.width - text_width) / 2
y = (self.height - text_height) / 2
self.surface.blit(text_surface, (x, y))
# Draw the outline.
rect = (0, 0, self.width - 1, self.height - 1)
line_wid = 1
pygame.draw.rect(self.surface, outline_color, rect, line_wid)
# Draw onto the game surface.
rect = self.get_bounds()
surface.blit(self.surface, rect)
Drawing text in Pygame is a bit awkward. First, you use font.render draw the text onto its own surface. Then you copy that surface onto the drawing surface where you want it to appear. This class performs a couple of other steps to draw the rectangle around the text.
The method starts by setting some color values depending on whether the sprite is currently enabled. It then uses font.render draw the text onto its own surface.
The text's surface is sized to just fit the text. The program uses larger text sprites so they can all be the same size and so they can have some extra space around the text. The code creates that larger surface filled with the sprite's background color and saves it in self.surface. It then blits the text's surface onto self.surface.
Next, the code draws the rectangle's outline onto self.surface.
Finally, the code blits the finished self.surface onto the display surface.
Helper Methods
In addition to update and draw, the TextSprite class provides a few helper methods. The following code shows the get_bounds method.
def get_bounds(self):
# See where we should draw it.
return self.surface.get_rect(center=self.center)
This method calls the sprite surface's get_rect method to get the rectangle where the sprite is drawn. The get_rect method makes the returned rectangle big enough to hold the surface. The center=self.center argument makes it position the rectangle so its center lies at self.center.
If you look back at the draw method again, you'll see that it uses get_bounds to figure out where to draw the sprite, so we know that get_bounds really does return the sprit's bounds.
The following code shows the is_moving helper method.
def is_moving(self):
'''Return True if this sprite is currently moving.'''
return self.target_points is not None
The is_moving method simply returns True if the sprite's target_points list is None. If the list is not None, the sprite is still moving towards the points it contains.
Here's the is_at method.
def is_at(self, point):
'''Return True if this point is inside the sprite.'''
if not self.enabled:
return False
rect = self.get_bounds()
return rect.collidepoint(point)
The is_at method first checks whether the sprite is enabled. If it is disabled, the method returns False so disabled sprites cannot respond to mouse clicks. (In this program, only the Go button responds to clicks.)
If the sprite is enabled, the code calls get_bounds to see where the sprite is and then calls the resulting rectangle's collidepoint method to see if the rectangle contains the indicated point.
The following code shows the last piece of the TextSprite class: the move_to method.
def move_to(self, target_points, pps=100):
'''Move the sprite through the list of target points.'''
self.pps = pps
self.target_points = target_points
This method simply saves the sprite's speed in pixels per second and the list of target points that the sprite should visit. The update method then uses those values to move the sprite.
Conclusion
I think that's enough for this post. You can download the example now if you like, or wait for my next post where I'll explain how the program's event loop creates the sprites and allows them to move.
|