Creating a Game in Python Using PyGame – Part Two – Creating a level


Python RSS ReaderAll right in Part one we actually created a semi-working almost-game, in part two we’re going to go a bit further, in part two we’re going to add the walls that will make help make PyMan (our python based PacMan clone) an actual game.

Note: Part one has been mistakenly half-deleted by me, so it is not fully available at this time. I am working to re-write it so it should be up soon.

Python RSS ReaderNote: I also changed the images that I used in Part one so now we have a new smaller snake.

You can download the full source for Part Two here.

What we are going to do is base some of our level code on a great PyGame tutorial over at DevShed specifically the idea of the level layout. This is something that has always puzzled me about games, how they create their levels? I’ve read some articles and beginnings of books but I’ve never really be able to understand it, but the multidimensional array approach discussed in the DevShed article is nothing if not simple.

So the idea is that our game world, or game board, will basically be a grid of 24×24 pixel items, and we will have a multidimensional array or list that we will used to describe it.

So for example lets say we have a grid like this:


0 0 0 0 0
1 1 1 1 1
1 0 0 0 1
1 1 1 1 1
0 0 0 0 0

Now that grid may result in the following:

PyGame Window

Where 0 is nothing and where 1 is a blue square. We could then add 2′s if we wanted to and make those red squares, or anything else that we wanted. So that is basically how our levels are going to be defined.

The first thing we are going to do is add a new file to our project called levelBase.Py, in that we will define a class called level which is as follows:

class Level:
    """The Base Class for Levels"""
    def getLayout(self):
        """Get the Layout of the level"""
        """Returns a [][] list"""
        pass
    def getImages(self):
        """Get a list of all the images used by the level"""
        """Returns a list of all the images used.  The indices 
        in the layout refer to sprites in the list returned by
        this function"""
        pass

Pretty simple not much going on there, getLayout returns a multidimensional list of numbers that represents the level, and getImages returns a list of the images used by the level.

It may seem complicated but once you see our first level it will start to make some sense. The next thing to do is add a file called level001.py to our project. I waffled on this a bit, I was unsure if it was better to have one file called levels.py that contained classes called level001, or make each level a separate file. In then end I went with each level being a separate file, just because it separates things, but I don’t really know if one method is superior over the other. Here is the code for level001.py:

#! /usr/bin/env python

import levelBase
from helpers import load_image

class level(levelBase.Level):
    """Level 1 of the PyMan Game"""
    
    def __init__(self):
        self.BLOCK = 1
        self.SNAKE = 2
        self.PELLET = 0
    
    def getLayout(self):
        return [[9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9 ,9, 9, 9, 9, 9, 9, 9, 9, 9, 9],\
                [9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9 ,9, 9, 9, 9, 9, 9, 9, 9, 9, 9],\
                [9, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ,1, 1, 1, 1, 1, 1, 1, 1, 1, 9],\
                [9, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 ,0, 0, 0, 0, 0, 0, 0, 0, 1, 9],\
                [9, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1 ,0, 1, 1, 1, 0, 1, 1, 0, 1, 9],\
                [9, 1, 0, 2, 0, 0, 0, 0, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0, 1, 9],\
                [9, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1 ,1, 1, 0, 1, 0, 1, 1, 0, 1, 9],\
                [9, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1 ,0, 0, 0, 1, 0, 0, 0, 0, 1, 9],\
                [9, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 9],\
                [9, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 9],\
                [9, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 9],\
                [9, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 9],\
                [9, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 9],\
                [9, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 9],\
                [9, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 9],\
                [9, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 9],\
                [9, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 9],\
                [9, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 9],\
                [9, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 9],\
                [9, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 9],\
                [9, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 9],\
                [9, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 9],\
                [9, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9]]         
        
    def getSprites(self):
        block, rect = load_image('block.png')
        pellet, rect = load_image('pellet.png',-1)
        snake, rect = load_image('snake.png',-1)
        return [pellet,block,snake]

You can see that in the __init__ function the level defines three values: BLOCK, SNAKE, and PELLET. These values correspond to the numbers in the list returned by getLayout(). So wherever there is a 1 in the list, that will correspond to a BLOCK, a 0 is a PELLET, and a 2 is our snake. In this example I use 9 to represent a blank square.

You’ll also notice that the getImages() function returns a list of images that are indexed by those same values. So in the list returned by getImages(), position [BLOCK] is the block image.

So far so good, the next thing we are going to do is add a base class for all of our sprites. In Part one we didn’t need to do this since we only had two sprites, but in this lesson (as we can see above) we are going to have at least three and that number will continue to grow. So we will add another file to our project called basicSprite.py, this will be the base class that we use for all of our sprites. It doesn’t do much besides add a few initialization features to pygame.sprite.Sprite class. The code for basicSprite.py is as follows:

#! /usr/bin/env python

import pygame

class Sprite(pygame.sprite.Sprite):
        
    def __init__(self, centerPoint, image):
        pygame.sprite.Sprite.__init__(self) 
        """Set the image and the rect"""
        self.image = image
        self.rect = image.get_rect()
        """Move the rect into the correct position"""
        self.rect.center = centerPoint

As you can see all that basicSprite.Sprite does is really add a few initialization values: centerPoint and image. Image is the image that the sprite will use and centerPoint is the center point of the rect, it controls where the image will be placed. The nice thing about using the center point is that it is correct regardless of the images size. So if you have a 16×16 image and a 24×24 image the center point will be the same, but the topleft point (which we used in part one) will be different for both.

Because of the addition of the basicSprite.Sprite we are going to have to change our snake sprite from part one, we are also going to move the sprite into it’s own file since it will start getting a bit complicated as time goes on. So we will add the file snakeSprite.py to our project. snakeSprite.py will contain a class called Snake, which will be based off of our basicSprite.Sprite class:

class Snake(basicSprite.Sprite):

Snake will initialize itself as follows:

def __init__(self, centerPoint, image):
    """initialize base class"""
    basicSprite.Sprite.__init__(self, centerPoint, image)
    """Initialize the number of pellets eaten"""
    self.pellets = 0
    """Set the number of Pixels to move each time"""
    self.x_dist = 3
    self.y_dist = 3 
    """Initialize how much we are moving"""
    self.xMove = 0
    self.yMove = 0

You’ll notice that the initialization is similar to the initialization in part one except this time we are using the basicSprite.Sprite class to set up our image and rect. Then we initialize the same values, except we have introduce two new ones, self.xMove and self.yMove. The reason for this is we are going to change the way that we move our snake.

If you tried out the last tutorial you’ll see that just moving the character on the key down events didn’t work all that well. It worked well enough but if you start pressing more then one key are the same time pyGame seemed to get a bit confused.

In order to combat this we will use another tip taken from the DevShed tutorial and pay attention to both the DOWN and UP events associated with keystrokes. We will also switch to using the update() function to update the snake.

So what we are going to is add two functions to the Snake class MoveKeyUp() and MoveKeyDown(). What these functions will do is adjust the xMove and yMove variables depending on what key has been pressed. Then when the update function is called the Snake will update it’s positions according the values of xMove and yMove. It may seem a bit complicated but it’s actually pretty simple:

def MoveKeyDown(self, key):
    """This function sets the xMove or yMove variables that will
    then move the snake when update() function is called.  The
    xMove and yMove values will be returned to normal when this 
    keys MoveKeyUp function is called."""
    
    if (key == K_RIGHT):
        self.xMove += self.x_dist
    elif (key == K_LEFT):
        self.xMove += -self.x_dist
    elif (key == K_UP):
        self.yMove += -self.y_dist
    elif (key == K_DOWN):
        self.yMove += self.y_dist
    
def MoveKeyUp(self, key):
    """This function resets the xMove or yMove variables that will
    then move the snake when update() function is called.  The
    xMove and yMove values will be returned to normal when this 
    keys MoveKeyUp function is called."""
    
    if (key == K_RIGHT):
        self.xMove += -self.x_dist
    elif (key == K_LEFT):
        self.xMove += self.x_dist
    elif (key == K_UP):
        self.yMove += self.y_dist
    elif (key == K_DOWN):
        self.yMove += -self.y_dist

As you can see when we press the K_LEFT key down we add -x_dist pixels to the xMove variable. This will then be used to move the snake in the update() function. Then when we let up the K_LEFT key we remove the -x_dist values that we added to xMove. For example if you hold down K_LEFT for a moment xMove will before -3 (move three pixels to the left every time the update() function is called), then when you let the K_LEFT key up xMove will become 0 (don’t move).

Then all we have to do is add the update function:

def update(self,block_group):
    """Called when the Snake sprit should update itself"""
    self.rect.move_ip(self.xMove,self.yMove)
    """IF we hit a block, don't move - reverse the movement"""
    if pygame.sprite.spritecollide(self, block_group, False):
        self.rect.move_ip(-self.xMove,-self.yMove)

Now the update() function is function in pyGames Sprite class. It really doesn’t do anything besides providing a convenient way to update a group of sprites. It will also pass the same parameters to all of your sprites in the group. So if you have a bunch of sprites that need to know the current time when you update them it’s a good idea to add them to the same group and then update them all at once by calling the group.update() function and pass the time.

You’ll also notice that there is a second parameter passed to this function called block_group, this is the group of all of our block sprites. We use that group to do a hit test. If the snake has collided with a block after we have adjusted it’s rect, we reverse the movement.

No we are going to go back to our PyManMain class and edit the LoadSprites() function. We are going to use this function to load the level that we created in level001.py and all of the sprites that we need:

def LoadSprites(self):
    """Load all of the sprites that we need"""
    """calculate the center point offset"""
    x_offset = (BLOCK_SIZE/2)
    y_offset = (BLOCK_SIZE/2)
    """Load the level"""        
    level1 = level001.level()
    layout = level1.getLayout()
    img_list = level1.getSprites()
    
    """Create the Pellet group"""
    self.pellet_sprites = pygame.sprite.Group()
    """Create the block group"""
    self.block_sprites = pygame.sprite.Group()
    
    for y in xrange(len(layout)):
        for x in xrange(len(layout[y])):
            """Get the center point for the rects"""
            centerPoint = [(x*BLOCK_SIZE)+x_offset,(y*BLOCK_SIZE+y_offset)]
            if layout[y][x]==level1.BLOCK:
                block = basicSprite.Sprite(centerPoint, img_list[level1.BLOCK])
                self.block_sprites.add(block)
            elif layout[y][x]==level1.SNAKE:
                self.snake = Snake(centerPoint,img_list[level1.SNAKE])
            elif layout[y][x]==level1.PELLET:
                pellet = basicSprite.Sprite(centerPoint, img_list[level1.PELLET])
                self.pellet_sprites.add(pellet)  
    """Create the Snake group"""            
    self.snake_sprites = pygame.sprite.RenderPlain((self.snake))

Since this function is a bit more complicated I will try to explain it in more detail. The first thing that we do is calculate the x_offset and the y_offset, using the value BLOCK_SIZE. BLOCK_SIZE is simple a global created in pyMan.py that refers to the size that each block will be. So in this case, BLOCK_SIZE = 24. The x_offset and y_offsets are the center points of a block. We use these offsets to position the sprites that we will create.

After that we load the level: we create an instance of the level001 class, and then get it’s layout and it’s image list. Then we create our pellet and block groups, these will be sprite groups that contain all the pellet or block sprites.

The next step is to loop through the layout of the level and create the necessary sprites at each position. We first calculate the current blocks center point, just to make the code a bit cleaner, and then we check to see what the current position in the layout is. It it’s a BLOCK position we create new block sprite and add it to the block_sprites group. If it’s a PELLET position, we create a new pellet sprite and add it to the pellet_sprites group.

The only sprite that we treat a bit differently is the snake sprite, since we will only have one snake per level, we only create one. And if the level designer mistakenly added two snakes, we will use the second snake as the snakes position.

You’ll also notice that both the pellet and the block sprites are simply basicSprites.Sprite’s since they don’t really do anything else besides get drawn to the screen.

Now we simply have to adjust our MainLoop() function to take into account all of the changes that we have made to our game:

def MainLoop(self):
        """This is the Main Loop of the Game"""
        
        """Load All of our Sprites"""
        self.LoadSprites();
        
        """Create the background"""
        self.background = pygame.Surface(self.screen.get_size())
        self.background = self.background.convert()
        self.background.fill((0,0,0))
        """Draw the blocks onto the background, since they only need to be 
        drawn once"""
        self.block_sprites.draw(self.background)
        
        while 1:
            for event in pygame.event.get():
                if event.type == pygame.QUIT: 
                    sys.exit()
                elif event.type == KEYDOWN:
                    if ((event.key == K_RIGHT)
                    or (event.key == K_LEFT)
                    or (event.key == K_UP)
                    or (event.key == K_DOWN)):
                        self.snake.MoveKeyDown(event.key)
                elif event.type == KEYUP:
                    if ((event.key == K_RIGHT)
                    or (event.key == K_LEFT)
                    or (event.key == K_UP)
                    or (event.key == K_DOWN)):
                        self.snake.MoveKeyUp(event.key)
            """Update the snake sprite"""        
            self.snake_sprites.update(self.block_sprites)
                        
            """Check for a snake collision/pellet collision"""
            lstCols = pygame.sprite.spritecollide(self.snake
                                                 , self.pellet_sprites
                                                 , True)
            """Update the amount of pellets eaten"""
            self.snake.pellets = self.snake.pellets + len(lstCols)
                        
            """Do the Drawing"""               
            self.screen.blit(self.background, (0, 0))     
            if pygame.font:
                font = pygame.font.Font(None, 36)
                text = font.render("Pellets %s" % self.snake.pellets
                                    , 1, (255, 0, 0))
                textpos = text.get_rect(centerx=self.background.get_width()/2)
                self.screen.blit(text, textpos)
               
            self.pellet_sprites.draw(self.screen)
            self.snake_sprites.draw(self.screen)
            
            pygame.display.flip()

It’s a lot of code, but it’s not really that different then the code was in part one. An obvious difference is the way in which we adjust the snakes position, using the MoveKeyUp, MoveKeyDown, and update function. Everything else is basically identical to the way that it was in part one.

The only other difference is the fact that we have to draw the block sprites. Since the blocks are static and will never be updated, we are going to draw then to the background surface. This will save us a bit of drawing time when we draw.

Python RSS ReaderWhen we run the code now we are greeted with the simple PyMan level that we saw at the beginning of this post. Now this isn’t close to being a finished game as there is no level two, and there are no bad guys, but our simple game it starting to take shape.

In the next part of this pyGame series I hope to add the bad guys to the level and the idea of the super pellets. If you want to download the code for this post you can do so here.

If you like this post remember to digg it

selsine

del.icio.us del.icio.us

23 Responses to “Creating a Game in Python Using PyGame – Part Two – Creating a level”

  1. guiga
    Says:

    in program:
    import sys
    from pygame import *
    init()

    size=width, height= 320,240
    speed=[2,2]
    black=0,0,0
    screen = display.set_mode(size)
    ball = image.load(“ball.bmp”)
    ballrect = ball.get_rect()

    while 1:
    for event in pygame.event.get():
    if event.type == pygame.QUIT:
    sys.exit()
    ballrect = ballrect.move(speed)
    if ballrect.left width:
    speed[0] = -speed[0]
    if ballrect.top height:
    speed[1] = -speed[1]
    screen.fill(black)
    screen.blit(ball, ballrect)
    display.flip()

    when I run this program, the following message apeears:
    Traceback (most recent call last):
    File “C:\Documents and Settings\GUILHERME\Desktop\game.py”, line 9, in ?
    ball = image.load(“ball.bmp”)
    error: Couldn’t open ball.bmp

    which directory should be the file “ball.bmp”?

  2. selsine

    selsine
    Says:

    Hi guiga,

    ball.bmp should be in the same directory as your game.py file.

  3. jonobacon@home » Raising the bar: Awesome Python and PyGTK tutorials
    Says:

    [...] It is a blog written by someone who wanted to learn Python, and as they learned, wrote articles about the many different subjects while learning. The site boasts a number of really high quality articles such as writing a WordPress offline blogging tool, writing custom widgets with PyGTK, building an application with Glade and creating a game with PyGame in three great parts. [...]

  4. nex
    Says:

    actually, it depends; on your system it could just as well be that the BMP is expected in the working directory from which you run the script. which is bad … that code is just wrong, overly simplistic. of course if you run it from a shell and first cd to where the file is, no problem. but it’s wrong to assume that this will always be the case.

  5. selsine

    selsine
    Says:

    Hi nex,

    You are very correct, there should be more work done to determine the location of the image file. Sadly this is an old tutorial and I was still learning python (and I still am) as I was writing it.

  6. Imperator
    Says:

    Hi,
    my IDLE can`t fin the modul levelBase.

  7. selsine

    selsine
    Says:

    Hi Imperator,

    Try writing your scripts into a .py file (located in the same directory as the levelBase.py file. That should solve your problems.

  8. McSweenus
    Says:

    Have you ever used ‘surfarray’? And have you ever made a program where the background scrolls under the character in the center? I’m still getting used to a lot of pygame functions and I’ve been trying to pull bits and pieces of ideas from your ‘pyman’ tutorials. My head hurts after a while, especially when I think my code should do something but it doesn’t.

    Do find that it works well for you to use separate files? I’m just not sure how values get shared between files because I’m not seeing a lot of return functions.

    I probably asked three questions in there. Hope this wasn’t too vague

    Thanks in advance

  9. selsine

    selsine
    Says:

    Hi McSweenus,

    I have not used ‘sufarray’ yet, and I’ve never made a side scrolling game yet. It’s something that I’ve wanted to do, but lately I just have not found the time.

    I like using separate files more and more now to keep my modules organized. You share values between files in the same way that you do between classes and modules, just pass them in.

  10. Warbucks
    Says:

    I really enjoyed your tutorial, I’ve been using python for a while, but am new to a lot of the add ons, such as pydev for eclipse.

    The reason i like your tutorial, is because its written at ground level, and straight to the point, without all of the mindless jargon, to show off.

    I was wondering if you ever considered going into more detail on using eclipse, because with that information, this tutorial would be a complete walk thru for us eclipse beginners.

  11. selsine

    selsine
    Says:

    Hi Warbucks,

    Thanks for the kind words. I would consider going into more depth on how to use Eclipse, except for the fact that I no longer use Eclipse! I liked Eclipse well enough, it’s just that I found it to be slow after a while.

    I’ve been doing most of my current programming on Linux and have started using Geany as my IDE.

    Thanks for the idea though I may look into it.

  12. Pymaniac
    Says:

    Question:

    using the layout code from above I designed a side scroller.(I modded the lewy to fit a side scroller)
    I have my player,
    and the layout,
    all I need for it all to come together is a gravity function.

    how would I write this?

    it needs to check for an empty square below the player and if so fall at speed but if there is some thing below the player(like the blocks) stop him.

  13. Bilbo
    Says:

    Hmmm, I get a CRC-error when I try to unzip this project.

  14. selsine

    selsine
    Says:

    Hi Bilbo,

    Thanks for the information I just tried this out on my system here and I am getting a CRC error as well. I think the contents of most of the project should be all right though. I’ll try to look for the original zip file.

  15. Trevor
    Says:

    i am having trouble with importing snake[or in my case player],in the main file
    from playerSprite import Player

    PS: i have changed all snake to player for my purpose

  16. -剣-
    Says:

    Hi, I compiled the game and ran it, but the snake was going too fast. I even made x_dist and y_dist to 1, but it was still moving very fast. When I had a look at your one I noticed there was no frame rate set on it.

  17. -剣-
    Says:

    Never mind about the previous problem I had, I fixed it with using pygame.time.wait(16) as the first code in the main loop.

  18. Pygame Tutorials and Pygame Assignment 1 « SCHS - Computer Engineering and Programming
    Says:

    [...] Part Two – Creating a Level [...]

  19. Jonathan
    Says:

    Why do you create a group for the snake sprite? It’s just one sprite?

  20. selsine

    selsine
    Says:

    You know it’s been a really long time since I wrote that code, but my gut may say that it’s just in case I wanted to add more snakes…or it just could be slightly wonky code…

  21. Anonymous
    Says:

    …[Trackback]…

    Additional po…

  22. pellet
    Says:

    pellet…

    [...]learning python » Blog Archive » Creating a Game in Python Using PyGame – Part Two – Creating a level[...]…

  23. Sunny
    Says:

    can you please help with the idea of creating a reflex action using pacman to eat all the food in the pellet and yet don.t get beaten by the ghost, say two ghost. I would need your assistance in python.

    Thanks sire!

Leave a Reply

 

Popular Posts