Saving and loading game data
In the final topic for the game structure chapter, we're going to set up a class that can be used in our project to manage data and settings. The more obvious game data we must save should include character stats, high scores, and other various data we may have included in our game. We should also keep track of certain options a game might have, such as whether the user has sounds muted or not, gore effects, and more. In this recipe, we're going to work with a class called SharedPreferences
, which will allow us to easily store data to a device for retrieval at a later time.
Note
The SharedPreferences
class is a great way to quickly store and retrieve primitive datatypes. However, as the data size increases, so will the needs of the method we use to store data. If our games do require a large amount of data to be stored, something to consider is to take a look into SQLite databases for data storage.
Getting ready
Please refer to the class named UserData
in the code bundle.
How to do it…
In this recipe, we're setting up a UserData
class that will store a Boolean variable to determine sound muting and an int
variable that will define the highest, unlocked level a user has reached. Depending on the needs of the game, there may be more or less datatypes to include within the class for different reasons, be it high score, currency, or other game-related data. The following steps describe how to set up a class to contain and store user data on a user's device:
- The first step involves declaring our constant
String
variables, which will hold references to our preference file, as well as "key" names, which will hold references to data within the preference file, as well as corresponding "value" variables. Additionally, we declare theSharedPreferences
object as well as an editor:// Include a 'filename' for our shared preferences private static final String PREFS_NAME = "GAME_USERDATA"; /* These keys will tell the shared preferences editor which data we're trying to access */ private static final String UNLOCKED_LEVEL_KEY = "unlockedLevels"; private static final String SOUND_KEY = "soundKey"; /* Create our shared preferences object & editor which will be used to save and load data */ private SharedPreferences mSettings; private SharedPreferences.Editor mEditor; // keep track of our max unlocked level private int mUnlockedLevels; // keep track of whether or not sound is enabled private boolean mSoundEnabled;
- Create an initialization method for our
SharedPreferences
file. This method will be called when our game is first launched, either creating a new file for our game if one does not already exist, or load existing values from our preference file if it does exist:public synchronized void init(Context pContext) { if (mSettings == null) { /* Retrieve our shared preference file, or if it's not yet * created (first application execution) then create it now */ mSettings = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); /* Define the editor, used to store data to our preference file */ mEditor = mSettings.edit(); /* Retrieve our current unlocked levels. if the UNLOCKED_LEVEL_KEY * does not currently exist in our shared preferences, we'll create * the data to unlock level 1 by default */ mUnlockedLevels = mSettings.getInt(UNLOCKED_LEVEL_KEY, 1); /* Same idea as above, except we'll set the sound boolean to true * if the setting does not currently exist */ mSoundEnabled = mSettings.getBoolean(SOUND_KEY, true); } }
- Next, we will provide getter methods for each of the values that are meant to be stored in our
SharedPreferences
file, so that we can access the data throughout our game:/* retrieve the max unlocked level value */ public synchronized int getMaxUnlockedLevel() { return mUnlockedLevels; }
- And finally, we must provide setter methods for each of the values that are meant to be stored in our
SharedPreferences
file. The setter methods will be responsible for saving the data to the device:public synchronized void unlockNextLevel() { // Increase the max level by 1 mUnlockedLevels++; /* Edit our shared preferences unlockedLevels key, setting its * value our new mUnlockedLevels value */ mEditor.putInt(UNLOCKED_LEVEL_KEY, mUnlockedLevels); /* commit() must be called by the editor in order to save * changes made to the shared preference data */ mEditor.commit(); }
How it works…
This class demonstrates just how easily we are able to store and retrieve a game's data and options through the use of the SharedPreferences
class. The structure of the UserData
class is fairly straightforward and can be used in this same fashion in order to adapt to various other options we might want to include in our games.
In the first step, we simply start off by declaring all of the necessary constants and member variables that we'll need to handle different types of data within our game. For constants, we have one String
variable named PREFS_NAME
that defines the name of our game's preference file, as well as two other String
variables that will each act as references to a single primitive datatype within the preference file. For each key constant, we should declare a corresponding variable that preference file data will be stored to when it is first loaded.
In the second step, we provide a means of loading the data from our game's preference file. This method only needs to be called once, during the startup process of a game in order to load the UserData
classes member variables with the data stored in the SharedPreferences
file. By first calling context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
, we check to see whether or not a SharedPreference
file exists for our application under the PREFS_NAME
string, and if not, then we create a new one a—MODE_PRIVATE
, meaning the file is not visible to other applications.
Once that is done, we can call getter methods from the preference file such as mUnlockedLevels = mSettings.getInt(UNLOCKED_LEVEL_KEY, 1)
. This will pass the data stored in the UNLOCKED_LEVEL_KEY
key of the preference file to mUnlockedLevels
. If the game's preference file does not currently hold any value for the defined key, then a default value of 1
is passed to mUnlockedLevels
. This would continue to be done for each of the datatypes being handled by the UserData
class. In this case, just the levels and sound.
In the third step, we set up the getter methods that will correspond to each of the datatypes being handled by the UserData
class. These methods can be used throughout the game; for example, we could call UserData.getInstance().isSoundMuted()
during level loading to determine whether or not we should call play()
on the Music
object.
In the fourth step, we create the methods that save data to the device. These methods are pretty straightforward and should be fairly similar regardless of the data we're working with. We can either take a value from a parameter as seen with setSoundMuted(pEnableSound)
or simply increment as seen in unlockNextLevel()
.
When we want to finally save the data to the device, we use the mEditor
object, using putter methods which are suitable for the primitive datatype we wish to store, specifying the key to store the data as well as the value. For example, for level unlocking, we use the method, mEditor.putInt(UNLOCKED_LEVEL_KEY, mUnlockedLevels)
as we are storing an int
variable. For a boolean
variable, we call putBoolean(pKey, pValue)
, for a String
variable, we call putString(pKey, pValue)
, and so on..
There's more...
Unfortunately, when storing data on a client's device, there's no way of guaranteeing that a user will not have access to the data in order to manipulate it. On the Android platform, most users will not have access to the SharedPreferences
file that holds our game data, but users with rooted devices on the other hand will be able to see the file and make changes as they see fit. For the sake of explanation, we used obvious key names, such as soundKey
and unlockedLevels
. Using some sort of misconstruction can help to make the file look more like gibberish to an average user who had accidentally stumbled upon the game data with their rooted device.
If we feel like going further to protect the game data, then the even more secure approach would be to encrypt the preference file. Java's javax.crypto.*
package is a good place to start, but keep in mind that encryption and decryption does take time and will likely increase the duration of loading times within the game.