How to Make a Space Invaders Clone with SFLM - Part 4

In this part of the N.A.S.I.C. series, I'll show you how I implemented an options screen. Really, most of the work in this segment took place in other tutorials I've written for this game. See the following tutorials for more info on some of the eye candy and data gathering:


Past Tutorials:

Basically, there are mostly housekeeping variables for keeping track of the menu state in the definition of the menu class. Most of the details - while these are important for the menu to serve its purpose - are in the implementation.


#ifndef OPTIONS_HPP
#define OPTIONS_HPP

#include <SFML/Graphics.hpp>
#include <SFML/Audio.hpp>
#include <THOR/Graphics.hpp>
#include <iostream>
#include <fstream>
#include <boost/archive/xml_oarchive.hpp>
#include <boost/archive/xml_iarchive.hpp>
#include <starfield.hpp>
#include <hexgrid.hpp>
#include <button.hpp>
#include <optionbox.hpp>
#include <opstruct.hpp>

namespace nasic
{

class options
{
public:
    options();
    ~options();
    void show(sf::RenderWindow& window);

    int optionState()
    {
        return m_optionstate;
    };

    enum optionstate
    {
        uninitialized,
        settingoptions,
        done
    };

private:
    static sf::Uint32 m_optionstate;
    nasic::opstruct m_options;
    float m_vol;
    float m_eff;
    float m_diff;
    std::string m_filename;
    sf::Uint32 m_initialVol;
    sf::Uint32 m_initialEff;
    sf::Uint32 m_initialDif;
};

}

#endif // OPTIONS_HPP

In the implementation, there is also a new class in use that I created to get input from the player. It is the optionbox class. The interface is simple, it consists of some buttons to control the settings for music volume, effects volume, and difficulty. It also has a slider to indicate to the player that settings have been altered. Lastly, it returns values based on the final state of the optionbox. However, there were hoops to jump through in its implementation. Here is the definition for that class:


#ifndef OPTIONBOX_HPP
#define OPTIONBOX_HPP

#include <iostream>
#include <SFML/Graphics.hpp>
#include <THOR/Graphics.hpp>
#include <button.hpp>

namespace gui
{
    class optionbox : public sf::Drawable
    {
        public:
            optionbox();
            optionbox(std::string label, sf::Vector2f pos, sf::Uint32 precision, sf::RenderWindow& window);
            ~optionbox();

            sf::Uint32 getVal(){return m_val;};
            void setVal(int v){m_val = v;};
            void incVal(){m_val++;};
            void decVal(){m_val--;};
            sf::Uint32 getUpBtn(){return m_up.getState();};
            sf::Uint32 getDownBtn(){return m_down.getState();};
            sf::Uint32 getUpClicks();
            sf::Uint32 getDownClicks();
            void setFocus(bool f){m_focused = f;};

            void update(sf::Event& e, sf::RenderWindow& window);

            virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const;

        private:

        sf::Text m_label;
        sf::Text m_valLabel;

        float m_initialPos;
        sf::Uint32 m_val;
        sf::Uint32 m_precision;
        sf::Uint32 m_returnval;
        sf::Uint32 m_upclicks;
        sf::Uint32 m_downclicks;
        bool m_focused;

        sf::ConvexShape m_box;

        gui::button m_up;
        gui::button m_down;

        sf::RectangleShape m_line;
        sf::RectangleShape m_marker;

        sf::Font myfont;
    };
}

#endif // OPTIONBOX_HPP

For now, I'll leave the implementation out - I plan to write a tutorial in the future regarding capturing data from your players. If you are feeling froggy, take a look at the code and tinker away. It may be worth your while to take some things about this into consideration. Although the optionbox class works for this simple game, you might consider building something more flexible. If you want something more flexible without all the legwork, then go grab a copy of SFGUI. It's been around for a while and has some nice features, including loading CSS-like files for settings on the fly so you can see your changes as you make them.

In the constructor, the only thing interesting going on is the loading of current settings from an XML file stored with the application. The file is read through the Boost serialization class automagically, then pass this data to the member variables for the class (m_initialVol, m_initialEff, and m_initialDif). We will not be altering the data members of the object we use for serialization, instead we will pass them back when the user exits the options screen. It's a good idea to always do this - keep those members private so you are only altering the members through the "load" and "save" interface. There are also three instances of optionbox - one for each settings block. You can see in the constructor for each of them, they are constructed with default values - and the default values in this case are the variables read from the XML file and passed to the private data members of this class. The rest of this block is boilerplate and you can probably figure it all out - if I'm wrong blow up the comments at the bottom. ;)


#include <options.hpp>

nasic::options::options()
{
    //reset the state each time the object is created
    using nasic::options;
    m_optionstate = optionstate::s_uninitialized;

    //sound and settings
    //read option settings from file
    m_filename =  std::string("data/options.xml");
    if(!m_options.loadOptions(m_options, m_filename.c_str()))
    {
        std::cerr<<"Could not load options data from options.xml"<<std::endl;
        m_initialVol = 3;
        m_initialEff = 5;
        m_initialDif = 1;
    }
    else
    {
        std::cout<<m_options.m_volume<<"\n"<<m_options.m_effects<<"\n"<<m_options.m_difficulty<<std::endl;

        m_initialVol = m_options.m_volume;
        m_initialEff = m_options.m_effects;
        m_initialDif = m_options.m_difficulty;
    }

    std::cout<<m_initialVol<<"\n"<<m_initialEff<<"\n"<<m_initialDif<<std::endl;
}

nasic::options::~options()
{

}

void nasic::options::show(sf::RenderWindow& window)
{
    using nasic::options;

    /////////////////////////////////////////////////////
    //***************IMPORTANT NOTE*********************
    //we're getting the video mode and creating a scale
    //variable for the drawables using 800 as the base
    //800*600 will be the smallest screen supported
    //*****KEEPING IN MIND****
    //we're blowing up small images...
    //this is not ideal for non-retro-looking sprites
    sf::VideoMode mode = sf::VideoMode::getDesktopMode();
    float scale = mode.width/800.f;

    window.setKeyRepeatEnabled(false);
    window.setFramerateLimit(60);//set the refresh limit to the current frame rate 60fps

    //bail if the options are not in uninitialized state
    if(!m_optionstate == optionstate::s_uninitialized)
        return;

    //create info stuff
    sf::Color bg = sf::Color(255,150,0,200);
    sf::Color none = sf::Color(0,0,0,0);
    sf::Color darkred = sf::Color(100,0,0,255);

    sf::Vector2f volpos = sf::Vector2f(window.getSize().x/2.f,window.getSize().y/4.f);
    sf::Vector2f effpos = sf::Vector2f(window.getSize().x/2.f,window.getSize().y/2.f);
    sf::Vector2f diffpos = sf::Vector2f(window.getSize().x/2.f,window.getSize().y/1.25f);
    gui::optionbox vol("Volume", volpos, 20, window);
    gui::optionbox eff("Effects Volume", effpos, 20, window);
    gui::optionbox dif("Difficulty", diffpos, 3, window);

    sf::Uint32 focus = 0;

    sf::Font myfont;
    if(!myfont.loadFromFile("fonts/contb.ttf"))
    {
        std::cerr<<"Could not load contb.ttf"<<std::endl;
    }

    sf::Text title("Options", myfont, 48);
    title.setColor(darkred);
    title.setPosition(window.getSize().x * 0.01f, window.getSize().y * 0.01f);

    sf::ConvexShape bgBox = thor::Shapes::roundedRect(sf::Vector2f(window.getSize().x, window.getSize().y), 0.f, sf::Color(bg), 0.f, sf::Color(none));
    bgBox.setPosition(0.f,0.f);

    nasic::starfield stars(window,nasic::starfield::starStyle::smallStars, scale);

    nasic::hexgrid hex(window, nasic::hexgrid::hexStyle::translucent, scale);

    m_optionstate = optionstate::s_settingoptions;

    //time stuff...
    sf::Clock tickClock;
    sf::Time timeSinceLastUpdate = sf::Time::Zero;
    sf::Time TimePerFrame = sf::seconds(1.f/60.f);

    vol.setVal(m_initialVol);
    eff.setVal(m_initialEff);
    dif.setVal(m_initialDif);

    sf::SoundBuffer menubuff;
    if(!menubuff.loadFromFile("sound/select.wav"))
    {
        std::cerr<<"Could not load select.wav."<<std::endl;
    }

    sf::Sound menusound;
    menusound.setBuffer(menubuff);
    menusound.setVolume(m_initialVol*5.f);

    sf::Music menumusic;
    if(!menumusic.openFromFile("sound/title.ogg"))
    {
        std::cout<<"Could not open stream for title.ogg"<<std::endl;
    }
    menumusic.setVolume(m_initialEff*5.f);
    menumusic.play();

    bool running = true;
    sf::Event e;

End boilerplate. Okay, the interesting things happen from here on. First we poll for events like usual, but when the player hits escape or close, we want to save the settings. Just like the main menu tutorial, there are variables for keeping track of player input (focus) and the state of the options screen (m_optionstate). The focus variable is simply used for keeping track of the player's place in the options (sound, effects, or difficulty) and altering the appearance of the background when the player hits the tab button. It will go from transparent to a translucent white when the player tabs to each option block. Of course, this is all handled internally by the optionbox class, so no messy spaghetti (or at least less...) in this file. Again, crack open the optionbox.cpp file to get more info - you'll appreciate the fact that that isn't hanging out in the code blocks below.


    while(running)
    {
        while(window.pollEvent(e))
        {
            if(e.type == sf::Event::Closed)
            {
                m_options.saveOptions(m_options,m_filename.c_str());
                m_optionstate = optionstate::s_done;
                return;
            }

            if(e.type == sf::Event::KeyPressed)
            {
                switch(e.key.code)
                {
                    case sf::Keyboard::Escape:
                    {
                        m_options.saveOptions(m_options,m_filename.c_str());
                        m_optionstate = optionstate::s_done;
                        return;
                    }
                    break;

                    case sf::Keyboard::Up:
                    {
                        if(focus > 0)
                        {
                            focus--;
                            menusound.play();
                        }

                    }
                    break;

                    case sf::Keyboard::Down:
                    {
                        if(focus < 2)
                        {
                            focus++;
                            menusound.play();
                        }
                    }
                    break;

                    case sf::Keyboard::Left:
                    {
                        menusound.play();
                    }
                    break;

                    case sf::Keyboard::Right:
                    {
                        menusound.play();
                    }
                    break;

                    case sf::Keyboard::Tab:
                    {
                        menusound.play();
                        if(focus >= 2)
                            focus = 0;
                        else
                            focus++;
                    }

                    default:
                        break;
                }
            }
            vol.update(e, window);
            eff.update(e, window);
            dif.update(e, window);
        }

        //handle game ticks and return a fixed dt
        //for updates
        timeSinceLastUpdate += tickClock.restart();
        while (timeSinceLastUpdate > TimePerFrame)
        {
            timeSinceLastUpdate -= TimePerFrame;
            stars.update(window, TimePerFrame);
        }

        m_vol = vol.getVal() * 5.f;
        menumusic.setVolume(m_vol);
        m_options.m_volume = vol.getVal();

        switch(focus)
        {
            case 0://volume is on focus
            {
                vol.setFocus(true);
                eff.setFocus(false);
                dif.setFocus(false);
            }
            break;

            case 1://effects is on focus
            {
                vol.setFocus(false);
                eff.setFocus(true);
                dif.setFocus(false);
            }
            break;

            case 2://difficulty is on focus
            {
                vol.setFocus(false);
                eff.setFocus(false);
                dif.setFocus(true);
            }
            break;

            default:
                break;
        }

        if(eff.getDownClicks() == 1)
            menusound.play();

        if(eff.getUpClicks() == 1)
            menusound.play();

        m_eff = eff.getVal() * 5.f;
            menusound.setVolume(m_eff);

        m_options.m_effects = eff.getVal();
        m_options.m_difficulty = dif.getVal();

        window.clear();
        window.draw(stars);
        window.draw(bgBox);
        window.draw(hex);
        window.draw(vol);
        window.draw(eff);
        window.draw(dif);
        window.draw(title);
        window.display();
    }
    std::cout<<m_options.m_volume<<"\n"<<m_options.m_effects<<"\n"<<m_options.m_difficulty<<std::endl;
    m_options.saveOptions(m_options,m_filename.c_str());
    return;
}

//instantiate static members
sf::Uint32 nasic::options::m_optionstate = nasic::options::optionstate::s_uninitialized;

Finally, the data is saved and the settings can be restored the next time the player fires up the game, and the game state class polls for the int optionState() method (see the definition of options.hpp at the top) which indicates that the player is back to the main menu. Make sure to create an options object in your state machine and handle the states accordingly (HINT: options has a method optionState() that returns the state of the options screen). If you don't feel like snapping this in there, don't worry, you can download all the source code on Github.

Pretty swell, huh? Stick around for the next tutorial and learn how to make the funnest part - the level, complete with a boss fight at the end.

Go to Part 5