BGE: Saved game directories

I have recently noticed that one of my posts on saving/loading was linked to as part of an answer to a question on the Blender Stack Exchange. Since the question was related to saving files in particular directories it highlighted a gap in this tutorial series: where do saved files go? How can we control where they get saved? How do we find them again? Since we’ve been talking about file I/O it makes sense to talk about directories. So, lets start with an experiment.

A quick experiment

with open('experiment.txt', 'w') as myFile:
    myFile.write("I am a blank file")
  1. Open up Blender. Attach the above script to the default cube (it should look familiar now) and use an always logic brick to set it to run once. Then press ‘P’ and run the game.
  2. Ok, now save the quick experiment we’ve just created somewhere and exit Blender. Then, go to where you’ve just saved our little experiment and open it by clicking on the file. Then run the game again.
  3. Finally, exit Blender again. This time launch Blender as you normally would and load our experiment file though file->recent projects, or file->open. And, you guessed it, run the game one final time.

We know that when we try and open a file in python that doesn’t exist it creates them for us. So there should be three text files somewhere on your computer. So where are they?

Current working directories and permissions

This daft experiment was designed to highlight the concept of the current working directory (cwd). This describes the location within a hierarchical file system that a process is running from. This directory will then be used as a relative reference point for creating and loading files and directory navigation.

In the first test you’ll get a console error: PermissionError [Errno 13] Permission denied, and no file will be created. This is because running Blender from a system directory (which happens when you just open Blender) meaning that the cwd is is also one (C:\Program Files\Blender Foundation\Blender for me). Windows will try to prevent a program from writing to protected system directories without the appropriate permissions. If you were to run Blender.exe as an administator then the script would run fine and experiment.txt would be created within \Blender Foundation\Blender\. The third experiment will encounter the same problems.

In the second test you’ll find that there are no problems and experiment.txt is created within the folder that the .blend is saved. This time, because we’ve opened Blender from within a local directory it’s cwd reflects this (C:\Users\Jay\Documents\Blender\directories in my case), and there’s no problems with permissions. Unless you saved your .blend within a protected directory!

So we need to think about where saved game files go and consider where the user might be running our games from and with what permissions. We’ll start by looking at how we can find where the game’s being run from, the cwd.

The BGE’s python API has it’s own methods for getting the cwd:

import bge

currentDir = bge.logic.expandPath("//")
print(currentDir)

We can use bge.logic.expandPath() which takes a string and converts it into a system file path. We can use this for navigating around directory structures. In this example, by passing “//”, which indicates a relative directory, we get a directory relative to the one we’re current working in. Since there is nothing else supplied after that, we get the cwd.

Python comes with its own handy module to dealing with operating systems and directory structures. We can enquire about the cwd by using this module

import os
print(os.getcwd())

The os module also provides us with a method to test access permissions:

import os

currentDir = os.getcwd()

if os.access(currentDir, os.W_OK):
    myFile = open('experiment.txt', 'w')
    myFile.write("I am a blank file")
    myFile.close()

os.access() takes 2 arguments: the first is the directory to be tested (in this example the cwd) and whether we are testing for read or write access (os.R_OK and os.W_OK respectively). It returns true/false accordingly.

It is more pythonic to ask for forgiveness than permission. Therefore, rather than check access you should wrap attempts to write/read a file within a try/except block and deal with the error accordingly. We could even use the specific access error as an indicator that we don’t have permission to write there. check out my other tutorials on saving/loading in the Blender section to see try/except in action.

Paths in python

Python, like many languages, was developed on Unix systems, which has led to features/quirks that make sense to Linux users, but will throw Windows users. File paths are one of these. Consider this example:

path = 'C:\Users\Jay\Documents\Blender\Directories Tutorial\Saves\saves.txt'        
with open(path) as saves:
    for line in saves:
        print(line)

Which will generate some error message even though the file path does exist and is correct. This is because back slashes are escape characters in python strings, they tell the interpreter that the character following the back slash has a special meaning (like “\n” for new line). Which makes sense for Unix-based programmers, as forward slashes are used as separators in file paths, whereas Windows uses back slashes. So to use paths in a windows system, we need to escape the escape characters:

path = "C:\\Users\\Jay\\Documents\\Blender\\Directories Tutorial\\Saves\\saves.txt"

Although Windows conventionally uses back slashes, it actually doesn’t care which way you use, so you could just forward slashes. Or, better yet, we can let python take care of our paths and use the appropriate path structures without us having to worry about each operating system’s preference. There’s two ways we can do this.

cwd = os.getcwd()
savesFolder = "Saves"
saveFile = "saves.txt"
path = os.path.join(cwd, savesFolder, saveFile)

os.path.join() allows use to join together a path from strings in a manner that is acceptable to the underlying operating system. Notice that the folder name “Saves” does not include any slash or separator, os.path.join() will add this for us. You could include them if you wanted, and os.path.join() would handle this as well, but it’s better to just let python do all the work here. Additionally, this avoids us having to hardcode paths, so if you change our file organisation in the future it becomes easier to update your scripts. The other way, is to use Blender’s built-in method:

savesFolder = "Saves/"
saveFile = "saves.txt"
path = bge.logic.expandPath("//" + savesFolder + saveFile)
     
with open(path) as saves:
    for line in saves:
        print(line)

This time around we need to include the separator in our folder names, as bge.logic.expandPath() will not add them for us. However, what it will do is convert foward slashes into the local system’s expected format. It is worth noting that bge.logic.expandPath() expects path separators to be forward slashes.

Ultimately, it doesn’t matter too much what approach you use, so long as you avoid single back slashes in your file paths.

Creating directories

So we know we’re running from somewhere in the hard drive, now we want to place all our save data in it’s own folder and keep things nice and tidy.

import os

currentDir = os.getcwd()

if not os.path.exists("saves"):
    os.mkdir('saves')    
with ('saves/experiment.txt', 'w') as myFile:
    myFile.write("I am a blank file")

If we try and create a directory that already exists we’ll get an error. So we can use os.path.exists() to check if the directory we’re going to create exists or not. Usefully, os.path.exists() can also be used to check that a file exist. We create the new folder using os.mkdir() and supplying the the folder name. Notice that we’re not supplying a full path to os.path.exists() or os.mkdir(), this is because any new folders will be created within the cwd.

If we want nested folder structures we can use os.makedirs(“some/folder/structure”). This functions like a recursive os.mkdir(). Additionally, from python 3.2+ it has another handy parameter exist_ok. Therefore, we could do:

import os

currentDir = os.getcwd()

os.makedirs("saves", exist_ok=True)
with open('saves/saves.txt', 'w') as myFile:
    myFile.write("I am a test file")

This will create the folder ‘saves’ if it doesn’t exist, and wont cause an error if it does.

So, to come back to the question from the Blender Stack Exchange: how can we use what we’ve covered to control where saved game files go? We can’t be sure where the user is going to run the game from. We can’t always guarantee permissions. When we release updates for our Blender games it would be nice if players can continue with their old saves, therefore having a saves file within the game folder my not be ideal.

What we need is a place that we can be pretty certain exists, outside of our game directory structure that we can find from any place the game might be run from that we are likely to have permissions to write and read from: the users home directory. And python provides us with a cross-platform way to access this:

os.path.expanduser('~')

This replaces the tide character with various environment variables to return the user’s local directory. On Windows, this will be something like C:\Users\YourUserName. You can add other paths after the tide character, like os.path.expanduser(‘~/BlenderSavedGames’), but all this will do is return a viable path name, not create it. Printing the output of that I get: C:\Users\YourUserName/BlenderSavedGames. Notice the mix of forward and back slashes in the output. While this is not catastrophic, it should be avoided. So we can use os.path.expanduser() to get a home directory, then construct a more consistent path using the methods we’ve previously discussed:

import os

userHome = os.path.expanduser('~')
gameProfileDir = "MyBlenderGame"
savedGamesDir = "Saves"
saveFile = "saves.txt"

path = os.path.join(userHome, gameProfileDir, savedGamesDir)

os.makedirs(path, exist_ok=True)
    
with open(os.path.join(path, saveFile), 'w') as myFile:
    myFile.write("I am a test")

This creates a game profile folder in the user’s home directory, and within that folder a saves folder. We could then, if needed, create folders within that game profile folder for config files, user added content and so forth.

A cautionary note

Since this tutorial is about the things I have missed so far in python file I/O series it seems fitting to end this part with some of the common pitfalls and dangers that can occur when messing around with files. As we’ve already seen, most operating systems will try and prevent an application from poking around in places that it shouldn’t, but this isn’t foolproof. So here are some possible dangers, as well as some pitfalls in file I/O:

  • When opening a file in write mode,remember, that if the file doesn’t exist it will create it, and if it does exist it will simply overwrite the old contents. This makes it possible to lose data or corrupt a file. While it is unlikely that you’ll be able to write over the top of some important system file, it’s plausible that you could overwrite your game. So make sure you’re opening the right file!
  • When creating/checking the existence of files/folders it is possible to get race conditions (for example, if a file/folder is created between checking its existence and creating it, it will raise an error). Therefore, keep your file manipulations in one place rather than lots of different scripts trying to create/edit/read the same file.
  • You should probably avoid using ‘file’ as a variable name. In earlier versions of python (2.x) this can be used as a synonym for open(). This was removed in python 3.x but it is still an object type, and can be used in type testing. While it is not massively problematic, there is the potential for hard to spot errors/bugs to be created
  • Watch out for backslashes in your file paths as these are escape characters. Using raw strings won’t solve this either (ie: path = r’C:\Users\Jay\Documents\Blender\Directories Tutorial\Saves\saves.txt’)
    . Let os.path.join() or bge.logic.expandPath() deal with paths for you instead.
  • Take care when using os.makedirs() as it can create unintended folders if you miss a path separator, the folder’s name contains a path separator character, or if the cwd is not the one you intended. This will not raise an error, but create bugs later down the line.
  • Checking for read/write access does not guarantee that you can perform an operation, there may be more complex security operations in place that os.access() does not check. It’s better to use try/except/else blocks, rather than checking access.

Final remarks

The os module provides us with a powerful, cross-platform way for constructing and navigating directory structures required for our games. Using this we can control where saved game files go so we can reliably write to the player’s disk and retrieve it again. However, care should be taken when engaging in any file I/O. Perhaps the biggest danger here is typos with unintended consequences. Normally, my tutorials would end with a simple, but since most of the examples here are short snippets it’s hard to build a concept around it. I’d suggest grabbing a copy of the Duck Duck Moose game from previous tutorials and expanding the save/load functions with what we’ve covered today.

As always, leave any questions, comments thoughts (or errors) in the comments below.

Advertisements

~ by Jay on January 3, 2015.

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: