Опубликован: 06.08.2013 | Доступ: свободный | Студентов: 783 / 11 | Длительность: 27:51:00
Лекция 6:

Wormy

< Лекция 5 || Лекция 6: 12 || Лекция 7 >

The Start Screen

128. def showStartScreen():
129.     titleFont = pygame.font.Font('freesansbold.ttf', 100)
130.     titleSurf1 = titleFont.render('Wormy!', True, WHITE, DARKGREEN)
131.     titleSurf2 = titleFont.render('Wormy!', True, GREEN)
132.
133.     degrees1 = 0
134.     degrees2 = 0
135.     while True:
136.         DISPLAYSURF.fill(BGCOLOR)

When the Wormy game program first begins running, the player doesn’t automatically begin playing the game. Instead, a start screen appears which tells the player what program they are running. A start screen also gives the player a chance to prepare for the game to begin (otherwise the player might not be ready and crash on their first game).

The Wormy start screen requires two Surface objects with the "Wormy!" text drawn on them. These are what the render() method calls create on lines 130 and 131. The text will be large: the Font() constructor function call on line 129 creates a Font object that is 100 points in size. The first "Wormy!" text will have white text with a dark green background, and the other will have green text with a transparent background.

Line 135 begins the animation loop for the start screen. During this animation, the two pieces of text will be rotated and drawn to the display Surface object.

Rotating the Start Screen Text

137.         rotatedSurf1 = pygame.transform.rotate(titleSurf1, degrees1)
138.         rotatedRect1 = rotatedSurf1.get_rect()
139.         rotatedRect1.center = (WINDOWWIDTH / 2, WINDOWHEIGHT / 2)
140.         DISPLAYSURF.blit(rotatedSurf1, rotatedRect1)
141.
142.         rotatedSurf2 = pygame.transform.rotate(titleSurf2, degrees2)
143.         rotatedRect2 = rotatedSurf2.get_rect()
144.         rotatedRect2.center = (WINDOWWIDTH / 2, WINDOWHEIGHT / 2)
145.         DISPLAYSURF.blit(rotatedSurf2, rotatedRect2)
146.
147.         drawPressKeyMsg()
148.
149.         if checkForKeyPress():
150.             pygame.event.get() # clear event queue
151.             return
152.         pygame.display.update()
153.         FPSCLOCK.tick(FPS)

The showStartScreen() function will rotate the images on the Surface objects that the "Wormy!" text is written on. The first parameter is the Surface object to make a rotated copy of. The second parameter is the number of degrees to rotate the Surface. The pygame.transform.rotate() function doesn’t change the Surface object you pass it, but rather returns a new Surface object with the rotated image drawn on it.

Note that this new Surface object will probably be larger than the original one, since all Surface objects represent rectangular areas and the corners of the rotated Surface will stick out past the width and height of original Surface. The picture below has a black rectangle along with a slightly rotated version of itself. In order to make a Surface object that can fit the rotated rectangle (which is colored gray in the picture below), it must be larger than the original black rectangle’s Surface object:


Рис. 6.3.

The amount you rotate it is given in degrees, which is a measure of rotation. There are 360 degrees in a circle. Not rotated at all is 0 degrees. Rotating to one quarter counter-clockwise is 90 degrees. To rotate clockwise, pass a negative integer. Rotating 360 degrees is rotating the image all the way around, which means you end up with the same image as if you rotated it 0 degrees. In fact, if the rotation argument you pass to pygame.transform.rotate() is 360 or larger, then Pygame automatically keeps subtracting 360 from it until it gets a number less than 360. This image shows several examples of different rotation amounts:


Рис. 6.4.

The two rotated "Wormy!" Surface objects are blitted to the display Surface on each frame of the animation loop on lines 140 and 145.

On line 147 the drawPressKeyMsg() function call draws the "Press a key to play." text in the lower corner of the display Surface object. This animation loop will keep looping until checkForKeyPress() returns a value that is not None, which happens if the player presses a key. Before returning, pygame.event.get() is called simply to clear out any other events that have accumulated in the event queue which the start screen was displayed.

Rotations Are Not Perfect

You may wonder why we store the rotated Surface in a separate variable, rather than just overwrite the titleSurf1 and titleSurf2 variables. There are two reasons.

First, rotating a 2D image is never completely perfect. The rotated image is always approximate. If you rotate an image by 10 degrees counterclockwise, and then rotate it back 10 degrees clockwise, the image you have will not be the exact same image you started with. Think of it as making a photocopy, and then a photocopy of the first photocopy, and the another photocopy of that photocopy. If you keep doing this, the image gets worse and worse as the slight distortions add up.

(The only exception to this is if you rotate an image by a multiple of 90 degrees, such as 0, 90, 180, 270, or 360 degrees. In that case, the pixels can be rotated without any distortion.)

Second, if you rotate a 2D image then the rotated image will be slightly larger than the original image. If you rotate that rotated image, then the next rotated image will be slightly larger again. If you keep doing this, eventually the image will become too large for Pygame to handle, and your program will crash with the error message, pygame.error: Width or height is too large.

154.         degrees1 += 3 # rotate by 3 degrees each frame
155.         degrees2 += 7 # rotate by 7 degrees each frame

The amount that we rotate the two "Wormy!" text Surface objects is stored in degrees1 and degrees2. On each iteration through the animation loop, we increase the number stored in degrees1 by 3 and degrees2 by 7. This means on the next iteration of the animation loop the white text "Wormy!" Surface object will be rotated by another 3 degrees and the green text "Wormy!" Surface object will be rotated by another 7 degrees. This is why the one of the Surface objects rotates slower than the other.

158. def terminate():
159.     pygame.quit()
160.     sys.exit()

The terminate() function calls pygame.quit() and sys.exit() so that the game correctly shuts down. It is identical to the terminate() functions in the previous game programs.

Deciding Where the Apple Appears

163. def getRandomLocation():
164.     return {'x': random.randint(0, CELLWIDTH - 1), 'y': random.randint(0,
CELLHEIGHT - 1)}

The getRandomLocation() function is called whenever new coordinates for the apple are needed. This function returns a dictionary with keys 'x' and 'y', with the values set to random XY coordinates.

Game Over Screens

167. def showGameOverScreen():
168.     gameOverFont = pygame.font.Font('freesansbold.ttf', 150)
169.     gameSurf = gameOverFont.rende('Game', True, WHITE)
170.     overSurf = gameOverFont.render('Over', True, WHITE)
171.     gameRect = gameSurf.get_rect()
172.     overRect = overSurf.get_rect()
173.     gameRect.midtop = (WINDOWWIDTH / 2, 10)
174.     overRect.midtop = (WINDOWWIDTH / 2, gameRect.height + 10 + 25)
175.
176.     DISPLAYSURF.blit(gameSurf, gameRect)
177.     DISPLAYSURF.blit(overSurf, overRect)
178.     drawPressKeyMsg()
179.     pygame.display.update()

The game over screen is similar to the start screen, except it isn’t animated. The words "Game" and "Over" are rendered to two Surface objects which are then drawn on the screen.

180.     pygame.time.wait(500)
181.     checkForKeyPress() # clear out any key presses in the event queue
182.
183.     while True:
184.         if checkForKeyPress():
185.             pygame.event.get() # clear event queue
186.             return

The Game Over text will stay on the screen until the player pushes a key. Just to make sure the player doesn’t accidentally press a key too soon, we will put a half second pause with the call to pygame.time.wait() on line 180 (The 500 argument stands for a 500 millisecond pause, which is half of one second).

Then, checkForKeyPress() is called so that any key events that were made since the showGameOverScreen() function started are ignored. This pause and dropping of the key events is to prevent the following situation: Say the player was trying to turn away from the edge of the screen at the last minute, but pressed the key too late and crashed into the edge of the board. If this happens, then the key press would have happened after the showGameOverScreen() was called, and that key press would cause the game over screen to disappear almost instantly. The next game would start immediately after that, and might take the player by surprise. Adding this pause helps the make the game more "user friendly".

Drawing Functions

The code to draw the score, worm, apple, and grid are all put into separate functions.

188. def drawScore(score>):
189.     scoreSurf = BASICFONT.render('Score: %s' % (score), True, WHITE)
190.     scoreRect = scoreSurf.get_rect()
191.     scoreRect.topleft = (WINDOWWIDTH - 120, 10)
192.     DISPLAYSURF.blit(scoreSurf, scoreRect)

The drawScore() function simply renders and draws the text of the score that was passed in its score parameter on the display Surface object.

195. def drawWorm(wormCoords):
196.     for coord in wormCoords:
197.         x = coord['x'] * CELLSIZE
198.         y = coord['y'] * CELLSIZE
199.         wormSegmentRect = pygame.Rect(x, y, CELLSIZE, CELLSIZE)
200.         pygame.draw.rect(DISPLAYSURF, DARKGREEN, wormSegmentRect)
201.         wormInnerSegmentRect = pygame.Rect(x + 4, y + 4, CELLSIZE - 8,
CELLSIZE - 8)
202.         pygame.draw.rect(DISPLAYSURF, GREEN, wormInnerSegmentRect)

The drawWorm() function will draw a green box for each of the segments of the worm’s body. The segments are passed in the wormCoords parameter, which is a list of dictionaries each with an 'x' key and a 'y' key. The for loop on line 196 loops through each of the dictionary values in wormCoords.

Because the grid coordinates take up the entire window and also begin a 0, 0 pixel, it is fairly easy to convert from grid coordinates to pixel coordinates. Line 197 and 198 simply multiply the coord['x'] and coord['y'] coordinate by the CELLSIZE.

Line 199 creates a Rect object for the worm segment that will be passed to the pygame.draw.rect() function on line 200. Remember that each cell in the grid is CELLSIZE in width and height, so that’s what the size of the segment’s Rect object should be. Line 200 draws a dark green rectangle for the segment. Then on top of this, a smaller bright green rectangle is drawn. This makes the worm look a little nicer.

The inner bright green rectangle starts 4 pixels to the right and 4 pixels below the topleft corner of the cell. The width and height of this rectangle are 8 pixels less than the cell size, so there will be a 4 pixel margin on the right and bottom sides as well.

205. def drawApple(coord):
206.     x = coord['x'] * CELLSIZE
207.     y = coord['y'] * CELLSIZE
208.     appleRect = pygame.Rect(x, y, CELLSIZE, CELLSIZE)
209.     pygame.draw.rect(DISPLAYSURF, RED, appleRect)

The drawApple() function is very similar to drawWorm(), except since the red apple is just a single rectangle that fills up the cell, all the function needs to do is convert to pixel coordinates (which is what lines 206 and 207 do), create the Rect object with the location and size of the apple (line 208), and then pass this Rect object to the pygame.draw.rect() function.

212. def drawGrid():
213.     for x in range(0, WINDOWWIDTH, CELLSIZE): # draw vertical lines
214.         pygame.draw.line(DISPLAYSURF, DARKGRAY, (x, 0), (x, WINDOWHEIGHT))
215.     for y in range(0, WINDOWHEIGHT, CELLSIZE): # draw horizontal lines
216.         pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, y), (WINDOWWIDTH, y))

Just to make it easier to visualize the grid of cells, we call pygame.draw.line() to draw out each of the vertical and horizontal lines of the grid.

Normally, to draw the 32 vertical lines needed, we would need 32 calls to pygame.draw.line() with the following coordinates:

pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, 0), (0, WINDOWHEIGHT))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (20, 0), (20, WINDOWHEIGHT))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (40, 0), (40, WINDOWHEIGHT))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (60, 0), (60, WINDOWHEIGHT))
...skipped for brevity...
pygame.draw.line(DISPLAYSURF, DARKGRAY, (560, 0), (560, WINDOWHEIGHT))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (580, 0), (580, WINDOWHEIGHT))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (600, 0), (600, WINDOWHEIGHT))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (620, 0), (620, WINDOWHEIGHT))

Instead of typing out all these lines of code, we can just have one line of code inside a for loop. Notice that the pattern for the vertical lines is that the X coordinate of the start and end point starts at 0 and goes up to 620, increasing by 20 each time. The Y coordinate is always 0 for the start point and WINDOWHEIGHT for the end point parameter. That means the for loop should iterate over range(0, 640, 20). This is why the for loop on line 213 iterates over range(0, WINDOWWIDTH, CELLSIZE).

For the horizontal lines, the coordinates would have to be:

pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, 0), (WINDOWWIDTH, 0))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, 20), (WINDOWWIDTH, 20))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, 40), (WINDOWWIDTH, 40))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, 60), (WINDOWWIDTH, 60))
...skipped for brevity...
pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, 400), (WINDOWWIDTH, 400))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, 420), (WINDOWWIDTH, 420))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, 440), (WINDOWWIDTH, 440))
pygame.draw.line(DISPLAYSURF, DARKGRAY, (0, 460), (WINDOWWIDTH, 460))

The Y coordinate ranges from 0 to 460, increasing by 20 each time. The X coordinate is always 0 for the start point and WINDOWWIDTH for the end point parameter. We can also use a for loop here so we don’t have to type out all those pygame.draw.line() calls.

Noticing regular patterns needed by the calls and using loops is a clever programmer trick to save us from a lot of typing. We could have typed out all 56 pygame.draw.line() calls and the program would have worked the exact same. But by being a little bit clever, we can save ourselves a lot of work.

219. if __name__ == '__main__':
220.     main()

After all the functions and constants and global variables have been defined and created, the main() function is called to start the game.

Don’t Reuse Variable Names

Take a look at a few lines of code from the drawWorm() function again:

199.         wormSegmentRect = pygame.Rect(x, y, CELLSIZE, CELLSIZE)
200.         pygame.draw.rect(DISPLAYSURF, DARKGREEN, wormSegmentRect)
201.         wormInnerSegmentRect = pygame.Rect(x + 4, y + 4, CELLSIZE - 8,
CELLSIZE - 8)
202.         pygame.draw.rect(DISPLAYSURF, GREEN, wormInnerSegmentRect)

Notice that two different Rect objects are created on lines 199 and 201. The Rect object created on line 199 is stored in the wormSegmentRect local variable and is passed to the pygame.draw.rect() function on line 200. The Rect object created on line 201 is stored in the wormInnerSegmentRect local variable and is passed to the pygame.draw.rect() function on line 202. Every time you create a variable, it takes up a small amount of the computer’s memory. You might think it would be clever to reuse the wormSegmentRect variable for both Rect objects, like this:

199.         wormSegmentRect = pygame.Rect(x, y, CELLSIZE, CELLSIZE)
200.         pygame.draw.rect(DISPLAYSURF, DARKGREEN, wormSegmentRect)
201.         wormSegmentRect = pygame.Rect(x + 4, y + 4, CELLSIZE - 8, CELLSIZE
- 8)
202.         pygame.draw.rect(DISPLAYSURF, GREEN, wormInnerSegmentRect)

Because the Rect object returned by pygame.Rect() on line 199 won’t be needed after 200, we can overwrite this value and reuse the variable to store the Rect object returned by pygame.Rect() on line 201. Since we are now using fewer variables we are saving memory, right?

While this is technically true, you really are only saving a few bytes. Modern computers have memory of several billion bytes. So the savings aren’t that great. Meanwhile, reusing variables reduces the code readability. If a programmer was reading through this code after it was written, they would see that wormSegmentRect is passed to the pygame.draw.rect() calls on line 200 and 202. If they tried to find the first time the wormSegmentRect variable was assigned a value, they would see the pygame.Rect() call on line 199. They might not realize that the Rect object returned by line 199’s pygame.Rect() call isn’t the same as the one that is passed to the pygame.draw.rect() call on line 202.

Little things like this make it harder to understand how exactly your program works. It won’t just be other programmers looking at your code who will be confused. When you look at your own code a couple weeks after writing it, you may have a hard time remembering how exactly it works. Code readability is much more important than saving a few bytes of memory here and there.

For additional programming practice, you can download buggy versions of Wormy from http://invpy.com/buggy/wormy and try to figure out how to fix the bugs.

< Лекция 5 || Лекция 6: 12 || Лекция 7 >
Константин Шилов
Константин Шилов
Россия
Павел Лафицкий
Павел Лафицкий
Украина