The PyGame Objects
The BaseObject class (defined in mainloop.py) serves as a blueprint to objects that the MainLoop class knows how to work with. For each type of object you want to display on the screen you should create a sub-class implement the __init__(), update(), and render() methods and in your program instantiate and add these objects to the MainLoop's self.objects list. MainLoop will take care of the rest and call update() and render() in each iteration. The render() method gets a PyGame screen object and you should use PyGame functions to render the visual representation of your object on the screen.
<code> class BaseObject(object): """A base class for visual objects managed by the main loop Each object has an __init__(), update() and render() methods. To use it sub-class BaseObject implement the methods and add your object to the main loop. """ def __init__(self): """ """ def update(self): """ """ def render(self, screen): """ """ </code>
Here are the object types that PolyArea uses. They are defined in the pygame_objects.py file.
The Line class is a simple visual object; see Figure 1.
It subclasses BaseObject of course and it implements only the __init__() and render() methods. In __init__() it stores the color the width and the two end points of the line (startpos and endpos). In render() it simply calls the pygame.draw.line() method with the screen object and the aforementioned arguments.
<code> class Line(BaseObject): def __init__(self, color, startpos, endpos, width=1): self.startpos = startpos self.endpos = endpos self.color = color self.width = width def render(self, screen): pygame.draw.line(screen, self.color, self.startpos, self.endpos, self.width) </code>
The Triangle class is almost as simple as the Line. It draws a filled triangle with a border (Figure 2).
The __init__() method stores the triangle vertices (points) the border and background color and the width of the border. The render() method draws a filled polygon with the background color and then another non-filled polygon with the border color and width. I don't really enforce that there are three points, so you can use this class to draw any polygon, but in PolyArea I draw only triangles using this object.
<code> class Triangle(BaseObject): def __init__(self, points, color=triangle_border_color, bg_color=triangle_bg_color, width=1): self.points = points self.color = color self.bg_color = bg_color self.width = width def render(self, screen): # Draw the filled interior first pygame.draw.polygon(screen, self.bg_color, self.points, 0) # Draw the border pygame.draw.polygon(screen, self.color, self.points, self.width) </code>
People often distinguish text from graphics, but they are not that different. In the end, both are manifested as pixels on the screen. In PyGame, writing text on the screen is a two-stage process where you create a PyGame Text object using your favorite font, then "blit" (short "block image transfer") it to the screen at a particular position (Figure 3):
</code> class Text(BaseObject): def __init__(self, text, font=None, size=36, color=(0, 0, 0), pos=(0, 0)): self.text= text self.font = pygame.font.Font(font, size) self.color = color self.pos = pos def render(self, screen): text = self.font.render(self.text, 1, self.color) screen.blit(text, self.pos) </code>
The Grid is pretty sophisticated compared to the other objects. It represents an interlocking grid of dashed lines with a border of three pixels. (Why three? Because I said so); see Figure 4. The rect parameter determines the total size of the grid. delta_x and delta_y specify the distance between vertical and horizontal grid lines and the color parameters control the color of background, border, and the dashed lines.
<code> class Grid(BaseObject): def __init__(self, rect, delta_x, delta_y, background_color, border_color, line_color): self.rect = rect self.delta_x = delta_x self.delta_y = delta_y self.background_color = background_color self.border_color = border_color self.line_color = line_color </code>
The render() method does a lot. It's not a couple of calls to pygame.draw.*() functions anymore. The grid is made of dashed lines, which PyGame doesn't support out of the box. So, the render() method defines a nested helper function called dashed_line() that draws a dashed line made of 5-on/5-off pixel segments. Each "on" segment is drawn using pygame.draw.line(). To simplify things, dashed_line() can draw either horizontal or vertical dashed lines (that's all I need for the grid):
<code> def render(self, screen): """Draws a grid on the screen""" def dashed_line(screen, line_color, start_pos, end_pos): """Draw a dashed line (must be horizontal or vertical)""" horiz_line_count = (end_pos - start_pos)/10 vert_line_count = (end_pos - start_pos)/10 if vert_line_count == 0: #draw horizontal lines for i in range(horiz_line_count): y = start_pos x1 = i * 10 + start_pos x2 = x1 + 5 pygame.draw.line(screen, line_color, (x1, y), (x2, y)) elif horiz_line_count == 0: #draw vertical lines for i in range(vert_line_count): x = start_pos y1 = i * 10 + start_pos y2 = y1 + 5 pygame.draw.line(screen, line_color, (x, y1), (x, y2)) else: raise Exception('Line must be horizontal or vertical') </code>
The render() method follows a simple flow of erasing the contents of the grid (that facilitates dynamic changes to objects rendered on top of it), drawing the border and then the grid lines. The grid lines are drawn using the dash_line() function. First all the horizontal lines are drawn separated by delta_y pixels from each other and then all the vertical lines.
<code> # Erase the contents of the grid pygame.draw.rect(screen, self.background_color, self.rect) # Draw the grid's border pygame.draw.rect(screen, self.border_color, self.rect, 3) # Draw the grid lines y = self.rect.top + self.delta_y x = self.rect.left + self.delta_x while y < self.rect.bottom: dashed_line(screen, self.line_color, (self.rect.left, y), (self.rect.right, y)) y += self.delta_y while x < self.rect.right: dashed_line(screen, self.line_color, (x, self.rect.top), (x, self.rect.bottom)) x += self.delta_x </code>
The Interactive UI
Okay, all the preliminaries are out of the way. You now understand how PyGame is used to draw simple geometric shapes on the screen and how visual objects can be created and displayed using the simple framework of the generic main loop and the BaseObject sub-class. It's time to delve into the actual interactive user interface part and see how it operates hand-in-hand with the algorithmic core discussed in Part 1.