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

Slide puzzle

< Лекция 3 || Лекция 4: 1234 || Лекция 5 >

Converting from Pixel Coordinates to Board Coordinates

203. def getSpotClicked(board, x, y):
204.     # from the x & y pixel coordinates, get the x & y board coordinates
205.     for tileX in range(len(board)):
206.         for tileY in range(len(board[0])):
207.             left, top = getLeftTopOfTile(tileX, tileY)
208.             tileRect = pygame.Rect(left, top, TILESIZE, TILESIZE)
209.             if tileRect.collidepoint(x, y):
210.                 return (tileX, tileY)
211.     return (None, None)

The getSpotClicked() function does the opposite of getLeftTopOfTile() and converts from pixel coordinates to board coordinates. The nested loops on lines 205 and 206 go through every possible XY board coordinate, and if the pixel coordinates that were passed in are within that space on the board, it returns those board coordinates. Since all of the tiles have a width and height that is set in the TILESIZE constant, we can create a Rect object that represents the space on the board by getting the pixel coordinates of the top left corner of the board space, and then use the collidepoint() Rect method to see if the pixel coordinates are inside that Rect object's area.

If the pixel coordinates that were passed in were not over any board space, then the value (None, None) is returned.

Drawing a Tile

215.     # draw a tile at board coordinates tilex and tiley, optionally a few
217.     left, top = getLeftTopOfTile(tilex, tiley)
TILESIZE, TILESIZE))
219.     textSurf = BASICFONT.render(str(number), True, TEXTCOLOR)
220.     textRect = textSurf.get_rect()
221.     textRect.center = left + int(TILESIZE / 2) + adjx, top + int(TILESIZE
222.     DISPLAYSURF.blit(textSurf, textRect)

The drawTile() function will draw a single numbered tile on the board. The tilex and tiley parameters are the board coordinates of the tile. The number parameter is a string of the tile's number (like '3' or '12'). The adjx and adjy keyword parameters are for making minor adjustments to the position of the tile. For example, passing 5 for adjx would make the tile appear 5 pixels to the right of the tilex and tiley space on the board. Passing -10 for adjx would make the tile appear 10 pixels to the left of the space.

These adjustment values will be handy when we need to draw the tile in the middle of sliding. If no values are passed for these arguments when drawTile() is called, then by default they are set to 0. This means they will be exactly on the board space given by tilex and tiley.

The Pygame drawing functions only use pixel coordinates, so first line 217 converts the board coordinates in tilex and tiley to pixel coordinates, which we will store in variables left and top (since getLeftTopOfTile() returns the top left corner's coordinates). We draw the background square of the tile with a call to pygame.draw.rect() while adding the adjx and adjy values to left and top in case the code needs to adjust the position of the tile.

Lines 219 to 222 then create the Surface object that has the number text drawn on it. A Rect object for the Surface object is positioned, and then used to blit the Surface object to the display Surface. The drawTile() function doesn't call pygame.display.update() function, since the caller of drawTile() probably will want to draw more tiles for the rest of the board before making them appear on the screen.

The Making Text Appear on the Screen

225. def makeText(text, color, bgcolor, top, left):
226.     # create the Surface and Rect objects for some text.
227.     textSurf = BASICFONT.render(text, True, color, bgcolor)
228.     textRect = textSurf.get_rect()
229.     textRect.topleft = (top, left)
230.     return (textSurf, textRect)

The makeText() function handles creating the Surface and Rect objects for positioning text on the screen. Instead of doing all these calls each time we want to make text on the screen, we can just call makeText() instead. This saves us on the amount of typing we have to do for our program (Though drawTile() makes the calls to render() and get_rect() itself because it positions the text Surface object by the center point rather than the topleft point and uses a transparent background color).

Drawing the Board

233. def drawBoard(board, message):
234.     DISPLAYSURF.fill(BGCOLOR)
235.     if message:
236.         textSurf, textRect = makeText(message, MESSAGECOLOR, BGCOLOR, 5,
5)
237.         DISPLAYSURF.blit (textSurf, textRect)
238.
239.     for tilex in range(len(board)):
240.         for tiley in range(len(board[0])):
241.             if board[tilex][tiley]:
242.                 drawTile(tilex, tiley, board[tilex][tiley])

This function handles drawing the entire board and all of its tiles to the DISPLAYSURF display Surface object. The fill() method on line 234 completely paints over anything that used to be drawn on the display Surface object before so that we start from scratch.

Line 235 to 237 handles drawing the message at the top of the window. We use this for the "Generating new puzzle…" and other text we want to display at the top of the window. Remember that if statement conditions consider the blank string to be a False value, so if message is set to '' then the condition is False and lines 236 and 237 are skipped.

Next, nested for loops are used to draw each tile to the display Surface object by calling the drawTile() function.

Drawing the Border of the Board

244.     left, top = getLeftTopOfTile(0, 0)
245.     width = BOARDWIDTH * TILESIZE
246.     height = BOARDHEIGHT * TILESIZE
247.     pygame.draw.rect(DISPLAYSURF, BORDERCOLOR, (left - 5, top - 5, width +
11, height + 11), 4)

Lines 244 to 247 draw a border around the tiles. The top left corner of the boarder will be 5 pixels to the left and 5 pixels above the top left corner of the tile at board coordinates (0, 0). The width and height of the border are calculated from the number of tiles wide and high the board is (stored in the BOARDWIDTH and BOARDHEIGHT constants) multiplied by the size of the tiles (stored in the TILESIZE constant).

The rectangle we draw on line 247 will have a thickness of 4 pixels, so we will move the boarder 5 pixels to the left and above where the top and left variables point so the thickness of the line won't overlap the tiles. We will also add 11 to the width and length (5 of those 11 pixels are to compensate for moving the rectangle to the left and up).

Drawing the Buttons

249.     DISPLAYSURF.blit(RESET_SURF, RESET_RECT)
250.     DISPLAYSURF.blit(NEW_SURF, NEW_RECT)
251.     DISPLAYSURF.blit(SOLVE_SURF, SOLVE_RECT)

Finally, we draw the buttons off to the slide of the screen. The text and position of these buttons never changes, which is why they were stored in constant variables at the beginning of the main() function.

Animating the Tile Slides

254. def slideAnimation(board, direction, message, animationSpeed):
255.     # Note: This function does not check if the move is valid.
256.
257.     blankx, blanky = getBlankPosition(board)
258.     if direction == UP:
259.         movex = blankx
260.         movey = blanky + 1
261.     elif direction == DOWN:
262.         movex = blankx
263.         movey = blanky - 1
264.     elif direction == LEFT:
265.         movex = blankx + 1
266.         movey = blanky
267.     elif direction == RIGHT:
268.         movex = blankx - 1
269.         movey = blanky

The first thing our tile sliding animation code needs to calculate is where the blank space is and where the moving tile is. The comment on line 255 reminds us that the code that calls slideAnimation() should make sure that the slide it passes for the direction parameter is a valid move to make.

The blank space's coordinates come from a call to getBlankPosition(). From these coordinates and the direction of the slide, we can figure out the XY board coordinates of the tile that will slide. These coordinates will be stored in the movex and movey variables.

The copy() Surface Method

271.     # prepare the base surface
272.     drawBoard(board, message)
273.     baseSurf = DISPLAYSURF.copy()
274.     # draw a blank space over the moving tile on the baseSurf Surface.
275.     moveLeft, moveTop = getLeftTopOfTile(movex, movey)
276.     pygame.draw.rect(baseSurf, BGCOLOR, (moveLeft, moveTop, TILESIZE,
TILESIZE))

The copy() method of Surface objects will return a new Surface object that has the same image drawn to it. But they are two separate Surface objects. After calling the copy() method, if we draw on one Surface object using blit() or the Pygame drawing functions, it will not change the image on the other Surface object. We store this copy in the baseSurf variable on line 273.

Next, we paint another blank space over the tile that will slide. This is because when we draw each frame of the sliding animation, we will draw the sliding tile over different parts of the baseSurf Surface object. If we didn't blank out the moving tile on the baseSurf Surface, then it would still be there as we draw the sliding tile. In that case, here is what the baseSurf Surface would look like:

Рис. 4.4.

And then what it would look like when we draw the "9" tile sliding upwards on top of it:

Рис. 4.5.

You can see this for yourself by commenting out line 276 and running the program.

278.     for i in range(0, TILESIZE, animationSpeed):
279.         # animate the tile sliding over
280.         checkForQuit()
281.         DISPLAYSURF.blit(baseSurf, (0, 0))
282.         if direction == UP:
283.             drawTile(movex, movey, board[movex][movey], 0, -i)
284.         if direction == DOWN:
285.             drawTile(movex, movey, board[movex][movey], 0, i)
286.         if direction == LEFT:
287.             drawTile(movex, movey, board[movex][movey], -i, 0)
288.         if direction == RIGHT:
289.             drawTile(movex, movey, board[movex][movey], i, 0)
290.
291.         pygame.display.update()
292.         FPSCLOCK.tick(FPS)

In order to draw the frames of the sliding animation, we must draw the baseSurf surface on the display Surface, then on each frame of the animation draw the sliding tile closer and closer to its final position where the original blank space was. The space between two adjacent tiles is the same size as a single tile, which we have stored in TILESIZE. The code uses a for loop to go from 0 to TILESIZE.

Normally this would mean that we would draw the tile 0 pixels over, then on the next frame draw the tile 1 pixel over, then 2 pixels, then 3, and so on. Each of these frames would take 1/30th of a second. If you have TILESIZE set to 80 (as the program in this book does on line 12) then sliding a tile would take over two and a half seconds, which is actually kind of slow.

So instead we will have the for loop iterate from 0 to TILESIZE by several pixels each frame. The number of pixels it jumps over is stored in animationSpeed, which is passed in when slideAnimation() is called. For example, if animationSpeed was set to 8 and the constant TILESIZE was set to 80, then the for loop and range(0, TILESIZE, animationSpeed) would set the i variable to the values 0, 8, 16, 24, 32, 40, 48, 56, 64, 72 (It does not include 80 because the range() function goes up to, but not including, the second argument). This means the entire sliding animation would be done in 10 frames, which would mean it is done in 10/30th of a second (a third of a second) since the game runs at 30 FPS.

Lines 282 to 289 makes sure that we draw the tile sliding in the correct direction (based on what value the direction variable has). After the animation is done, then the function returns. Notice that while the animation is happening, any events being created by the user are not being handled. Those events will be handled the next time execution reaches line 70 in the main() function or the code in the checkForQuit() function.

< Лекция 3 || Лекция 4: 1234 || Лекция 5 >

Студенты

Константин Шилов
 Россия
Павел Лафицкий
 Украина