How to Use the Boost Library to Save and Load Game Data

Loading and saving data for your game can be as easy as parsing a text file using std::fstream or even using a few global variables (although globals are very much frowned upon). I'm of the mind that if there are only a few of them, you should be alright. However, globals should definitely be avoided in most cases for games and applications that are non-trivial, expansive in scope, or large in size. That said, I will show you a more flexible, data-driven, and object-oriented approach to saving and loading data via boost::serialize. The boost serialization library allows you to create classes that read and write their own member data from files in many formats. In this example, we'll create a class that serializes its own data using the XML file format. Because XML is widely understood and is easy to read, an explanation of what it is and how it works will not be provided here. Besides, googling it is more fun, right? In the event you don't want to hear me jabber on or you just want some code - check out the repo on github.

This example springs forth from a need for my Space Invaders clone to load and save some very simple user data. Namely, it is used to save and load options for music volume, sound effects volume, and the difficulty level of the game. I could very well have used plain text, but examples of this type abound on the internets - and I wanted to contribute an example that has maybe eluded many for some time. That said, here is the definition of our class (opstruct.hpp):


#ifndef OPSTRUCT_HPP
#define OPSTRUCT_HPP

#include <iostream>
#include <fstream>
#include <boost/archive/xml_oarchive.hpp>
#include <boost/archive/xml_iarchive.hpp>
#include <boost/serialization/nvp.hpp>

namespace nasic
{

class opstruct
{
    public:
        opstruct();
        ~opstruct();
        bool saveOptions(nasic::opstruct& op, const char * filename);
        bool loadOptions(nasic::opstruct& op, const char * filename);

        int m_volume;
        int m_effects;
        int m_difficulty;

    friend class boost::serialization::access;
    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
        ar & BOOST_SERIALIZATION_NVP(m_volume);
        ar & BOOST_SERIALIZATION_NVP(m_effects);
        ar & BOOST_SERIALIZATION_NVP(m_difficulty);
    }
};

}
#endif // OPSTRUCT_HPP

As you can see, there are only three data members and two methods we are concerned with here. The data members are simply music volume, sound effects volume, and difficulty. The template function for serializing the data simply tells Boost which members it will have access to. In order to fully understand why it is implemented the way it is, you will have to dive deeper into the Boost documentation. For our purposes, there is little required in terms of explanation. The macro BOOST_SERIALIZATION_NVP assigns a name to each node of the XML document corresponding to the data members we are serializing. Pretty nifty, huh? Here is a peek at the implementation (opstruct.cpp):



#include <opstruct.hpp>
nasic::opstruct::opstruct()
{

}

nasic::opstruct::~opstruct()
{

}

bool nasic::opstruct::saveOptions(nasic::opstruct& op, const char * filename)
{
    std::ofstream ofs(filename);
    boost::archive::xml_oarchive xml(ofs);
    xml<<boost::serialization::make_nvp("Options", op);
    return true;
}

bool nasic::opstruct::loadOptions(nasic::opstruct& op, const char * filename)
{
    std::ifstream ifs(filename);
    boost::archive::xml_iarchive xml(ifs);
    xml>>boost::serialization::make_nvp("Options", op);
    return true;
}

This is ultra simple, and cuts down on much of the code we would use parsing a text file, handling errors, and mapping the data in the text file to code in the application. Because we have implemented each method as bool, we have an interface with which to handle exceptions should the need arise. Now, a minimal example using the opstruct class:



#include <iostream>
#include <opstruct.hpp>

int main()
{
    nasic::opstruct op;
    int vol, eff, diff;

    if(!op.loadOptions(op,"options.xml"))
    {
        std::cerr<<"Could not load options."<<std::endl;
        vol = 2;
        eff = 4;
        diff = 1;
    }

    vol = op.m_volume;
    eff = op.m_effects;
    diff = op.m_difficulty;

    std::cout<<"Music volume: "<<vol<<"\n"<<"Effects volume: "<<eff<<"\n"<<"Difficulty: "<<diff<<std::endl;

    std::cout<<"Enter music volume: "<<std::endl;
    std::cin>>vol;

    std::cout<<"Enter sound effects volume: "<<std::endl;
    std::cin>>eff;

    std::cout<<"Enter difficulty level: "<<std::endl;
    std::cin>>diff;

    std::cout<<"Music volume: "<<vol<<"\n"<<"Effects volume: "<<eff<<"\n"<<"Difficulty: "<<diff<<std::endl;

    op.m_volume = vol;
    op.m_effects = eff;
    op.m_difficulty = diff;

    if(!op.saveOptions(op,"options.xml"))
    {
        std::cerr<<"Could not save options."<<std::endl;
    }

    return 0;
}

This, being a very simple, very minimal example of how to use the class, provides no error handling and assumes you are simply testing it to see how to use it. You would not want to use it in this manner, but you already knew that, right? That said, the Boost library provides pretty nice serialization of your classes out of the box. Play around with it, improve it, go forth and serialize some data.