how do i save levels in a basic platformer game using c++ and SDL?

Started by
8 comments, last by ASnogarD 11 years, 8 months ago
I am fairly new at programming games, and so far i have programmed basic games like pong. right now i was working on a small platformer game. right now i use a basic level creator that i made to create a level on the fly, and it's really annoying to recreate a level every time i compile my game. could anyone please teach me how to save my levels and load them so i dont have to recreate a level over and over again?
Advertisement
If you're just starting out a plain text format will probably be the easiest to handle, just decide on a way to represent the level as text and push it to a file using ofstream.

If you want to move to a binary representation instead just change things so that you represent your level as a series of numbers instead.
[size="1"]I don't suffer from insanity, I'm enjoying every minute of it.
The voices in my head may not be real, but they have some good ideas!
As SimonForsman mentioned, plain text files would probably be easiest to learn.

In C++, you can load files with std::[color=#ff0000]ifstream, and write files with std::[color=#ff0000]ofstream ([color=#ff0000]if = input from file, [color=#ff0000]of = output to file).
[std::ifstream documentation] [std::ofstream documentation]

It works pretty similar to writing to std::cout. Something like this:
#include <fstream> //Contains both std::ofstream and std::ifstream.

void WriteMyLevel(std::string path_to_file_to_write)
{
std::ofstream file(path_to_file_to_write.c_str()); //std::ofstream and std::ifstream expect a char* string as parameters. c_str() takes the std::string and returns it as char*.

int mapDatum1 = 12345;
int mapDatum2 = 100000;
int mapDatum3 = 357;

//Write the three values seperated by newlines.
file << mapDatum1 << "\n" << mapDatum2 << "\n" << mapDatum3 << std::endl;
}


To read it, you could do something like this:
#include <fstream> //For std::ofstream and std::ifstream, to read and write to files.
#include <sstream> //For std::stringstream, used by our StringToInt() function.

//Converts a std::string to an int.
int StringToInt(std::string str)
{
std::stringstream stream(str);

int value;
stream >> value;

return value;

}


void ReadMyLevel(std::string path_to_file_to_read)
{

std::ifstream file(path_to_file_to_read.c_str()); //We're using an (i)fstream now, for (i)nput.

//Check to see if the file loaded correctly.
if(!file)
{
//If the file didn't load correctly, report an error and return.
std::cout << "Error! The file, \"" << path_to_file_to_read << "\" failed to load." << std::endl;
return;
}

int mapDatum1 = 0;
int mapDatum2 = 0;
int mapDatum3 = 0;

//We can use the getline() function to read each line of the file into our temporary string.
std::string myBuffer;

getline(file, myBuffer); //Read the first line.
mapDatum1 = StringToInt(myBuffer); //Process the data read.

getline(file, myBuffer); //Read the second line.
mapDatum2 = StringToInt(myBuffer); //Process the data read.

getline(file, myBuffer); //Read the third line.
mapDatum3 = StringToInt(myBuffer); //Process the data read.

//Display the three values:
std::cout << "The three values are: " << mapDatum1 << ", " << mapDatum2 << ", " << mapDatum3 << std::endl;
}

[getline documentation]

If you're just starting out a plain text format will probably be the easiest to handle, just decide on a way to represent the level as text and push it to a file using ofstream.

If you want to move to a binary representation instead just change things so that you represent your level as a series of numbers instead.



i tried to do it the way you two described it. for now i use a class called blocks for each platform, and to save a level i save each block's location onto a txt file.
i cant seem to upload my code for some reason, it always gets cut off.


void save()
{
ofstream file("level.txt");
//write the location of each block onto a text file
for(vector<blocks>::iterator size = blockbox.begin(); size != blockbox.end(); size++)
{
//while saving add the with of the blocks to make up for the constructor's width and height subtraction
file << size->box.x + 8 << " " << size->box.y + 8<< "\n";
}

//close the file
file.close();

}

void load()
{
ifstream file("level.txt");

string s;
while(file >> s)
{
//convert to int and create the x location
int x = atoi(s.c_str());

//move on to y location then move to the next line
file.ignore();
file >> s;

//convert to int and create the y location
int y = atoi(s.c_str());

//move on to next line
file.ignore();

//create the block
blocks block1(x,y);
blockbox.push_back(block1);
}

//close the file
file.close();
}
Well, I haven't used std::ifstream::ignore() before, but it seems from the documentation that you might be using it wrong. The way you are currently using it, it'll ignore only one single character. The problem, however, is that on Windows each line ends with two characters (The 'carriage return' character ([color=#FF0000]\r) and then the 'newline' character ([color=#FF0000]\n)).

(You can paste code between [ code] and [ /code] tags)

Try this:
#include <fstream> //For std::ofstream and std::ifstream, to read and write to files.
#include <sstream> //For std::stringstream, used by our StringToInt() function.

//Converts a std::string to an int.
int StringToInt(std::string str)
{
std::stringstream stream(str);

int value;
stream >> value;

return value;

}

BlockBox Load(std::string filepath)
{
std::ifstream file(filepath.c_str());

//Check to see if the file loaded correctly. This is important! You shouldn't ignore checking for errors.
if(!file)
{
//If the file didn't load correctly, report an error and return.
std::cout << "Error! The file, \"" << filepath << "\" failed to load." << std::endl;
return BlockBox(); //Return an empty BlockBox.
}

std::string myBuffer;

BlockBox blockBox;

//Keep looping until we hit the end of the file.
while(file.eof() == false)
{
//Read a line of text into our buffer. Stop at the first space we find.
getline(file, myBuffer, ' ');

//Convert the text into a number.
int x = StringToInt(myBuffer);

//Read the next line of text into our buffer. Stop at the next space we find.
getline(file, myBuffer, ' ');

//Convert the text into a number.
int y = StringToInt(myBuffer);

//Read the last line of text into our buffer. Stop at the first end-of-line we find.
getline(file, myBuffer, '\n');

//Convert the text into a number.
int typeOfBlock = StringToInt(myBuffer);

//Create the new block.
Block block(x, y, typeOfBlock);
blockBox.push_back(block);
}

//Return the level.
return blockBox;
}

Well, I haven't used std::ifstream::ignore() before, but it seems from the documentation that you might be using it wrong. The way you are currently using it, it'll ignore only one single character. The problem, however, is that on Windows each line ends with two characters (The 'carriage return' character ([color=#FF0000]\r) and then the 'newline' character ([color=#FF0000]\n)).

(You can paste code between [ code] and [ /code] tags)

Try this:
#include <fstream> //For std::ofstream and std::ifstream, to read and write to files.
#include <sstream> //For std::stringstream, used by our StringToInt() function.

//Converts a std::string to an int.
int StringToInt(std::string str)
{
std::stringstream stream(str);

int value;
stream >> value;

return value;

}

BlockBox Load(std::string filepath)
{
std::ifstream file(filepath.c_str());

//Check to see if the file loaded correctly. This is important! You shouldn't ignore checking for errors.
if(!file)
{
//If the file didn't load correctly, report an error and return.
std::cout << "Error! The file, \"" << filepath << "\" failed to load." << std::endl;
return BlockBox(); //Return an empty BlockBox.
}

std::string myBuffer;

BlockBox blockBox;

//Keep looping until we hit the end of the file.
while(file.eof() == false)
{
//Read a line of text into our buffer. Stop at the first space we find.
getline(file, myBuffer, ' ');

//Convert the text into a number.
int x = StringToInt(myBuffer);

//Read the next line of text into our buffer. Stop at the next space we find.
getline(file, myBuffer, ' ');

//Convert the text into a number.
int y = StringToInt(myBuffer);

//Read the last line of text into our buffer. Stop at the first end-of-line we find.
getline(file, myBuffer, '\n');

//Convert the text into a number.
int typeOfBlock = StringToInt(myBuffer);

//Create the new block.
Block block(x, y, typeOfBlock);
blockBox.push_back(block);
}

//Return the level.
return blockBox;
}




Thanks for all the help so far, but i have two questions. Why do you use stringstream instead of atoi? also when you use getline() to read the y location, does it start reading from the space between the x and y location or does it start after the space?
getline() will discard the space (or newline, or whatever the third parameter is).

"[color=#008080][font=verdana, arial, helvetica, sans-serif]

If the delimiter [/font][color=#000000][font=verdana, arial, helvetica, sans-serif]

(the third parameter)[/font][color=#008080][font=verdana, arial, helvetica, sans-serif]

is found, it is extracted and discarded, i.e. it is not stored and the next input operation will begin after it.[/font][color=#000000][font=verdana, arial, helvetica, sans-serif]

"[/font]

[color=#000000][font=verdana, arial, helvetica, sans-serif]

I mostly use std::stringstream because it is much more powerful, allowing easier formatting of complex strings. I try not to use atoi(), atof(), and etc... to try to force myself to use std::stringstream, as a personal choice, so I can learn the flexibillity and power that std::stringstream provides.[/font]

[font="verdana, arial, helvetica, sans-serif"][color="#000000"]

That said, the way I'm using it (wrapped in a StringToInt() function) is just as inflexible as atoi() is, so it's a rather pointless substitution in that situation. [/font]biggrin.png
For myself personally, I have it on my todo list to "learn proper usage of standard streams" (along with a few other parts of the standard library that I don't yet take proper advantage of), but haven't actually got around to studying it. So it's mostly my personal preference to try to encourage me to learn something I've been too lazy to learn.

One more (unrelated) thing I would do, is I have these functions in my code base that I find very helpful:
typedef std::vector<std::string> StringList;

namespace String
{
//Divides up a string into multiple segments seperated by 'divider', and returns each segment in a StringList.
//If any segment is empty, and if 'ignoreEmptySegments' is true, the empty segments are not added to the StringList.
StringList Seperate(const std::string &str, const std::string &divider, bool ignoreEmptySegments)
{
StringList stringList;

//Check for empty string.
if(str.empty() || divider.empty())
return stringList;

size_t start = 0;
size_t end = str.find(divider, start);

//Keep looping, as long as there are more segments.
while(end != std::string::npos)
{
std::string subString = str.substr(start, end - start);

if(subString.size() > 0 || ignoreEmptySegments == false)
{
stringList.push_back(subString);
}

start = end + 1;
end = str.find(divider, start);
}

//Get the final (or the only) segment.
std::string subString = str.substr(start, str.size() - start);
if(subString.size() > 0 || ignoreEmptySegments == false)
{
stringList.push_back(subString);
}

return stringList;
}

//Not exposed globally in a header, only locally, as an implementation detail.
struct priv_PreservedSegment
{
std::string placeholder;
std::string text;
};

//Divides a string up, exactly like Seperate(), except if a divider is found between a 'beginPreservation' and a 'endPreservation', it doesn't divide them.
//Example: SeperateAndPreserve("Divide, by, "commas, except, where", in, quotes", ",", '"')
//Result:
// Divide
// by
// "commas, except, where"
// in
// quotes
StringList SeperateAndPreserve(const std::string &str, const std::string &divider, char beginPreservation, char endPreservation, bool ignoreEmptySegments)
{
std::vector<priv_PreservedSegment> preservedSegments;
std::string newStr = str;

size_t offset = 0;

auto matchingBrackets = String::FindMatchingBrackets(str, beginPreservation, endPreservation);
for(const auto &match : matchingBrackets)
{
size_t begin = match.first - offset;
size_t end = match.second - offset;
size_t length = (end-begin) + 1;

priv_PreservedSegment segment;
segment.placeholder = "%%^" + IntToString(preservedSegments.size()) + "^%%";
segment.text = newStr.substr(begin, length);
preservedSegments.push_back(segment);

newStr.replace(begin, length, segment.placeholder);

//Since the string size now changed, we need to adjust for that.
offset += segment.text.size();
offset -= segment.placeholder.size();
}

//Seperate the string like normal.
StringList stringList = Seperate(newStr, divider, ignoreEmptySegments);

//Re-add the preserved parts.
for(const auto &segment : preservedSegments)
{
for(auto &string : stringList)
{
string = String::Replace(string, segment.placeholder, segment.text);
}
}

return stringList;
}

StringList SeperateAndPreserve(const std::string &str, const std::string &divider, char preservationMark, bool ignoreEmptySegments)
{
return SeperateAndPreserve(str, divider, preservationMark, preservationMark, ignoreEmptySegments);
}

//Takes a StringList and combines each string in the list into a single string, seperated by 'divider'.
//If any string in the StringList is empty, and 'ignoreEmptySegments' is true, the empty strings are skipped.
std::string Combine(const StringList &stringList, const std::string &divider, bool ignoreEmptySegments)
{
std::string combinedString;

if(stringList.empty())
return combinedString;

for(const std::string &str : stringList)
{
if(ignoreEmptySegments && str.empty())
continue;

combinedString += str;
combinedString += divider;
}

combinedString.erase(combinedString.size() - divider.size());

return combinedString;
}

//Pushes a string into the StringList, and ensures that it is unique in the string removing any other matching occurance.
StringList PushUniqueToBack(const StringList &stringList, const std::string &str)
{
StringList newStringList = stringList;

//Remove duplicates.
std::remove(newStringList.begin(), newStringList.end(), str);
newStringList.push_back(str);

return newStringList;
}

StringList PushUniqueToFront(const StringList &stringList, const std::string &str)
{
StringList newStringList = stringList;

//Remove duplicates.
std::remove(newStringList.begin(), newStringList.end(), str);
newStringList.insert(newStringList.begin(), str);

return newStringList;
}

//Ensures 'stringList' only contains unique elements. Any duplicate entries will be removed.
StringList RemoveDuplicates(const StringList &stringList)
{
StringList newStringList = stringList;

auto newEnd = newStringList.end();
auto iterator = newStringList.begin();
while(iterator != newEnd)
{
newEnd = std::remove_if((iterator + 1), newEnd, /* Lambda */ [&iterator](const std::string &str){ return (str == *iterator); } /* End */);

iterator++;
}

//Despite its name, std::remove_if() just moves the elements to the end of the vector, and we need to delete them ourselves afterward.
newStringList.erase(newEnd, newStringList.end());

return newStringList;
}
} //End of namespace.

([size=2]This is the entire file for that type of code on the off-chance you are interested, but in particular I'm talking about the String::Seperate() function and the StringList typedef - which is just a std::vector<std::string>, but I use those so frequently I built a few functions around them for convenience)

Using that, I'd go like this:
std::string myBuffer;
int fileLine = 1;
while(file.eof() == false)
{
getline(file, myBuffer); //Get the entire line.
StringList segments = String::Seperate(myBuffer, ' ', true); //Split the line into segments divided by the space.

if(segments.size() < 3)
{
//Error!
std::cout << "Invalid syntax in file '" << fileName << "' at line " << fileLine << "." << std::endl;
//Continue to the next line.
}

int x = StringToInt(segment[0]); //Or 'atoi()'.
int y = StringToInt(segment[1]);
int typeOfBlock = StringToInt(segment[2]);

Block block(x, y, typeOfBlock);
blockBox.push_back(block);

fileLine++;
}

Sorry to drag ths old question out but its better than a new topic as it relates to the answers in here...

I wanted to convert my fscanf calls that all the tutorials I read used to load levels, to ifstream to use the >> operators... it works in saving a level, but >> is not a recognised operator for ifstream objects... VS IDE has no issue but wont compile.

example

std::ifstream map(FILE);

map >> oneparameter >> ":" >> secondparameter // doesnt work

std::ofstream map(FILE);

map << oneparameter << ":" << secondparameter // saves perfectly.

to get the load part to work it seems I need to use getline with a delimiter , right ? This means I need a string buffer that needs to be converted to an int to use in the level, I need to also use a getline per parameter and the ":" to read the 3 components of a tile in my level...

How is this better than

FILE* map = fopen(File, "r");

fscanf(map, "%d:%d ", &oneparameter, &secondparameter);

??

It would seem to me that the old C style is more elegant than the C++ style ? I understand I could in theory overload the >> operator in ifstream to do the map >> oneparmeter (etc) style but it is still awkward compared to the older C style.
Or is there a suitable C++ manner to get the same elegant style ?
If nothing else, type safety. The fscanf code will cause massive (and difficult to track down) problems if any of the types don't match or a buffer is too short.

Besides, it's fairly simple to write iostream compliant code that would accept syntax like this:
is >> oneparameter >> expect_string(":") >> secondparameter;
which will set the fail bit if the expected string is not found.
I assume the expect_string(":") is a user created function ?

I tried it and if didnt recognise the function, then I did some searches for that fuction, and no luck so I assume you are saying the issue with me using the >> format is that the ":" is not allowed in the ifstream >> operator function ?
I would then have to make a function that would accept that string and be acceptable to the >> opertator ?

... I'll put this issue aside, my level loads fine and displays fine with the fopen and fscanf functions.

This topic is closed to new replies.

Advertisement