Creating a City Building Game with SFML Part 2: The First State

4 Daniel Mansfield Aug 8, 2014

Get started with this tutorial series here!

Now that we can change the state of the game we need a state to change to! Our game will use two states in this tutorial, GameStateStart and GameStateEditor. The first will be the main game menu and the second will be the game itself. For now the class definitions of each will be almost identical, but that will change soon. Both of these states will inherit from the base GameState class, and should be put in game_state_start.hpp and game_state_editor.hpp.

#ifndef GAME_STATE_START_HPP
#define GAME_STATE_START_HPP

#include <SFML/Graphics.hpp>

#include "game_state.hpp"

class GameStateStart : public GameState
{
    private:

    sf::View view;

    public:

    virtual void draw(const float dt);
    virtual void update(const float dt);
    virtual void handleInput();

    GameStateStart(Game* game);
};

#endif /* GAME_STATE_START_HPP */

For GameStateStart and

#ifndef GAME_STATE_EDITOR_HPP
#define GAME_STATE_EDITOR_HPP

#include <SFML/Graphics.hpp>

#include "game_state.hpp"

class GameStateEditor : public GameState
{
    private:

    sf::View gameView;
    sf::View guiView;

    public:

    virtual void draw(const float dt);
    virtual void update(const float dt);
    virtual void handleInput();

    GameStateEditor(Game* game);
};

#endif /* GAME_STATE_EDITOR_HPP */

For GameStateEditor, most of this should be pretty clear: we've overridden the pure virtual functions in the base class, and we've added constructors that take a pointer to the Game that created them (remember in GameState we need such a pointer). We've also added private sf::View variables. A view is a lot like a camera, and the window displays what all the cameras are seeing. Because we're making a 2D game, the world coordinates will be in pixels, but they're unbounded unlike screen coordinates! (Whereas screen coordinates may be between 0 and 1024 horizontally and 0 to 768 vertically.)

A view looks at a certain section of world coordinates, and then draws those in a certain place in the window. We can move the view around, scale it, and even rotate it, which allows us to change how the player views the world without changing the world itself. Whilst the default view that sf::Window creates would suffice in our GameStateStart class, we're going to make our own for clarity and consistency, since we'll be making two in GameStateEditor. As for why we're doing that, I'll be explained when we get there.

A view

Now let's make game_state_start.cpp. This is a longer file, so we'll put the source code in sections. First up are the headers and the draw and update functions.

#include <SFML/Graphics.hpp>

#include "game_state_start.hpp"
#include "game_state_editor.hpp"
#include "game_state.hpp"

void GameStateStart::draw(const float dt)
{
    this->game->window.setView(this->view);

    this->game->window.clear(sf::Color::Black);
    this->game->window.draw(this->game->background);

    return;
}

void GameStateStart::update(const float dt)
{
}

Nothing particularly new here, except for the setView and draw calls. setView does what it says; it changes the current view that will be draw to window (which we defined in GameState). We'll actually create that view in the constructor, but it will take up the entire window and everything in this state will be drawn to it. We then have the draw call, which again is an sf::Window member function. See how we're using game to get a pointer to that window? We could just as easily pass the window as an argument to the draw function in this case, but later on some of the virtual functions will have arguments that we didn't know about when we created the GameState class. And since we can't change the number of arguments in a virtual function, we'll have to use a mechanism like this anyway.

The draw call, unsurprisingly, draws its argument to the window's current view. In this case we're drawing something called background, which doesn't actually exist yet! We'll go back and create it after we finish this state. You can probably guess what it is though! With those rather boring functions done, we move on to the more interesting ones; handleInput and the constructor:

void GameStateStart::handleInput()
{
    sf::Event event;

    while(this->game->window.pollEvent(event))
    {
        switch(event.type)
        {
            /* Close the window */
            case sf::Event::Closed:
            {
                game->window.close();
                break;
            }
            /* Resize the window */
            case sf::Event::Resized:
            {
                this->view.setSize(event.size.width, event.size.height);
                this->game->background.setPosition(this->game->window.mapPixelToCoords(sf::Vector2i(0, 0)));
                this->game->background.setScale(
                    float(event.size.width) / float(this->game->background.getTexture()->getSize().x),
                    float(event.size.height) / float(this->game->background.getTexture()->getSize().y));
                break;
            }
            case sf::Event::KeyPressed:
            {
                if(event.key.code == sf::Keyboard::Escape) this->game->window.close();
                break;
            }
            default: break;
        }
    }

    return;
}

That's a whole lot of SFML code there! We'll start by examining how SFML processes input. When the user presses a key, moves the mouse, resizes the window or anything else that is an interaction with the program an event is triggered. These events store all the information that comes with the interaction, so a "button is pressed" event will contain which button has been pressed, and so on. To process these events we must use the pollEvent function, which is once again an sf::Window member function, and which takes a single sf::Event as an argument. pollEvent returns true whilst there are still events left to process, and false otherwise, so by placing it in a while loop we can process every event in sequence. We then use a switch statement on the type of the event in order to process it.

Here we have three events, a Closed event, a Resized event, and a KeyPressed event. When we see a Closed event we know that the user is trying to close the window, so we should let them do that by telling the window to close with the handy close function. When we see a KeyPressed event we check to see which key was pressed, and if it was the escape key we close the window.

Things are somewhat more complex with the Resized event. Usually when a window is resized, the view will continue to look at the same part of the game world but the window will change its size. This means that what the user sees will become stretched and distorted as they resize the window, which is not very visually appealing! To fix this, we change the view so that the number of pixels that the view can see is the same as the number of pixels the window displays. This way there is a 1:1 relationship between view size and window size and it doesn't become stretched. That's what we do with the setSize function.

We don't want this!

The problem with expanding the viewing area is that if we have a background image (background) we will quickly see an area of the screen that the background image does not cover. Because of this we need to expand the background, too. Of course, expanding the background has exactly the same problem as expanding the window: it'll be stretched! This isn't a problem for us though as our background is just a nice, smooth gradient. If we had an actual picture instead, then we would have to create the picture at a larger size and only show part of it, or just stop the user from resizing the window (add a third argument to window.create equal to sf::Style::Titlebar | sf::Style::Close).

Returning to the background, let's examine that setPosition call. Going from the inside to the outside, we first create an sf::Vector2i object. Unlike an std::vector<int>, an sf::Vector2i is a mathematical vector, and in this case a mathematical vector that can only take integer arguments and that has two dimensions. In other words, it's just the point (0, 0)! But what is that rather long function call next to it? Well as we said before window coordinates and world coordinates are different things. A ball placed at the point (320, 521) in the game world might be displayed at the point (220, 421) in the window due to our use of views.

What the mapPixelToCoords function does is convert a position in window coordinates to its equivalent position in world coordinates using the current view. By using that function we ensure that the user always sees the background in the same place, regardless of how much the view is moved and scaled. Note that setPosition requires an sf::Vector2f (which is what mapPixelToCoords returns) and not an sf::Vector2i (which is what mapPixelToCoords takes).

Lastly in the Resized event we have a setScale function called on the background. This just makes sure that the background takes up the entire window like we said before. The scale factor that we use will be different for each dimension (since the window is not necessarily scaled evenly) and is equal to the size of the window in that dimension divided by the size of the image in that dimension. Or to use SFML speak, the size of the background's texture. We'll discuss exactly what textures are soon.

GameStateStart::GameStateStart(Game* game)
{
    this->game = game;
    sf::Vector2f pos = sf::Vector2f(this->game->window.getSize());
    this->view.setSize(pos);
    pos *= 0.5f;
    this->view.setCenter(pos);
}

Lastly we have the constructor, which mostly sets up the view by setting its size to that of the window (awkwardly view.setSize takes an sf::Vector2f as an argument but window.getSize returns an sf::Vector2i, hence the typecasts). We then center the view on the, well, center of the window. sf::Vector2f and sf::Vector2i work just like mathematical vectors, and so we can multiply them by a scalar (int, float, double, and so on), and we can add and subtract them together.

If you aren't familiar with vectors, addition and subtraction is done component-wise, so (a,b) + (c,d) = (a+c,b+d), and scalar multiplication is a * (b, c) = (a*b, a*c). Since the middle is just halfway along both sides of the window, we multiply the size of the window by 0.5 to get the coordinates of the center. Why multiply by 0.5 instead of dividing by 2? No reason, I just prefer multiplication!

Before we move on, we have an important change to make in main.cpp

#include "game.hpp"
#include "game_state_start.hpp"

int main()
{
    Game game;

    game.pushState(new GameStateStart(&game));
    game.gameLoop();

    return 0;
}

First we include game_state_start.hpp so we can create a new state, and then once we've created the game we create a new GameStateStart state and push it to the stack, changing the state the game is in to the start state. Remember that we have to use new here, and don't fret about cleaning up the memory, Game does that for us. Next let's create GameStateEditor, or at least some of it. We've already created the header file, so all that's left is to write the source file. Until we create all of the game logic there isn't too much that we can do. Regardless, in game_state_editor.cpp add

#include <SFML/Graphics.hpp>

#include "game_state.hpp"
#include "game_state_editor.hpp"

void GameStateEditor::draw(const float dt)
{
    this->game->window.clear(sf::Color::Black);
    this->game->window.draw(this->game->background);

    return;
}

void GameStateEditor::update(const float dt)
{
    return;
}

void GameStateEditor::handleInput()
{
    sf::Event event;

    while(this->game->window.pollEvent(event))
    {
        switch(event.type)
        {
            /* Close the window */
            case sf::Event::Closed:
            {
                this->game->window.close();
                break;
            }
            /* Resize the window */
            case sf::Event::Resized:
            {
                gameView.setSize(event.size.width, event.size.height);
                guiView.setSize(event.size.width, event.size.height);
                this->game->background.setPosition(this->game->window.mapPixelToCoords(sf::Vector2i(0, 0), this->guiView));
                this->game->background.setScale(
                    float(event.size.width) / float(this->game->background.getTexture()->getSize().x), 
                    float(event.size.height) / float(this->game->background.getTexture()->getSize().y));
                break;
            }
            default: break;
        }
    }

    return;
}

GameStateEditor::GameStateEditor(Game* game)
{
    this->game = game;
    sf::Vector2f pos = sf::Vector2f(this->game->window.getSize());
    this->guiView.setSize(pos);
    this->gameView.setSize(pos);
    pos *= 0.5f;
    this->guiView.setCenter(pos);
    this->gameView.setCenter(pos);
}

As you can see this is almost identical to game_state_start.cpp, the only difference being that GameStateEditor handles two different views, and not one. Why does it have two if they're identical? Well we're going to draw our game world and move around it in this state, but we'll also want to display information about the city; a HUD, essentially. We'll want those to stay in the same position regardless of where we've moved the game camera, and so we'll need two views. Moving back to GameStateStart we need to add the code to transition from that state to this one. Declare a private loadgame function in the class definition

class GameStateStart : public GameState
{
    private:

    sf::View view;

    void loadgame();

    public:

And then define the function in game_state_start.cpp

void GameStateStart::loadgame()
{
    this->game->pushState(new GameStateEditor(this->game));

    return;
}

This is identical code to how we started the game in main, but this time we're creating a new GameStateEditor and not a new GameStateStart. Finally, we need some way to call the loadgame function. We could do this with a simple spacebar keypress event, exactly like we did with the escape keypress event

case sf::Event::KeyPressed:
{
    if(event.key.code == sf::Keyboard::Escape) this->game->window.close();
    else if(event.key.code == sf::Keyboard::Space) this->loadgame();
    break;
}
default: break;

Sadly the code won't compile just yet, as we haven't defined background, but if you were to comment out those lines then compiling the program and pressing space should take you to the second state! It'll be rather hard to tell of course, the only real difference is that the escape key won't close GameStateEditor.

Source code for this section

4 comments


Or enter your name and Email
  • IS inyeol sohn 2 years ago
    i had too add change resize event as following for the resize to work correctly case sf::Event::Resized: { //some more codes this->view.setCenter(event.size.width / 2, event.size.height / 2); //rest of the codes }
    • IS inyeol sohn 2 years ago
      i had to add another line* just woke up and didn't realize what i was writing
  • J Jananton 3 years ago
    Hmm, although one only finds this out after adding the background sprite in the next lesson the window resizing doesn't work as advertised, at least in SFML 2.2 and VC++ Express 2010. I keep getting black borders when enlarging or shrinking the window. I've checked the code here as well as in the master.zip and there's no difference at all, so, any advice?
    • G Gurvan 3 years ago
      I solved it a this->view in mapPixelToCoords in game_state_start.cpp in the method handleInput(). The line is this->game->background.setPosition(this->game->window.mapPixelToCoords(sf::Vector2i(0, 0), this->view)); It was this->game->background.setPosition(this->game->window.mapPixelToCoords(sf::Vector2i(0, 0))); in the original code.