Top Down Shoot Em Up Mechanics - Part 3

 

In the last segment of the shoot-em-up tutorial, we left off with a farily good look at a "flock" of Samsquamptches. I also pointed out that there are some real problems that could be solved to convince players that the entities that are in pursuit of the player are more intelligent, or at least a force to be reckoned with.

The problems that need to be solved for creating realistic steering behavior, or in this case "flocking", revolves around the concepts of alignment, cohesion, and separation. My reference of choice is the book Programming Game AI by Example - a book by Mat Buckland.

In this tutorial, we'll take a step back and set up the basic skeleton of the game using the xygine framework. Here is a look at the progress we'll make for Total Hackage in this tutorial:

So, before we get to the fun AI stuff, the first thing that needs to be accomplished is downloading and building the xygine framework, created by Mat Marchant. Probably better known in SFML programming circles for his work on the SFML TMX Loader for the Tiled map editor, this framework is quite good and incorporates many features that are required for any serious game development. As a bonus, it is fairly quick to set up a basic project. A few features that I find useful are a state stack for managing your game states, a scene graph for managing each scene in your game, and a robust entity component system for managing the complexity related to the game entities you need to create.

Okay, assuming you've downloaded and built xygine - let's get started on Total Hackage! First, we'll need to inherit from xy::App to create our game class, which we will be creating one, and only one, instance of. According to the wiki, these are the basic required overrides for the Game class:


#ifndef GAME_HPP
#define GAME_HPP

#include <xygine/App.hpp>

class Game final : public xy::App
{
    public:
        Game();
        ~Game() = default;
        Game(const Game&) = delete;
        Game& operator = (const Game&) = delete;

        void initialise() override;

        void handleEvent(const sf::Event& e) override;
        void handleMessage(const xy::Message& msg) override;

        void registerStates() override;
        void updateApp(float dt) override;

        void draw() override;

        void finalise() override;

    private:
        xy::StateStack m_states;

};

#endif // GAME_HPP

It is very likely you have seen this before many times. Most games will require this or something very similar. Basically, the Game class has methods that will work on the state stack by handling initialization, input, updates, rendering, and cleanup. In addition to this, xygine has methods that we'll need to override for registering states in the state stack and handling messages since xygine has a baked-in messaging system. A message system is useful for broadcasting messages to all of the entities in your game so that they can respond to them accordingly if necessary. When things get complex, this type of system can really untangle connections between entities and simplify your entities so you are not putting a lot of code in them to handle communication between other entities. Thus far, the implementation is looking very simple:


#include <Game.hpp>

Game::Game()
: xy::App()
, m_states(xy::State::Context(getRenderWindow(),*this))
{

}

void Game::initialise()
{
    getRenderWindow().setTitle("Total Hackage - F1: Console F2: Stats");
}

void Game::handleEvent(const sf::Event& e)
{
    m_states.handleEvent(e);
}

void Game::handleMessage(const xy::Message& msg)
{
    switch (msg.id)
    {
    case xy::Message::Type::UIMessage:
    {
        auto& msgData = msg.getData<xy::Message::UIEvent>();
        switch (msgData.type)
        {
        case xy::Message::UIEvent::ResizedWindow:
            m_states.updateView();
            break;
        default: break;
        }
        break;
    }
    default: break;
    }

    m_states.handleMessage(msg);
}

void Game::registerStates()
{

}

void Game::updateApp(float dt)
{
    m_states.update(dt);
}

void Game::draw()
{
    m_states.draw();
}

void Game::finalise()
{
    m_states.clearStates();
    m_states.applyPendingChanges();
}

None of these things should be super surprising. The only things that are a little less obvious are the handleMessage(...) and finalise() methods. Don't worry about these for now - we'll need to add more to our Game class, and hopefully these methods will become clear as we move forward.

If you've read any of my other tutorials, you might have run into my Carousel class from the Walking Dead screensaver tutorial. I'll be using this carousel for the intro screen to add a nice fade in/fade out intro screen showing both the SFML and xygine project graphics. This is the first state we'll add to the state stack and register with the Game class. Here is the class delaration for the Intro state:


#ifndef INTROSTATE_HPP
#define INTROSTATE_HPP

#include <xygine/State.hpp>
#include <xygine/Resource.hpp>

#include <StateID.hpp>
#include <Carousel.hpp>

class IntroState final : public xy::State
{
    public:
        IntroState(xy::StateStack& stateStack, Context context);
        ~IntroState() = default;

        bool update(float dt) override;
        void draw() override;
        bool handleEvent(const sf::Event& evt) override;
        void handleMessage(const xy::Message&) override;
        xy::StateID stateID() const override
        {
            return States::ID::Intro;
        }

    private:
        float m_screenDuration;
        sf::Time m_duration;
        float m_effectDuration;
        Carousel m_carousel;
        xy::TextureResource m_textureResource;
};

#endif // INTROSTATE_HPP

Nice and simple for the first game state. As you can see, there is just the Carousel class and a few variables for housekeeping. The xy::TextureResource class is used to manage textures. Using it here was probably overkill, however textures are expensive resources and getting used to managing them is a good idea to keep things running smoothly. In short, this class ensures that you are not keeping more than one copy of each texture you are using. The stateID() method just returns the value from the Enum (found in StateID.hpp) that it represents - in this case States::ID::Intro.

In the implementation file for the Intro state, you can see that most of the action is in the constructor and in the update method. First, the constructor contains the first use of the texture resource manager, used to set the sprite texture that we add to the image carousel. In the update method, we just keep track of the ticks and update the carousel accordingly. Once the screen is up for 6 seconds it will be popped off the stack. Later, we'll go back in here and make sure to push the next state onto the stack.


#include <xygine/App.hpp>

#include <IntroState.hpp>

IntroState::IntroState(xy::StateStack& stateStack, Context context)
: State(stateStack, context)
, m_screenDuration(0.f)
, m_duration(sf::Time::Zero)
, m_effectDuration(3.f)
, m_carousel(getContext().renderWindow,{1.f,1.f},CarouselType::FADE,m_effectDuration)
{
    sf::Vector2f wsize = (sf::Vector2f) getContext().renderWindow.getSize();
    sf::Sprite spr;
    spr.setTexture(m_textureResource.get("images/sfml-logo-big.png"));
    spr.setOrigin(spr.getLocalBounds().width/2.f,spr.getLocalBounds().height/2.f);
    spr.setPosition(wsize/2.f);
    m_carousel.addItem(spr);

    spr.setTexture(m_textureResource.get("images/xygine.png"));
    spr.setOrigin(spr.getLocalBounds().width/2.f,spr.getLocalBounds().height/2.f);
    spr.setPosition(wsize/2.f);
    m_carousel.addItem(spr);
}

bool IntroState::update(float dt)
{
    m_screenDuration += dt;
    sf::Time time = sf::seconds(dt);
    m_carousel.update(time);

    if(m_duration.asSeconds() >= m_effectDuration)
    {
        m_carousel.autoScroll(CarouselMovement::FORWARD);
        m_duration = sf::Time::Zero;
    }
    else
        m_duration += time;

    if(m_screenDuration >= m_effectDuration*2.f)
    {
        requestStackPop();
    }
}

void IntroState::draw()
{
    auto& window = getContext().renderWindow;
    window.draw(m_carousel);
}

bool IntroState::handleEvent(const sf::Event& evt)
{

}

void IntroState::handleMessage(const xy::Message&)
{
    //there are no messages to handle...
}

Back in the Game.cpp file, we need to add this state to the registerStates() method:


void Game::registerStates()
{
    m_states.registerState<IntroState>(States::ID::Intro);
}

Then we need to make sure the registerStates() method is called in the initialise() method and push it onto the state stack:


void Game::initialise()
{
    getRenderWindow().setTitle("Total Hackage - F1: Console F2: Stats");
    registerStates();
    m_states.pushState(States::ID::Intro);
}

Next, we're going to need to set up a main menu. We'll utilize the xy::UI objects available in xygine and incorporate an xy::MessageBus as well to keep track of UI actions. Here is the class declaration:


#ifndef MENUSTATE_HPP
#define MENUSTATE_HPP

#include <xygine/State.hpp>
#include <xygine/Resource.hpp>
#include <xygine/ui/Container.hpp>

#include <SFML/Graphics/Sprite.hpp>

#include <StateID.hpp>

//forward declarations
namespace xy
{
    class MessageBus;
}

class MenuState final : public xy::State
{
public:
    MenuState(xy::StateStack&, Context);
    ~MenuState() = default;

    bool update(float) override;
    void draw() override;
    bool handleEvent(const sf::Event&) override;
    void handleMessage(const xy::Message&) override;
    xy::StateID stateID() const override
    {
        return States::ID::MenuMain;
    }

    void initialize();

private:
    xy::MessageBus& m_messages;
    xy::UI::Container m_ui;
    xy::TextureResource m_textures;
    xy::FontResource m_fonts;
    sf::Sprite  m_title;

};
#endif //MENUSTATE_HPP

The class definition is similar to the IntroState class, except there is additional setup of UI elements:


#include <xygine/App.hpp>
#include <xygine/ui/Button.hpp>
#include <xygine/util/Random.hpp>
#include <xygine/util/Position.hpp>

#include <SFML/Window/Mouse.hpp>

#include <MenuState.hpp>

MenuState::MenuState(xy::StateStack& stack, Context context)
: State(stack, context)
, m_messages(context.appInstance.getMessageBus())
, m_ui(m_messages)
, m_textures()
, m_fonts()
, m_title()
{

    initialize();

}

void MenuState::initialize()
{
    sf::Vector2f wsize = (sf::Vector2f) getContext().defaultView.getSize();
    sf::FloatRect wpos = getContext().defaultView.getViewport();
    const auto& font = m_fonts.get("fonts/FIXED_BO.ttf");

    //image made with following sites:
    //http://www.asciiworld.com/-Guns-.html
    //http://patorjk.com/software/taag/#p=display&f=Graffiti&t=Total%20Hackage
    m_title.setTexture(m_textures.get("images/title.png"));
    m_title.setScale(2.f,2.f);
    xy::Util::Position::centreOrigin(m_title);
    m_title.setPosition(wsize.x/2.f, wsize.y/2.f);

    auto button = std::make_shared<xy::UI::Button>(font, m_textures.get("images/button.png"));
    button->setText("Start");
    button->setAlignment(xy::UI::Alignment::Centre);
    button->setFontSize(16u);
    button->setPosition(wsize.x/2.f, wsize.y/3.f);
    button->addCallback([this]()
    {
        requestStackPop();
        //push game start state onto stack...
    });
    m_ui.addControl(button);

    button = std::make_shared<xy::UI::Button>(font, m_textures.get("images/button.png"));
    button->setText("Options");
    button->setAlignment(xy::UI::Alignment::Centre);
    button->setFontSize(16u);
    button->setPosition(wsize.x/2.f, wsize.y/2.f);
    button->addCallback([this]()
    {
        requestStackPop();
        requestStackPush(States::ID::Options);
    });
    m_ui.addControl(button);

    button = std::make_shared<xy::UI::Button>(font, m_textures.get("images/button.png"));
    button->setText("Quit");
    button->setAlignment(xy::UI::Alignment::Centre);
    button->setFontSize(16u);
    button->setPosition(wsize.x/2.f, 2*wsize.y/3.f);
    button->addCallback([this]()
    {
        requestStackClear();
        requestStackPush(States::ID::Intro);
    });
    m_ui.addControl(button);
}

bool MenuState::update(float dt)
{
    m_ui.update(dt);
    return false;
}

void MenuState::draw()
{
    auto& window = getContext().renderWindow;
    window.setView(getContext().defaultView);
    window.clear(sf::Color(100,0,0,255));

    window.draw(m_title);
    window.draw(m_ui);
}

bool MenuState::handleEvent(const sf::Event& e)
{
    const auto& window = getContext().renderWindow;
    auto mousePos = window.mapPixelToCoords(sf::Mouse::getPosition(window));

    m_ui.handleEvent(e, mousePos);

    return false;
}

void MenuState::handleMessage(const xy::Message& msg)
{

}

As you can tell, xy::UI::Container has a factory method for adding UI components (addControl(your UI control)). Buttons are constructed using std::make_shared, which makes perfect sense because all buttons share the same texture and font (at least for this game). Don't forget to update and draw the UI container. The xy::MessageBus was not used quite yet, but may be in the future. This code would be placed in the handleMessage(...) method.

Finally, we're going to add an OptionsState class to our state stack. The class declaration is somewhat similar to the MenuState class declaration. There are additional UI-related objects, and this time they do broadcast messages via the message bus. Because I pretty much copied this from the example demo and adapted it to my needs, I will only show the class declaration and discuss the important parts that I adapted in the definition:


/*********************************************************************
Matt Marchant 2014 - 2016
http://trederia.blogspot.com

xygine - Zlib license.

This software is provided 'as-is', without any express or
implied warranty. In no event will the authors be held
liable for any damages arising from the use of this software.

Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute
it freely, subject to the following restrictions:

1. The origin of this software must not be misrepresented;
you must not claim that you wrote the original software.
If you use this software in a product, an acknowledgment
in the product documentation would be appreciated but
is not required.

2. Altered source versions must be plainly marked as such,
and must not be misrepresented as being the original software.

3. This notice may not be removed or altered from any
source distribution.
*********************************************************************/

/*****************************************************************************
ADAPTED/ALTERED FOR THE PURPOSES OF THE TUTORIAL LOCATED AT
https://code.markrichards.ninja/sfml/top-down-shoot-em-up-mechanics-part-3
******************************************************************************/

#ifndef OPTIONSSTATE_HPP
#define OPTIONSSTATE_HPP

#include <StateID.hpp>

#include <xygine/State.hpp>
#include <xygine/Resource.hpp>
#include <xygine/ui/Container.hpp>
#include <xygine/ui/Window.hpp>

#include <SFML/Graphics/Sprite.hpp>
#include <SFML/Graphics/Text.hpp>

namespace xy
{
    class MessageBus;
}
class OptionsState final : public xy::State
{
public:
    OptionsState(xy::StateStack& stateStack, Context context);
    ~OptionsState() = default;

    bool update(float dt) override;
    void draw() override;
    bool handleEvent(const sf::Event& evt) override;
    void handleMessage(const xy::Message&) override;
    xy::StateID stateID() const override
    {
        return States::ID::Options;
    }
private:
    xy::MessageBus& m_messageBus;
    sf::Sprite m_menuSprite;
    sf::Sprite m_cursorSprite;
    std::vector<sf::Text> m_texts;

    xy::TextureResource m_textureResource;
    xy::FontResource m_fontResource;

    xy::UI::Window m_window;

    void buildMenu(const sf::Font&);
    void close();
};

#endif // OPTIONSSTATE_HPP

So, in keeping with the xygine demo, I reused what I could and really just re-skinned the UI elements and repositioned them. The font I used was a bit large, but this was not a problem because xygine provides similar functionality for making font adjustments like vanilla SFML. The options menu consists of options to set the resolution, enable full-screen mode, enable the controller for input, set the difficulty level, set the volume level, mute all sound, apply changes, and return to the main menu. This screen showcases a few UI elements that are handy and very easy to set up, including a slider, checkboxes, and selectors. Again, the code is part of the xygine demo - so take a look at it (MenuOptionState.cpp) and familiarize yourself with it. Also, don't forget to #include and register the two additional states in the Game class like so:


void Game::registerStates()
{
    m_states.registerState<IntroState>(States::ID::Intro);
    m_states.registerState<MenuState>(States::ID::MenuMain);
    m_states.registerState<OptionsState>(States::ID::Options);
}

In the next part of the series, we'll begin to create a scene, some entities for the scene, and lay down some basic functionality for Total Hackage.