BGE: Saving and loading with python’s built-in file handling

In any game you’ll probably want to save and load data from somewhere other than a .blend. This could be anything from something as simple as scores through to the state and layout of complete worlds. Python provides us with many ways to save, load and manipulate external data. This tutorial will start at the beginning and we’ll look at how we can used python’s built-in file handling to read and save data from a text file. We’ll use this to save and load a high score and take this idea further to see it being used to save and load a number of high scores in a game context.

Setting the scene

We’ll start by getting our .blend file ready. For the first example, we’ll start simple: Create a text object and give it a text property. Then give it some simple logic by attaching an always sensor to a python controller. For now, we’ll leave the python file blank. Once that’s done, save and close Blender

Now, create a new text file using notepad or you’re favourite text editor (don’t use word, we’re after a simple .txt file) and fill it with this:

100
3456
7653
593
2
5069
56

Save this in the same directory as the .blend file and call it ‘score’. Then we’re ready to get stuck in to writting some python, so open up your .blend again and create a new python script and select it in the python controller we created earlier.

 Reading from a file

We’ll start with some simple python code:

import bge

cont = bge.logic.getCurrentController()
own = cont.owner
scene = bge.logic.getCurrentScene()

text = scene.objects['Text']

myFile = open('score.txt', 'r')
currentHigh = 0
for line in myFile:
    if int(line) > currentHigh:
        currentHigh = int(line)
text['Text'] = currentHigh

myFile.close()

We access our file by calling .open(), which takes 2 arguments and returns a file object. The first argument is the file name, the second is the mode to open the file in. In our example we pass ‘r’ to indicate read mode. This means we can load the data from the file but are unable to write to it.

There other ways to read data in file object with python, but the most sensible is by using a for loop. This way to don’t have to load the entire file into memory or worry about our position within a file. We can loop through lines in a file just like any other list or iterable object. Each line is a string, therefore we need to use the built-in method int() to convert the returned string to an integer so that we can do something with it. Here, we’re comparing values to determine which is the highest saved score for display.

The final thing we do when using file objects is close the file after reading to free up system resources. Any attempt to access the file object now will result in an error. We can use the myFile.closed attribute to check if a file object has been closed.

Saving to our file

To write to a file we need to open the file in write mode:

myFile = open('score.txt', 'w')

A useful feature of write mode is that if the file does not exist python will create it for us. We can then write to the file using the .write() method. This takes a string, so we would need to convert any numbers before writting:

score = str(1000)
myFile.write(score)

Now, if we run this code you’ll find that all the other scores stored in the file have been replaced. This is because write mode overwrites anything already existing in the file. To prevent this we can open the file in append mode by passing ‘a’ to .open() instead of ‘w’. Then, when we use .write() the string will be added to the end of the file.

When we’ve got a file open in write mode it is not possible to preform read operations, and vice-versa. Trying to do so will just cause an error. If both are needed within the same setting then add the ‘+’ to the file open mode (ie. ‘r+’ or ‘a+’). However, reading and writing a file at the same time can be trickier than you might think (though I’m not sure why you would do this). Lets move on to take a look at saving/loading in practice

Duck, Duck, Moose

Have a look at the game (see resources below). There’s a lot more going on here to make the game work, so lets just separate out the loading and saving code

Loading (attached to the high score list text):

import bge

cont = bge.logic.getCurrentController()
own = cont.owner

try:
    with open('scores.txt', 'r') as savedScores:
        bge.scores = [int(line) for line in savedScores]
except FileNotFoundError:
    print("file not found.")
    bge.scores = []

bge.scores.sort(reverse=True)

text = "High Scores: \n"

for i in range(10):
    text += str(i+1) + ") "
    if i < len(bge.scores):
        text += str(bge.scores[i])
    text += "\n"

own['Text'] = text

The loading code my look a little different to what we’ve already covered, but the same things are going on. The opening of the file is in a try/expect block. Python will try to open in the file and if an error occurs it’ll do something else. We’ve specified what error we’re looking for so that other errors do not get suppressed. We can deal with the file being missing, but need to know if something else is going wrong.

The with statement is used to wrap a section of code with methods specified by the context manager. It does some pretty useful things, but for this setting all you need to know is that it closes the file for us when we’re finished with it so we don’t have to. A particularly useful feature is that it’ll close the file if an exception occurs, so we don’t have to worry about something going wrong and the file never being closed.

We’re still looping through the list of scores and converting them into a list of integers like before. However, instead of a for loop we’re using list comprehension. Although the script will change the scores back to a string later, we need them as integers so we can order them using the .sort() method. The list is stored in a global variable so that the saving script can access the scores. The rest of the code is formats the scores into a string for display in the game.

Saving (attached to the clock text)

import bge

scene = bge.logic.getCurrentScene()

playerScore = scene.objects['ScoreText']['score']

bge.scores.append(playerScore)
bge.scores.sort(reverse=True)
toSave = [str(i) for i in bge.scores[:10]]
toSave = "\n".join(toSave)

try:
    with open('scores.txt', 'w') as savedScores:
        savedScores.write(toSave)
except IOError:
    print('Could not save scores')

Here, we get the players scores and add it to our list of scores. We convert it back to a set of strings for saving. The conversion is done using list comprehension again, but this time we’ve specified a slice of the list since we only need to save the first 10 top scores. Because we’re only taking the first 10 scores the list needs to be sorted again prior to the conversion so new scores do not get missed. Line 10 is used to join each score in the list to a new line, otherwise it would all be saved as one line, which wouldn’t load properly later.

Because we previously stored all the scores in a global variable we can open the save file using write mode and overwrite any existing data in there. This means we don’t need to worry about appending to the file, sneaky blank lines creeping in, or the there ever being more than 10 scores in there.

Final words

The built in I/O is great if you’re just loading one value, or a number of values of the same type for the same purpose. But what it you wanted to save a player’s state, position, current inventory items and stats? How do you think this code would look when you can’t predict the order of values in the text file and they’re of varying data types? What if you want to save a more complex data type like a list? You’ll find that the code would quickly get unwieldy. Luckily python has a number of modules that can do a better job. Check out the saving/loading section of my Blender page for other ways.

As always, if you’ve got any questions or comments (or spotted any errors!), leave them below.

Tutorial Resources

 Duck Duck Moose game .blend

Advertisements

~ by Jay on May 3, 2014.

One Response to “BGE: Saving and loading with python’s built-in file handling”

  1. […] The config parser does not write the changes to disk for us. We need to explicitly do that. The configparser has a .write() method that takes the file object to be written to as its parameter. This section of script behaves just as we have seen using built-in IO methods. […]

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

 
%d bloggers like this: