Sign in to follow this  

C++ Game saving

This topic is 2541 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I am making a C++ Game with OpenGL and SDL.
I am at the point now that I want to save
the position of the player and other
information to a file.

I thought it would be smart to make some
file saving program that stands alone before
adding it to my game.

I want to know whats the best way to go about
doing this? I do not want the user to be able
to modify it. So I will need to disarrange
the data, but I can do that later.

Are iostream and fstream cross platform?
This needs to run on Mac, Linux, and Windows.

I see how to write ints to textfiles, but
how do I read them back? Also should I be
using binary files as opposed to textfiles?

Will be checking post often.
Thanks in advance.
Walker


// Walker
// 10-12-29
// Variable Text save attempt
#include <iostream>
#include <fstream>
using namespace std;

int number=72192;

int main ()
{
ofstream file;
file.open("save.txt");
if (file.is_open())
{
file << number << endl;
}
else { cout << "Unable to open file"; }
cout << "wrote " << number << " to file\n";
file.close();
system("pause");
return 0;
}

Share this post


Link to post
Share on other sites
Quote:
Also should I be using binary files as opposed to textfiles?
Well, it really doesn't matter - if you use binary you need to specify, though. As far as text files: they don't necessarily need to be of type .txt but can have any extension (even made-up ones) as long as they contain plain text. The user wouldn't be able to edit it if it isn't of filetype .txt... or at least it would be much much harder to edit.
Quote:
I see how to write ints to textfiles, but how do I read them back?
Well, fstream header also has ifstream (input) class, which reads from the file, as opposed to ofstream (output), which you used to write. Example:

ifstream input;
input.open("filename.extension");
if ( !input.good() ) {
//If there was error opening file...
}
while ( !input.eof() ) {
//eof - End Of File: loop while end of file is not reached...
int number;
input >> number;
//Continue reading....
}
//Once eof is reached, loop breaks....
input.close();



So, instead of the << operator to write using ofstream, the >> operator is used to read using ifstream. Also, any type of data can be written/read to/from a file. (int, char, string, bool, float/double, etc.)

Oh, and sorry, I don't know if fstream is cross platform - I've only used Windows...

Share this post


Link to post
Share on other sites
iostream and fstream are definitly cross platform (I use them everyday on both Linux and windows AND they are part of the standart C++ libraries), and so is cstdio.

I am really not sure about this, and you will most probably get better answers with some googling, but you should definitly not write a few numbers in a text file and trust the player not to cheat by altering the saves in text editor! So yes, use binary files and write your data with fwrite (from cstdio).

Reading and writing to file are pretty basic stuff, but take a look here, here and here

Quote:
Original post by BioProtonThe user wouldn't be able to edit it if it isn't of filetype .txt... or at least it would be much much harder to edit.

Why is that? :O

Share this post


Link to post
Share on other sites
Quote:
Original post by 00chris00
So yes, use binary files and write your data with fwrite (from cstdio).


Not the best idea.

This can bring up all kinds of problems: endianness, portability concerns due to encoding differences, byte/word width issues, type safety problems, and so on.

For starting out, just use text until you get a good handle on file I/O processes. You can worry about anti-cheating and binary formats later. In general, you should also stick with the iostream classes from the C++ standard library rather than falling back on the old C-compatibility functions like fwrite, because they provide type safety, which helps prevent a large number of bugs and security problems.

In general, the idea of taking arbitrary program data and converting it to a byte sequence (text or "binary") is known as serialization. It comes with a whole host of small gotchas and pitfalls, so be sure to do some research before committing to a particular approach or implementation.

Share this post


Link to post
Share on other sites
Whether you write in binary or not is your choice. Personally I prefer binary except when I want the files to be human readable. Most software is designed the same way.

First, you need to open the ifstream/ofstream using the std::ios::binary flag. This flag is important as it allows new line characters to be handled in the correct way.

To read and write binary data, you use the 'read' and 'write' methods of the stream object. You don't use the << and >> operators, are they're for parsing text. Even if you open the file with std::ios::binary, using << will still insert plain text into the file. This confused me for a while.
The read and write methods take a char pointer and a size in bytes, so it's necessary to reinterpret_cast the data to or from char*. No type safety here.
Here you get to choose between the easy but possibly unsafe method, and the more involved but more reliable method:

The easy way to write out a struct or class to disk is to just do:
myFile.write(reinterpret_cast<char*>(&myStruct), sizeof(myStruct));
This will store a copy of the struct on disk, as it appears in memory. But there are problems which have been mentioned in this thread.

1) If you read the data on a machine with a different endianness, the data will be scrambled.
2) If you read the data using software compiled with different alignment settings, the data could possibly be scrambled. If you only intend to use the same compiler, same settings, then you'll be okay.
3) This doesn't work if your struct contains STL containers or dynamically allocated memory. You'll just end up writing a pointer out to disk, which is meaningless.

Now the safer alternative is to define special serialise/deserialise methods in your objects which handle reading and writing to disk. Basically you call fwrite once for each primitive data member in your object. One way to handle endianness is to choose little endian as your storage format, detect the machine endianess and perform the necessary conversion if you're writing or reading on a machine which is big endian. (Which these days is quite uncommon for a desktop PC).
The act of calling fread/fwrite on individual primitive types should allow the compiler to take care of any alignment issues automatically.
Finally, to serialise an STL container, you want to first write a value indicating the number of elements in the container. Then you can iterate over the container, calling fwrite on each. The code that reads the data need only read the value indicating the number of elements, and then perform the corresponding number of push_back/insert/whatever operations. If the container holds complex objects then you'll want to provide a serialise method for that object too, and you can pass in a reference to the fstream object which you were already using.
Complicated hierarchies of objects can be handled in this way, although it relies on a clear definition of ownership. If your classes hold pointers to other objects without a clear indication of what owns what, then serialisation is a lot harder.

Share this post


Link to post
Share on other sites
Quote:
Quote:
Original post by BioProtonThe user wouldn't be able to edit it if it isn't of filetype .txt... or at least it would be much much harder to edit.

Why is that? :O


Well, if it's a normal .txt text file anybody can easily open it using Notepad (or any plain text editor) - and then edit and save it. Not that a made-up extension couldn't be opened in Notepad, it could, but the user would have to associate it with a program to open it. Considering all the unknown file extensions on one's system (program files, etc.), a person would probably have to be pretty desperate to go through the trouble of opening it...

Even if a player were to open the file, they wouldn't know how to edit what they want if the save-file is organized in a way they don't know. They would probably see it as random numbers, while the programmer would know which numbers represent whatever info needed to be saved.

Share this post


Link to post
Share on other sites
Quote:
Original post by ApochPiQ
Quote:
Original post by 00chris00
So yes, use binary files and write your data with fwrite (from cstdio).


Not the best idea.

This can bring up all kinds of problems: endianness, portability concerns due to encoding differences, byte/word width issues, type safety problems, and so on.

I didn't know binary files were that dangerous :P ... And to grab an opportunity to learn something new: what do you mean by encoding differences in binary files :O? Do you or anybody else have any links on different techinques of game saving?

@BioProton: I see what you mean, but probably anyone who would take the time to open the saves directory would be willing to at least try opening the save file in a text editor. On the other hand yes, at this point worrying about possible cheating is a little bit extreme :).

Share this post


Link to post
Share on other sites
Trying to stop hacking of save files is just like trying to stop software piracy. There is only so much you can do, and if someone really wants to crack something, they can.

If you can figure out the whole binary file thing, then I would go for it. If not, take the easy route, and use a renamed text file. Just make sure the numbers aren't easy to figure out, or labeled in any form. For example, if you are having save points, and cannot save just anywhere, don't put an x, y position, rather just index a list of savepoints. Sure anything is hackable, even the memory of the game itself at run-time. But, if you do these easy things, you will atleast deter the "casual" hackers, and since the "real" hackers won't be stopped anyway, it probably isn't worth the time for an indie developer, and much less a non-commercial hobbyist developer.

For example, in my games, my resources are stored in zip archives, with only a whole file password, and a renamed extension. And, the files themselves are not name .ogg, rather other random extensions. Since file content is what matters, some sound APIs don't care about the extension, so I have really easy double protection. Of course, it is possible to rename the zip extension, and possible(though time consuming) to crack a zip archive password, and then you can look at the headers and file contents if you really wanted to steal my music. But, that is a lot of work for a "casual" hacker. My suggestion is to do something in the same idea for save games. Stop the casual guys, but ignore the real hackers because you won't stop them anyway.

Share this post


Link to post
Share on other sites
Quote:
Original post by 00chris00
And to grab an opportunity to learn something new: what do you mean by encoding differences in binary files :O?



Check into the differences (subtle but important) between lower and upper ASCII, 7-bit ANSI, 8-bit ANSI, UTF-8, UTF-16, UTF-32, EBCDIC.....

And that's just for text! Then you get into direct representation of data at the bit level. There's one's complement signed integers, two's complement signed integers, unsigned integers, binary-coded decimal, fixed point formats, floating point formats, etc. etc.

Share this post


Link to post
Share on other sites
I say do what's easiest for you. Considering you're asking this question, worrying about what types of text (ASCII, UNICODE, etc.) or endianess should not be part of the equation.

Think of it like this. In a simple example, you have a world state, and it may contain all you need to know to save the game. Something like this:


struct
{
int PlayerType;
int Health;
int Xposition;
int Yposition;
int WeaponType;
int ArmorType;

} tPlayerState;


struct
{
int Level;
tPlayerState MainPlayerState;
List MonstersAliveList; // a list of tPlayerState, for example
List UpgradeAvailList;

} tWorldState;

tWorldState gWorldState;



To save the world state, you have to be careful how you store the Lists, since they have dynamic sizes. Set a pre-defined layout for your save file, and fill it in. I would do something like this (pseudo code):

void SaveGame(void)
{
int Num;
tPlayerState Monster;

fOutput = FileOpen("SaveGame.dat")

// store the Level 1st
Write(fOutput, &gWorldState.Level, sizeof(gWorldState.Level));

// store the main player state
Write(fOutput, &gWorldState.MainPlayerState, sizeof(gWorldState.MainPlayerState));

// write the number of monsters to be read back
Num = gWorldState.MonstersAliveList.Size();
Write(fOutput, &Num, sizeof(int));

// loop through and store each monster still alive
for (int i = 0; i < Num; i++)
{
Monster = WorldState.MonstersAliveList.GetMonster(i);
Write(fOutput, &Monster, sizeof(tPlayerState));
}

// write the number of upgrade objects to be read back
Num = gWorldState.UpgradeAvailList.Size();
Write(fOutput, &Num, sizeof(int));

// loop the same for the upgrade objects list



Close(fOutput);
}

// now load the game
void LoadGame(void)
{
int Num;
tPlayerState Monster;

fOutput = FileOpen("SaveGame.dat")

// read the Level 1st
Read(fOutput, &gWorldState.Level, sizeof(gWorldState.Level));

// read the main player state
Read(fOutput, &gWorldState.MainPlayerState, sizeof(gWorldState.MainPlayerState));

// Read the number of monsters to be read back
Read(fOutput, &Num, sizeof(int));

// generate an empty list for world state
gWorldState.MonstersAliveList = new List(Num);

// loop through and read each monster still alive
for (int i = 0; i < Num; i++)
{
Read(fOutput, &Monster, sizeof(tPlayerState));
WorldState.MonstersAliveList.SetMonster(i, Monster);
}

// read the number of upgrade objects to be read back
Read(fOutput, &Num, sizeof(int));

// loop the same for the upgrade objects list



Close(fOutput);

}


Share this post


Link to post
Share on other sites
If you can figure out what the binary file, then I would go. If not, take a simple approach and use a text file rename. Just make sure the number is not easy to understand, or any form of tags. For example, if you have a save point, can not easily find a place to save, not to an x, y position, but simply keep a list of index points. Of course what is hackable, even the game itself in the run-time memory. However, if you do these simple things, you will atleast deter "casual" hackers, because the "real" hackers will not stop, however, it may not be worth the time, an independent developer, but not non-Business lovers developers.

Share this post


Link to post
Share on other sites
If you want to be able to tell if the save has been modified, you could run an md5 hash on it and drop it at the end of the file. Of course, there's nothing to stop a cracker regenerating this. Maybe you could hide it in the file. This might be a bit harder to spot if you encrypt the file first using something simple like a 64-bit monoalphabetic cipher followed by some transposition cipher. Security through obscurity does kind of suck though. Maybe you could implement some public key crypto-system where the private key is hidden in the program source - they'd have to reverse engineer your application to extract it.

Chris

Share this post


Link to post
Share on other sites

This topic is 2541 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this