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.
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.
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
.
Author: Daniel Mansfield