Adding Mainline Logic
In U1-9 we learned that writing our own functions, and using mainline logic, make long programs easier to write and understand. Any code written outside a function has global scope, and it’s considered a bad design to have variables and code with global scope. We know that whenever possible code should be organized into functions. When your code is inside functions, the scope mechanism protects local variables from inadvertent changes and conflicts with other variables with the same names.
I intentionally avoided mainline logic in our examples last day because I wanted you to focus on the basic steps of the IDEA/ALTERframework. Now our programs will start to get more complicated, and it’s important to incorporate mainline logic in our code. Let’s add mainline logic to the “blue screen” example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# I - Import and Initialize import pygame pygame.init() def main(): '''This function defines the 'mainline logic' for our game.''' # D - Display configuration screen = pygame.display.set_mode((640, 480)) pygame.display.set_caption("Hello, world!") # E - Entities (just background for now) background = pygame.Surface(screen.get_size()) background = background.convert() background.fill((0, 0, 255)) # A - Action (broken into ALTER steps) # A - Assign values to key variables clock = pygame.time.Clock() keepGoing = True # L - Loop while keepGoing: # T - Timer to set frame rate clock.tick(30) # E - Event handling for event in pygame.event.get(): if event.type == pygame.QUIT: keepGoing = False # R - Refresh display screen.blit(background, (0, 0)) pygame.display.flip() # Close the game window pygame.quit() # Call the main function main() |
As you can see, the Import and Initialize step (IDEA) still happens at the global scope. This is because you will generally have other functions in your program that will need shared access to the pygame module. If you have other functions (and we will as our games get more complex) you should define them before the main() function. The remainder of the IDEA/ALTER steps take place inside the main() function.
Drawing Shapes
Squares can be a lot of fun, but you’re probably eager to draw other kinds of shapes! The pygame.draw module includes a wide variety of shape drawing functions.
Here’s a demonstration:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# I - Import and Initialize import pygame, math pygame.init() def drawStuff(background): """ given a Surface, this function draws a bunch of shapes on it """ #draw a line from (5, 100) to (100, 100) pygame.draw.line(background, (255, 0, 0), (5, 100), (100, 100)) #draw an unfilled square pygame.draw.rect(background, (0, 255, 0), ((200, 5), (100, 100)), 3) #draw a filled circle pygame.draw.circle(background, (0, 0, 255), (400, 50), 45) #draw an ellipse pygame.draw.ellipse(background, (204, 204, 0), ((150, 150), (150, 100)), 0) #draw an arc pygame.draw.arc(background, (0, 0, 0), ((5, 150), (100, 100)), 0, math.pi/2, 5) #draw lines, points = ( (370, 160), (370, 237), (372, 193), (411, 194), (412, 237), (412, 160), (412, 237), (432, 227), (436, 196), (433, 230) ) pygame.draw.lines(background, (0xFF, 0x00, 0x00), False, points, 3) #draw polygon points = ( (137, 372), (232, 319), (383, 335), (442, 389), (347, 432), (259, 379), (220, 439), (132, 392) ) pygame.draw.polygon(background, (0x33, 0xFF, 0x33), points) #compare normal and anti-aliased diagonal lines pygame.draw.line(background, (0, 0, 0), (480, 425), (550, 325), 1) pygame.draw.aaline(background, (0, 0, 0), (500, 425), (570, 325), 1) def main(): # D - Display configuration screen = pygame.display.set_mode((640, 480)) pygame.display.set_caption("Drawing commands") # E - Entities (just background for now) background = pygame.Surface(screen.get_size()) background = background.convert() background.fill((255, 255, 255)) drawStuff(background) # A - Action (broken into ALTER steps) # A - Assign values to key variables clock = pygame.time.Clock() keepGoing = True # L - Loop while keepGoing: # T - Timer to set frame rate clock.tick(30) # E - Event handling for event in pygame.event.get(): if event.type == pygame.QUIT: keepGoing = False elif event.type == pygame.MOUSEBUTTONUP: print(pygame.mouse.get_pos()) # R - Refresh display screen.blit(background, (0, 0)) pygame.display.flip() # Close the game window pygame.quit() # Call the main function main() |
The first thing to notice is that I put all of the drawing commands into one function called drawStuff(). The main() code simply calls that function during the Entities step (IDEA) to handle the drawing. Because I will be drawing on the background surface, I pass it as an argument.
Here is a summary of the pygame.draw functions:
line(Surface, (R, G, B), (x1, y1), (x2, y2), width=1)
This function draws a line on a given Surface object, in a specified (R, G, B) colour, starting at coordinates (x1, y1), ending at coordinates (x2, y2), and given pixel width. The width has a default parameter value of 1 pixel.
rect(Surface, (R, G, B), ( (x, y), (width, height) ), width=0)
This function draws a rectangle on a given Surface object, in a specified (R, G, B) colour. The upper-left corner of the rectangle is at (x, y), and its width and height are defined by (width, height). The final width parameter specifies the line thickness and has a default parameter value of 0 that will cause the rectangle to be drawn filled-in.
circle(Surface, (R, G, B), (x, y), radius, width=0)
This function draws a circle on a given Surface object, in a specified (R, G, B) colour. The centre of the circle is at (x, y), the radius is self-explanatory. The width specifies the line thickness and has a default parameter value of 0 that will cause the circle to be drawn filled-in.
ellipse(Surface, (R, G, B), ( (x, y), (width, height) ), width=0)
An ellipse is an oval shape. This function draws an ellipse on a given Surface object, in a specified (R, G, B) colour. The tuples ( (x, y), (width, height) ) define a bounding rectangle into which the ellipse will be drawn. The width specifies the line thickness and has a default parameter value of 0 that will cause the ellipse to be drawn filled-in.
arc(Surface, (R, G, B), ( (x, y), (width, height) ), start_angle, end_angle, width=1)
You can think of an arc as a section of an ellipse. . This function draws an arc on a given Surface object, in a specified (R, G, B) colour. The tuples ( (x, y), (width, height) ) define a bounding rectangle into which the arc will be drawn. The width specifies the line thickness and has a default parameter value of 1 pixel.
Pygame does most of its angle measurements using radians rather than degrees. Radians are expressed as fractions of pi (3.14159). The diagram to the right shows some common angle measurements in radians. For example, 0 degrees would be 0pi, 90 degrees would be pi/2 etc… The start_angle and end_angle parameters indicate (in radians) where the section of the arc should start and end. In our example, I’ve imported the math module to get access to the math.pi constant.
lines(Surface, (R, G, B), closed, points, width = 1)
This function draws a series of connected lines on a given Surface object, in a specified (R, G, B) colour. The points parameter is a nested tuple containing (x, y) tuples of vertex points. If closed = True, then the first and last points are also connected, creating a polygon. The width specifies the line thickness and has a default parameter value of 1 pixel.
polygon(Surface, (R,G, B), points, width = 0)
This function draws a closed polygon (2D shape) using the points parameter, which is a nested tuple containing (x, y) tuples of vertex points. The width specifies the line thickness and has a default parameter value of 0 that will cause the polygon to be drawn filled-in.
Anti-Aliased Lines
Let’s take a closer look at the last two lines being drawn:
1 2 3 |
#compare normal and anti-aliased diagonal lines pygame.draw.line(background, (0, 0, 0), (480, 425), (550, 325), 1) pygame.draw.aaline(background, (0, 0, 0), (500, 425), (570, 325), 1) |
I have explained how the line() function works already, but you’ll see that there is another line drawing function called aaline(). Why two? If you look very carefully at the image on the right you’ll see that the first line (created with line()) looks a little bit jagged. Video display hardware actually generates rectangular pixels. If a line is absolutely vertical or horizontal, it looks perfectly smooth, but diagonal lines ultimately have a “stair-step” look to them if you zoom in close enough.
The second function, aaline() takes identical parameters as line() but it draws what is called an anti-aliased line. Anti-aliasing is a graphics trick used to make diagonal lines look smoother on a computer screen. The second line illustrates how an anti-aliased line looks. To get this effect, the computer generates various shades of colour between the background colour and the line colour and uses more than one colour to give the line the illusion of being smooth.
Anti-aliasing is also often used for rendering fonts so that text appears smooth yet crisp on the screen regardless of the font size. The only downside of anti-aliasing is that it takes more processing time.
Handling a Mouse Event
To finish off this example, I want to point out something interesting in the Event handling step (ALTER) of this example.
1 2 3 4 5 6 |
# E - Event handling for event in pygame.event.get(): if event.type == pygame.QUIT: keepGoing = False elif event.type == pygame.MOUSEBUTTONUP: print(pygame.mouse.get_pos()) |
Here I’m checking for a pygame.MOUSEBUTTONUP event, that is, when the user releases the mouse button. Each time this happens, I display (in the Python shell) the result of the pygame.mouse.get_pos() function, which returns a tuple containing the current (x, y) coordinate of the mouse in the game window. This is a very useful trick to help you figure out where various coordinates lie in the game window. We’ll get into other useful mouse events in upcoming lessons.
Notice that using the print() function is actually a handy debugging technique since you can display information about your game in the Python shell as it runs in a separate game window.
You Try!
- Start a new page in your Learning Journal titled “4-5 Drawing with pygame”. Carefully read the notes above and in your own words summarize the key ideas from each section.
- Why is it best to put your code into functions?
- What is the main advantage and disadvantage of anti-aliasing?