Jump to content

  • Log In with Google      Sign In   
  • Create Account

FREE SOFTWARE GIVEAWAY

We have 4 x Pro Licences (valued at $59 each) for 2d modular animation software Spriter to give away in this Thursday's GDNet Direct email newsletter.


Read more in this forum topic or make sure you're signed up (from the right-hand sidebar on the homepage) and read Thursday's newsletter to get in the running!


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


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

  • You cannot reply to this topic
9 replies to this topic

#1 endless11111   Members   -  Reputation: 151

Like
0Likes
Like

Posted 05 July 2012 - 02:35 PM

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?

Sponsor:

#2 SimonForsman   Crossbones+   -  Reputation: 6318

Like
2Likes
Like

Posted 05 July 2012 - 03:21 PM

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 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!

#3 Servant of the Lord   Crossbones+   -  Reputation: 21147

Like
3Likes
Like

Posted 05 July 2012 - 04:01 PM

As SimonForsman mentioned, plain text files would probably be easiest to learn.

In C++, you can load files with std::ifstream, and write files with std::ofstream (if = input from file, 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. [b]c_str()[/b] 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]
It's perfectly fine to abbreviate my username to 'Servant' rather than copy+pasting it all the time.
All glory be to the Man at the right hand... On David's throne the King will reign, and the Government will rest upon His shoulders. All the earth will see the salvation of God.
Of Stranger Flames - [indie turn-based rpg set in a para-historical French colony] | Indie RPG development journal

[Fly with me on Twitter] [Google+] [My broken website]

[Need web hosting? I personally like A Small Orange]


#4 endless11111   Members   -  Reputation: 151

Like
0Likes
Like

Posted 08 July 2012 - 01:47 PM

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();
}

Edited by endless11111, 08 July 2012 - 01:51 PM.


#5 Servant of the Lord   Crossbones+   -  Reputation: 21147

Like
1Likes
Like

Posted 08 July 2012 - 02:32 PM

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 (\r) and then the 'newline' character (\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;
}

Edited by Servant of the Lord, 08 July 2012 - 02:38 PM.

It's perfectly fine to abbreviate my username to 'Servant' rather than copy+pasting it all the time.
All glory be to the Man at the right hand... On David's throne the King will reign, and the Government will rest upon His shoulders. All the earth will see the salvation of God.
Of Stranger Flames - [indie turn-based rpg set in a para-historical French colony] | Indie RPG development journal

[Fly with me on Twitter] [Google+] [My broken website]

[Need web hosting? I personally like A Small Orange]


#6 endless11111   Members   -  Reputation: 151

Like
0Likes
Like

Posted 10 July 2012 - 12:25 PM

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 (\r) and then the 'newline' character (\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?

#7 Servant of the Lord   Crossbones+   -  Reputation: 21147

Like
1Likes
Like

Posted 10 July 2012 - 02:21 PM

getline() will discard the space (or newline, or whatever the third parameter is).

"If the delimiter (the third parameter) is found, it is extracted and discarded, i.e. it is not stored and the next input operation will begin after it."

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.

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. Posted Image
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.
(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++;
}

It's perfectly fine to abbreviate my username to 'Servant' rather than copy+pasting it all the time.
All glory be to the Man at the right hand... On David's throne the King will reign, and the Government will rest upon His shoulders. All the earth will see the salvation of God.
Of Stranger Flames - [indie turn-based rpg set in a para-historical French colony] | Indie RPG development journal

[Fly with me on Twitter] [Google+] [My broken website]

[Need web hosting? I personally like A Small Orange]


#8 ASnogarD   Members   -  Reputation: 212

Like
0Likes
Like

Posted 20 August 2012 - 06:21 AM

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 ?

#9 BitMaster   Crossbones+   -  Reputation: 4435

Like
0Likes
Like

Posted 20 August 2012 - 08:13 AM

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.

#10 ASnogarD   Members   -  Reputation: 212

Like
0Likes
Like

Posted 20 August 2012 - 09:32 AM

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.




Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS