A PyGame Working Example, continued

In this fourth article in our series covering the creation of a video game using Python and PyGame, we will code the internals of our game.

The Game

Now it’s time to begin coding the internals of our game. Create a file called pydodge.py (after all, our game is about dodging objects) to store our game module, and let’s jump into some code. First, we’ll need to import the required modules:

import imp
import gamesprites
import pygame
import sys

First, we import the imp module. The imp module will allow us to import modules in a more dynamic fashion. Since we store our levels as Python classes and will access them by importing them as modules, this functionality is needed. We then import the gamesprites module, which we constructed earlier to store the game’s Sprite classes, as well as pygame itself. Finally, sys is imported since we need to make use of sys.exit.

Next, we’ll need to take care of some global variables:

level = None
player = None
playerSprite = None
objects= None
background = None
rows = None
columns = None
layout = None
screen = None

Don’t worry about what each one of these means right now. We’ll cover the purpose of each one as it is used.

Loading Levels

The first thing that needs to be done is the loading of levels. We’ll need to import the specified level file and then extract some basic information from it, such as the object images and the layout of the level as a whole:

def loadLevel(levelFile):

   # Import the level and extract the data
   global level, player, objects, background, rows, columns,
layout
   level = imp.find_module(levelFile)
   level = imp.load_module(‘level’, level[0], level[1], level[2])
   level = level.Level()
   player = level.getPlayer()
   objects = level.getObjects()
   background, rows = level.getBackground()
   layout = level.getLayout()
   columns = len(layout[0])

In the above function, we use the imp module to search for the level by its name. Then, we import it as level. However, since we only need the single Level class that it contains, we re-assign level to an instance of the contained Level class. We then extract the player image and assign it to the player variable, and we get the list of object images and assign it to the objects variable. Next, we get the background image and the number of rows that will be visible at once. Finally, we get the layout list and retrieve the number of columns it calls for.

{mospagebreak title=Setting Things Up}

Before our game is allowed to work, we must initialize PyGame and create the necessary Sprite objects. The first function we’ll define is the setup function, which simply initializes PyGame and creates a screen for us to work with:

def setup():

   global screen
   pygame.init()
   screen = pygame.display.set_mode((background.get_rect().width,
background.get_rect().height))

Now that we have a Surface object to draw on, we must blit the background image onto the Surface object. As you know already, this isn’t rocket science:

def loadBackground():

   screen.blit(background, background.get_rect())
   pygame.display.update()

Finally, we have to create our game’s Sprite objects. The first Sprite object we’ll have to create is the player’s sprite. We’ll position the sprite in the bottom left of the player’s screen to begin with, which will require a few calculations based on the data we retrieved when loading the level. The player Sprite should also be added to its own group:

def loadSprites():

   global player, playerSprite

   # Find the position of the player
   colWidth = background.get_rect().width / columns
   xPlayer = colWidth / 2
   rowHeight = background.get_rect().height / rows
   yPlayer = (rowHeight * (rows – 1)) + (rowHeight / 2)

   # Load the player sprite
   playerSprite = gamesprites.Player(player, xPlayer, yPlayer)

   # Create a player sprite group
   player = pygame.sprite.RenderUpdates(playerSprite)

Next come the object sprites. Each non-zero position in the layout list will need to be converted into a sprite, and we’ll need to calculate the exact x-position of each object:

def loadSprites():

   …

   # Load each object sprite
   for y in xrange(len(layout)):
      for x in xrange(len(layout[y])):
         if layout[y][x]:
            layout[y][x] = gamesprites.Object(objects[layout[y]
[x] – 1], (colWidth * (x)) + (colWidth / 2))

In the above code section, we simply iterate through the list and replace every non-zero element with a corresponding Sprite object. We’ll also need to convert each row in the layout list to a group. This way, we can move an entire row of objects at once:

def loadSprites():

   …

   # Turn each layout row into a sprite group
   for y in xrange(len(layout)):
      group = pygame.sprite.RenderUpdates()
      for x in xrange(len(layout[y])):
         if layout[y][x]:
            group.add(layout[y][x])
      group.y = 0
      layout[y] = group

Now, we’ve reduced our list to a simple list of sprite groups.

{mospagebreak title=Cleaning Up}

In earlier examples, we never used a very complex background. Because of this, erasing a sprite was simply a matter of filling up its position with a solid color. However, this time, we have a graphical background with many different elements at unique positions. Each time an object moves, we’ll need to replace its old position with the corresponding piece of background. Otherwise, things definitely wouldn’t look right.

Thankfully, this is quite easy to do. Recall that when loading the background image, we store it in a variable called background. This object certainly hasn’t gone anywhere, and we’re free to access its contents—an original copy of the background—at any time. So, to erase an object in our game, we’ll simply clip the corresponding rectangle from the original background image and draw it in its proper place on the screen:

def erase(screen, rect):

   # Get the piece of the original background and copy it to the
screen
   screen.blit(background.subsurface(rect).copy(), rect)

The subsurface method simply creates a “Surface object within a Surface object.” Both of the objects share pixels, so by creating a Surface object within our background, we have access to the exact contents of the area we need to access. We simply copy it and blit it to its proper place on the screen. It’s a lot easier than it sounds, taking just one line to do.

{mospagebreak title=Constructing the Main Loop}

Now that we’ve created functions to get things set up for us, we’re ready to get into the function containing the main loop of the game. Before we get into our function’s actual loop, though, we’ll have to do a bit of configuration. First, we’ll have to get the size of each position on the screen. This simply involves dividing the height and width of the background by the number of rows and columns. Then, we’ll have to define two variables that work with movement. When the user presses a key, the first variable will need to be set to the direction of the movement. When the user releases a key, the second variable will need to be set to True, which will signal movement to stop. This way, a user can hold down a key and see movement rather than have to repeatedly tap the key.

Though we have a list of sprite groups, we also need a list that will store the sprite groups that are actually visible on the screen. This way, we’ll know which ones to update. Next, we need to actually draw the player. Finally, we’ll have to load custom events into the timer. This is done by setting the timer to trigger our own event IDs at set intervals. The first event will update the player, and the second event will update the objects. The speeds at which things will be updated will be passed as arguments.

Here’s all this in action:

def run(playerSpeed = 250, objectSpeed = 1000):

   # Get the row and column widths
   colWidth = background.get_rect().width / columns
   rowHeight = background.get_rect().height / rows

   # Define a variable that stores whether an arrow key is
pressed
   # This is used for continuous/scrolling movement of the player
   moving = False

   # We should also define a variable that signals stops
   # Otherwise, if a player pushes a key in between updates and
releases
   # it, the move will not be registered
   movingStop = False

   # Create a list to store the visible groups
   visible = []

   # Blit the player
   updateRects = player.draw(screen)
   pygame.display.update(updateRects)

   # Load a screen update event into the timer
   # This will update the player only
   pygame.time.set_timer(pygame.USEREVENT + 1, playerSpeed)

   # Load a screen update event into the timer
   # This will update the objects
   pygame.time.set_timer(pygame.USEREVENT + 2, objectSpeed)

With all that done, we’re now ready to jump into the main loop. First, we’ll need to check for quit events and key presses:

def run(playerSpeed = 250, objectSpeed = 1000):

   …

   while True:

      for event in pygame.event.get():
         if event.type == pygame.QUIT:
            sys.exit()

         # Check for a key push by the user
         elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
               moving = ‘right’
            elif event.key == pygame.K_LEFT:
               moving = ‘left’

         # Check by a key release by the user
         elif event.type == pygame.KEYUP:
            if event.key == pygame.K_RIGHT or pygame.K_LEFT:
               movingStop = True

The above code simply sets the player to move when a key is pressed, and it sets it to stop moving when a key is released. It also enables the player to exit the game.

The next task ahead of us is moving the player around. To do this, we first need to check to see whether our custom player update event has been triggered. If it has, we then need to move the player in the proper direction, if any direction at all. We finally need to check to see whether the player has collided with any of the objects and then redraw the player:

def run(playerSpeed = 250, objectSpeed = 1000):

   …

         # Check for the update player event
         elif event.type == pygame.USEREVENT + 1:

            # Move the player if needed
            if moving == ‘right’:
               player.update(colWidth)
            if moving == ‘left’:
               player.update(-colWidth)

            # Stop movement if needed
            if movingStop:
               moving = False
               movingStop = False

            # Collision detection
            for group in visible:
               if pygame.sprite.spritecollideany(playerSprite,
group):
                  return False

            # Redraw the player
            player.clear(screen, erase) 
            updateRects = player.draw(screen)

            pygame.display.update(updateRects)

As I mentioned earlier, the visible list above will contain a list of all the visible sprite groups. To check for collision, we iterate through this list and use pygame.sprite.spritecollideany. There’s no need to use a method that kills the colliding sprites since we want to completely back out of the loop. What object the player hits doesn’t really matter to us. As you can see, we return False to get out of the loop.

All that’s left now is moving the objects around, an extremely simple task. This is done when our custom object update event has been triggered. If anything remains in the layout list, we add it to our visible list. Likewise, we delete any sprite group that has fallen off the screen. Aside from that, we simply update each sprite group, and if the visible list is empty, then the player has won the level.

def run(playerSpeed = 250, objectSpeed = 1000):

   …

         # Check for the update object event
         elif event.type == pygame.USEREVENT + 2:

            # Add a row
            if layout:
               visible.append(layout.pop())

            # Delete passed rows
            if visible:
               if visible[0].y >= screen.get_rect().height:
                  visible.pop(0)

            # If there are no visible rows, the player has won
            else:
               return True

            # Make a list of rectangles to be updated
            updateRects = []

            # Update each group
            for group in visible:
               group.clear(screen, erase)
               group.update(rowHeight)
               group.y = group.y + rowHeight
               updateRects.extend(group.draw(screen))

            pygame.display.update(updateRects)

{mospagebreak title=The Game in Action}

All that’s left now is creating a Python script that will make use of our level and game module. To run our level, simply create a file named playAsteroid.py:

import pydodge

pydodge.loadLevel(‘asteroid’)
pydodge.setup()
pydodge.loadBackground()
pydodge.loadSprites()
pydodge.run()

Of course, we can always customize our game a bit more. Let’s say that we want to use a title screen rather than forcing the user to jump right into the level. Also, let’s display either “Level Complete” or “Game Over”:

import pydodge
import pygame

pydodge.loadLevel(‘asteroid’)
pydodge.setup()
pydodge.loadBackground()

# Add a title
font1 = pygame.font.Font(None, 25)
text1 = font1.render(‘PyDodge Asteroid’, True, (255, 255, 255))
textRect1 = text1.get_rect()
textRect1.centerx = pydodge.screen.get_rect().centerx
textRect1.y = 100
pydodge.screen.blit(text1, textRect1)

# Add “Press <Enter> To Play”
font2 = pygame.font.Font(None, 17)
text2 = font2.render(‘Press <Enter> To Play’, True, (255, 255,
255))
textRect2 = text2.get_rect()
textRect2.centerx = pydodge.screen.get_rect().centerx
textRect2.y = 150
pydodge.screen.blit(text2, textRect2)

# Update the screen
pygame.display.update()

# Wait for enter to be pressed
# The user can also quit
waiting = True
while waiting:
   for event in pygame.event.get():
      if event.type == pygame.QUIT:
         sys.exit()
      elif event.type == pygame.KEYDOWN:
         if event.key == pygame.K_RETURN:
            waiting = False
            break

pydodge.loadBackground()
pydodge.loadSprites()

# The user has won the game
if pydodge.run(100, 300):
   text3 = font1.render(‘Level Complete’, True, (255, 255, 255))
   textRect3 = text3.get_rect()
   textRect3.centerx = pydodge.screen.get_rect().centerx
   textRect3.y = 150
   pydodge.screen.blit(text3, textRect3)

# The user has lost the game
else:
   text3 = font1.render(‘Game Over’, True, (255, 255, 255))
   textRect3 = text3.get_rect()
   textRect3.centerx = pydodge.screen.get_rect().centerx
   textRect3.y = 150
   pydodge.screen.blit(text3, textRect3)

pygame.display.update()

# Wait for the user to quit
while True:
   for event in pygame.event.get():
      if (event.type == pygame.QUIT) or (event.type == pygame.KEYDOWN):
         sys.exit()

Conclusion

As you can see, creating a functioning game with PyGame is rather easy. Our game module weighs in at around five kilobytes. Using the module, you can also customize games, loading whatever levels you would like and displaying extra messages and what-not.

From here, try customizing your game even further. You can try adding a menu where the user can select a difficulty level. You can also link multiple levels together and randomize the layout lists in levels. It’s up to your imagination.

Of course, there’s a lot more to PyGame than a simple space game like this, so feel free to explore the library and examine one of the many example games available on the PyGame website. Good luck!

Google+ Comments

Google+ Comments