Game Saving, New Levels, and Pickles
In this article we will use the Python cPickle module to implement game saving and loading. We will also learn how to add new levels and locations to the game. The implementation of cPickle discussed in this article can be used in a wide variety of simple python games!
Download the entire Pygame5 folder from google drive for everything discussed in this article. That folder can be found here.
You will need the forestfront.jpg file found here in order to complete this article.
First we will add a start menu that will display before the game begins. This menu will give the user the option to start a new game or load a game.
Add the following startMenu function to your program:
(47 lines) codebit 1
The structure for this startMenu function is very similar to the inventoryMenu function we implemented in the Pygame3 article. For the details of menu making, go check out that article.
It is important to take note of a few features of this start menu.
In the if/else statement for K_SPACE if selectNew == True then we call the player.get_location method. This is used for the purpose of adding multiple locations or levels to the game. We will set up this method later in the article. Also, in the else of this if/else statement you will notice a try/except statement. This is used for the importGame function, which we will implement later in this article to load the game. If any errors occur in the loading process, the game simply shows a message telling the player to start a new game.
Everything else in this menu function is part of a basic Pygame game loop.
Now we need to add the necessary properties and methods to the Player class.
Open PlayerClasses.py and add self.location = None to the __init__ method:
(15 lines) codebit 2
Now add the following get_location and set_location methods:
(5 lines) codebit 3
These methods are how we set and get the location function without directly using player.location.
Finally, back at the bottom MainGame.py set the player starting location to level and change the starting game function from level to startMenu. To do this, type the following:
(4 lines) codebit 4
You should now be able to run the code and navigate the start menu and the start a new game. If you attempt to load a game the try/except statement will catch the missing method error and will ask you to start a new game.
Next let's add a pause menu. In MainGame.py, add the following pauseMenu function:
(48 lines) codebit 5
Once again this is a similar menu to the start and inventory menus. Refer to the inventory article for more details.
Now we need to implement the saveMenu and loadMenu functions called within this pauseMenu function. Add the following two menu functions:
(51 lines) codebit 6
After these two functions are added we just need to add a key event to our event loop to display the pause menu! In the if event.type==pygame.KEYDOWN statement inside the eventloop, add the following:
(4 lines) codebit 7
You should now be able to start the game, navigate the start menu, play the game, press escape to bring up the pause menu, and navigate the pause menu.
You will notice that if you try to save the game in the pause menu you get a game stopping error. We will fix this in the next section by implementing all of the saving, loading, and pickle functions!
In python a pickle is any object saved to a file using the pickle or cPickle modules.
In this article we will be using and discussing the cPickle module. cPickle and pickle both turn a Python object into a series of bytes that can be saved to a computer system. pickle does this using Python, and cPickle does this using the C programming language. cPickle is much faster than pickle but does not allow the user to subclass from pickle. cPickle uses the same algorithm as pickle and, for the purposes of this project, is superior to using pickle.
Begin by importing cPickle as pickle like so:
(1 line) codebit 8
Next let's set up and analyze our saveGame and saveObject functions. Globally add the saveGame and saveObject functions below the saveMenu function.
(12 lines) codebit 9
Running through the saveGame method, first we create an empty list variable called allNames. This variable will hold the string name of every sprite in the game (walls and items right now). Later we will add this name variable to every sprite.
Next we have the playerTraits tuple that holds every property we want to save about the player (x, y, inventory dictionary, and player location). The location of the player is used for when the player goes to a new location, or level, and will be demonstrated later in this article.
Finally we call the saveObject method twice. This function has two arguments. First is the variable to be saved, and second is a string that will be the save file name.
The saveObject function is what actually saves information to a file for later retrieval. The obj parameter is the object to be saved (pickled). According to the Python documentation, the following are all the datatypes that can be pickled:
This function simply opens a file with the title of the string in filename and makes it a writeable file ('w' for write and 'r' for read). We then use cPickle as pickle and use the dump method to save obj to the opened file (output). We use pickle with the HIGHEST_PROTOCOL property, more information about what this means can be found in the Python documentation.
Now let's add and discuss the import functions. Globally add these three functions:
(21 lines) codebit 10
Let's begin by examining the importGame function. This function begins by importing all sprite names with the importNames function. It then uses a for loop to test if sprite names are in the allNames list. If they aren't the sprite is killed.
Next all of the player traits are loaded using the importPlayer function. Then the Player set methods are used to match the game player properties to the loaded traits. The game begins by calling the player's current location function.
The importPlayer and importNames functions are basically identical. In fact, only one importObject function is necessary here but I use these two to avoid confusion.
This function simply opens a file with the title of the string in filename and makes it a readable file ('w' for write and 'r' for read). Then the object saved in the file is loaded with pickle.load and returned.
Now we need to update all of the sprite class definitions to implement the methods called by our load and save functions.
For the Item class, adjust the __init__ method and add the method as follows:
(12 lines) codebit 11
For the Wall class, adjust the __init__ method and add the method as follows:
(11 lines) codebit 12
For the Player class, add the following methods:
(22 lines) codebit 13
Game saving is now ready for use we just need to assign unique string names to every sprite and create a sprite group of all our game sprites. Delete all previously created sprites and sprite groups (except the player) for our level and add the following:
(14 lines) codebit 14
This code creates some item and wall sprites with unique names and adds them to their appropriate sprite groups. All of the sprite groups are added to the all_sprites group for game saving and loading.
Adjust the code inside the level game loop to use levelitems and levelwalls like this:
(16 lines) codebit 15
You can now run the game and test out the saving and loading features we have added! If you receive any errors with loading or saving, make sure your files are being saved inside the same directory as MainGame.py. Also, read over the pickle documentation.
Feel free to add items and walls! Be sure every sprite has a unique name otherwise things may not load correctly.
First copy and delete the global background variable. Then paste it inside the level function but before the game loop. Also set the player's location as follows:
(5 lines) codebit 16
Now let's add a test to see if the player is near the left side of the screen. If this is true the player will go to another location. Add the following after the event loop but inside the game loop:
(3 lines) codebit 17
Now we just need to add level2. It will be very similar to the level function so I am not explaining it in this article. If making this level is confusing refer to the previous articles or leave a comment for help! To create level2, add the following block of code to MainGame.py:
(55 lines) codebit 18
level2 is very similar to the level function with just a different background, sprite lists, and level entrance. In this location you can approach the right side of the screen to return to the other location.
Be sure to download forestfront.jpg and save it in the backgrounds folder! You can download the image here.
Run the game and enjoy all of the freedom to save, load, and create new game locations!!
Please share location and game ideas in the comments! I would like to know what people are doing in their own games :)
Download the entire Pygame5 folder from google drive for everything discussed in this article. That folder can be found here.
You will need the forestfront.jpg file found here in order to complete this article.
Game Start and Pause Menus
Welcome back! I know that everyone is reading this to find out about game saving and loading, but before we get to that exciting stuff let's add a few more game menus to improve the user experience.First we will add a start menu that will display before the game begins. This menu will give the user the option to start a new game or load a game.
Add the following startMenu function to your program:
(47 lines) codebit 1
def startMenu(): background = pygame.image.load("backgrounds/forest.jpg").convert() selectNew = True loadingFail = False xcoord = 212 Titletext = inventoryFont.render('Welcome to your adventure game!', True, BLACK) while True: ycoord = 150 DISPLAYSURF.blit(background, (0,0)) DISPLAYSURF.blit(scroll, (scrollX,scrollY)) for event in pygame.event.get(): if (event.type==pygame.QUIT): pygame.quit() sys.exit() if (event.type==pygame.KEYDOWN): if (event.key==pygame.K_w): selectNew = True if (event.key==pygame.K_s): selectNew = False if (event.key==K_SPACE): if selectNew == True: player.get_location()() else: try: importGame() except: loadingFail = True selectNew = True print('Make sure the save files are saved as ' + 'c:\GuyGameSpriteSave and c:\GuyGamePlayerSave') if selectNew == True: Newtext = selectFont.render('New Game!', True, BLACK) Loadtext = inventoryFont.render('Load Game!', True, BLACK) else: Newtext = inventoryFont.render('New Game!', True, BLACK) Loadtext = selectFont.render('Load Game!', True, BLACK) if loadingFail: Loadtext = inventoryFont.render('Save files not found. Try a new game.', True, BLACK) selectNew = True DISPLAYSURF.blit(Titletext, (xcoord,ycoord)) ycoord += 50 DISPLAYSURF.blit(Newtext, (xcoord,ycoord)) ycoord += 50 DISPLAYSURF.blit(Loadtext, (xcoord,ycoord)) FPSCLOCK.tick(FPS) pygame.display.update()
It is important to take note of a few features of this start menu.
In the if/else statement for K_SPACE if selectNew == True then we call the player.get_location method. This is used for the purpose of adding multiple locations or levels to the game. We will set up this method later in the article. Also, in the else of this if/else statement you will notice a try/except statement. This is used for the importGame function, which we will implement later in this article to load the game. If any errors occur in the loading process, the game simply shows a message telling the player to start a new game.
Everything else in this menu function is part of a basic Pygame game loop.
Now we need to add the necessary properties and methods to the Player class.
Open PlayerClasses.py and add self.location = None to the __init__ method:
(15 lines) codebit 2
def __init__(self, x, y, images): pygame.sprite.Sprite.__init__(self) self.load_images(images) self.image = self.i0 self.imageNum = 0 self.timeTarget = 7 self.timeNum = 0 self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.left = False self.down = False self.moving = False self.inventory = {} self.location = None #--add this--
(5 lines) codebit 3
def get_location(self): return self.location def set_location(self, location): self.location = location
Finally, back at the bottom MainGame.py set the player starting location to level and change the starting game function from level to startMenu. To do this, type the following:
(4 lines) codebit 4
#start location--------------- player.set_location(level) #start game-------------- startMenu()
Next let's add a pause menu. In MainGame.py, add the following pauseMenu function:
(48 lines) codebit 5
def pauseMenu(): inMenu = True xcoord = 212 selection = 0 Titletext = inventoryFont.render('Paused -- Press escape to return to the game.', True, BLACK) menuTexts = ['Save Game', 'Load Game', 'Exit Game'] while inMenu: ycoord = 150 for event in pygame.event.get(): if (event.type==pygame.QUIT): pygame.quit() sys.exit() if (event.type==pygame.KEYDOWN): if (event.key==K_w): if selection <= 0: selection = 2 else: selection -= 1 if (event.key==K_s): if selection >= 2: selection = 0 else: selection += 1 if (event.key==K_SPACE): inMenu = False if (event.key==K_ESCAPE): return DISPLAYSURF.blit(scroll, (scrollX,scrollY)) DISPLAYSURF.blit(Titletext, (xcoord,ycoord)) ycoord += 50 for text in menuTexts: if menuTexts.index(text) == selection: Selecttext = selectFont.render(text, True, BLACK) DISPLAYSURF.blit(Selecttext, (xcoord,ycoord)) ycoord += 50 else: Notext = inventoryFont.render(text, True, BLACK) DISPLAYSURF.blit(Notext, (xcoord,ycoord)) ycoord += 50 FPSCLOCK.tick(FPS) pygame.display.update() if selection == 0: saveMenu() elif selection == 1: loadMenu() else: pygame.quit() sys.exit()
Now we need to implement the saveMenu and loadMenu functions called within this pauseMenu function. Add the following two menu functions:
(51 lines) codebit 6
def loadMenu(): Loadtext = inventoryFont.render("Restart the game and choose 'load game' at the start menu.", True, BLACK) Loadtext2 = inventoryFont.render("Press esc to return to the game.", True, BLACK) while True: for event in pygame.event.get(): if (event.type==pygame.QUIT): pygame.quit() sys.exit() if (event.type==pygame.KEYDOWN): if (event.key==K_ESCAPE): return DISPLAYSURF.blit(scroll, (scrollX,scrollY)) DISPLAYSURF.blit(Loadtext, (170,150)) DISPLAYSURF.blit(Loadtext2, (170,200)) FPSCLOCK.tick(FPS) pygame.display.update() def saveMenu(): inMenu = True save = False xcoord = 212 Titletext = inventoryFont.render('Save the game?', True, BLACK) while inMenu: ycoord = 150 for event in pygame.event.get(): if (event.type==pygame.QUIT): pygame.quit() sys.exit() if (event.type==pygame.KEYDOWN): if (event.key==K_w): save = True if (event.key==K_s): save = False if (event.key==K_SPACE): inMenu = False if save == True: Yestext = selectFont.render('Yes!', True, BLACK) Notext = inventoryFont.render('No!', True, BLACK) else: Yestext = inventoryFont.render('Yes!', True, BLACK) Notext = selectFont.render('No!', True, BLACK) DISPLAYSURF.blit(scroll, (scrollX,scrollY)) DISPLAYSURF.blit(Titletext, (xcoord,ycoord)) ycoord += 50 DISPLAYSURF.blit(Yestext, (xcoord,ycoord)) ycoord += 50 DISPLAYSURF.blit(Notext, (xcoord,ycoord)) FPSCLOCK.tick(FPS) pygame.display.update() if save == True: saveGame()
(4 lines) codebit 7
if (event.key==K_ESCAPE): moveX=0 moveY=0 pauseMenu()
You will notice that if you try to save the game in the pause menu you get a game stopping error. We will fix this in the next section by implementing all of the saving, loading, and pickle functions!
Saving and Loading the Game
Pickles...
In this article we will be using and discussing the cPickle module. cPickle and pickle both turn a Python object into a series of bytes that can be saved to a computer system. pickle does this using Python, and cPickle does this using the C programming language. cPickle is much faster than pickle but does not allow the user to subclass from pickle. cPickle uses the same algorithm as pickle and, for the purposes of this project, is superior to using pickle.
Begin by importing cPickle as pickle like so:
(1 line) codebit 8
import cPickle as pickle
(12 lines) codebit 9
def saveGame(): allNames = [] for sprite in all_sprites: allNames.append(sprite.get_name()) playerTraits = (player.get_x(),player.get_y(), player.get_inventory(),player.get_location()) saveObject(playerTraits, 'learnpygame_player_save.obj') saveObject(allNames, 'learnpygame_sprite_save.obj') def saveObject(obj, filename): with open(filename, 'w') as output: pickle.dump(obj, output, pickle.HIGHEST_PROTOCOL)
Next we have the playerTraits tuple that holds every property we want to save about the player (x, y, inventory dictionary, and player location). The location of the player is used for when the player goes to a new location, or level, and will be demonstrated later in this article.
Finally we call the saveObject method twice. This function has two arguments. First is the variable to be saved, and second is a string that will be the save file name.
The saveObject function is what actually saves information to a file for later retrieval. The obj parameter is the object to be saved (pickled). According to the Python documentation, the following are all the datatypes that can be pickled:
- None, True, and False
- integers, long integers, floating point numbers, complex numbers
- normal and Unicode strings
- tuples, lists, sets, and dictionaries containing only picklable objects
- functions defined at the top level of a module
- built-in functions defined at the top level of a module
- classes that are defined at the top level of a module
- instances of such classes whose __dict__ or the result of calling __getstate__() is picklable.
This function simply opens a file with the title of the string in filename and makes it a writeable file ('w' for write and 'r' for read). We then use cPickle as pickle and use the dump method to save obj to the opened file (output). We use pickle with the HIGHEST_PROTOCOL property, more information about what this means can be found in the Python documentation.
Now let's add and discuss the import functions. Globally add these three functions:
(21 lines) codebit 10
def importGame(): allNames = importNames('learnpygame_sprite_save.obj') for sprite in all_sprites: if sprite.get_name() not in allNames: sprite.kill() playerTraits = importPlayer('learnpygame_player_save.obj') player.set_rect(playerTraits[0],playerTraits[1], player.get_width(),player.get_height()) player.set_inventory(playerTraits[2]) player.set_location(playerTraits[3]) player.get_location()() def importPlayer(filename): with open(filename, 'r') as input: playerTraits = pickle.load(input) return playerTraits def importNames(filename): with open(filename, 'r') as input: allNames = pickle.load(input) return allNames
Next all of the player traits are loaded using the importPlayer function. Then the Player set methods are used to match the game player properties to the loaded traits. The game begins by calling the player's current location function.
The importPlayer and importNames functions are basically identical. In fact, only one importObject function is necessary here but I use these two to avoid confusion.
This function simply opens a file with the title of the string in filename and makes it a readable file ('w' for write and 'r' for read). Then the object saved in the file is loaded with pickle.load and returned.
Now we need to update all of the sprite class definitions to implement the methods called by our load and save functions.
For the Item class, adjust the __init__ method and add the method as follows:
(12 lines) codebit 11
def __init__(self, name, x, y, itemType, image, sound=None):#-adjust this pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(image).convert_alpha() self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.name = name#--------add this self.itemType = itemType self.load_sound(sound) def get_name(self):#--------add this return self.name
(11 lines) codebit 12
def __init__(self, name, x, y, width, height):#-adjust this pygame.sprite.Sprite.__init__(self) self.image = pygame.Surface([width, height]) self.image.convert_alpha() self.rect = self.image.get_rect() self.rect.y = y self.rect.x = x self.name = name#--------add this def get_name(self):#--------add this return self.name
(22 lines) codebit 13
def get_width(self): return self.rect.w def get_height(self): return self.rect.h def get_x(self): return self.rect.x def get_y(self): return self.rect.y def set_rect(self, newx, newy, newwidth, newheight): self.oldx = self.rect.x self.oldy = self.rect.y self.rect.x = newx self.rect.y = newy self.rect.width = newwidth self.rect.height = newheight def set_inventory(self, inventory): self.inventory = inventory
(14 lines) codebit 14
all_sprites = pygame.sprite.Group() levelitems = pygame.sprite.Group() coin = Item('c1',500,600,{'coin':1},coinImg,coinSound) coin2 = Item('c2',220,530,{'coin':1},coinImg,coinSound) mapitem = Item('themap',900,500,{'map':1},mapImg) levelitems.add(coin, coin2, mapitem) levelwalls = pygame.sprite.Group() wall = Wall('w1',0,450,1000,10) wall1 = Wall('w2',0,683,1000,10) wall3 = Wall('w3',1020,450,5,300) levelwalls.add(wall, wall1, wall3) all_sprites.add(levelwalls,levelitems)
Adjust the code inside the level game loop to use levelitems and levelwalls like this:
(16 lines) codebit 15
DISPLAYSURF.blit(background, (0,0)) player.update(moveX,moveY,levelitems,levelwalls)#---adjust this player.draw(DISPLAYSURF) levelitems.draw(DISPLAYSURF)#---adjust this FPSCLOCK.tick(FPS) pygame.display.update()
Feel free to add items and walls! Be sure every sprite has a unique name otherwise things may not load correctly.
Adding More Levels
A game usually isn't fun if it doesn't have multiple locations or levels! Let's quickly go over how to add new locations to our game.
(5 lines) codebit 16
def level(): moveX,moveY=0,0 player.set_location(level) background = pygame.image.load("backgrounds/forest.jpg").convert() while True:#Game Loop ...
(3 lines) codebit 17
if player.get_x() < 5: player.set_rect(DISPLAYWIDTH-100, player.get_y(), player.get_width(), player.get_height()) level2()
(55 lines) codebit 18
level2items = pygame.sprite.Group() coin = Item('c3',350,575,{'coin':1},coinImg,coinSound) level2items.add(coin) level2walls = pygame.sprite.Group() wall = Wall('w4',0,450,1000,10) wall1 = Wall('w5',0,683,1000,10) wall2 = Wall('w6',0,450,5,300) level2walls.add(wall, wall1, wall2) all_sprites.add(level2walls,level2items) def level2(): moveX,moveY=0,0 player.set_location(level2) background = pygame.image.load("backgrounds/forestfront.jpg").convert() while True: for event in pygame.event.get(): if event.type==pygame.QUIT: pygame.quit() sys.exit() if event.type==pygame.KEYDOWN: if event.key==pygame.K_a: moveX = -5 if event.key==pygame.K_d: moveX = 5 if event.key==pygame.K_w: moveY = -5 if event.key==pygame.K_s: moveY = 5 if event.key==pygame.K_e: moveX = 0 moveY = 0 inventoryMenu() if (event.key==K_ESCAPE): moveX=0 moveY=0 pauseMenu() if event.type==pygame.KEYUP: if event.key==pygame.K_a: moveX=0 if event.key==pygame.K_d: moveX=0 if event.key==pygame.K_w: moveY=0 if event.key==pygame.K_s: moveY=0 if player.get_x() > DISPLAYWIDTH-90: player.set_rect(10, player.get_y(), player.get_width(), player.get_height()) level() DISPLAYSURF.blit(background, (0,0)) player.update(moveX,moveY,level2items,level2walls) player.draw(DISPLAYSURF) level2items.draw(DISPLAYSURF) FPSCLOCK.tick(FPS) pygame.display.update()
Be sure to download forestfront.jpg and save it in the backgrounds folder! You can download the image here.
Run the game and enjoy all of the freedom to save, load, and create new game locations!!
Please share location and game ideas in the comments! I would like to know what people are doing in their own games :)
- If you just can't wait for more Python/Pygame knowledge, check out some of these awesome books!
- Game Development with Python
- Program Arcade Games: With Python and Pygame
- Python: Learn Python in One Day and Learn It Well. Python for Beginners with Hands-on Project. (Learn Coding Fast with Hands-On Project Book 1)
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.