How to Save Game State

What is the best way to save game state?

But I heard this way has some issues and not suitable for save.

That's right. On some devices, there are issues with BinaryFormatter. It gets worse when you update or change the class. Your old settings might be lost since the classes non longer match. Sometimes, you get an exception when reading the saved data due to this.

Also, on iOS, you have to add Environment.SetEnvironmentVariable("MONO_REFLECTION_SERIALIZER", "yes"); or you will have problems with BinaryFormatter.

The best way to save is with PlayerPrefs and Json. You can learn how to do that here.

In my case, save format must be byte array

In this case, you can convert it to json then convert the json string to byte array. You can then use File.WriteAllBytes and File.ReadAllBytes to save and read the byte array.

Here is a Generic class that can be used to save data. Almost the-same as this but it does not use PlayerPrefs. It uses file to save the json data.

DataSaver class:

public class DataSaver
{
//Save Data
public static void saveData<T>(T dataToSave, string dataFileName)
{
string tempPath = Path.Combine(Application.persistentDataPath, "data");
tempPath = Path.Combine(tempPath, dataFileName + ".txt");

//Convert To Json then to bytes
string jsonData = JsonUtility.ToJson(dataToSave, true);
byte[] jsonByte = Encoding.ASCII.GetBytes(jsonData);

//Create Directory if it does not exist
if (!Directory.Exists(Path.GetDirectoryName(tempPath)))
{
Directory.CreateDirectory(Path.GetDirectoryName(tempPath));
}
//Debug.Log(path);

try
{
File.WriteAllBytes(tempPath, jsonByte);
Debug.Log("Saved Data to: " + tempPath.Replace("/", "\\"));
}
catch (Exception e)
{
Debug.LogWarning("Failed To PlayerInfo Data to: " + tempPath.Replace("/", "\\"));
Debug.LogWarning("Error: " + e.Message);
}
}

//Load Data
public static T loadData<T>(string dataFileName)
{
string tempPath = Path.Combine(Application.persistentDataPath, "data");
tempPath = Path.Combine(tempPath, dataFileName + ".txt");

//Exit if Directory or File does not exist
if (!Directory.Exists(Path.GetDirectoryName(tempPath)))
{
Debug.LogWarning("Directory does not exist");
return default(T);
}

if (!File.Exists(tempPath))
{
Debug.Log("File does not exist");
return default(T);
}

//Load saved Json
byte[] jsonByte = null;
try
{
jsonByte = File.ReadAllBytes(tempPath);
Debug.Log("Loaded Data from: " + tempPath.Replace("/", "\\"));
}
catch (Exception e)
{
Debug.LogWarning("Failed To Load Data from: " + tempPath.Replace("/", "\\"));
Debug.LogWarning("Error: " + e.Message);
}

//Convert to json string
string jsonData = Encoding.ASCII.GetString(jsonByte);

//Convert to Object
object resultValue = JsonUtility.FromJson<T>(jsonData);
return (T)Convert.ChangeType(resultValue, typeof(T));
}

public static bool deleteData(string dataFileName)
{
bool success = false;

//Load Data
string tempPath = Path.Combine(Application.persistentDataPath, "data");
tempPath = Path.Combine(tempPath, dataFileName + ".txt");

//Exit if Directory or File does not exist
if (!Directory.Exists(Path.GetDirectoryName(tempPath)))
{
Debug.LogWarning("Directory does not exist");
return false;
}

if (!File.Exists(tempPath))
{
Debug.Log("File does not exist");
return false;
}

try
{
File.Delete(tempPath);
Debug.Log("Data deleted from: " + tempPath.Replace("/", "\\"));
success = true;
}
catch (Exception e)
{
Debug.LogWarning("Failed To Delete Data: " + e.Message);
}

return success;
}
}

USAGE:

Example class to Save:

[Serializable]
public class PlayerInfo
{
public List<int> ID = new List<int>();
public List<int> Amounts = new List<int>();
public int life = 0;
public float highScore = 0;
}

Save Data:

PlayerInfo saveData = new PlayerInfo();
saveData.life = 99;
saveData.highScore = 40;

//Save data from PlayerInfo to a file named players
DataSaver.saveData(saveData, "players");

Load Data:

PlayerInfo loadedData = DataSaver.loadData<PlayerInfo>("players");
if (loadedData == null)
{
return;
}

//Display loaded Data
Debug.Log("Life: " + loadedData.life);
Debug.Log("High Score: " + loadedData.highScore);

for (int i = 0; i < loadedData.ID.Count; i++)
{
Debug.Log("ID: " + loadedData.ID[i]);
}
for (int i = 0; i < loadedData.Amounts.Count; i++)
{
Debug.Log("Amounts: " + loadedData.Amounts[i]);
}

Delete Data:

DataSaver.deleteData("players");

C saving game state, reducing passed variables

Here is a dynamic way to do something similar to what you describe in your 1st option, i.e. track a game state using pointer to struct but that also stores and recovers that data to and from a binary file...

The following .c and .h files illustrate the idea that you can use a compound struct, i.e. one containing several other structs, that at any one time contain all of the values representing states in your program. This is very easily passed via a function(s) with very few arguments (prototyped eg. as char *GetState(state *in, const char *filespec); and char *SetState(state *out, const char *filespec); ) which in turn would write or read the contents of the struct into/out of a binary buffer. I have used this method to store and retrieve multiple sets of state data within files.

Note, the structs have random fields that of course you will modify as needed, but the idea is that you can pass a single pointer value that points to all the state data, in every function where state date is updated, or needs to be stored.

so_SaveGameState.c

    #include <so_SaveGameState.h>

unsigned char *pByteA;

GAME_STATE game = {{{"jim", "C:\\ico1.ico", {10, 120, 3}}, {"joe", "C:\\ico2.ico", {80, 10, -5}},{"larry", "C:\\ico3.ico", {15, -45, -45}},{"sue", "C:\\ico4.ico", {-100, -45, 45}}}, ENVR_3};
GAME_STATE *pGame = NULL;

int main(void)
{
pGame = &game;//point to populated memory


printf("Player 3 position\nx = %d\ny = %d\nz = %d\n", game.plyr[2].pos.x, game.plyr[2].pos.y, game.plyr[2].pos.z);
//example function that changes game state
UpdatePlayerPosition(&pGame, 2);
printf("Player 3 position\nx = %d\ny = %d\nz = %d\n", game.plyr[2].pos.x, game.plyr[2].pos.y, game.plyr[2].pos.z);
UpdatePlayerPosition(&pGame, 2);
printf("Player 3 position\nx = %d\ny = %d\nz = %d\n", game.plyr[2].pos.x, game.plyr[2].pos.y, game.plyr[2].pos.z);
UpdatePlayerPosition(&pGame, 2);
printf("Player 3 position\nx = %d\ny = %d\nz = %d\n", game.plyr[2].pos.x, game.plyr[2].pos.y, game.plyr[2].pos.z);
//prepare an instance of game state for storeing
(const GAME_STATE *)pByteA = &game;
int len1 = sizeof(game);
BOOL status = storeState("C:\\tempextract\\binFileStruct.bin", pByteA, len1);

//recover a stored state
unsigned char *buf = recoverState("C:\\tempextract\\binFileStruct.bin");
GAME_STATE *game_2 = (GAME_STATE *)buf;

free(game_2);
return 0;
}

unsigned char * recoverState(const char *filespec)
{
size_t sz = 0;
int n = 0;
unsigned char *binBuf = NULL;
FILE *fp = fopen(filespec, "rb");
if(fp)
{
fseek(fp, 0L, SEEK_END);
sz = ftell(fp);
fseek(fp, 0L, SEEK_SET);
rewind(fp);

binBuf = calloc(sz, sizeof(*binBuf));
n = fread(binBuf, sizeof(unsigned char), sz, fp);
fclose(fp);
}
if(n == sz)
{
return binBuf;
}
else
{
return NULL;
}
}

int storeState(const char *filespec, const unsigned char *buf, size_t sz)
{
int count = 0;
FILE *fp = fopen(filespec, "wb");
if(fp)
{
count = fwrite(buf, sizeof(unsigned char), sz, fp);
fclose(fp);
}
return (count == sz) ? 1 : 0;
}

void UpdatePlayerPosition(GAME_STATE **game, int player)
{
static int x=0, y=0, z=0;

static BOOL toggle = TRUE;

toggle = (toggle == 1) ? -1 : 1;

srand(clock());
//using fake assignment here
//i.e. you would have other criteria to set actual position
x += toggle * rand()%300;
y += toggle * rand()%300;
z += toggle * rand()%300;

(*game)->plyr[player].pos.x = x;
(*game)->plyr[player].pos.y = y;
(*game)->plyr[player].pos.y = z;
}

so_StoreGameState.h

    typedef enum {//environment
ENVR_1, //bad weather
ENVR_2, //hill
ENVR_3, //pit
ENVR_4, //angry birds
ENVR_5, //enemy guard
MAX_OBST
}ENVR_TYPE;

typedef struct {
int x;
int y;
int z;
}POS;

typedef struct {
ENVR_TYPE envir;
//...
}ENVIR;

typedef struct {
char name[20];
char iconFile[260];
POS pos;
//...
}PLAYER;

typedef struct {
PLAYER plyr[4];
ENVIR env;
//...
}GAME_STATE;

extern GAME_STATE game;

unsigned char * recoverState(const char *filespec);
int storeState(const char *filespec, const unsigned char *buf, size_t sz);

Saving Game state Android

You can't save an Instance / Object unless you serialize it and save it to some text/binary/database file. Your two options are kind of identical therefore.

What you need to save is all information that you need to reconstruct your game state. There are probably some information that you can derive from here.

If you have just a small fixed set of variables that define your gamestate then use SharedPreferences.

If you want to keep more than one state and / or it is more complex to save use some text (xml, json, ...)/binary/database/.. representation and store that.

How to save a game state?

This can be a very complex problem but in your case I would suggest starting out simple and going from there as your game becomes more complex. Essentially the probablem is known as Serialization. Or, writing the memory of your application out to disk or a database in a way that it can be read back into memory later.

There are a lot of techniques for doing this, such as converting your objects into XML or JSON or some other binary file format. A bitmap file for example is just a serialized form of some image structures in memory.

I don't really know the technology you are using to make your game other than C# (XNA?) But essentially you want to use some sort of file-system API that's available in your environment and write your serialized objects there. Based on your question you might need to re-design some of your game to facilitate this. Generally it's good practice to have a single 'source of truth' representation of your game state but even if you don't you can still probably figure it out.

So here is a rough outline of steps I would take...

Serialization

  1. Write a routine that takes your game state and outputs simpler, serializable objects.
    • These simple objects will mostly have all public getter/setter properties with no functionality.
    • Try to write out only the bare minimum information you need to recreate your game state again later.
    • for example see how chess serializes game state: chess notation
  2. Serialize those objects into JSON (because its easy and flexible)
  3. Write that json to disk.
    • I would probably use a convention base approach here.
    • such as "Saves\game1.json" is the state for the first game slot.
    • Just infer the saved games based on the presence of the files of the right name.

Deserialization

  1. Look for game saves based on the above convention.
  2. Deserialize the JSON back into the simple serialization objects.
  3. Recreate the more complex game state by looking at the serialization objects.
    • Consider using a visitor pattern.
  4. Resume the game using the newly hydrated game state.

[EDIT: adding some extra links and a quick code sample]

Here is an open source json library that is quite good: http://json.codeplex.com/

// somehow convert your game state into a serializable object graph
var saveState = gameState.Save();

// use json.net to convert that to a string
string json = JsonConvert.SerializeObject(saveState, Formatting.Indented);

// save that string to the file system using whatever is available to you.
fileSystemService.WriteAllText("Saves\game1.json", json);

Storing game state when game state includes functions

First off, most commenters here are correct in that this is mainly a data model issue. I am not looking to refactor the model, so I am considering the following workaround the correct answer:

Serialize functions as string references:

function saveGame() {
var triggers = [];

game.triggerFnSet.forEach(function (trigger){
triggers.push(trigger.name);
});

// Serialize regular game data
window.localStorage.setItem('gameData', JSON.stringify(game));

// Serialize triggerFns
window.localStorage.setItem('triggers', JSON.stringify(triggers));
}

Then deserialize by doing a lookup on the functions through the window object, converting them back to functions:

function loadGame() {
var triggers = JSON.parse(window.localStorage.getItem('triggers'));
var gameData = JSON.parse(window.localStorage.getItem('gameData'));

if (gameData !== null) {
game = gameData;
game.triggerFnSet = new Set();

triggers.forEach(function (trigger){
game.triggerFnSet.add(window[trigger]);
});
}
}

This works because my functions are not anonymous and exist in a global context that can be accessed easily by the window!



Related Topics



Leave a reply



Submit