Game loops and frames
Now talking a little more in-depth about the loop we have placed in the run()
function. This loop is most often called the
main loop or the game loop because it controls the lifetime of an application. As long as this one continues to iterate, the application will stay alive. In our case, we would like our application to terminate its execution as soon as the window ceases to exist.
Now what do we do during an iteration of this loop? First we process the events from the window, then we update the game, and finally we render the results on the screen. An iteration of the game loop is most often called a frame or a tick. You might have heard of the term frames per second (FPS). This is a measurement of how many loop iterations the game can do during a second. Sometimes, the concept of FPS only accounts for rendering times, but it is not unusual for it to encompass the input processing and logic updates as well.
We can explain this visually with a flow chart to further help you see clearly the logic of our loop.
It accurately describes what our application does at the moment. The only thing left out is the event processing. That functionality could have its own flow chart. But it does one task only in our basic example. It tells the window to close itself if the user requests it.
Now, the way we work with the computer in C++ is very linear. Everything is done in the set order that we give it, and the computer does nothing for us unless we explicitly tell it to. So, if we don't tell it to draw the circle, it won't draw it. If the state of the game somehow changes, and we don't tell the computer how to render a new frame, then nothing will change on the screen because the computer won't know that the graphics have changed.
Now that we got this sorted out, let's see if we can get something to happen over several frames. We make the circle move by pressing keys on our keyboard.
Input over several frames
First we have to be able to detect that the user is pressing down a key on his keyboard. SFML provides this functionality in several ways, but for now we will settle with input detection by responding to events.
What are events? The word itself implies something that is happening with our window, emitting a notice of the happening. As soon as the user somehow interacts with our window, the operating system sends an event that we can process. For our convenience, SFML translates events from the underlying operating systems to a uniform structure that we can use with ease: sf::Event
. Once the window internally detects that some kind of input has happened, it will store an
sf::Event
object containing information about that input. We will then poll all those events as fast as we can, in order to respond to them.
SFML supports a wide variety of events, but there are two event types that interest us here: sf::Event::KeyPressed
and sf::Event::KeyReleased
. They represent a key being pressed and released respectively.
So let's change our code so that it can handle this. We again poll the window for events, and have a case differentiation on the event type.
void Game::processEvents() { sf::Event event; while (mWindow.pollEvent(event)) { switch (event.type) {
For each time the while
loop iterates, it means a new event that was registered by the window is being handled. While there can be many different events, we will only check for some types of events, which are of our interest right now.
case sf::Event::KeyPressed: handlePlayerInput(event.key.code, true); break; case sf::Event::KeyReleased: handlePlayerInput(event.key.code, false); break; case sf::Event::Closed: mWindow.close(); break; } } }
In the handlePlayerInput()
function, we check which key on the keyboard has been pressed or released. To store this information, we use four Boolean member variables: mIsMovingUp
, mIsMovingDown
, mIsMovingLeft
, and mIsMovingRight
. We set the corresponding variable depending on the key being pressed or released.
void Game::handlePlayerInput(sf::Keyboard::Key key,bool isPressed) { if (key == sf::Keyboard::W) mIsMovingUp = isPressed; else if (key == sf::Keyboard::S) mIsMovingDown = isPressed; else if (key == sf::Keyboard::A) mIsMovingLeft = isPressed; else if (key == sf::Keyboard::D) mIsMovingRight = isPressed; }
In Game::handlePlayerInput()
we receive the enumerator describing the key that was pressed or released. The flag describing whether a press or release occurred is passed as the second argument. So we check what key the user is manipulating, and change our state depending on that.
Now, we have a way to perceive that the user is pressing a key. We know when we want to move up, down, left, and right. We know at each iteration of the main loop exactly, what the user wants; we just have to update the circle with a new position depending on this input. This method gives us a great advantage. So finally we can write something in our update()
function, namely, the movement of our player. We check which of the four Boolean member variables is true, and determine the movement accordingly. By using +=
(instead of =
) and if
(instead of else if
), we implicitly handle the case where two opposite keys, such as right and left are pressed at the same time—the movement
stays zero. The update()
function is shown in the following code snippet:
void Game::update() { sf::Vector2f movement(0.f, 0.f); if (mIsMovingUp) movement.y -= 1.f; if (mIsMovingDown) movement.y += 1.f; if (mIsMovingLeft) movement.x -= 1.f; if (mIsMovingRight) movement.x += 1.f; mPlayer.move(movement); }
We introduce two new things here: a vector and the move()
function on the circle shape. The move()
function does what its name says, it moves the shape by the amount we provide it.
Vector algebra
Vectors are an important part of algebraic mathematics. They imply lots of rules and definitions, which go beyond the scope of our book. However, SFML's sf::Vector2
class template is way more practical, both in concept and functionality. To be as simple as we could possibly be, we know that a coordinate in a two-dimensional Cartesian system would need two components: x
and y
. Because in graphics all coordinates are expressed with the decimal float
data type, sf::Vector2
is instantiated as sf::Vector2<float>
, which conveniently has a typedef named sf::Vector2f
. Such an object is made to contain two member variables, x
and y
. This makes our life simpler, because now we don't need to pass two variables to functions, as we can fit both in a single sf::Vector2f
object. sf::Vector2f
also defines common vector operations, such as additions and subtractions with other vectors, or multiplications and divisions with scalars (single values), effectively shortening our code.
To be a little more precise in the explanation, vectors are not only used to define positions, but they also are a perfect fit to define orientations. So, a vector is great to store a two-component coordinate, be it an absolute or relative position, or even to express a direction to follow, or to shoot a bullet towards. There is a little more to know about two-dimensional vectors, especially if they are directions, such as the concept of normalization or unit vector. This operation applies only to directions, as it makes no sense in positions. We consider a vector normalized if it has length one (hence the term unit vector) and the vector still expresses the same direction as before normalization. The following figure visualizes the vector (2, 3). This vector represents a translation of 2 units to the right and 3 units down.
Please do not confuse sf::Vector2f
with std::vector
. While their names are similar, the first refers to the mathematical concept; the latter is simply a dynamically allocated array from the standard C++ library.
In our case, our vector called
movement
expresses a movement from the origin of the current coordinate system. For us, this origin is the shape's position. It might be a bit tricky getting into the whole way of thinking in different spaces if you don't like math.
Vector algebra is very interesting, and definitely something very useful if you know it. So we recommend you study it. Mathematics is your friend as soon as you stop fighting it; it really makes a lot of things easier for you in programming. It is almost safe to claim that this subsection of math is the single most important topic when we need to implement gameplay mechanics. A wide range of problems that you will face in almost any kind of game are already solved and well-studied before, so you're better off learning this subject than reinventing the wheel every time. To avoid leaving you hanging, here's an example: Let's say you have point A and point B, which represent two characters in an action game. When the enemy at point A wants to shoot our player at point B, it needs to know the direction in which to shoot the projectile. Why waste your brains thinking on how to solve this problem if this field of math defines this operation as one of its most basic rules? All you need is to find the direction vector C, which is obtained by calculating B minus A. The difference between two positions gives us the direction between the two. Yes, that easy!
Frame-independent movement
If you run everything we have done so far, you will be able to move the circle, but it won't move uniformly. It will probably be very fast, because currently we have done the movement in a very naive way. Right now your computer will be running the update()
function as fast as it can, which means it will probably call it a couple of hundreds of times each second, if not more. If we move the shape by one pixel for every frame, this can count up to several 100 pixels every second, making our little player fly all over the screen. You cannot just change the movement
value to something lower, as it will only fix the problem for your computer. If you move to a slower or faster computer, the speed will change again.
So how do we solve this? Well, let's look at the problem we are facing. We are having a problem because our movement is frame-dependent. We want to provide the speed in a way that changes depending on the time a frame takes. There is a simple formula you should remember from your old school days. It's the formula that goes: distance = speed * time. Now why is this relevant for us? Because with this formula we can calculate a relevant speed for every frame, so that the circle always travels exactly the distance we want it to travel over one second, no matter what computer we are sitting on. So let's modify the function to what we actually need to make this work.
void Game::update(sf::Time deltaTime) { sf::Vector2f movement(0.f, 0.f); if (mIsMovingUp) movement.y -= PlayerSpeed; if (mIsMovingDown) movement.y += PlayerSpeed; if (mIsMovingLeft) movement.x -= PlayerSpeed; if (mIsMovingRight) movement.x += PlayerSpeed; mPlayer.move(movement * deltaTime.asSeconds()); }
The major difference we have made here is that we now receive a time value every time we call the update. We calculate the distance we want to travel every frame, depending on how much time has elapsed. We call the time that has elapsed since the last frame delta time (or time step), and
often abbreviate it as dt
in the code. But how do we get this time? We are lucky because SFML provides the utilities for it.
In SFML, there is a class that measures the time from when it was started. What we have to do is to measure the time each frame takes. We are talking about the class sf::Clock
. It has a function called restart(),
which lets the clock return the elapsed time since its start, and restarts the clock from zero, making it ideal for our current situation. SFML uses the class sf::Time
for all time formats; it is a convenient data type that can be converted from and to seconds, milliseconds, and microseconds. Here's the modified Game::run()
member function:
void Game::run() { sf::Clock clock; while (mWindow.isOpen()) { sf::Time deltaTime = clock.restart(); processEvents(); update(deltaTime); render(); } }
There is no big difference; we create a clock
, and in every frame we query it for its current elapsed time, restart the clock
, and then pass this time to the update function.
Fixed time steps
The solution we have come up with so far is sufficient for many cases. But it is not a perfect solution, because you can have problems in certain scenarios where delta times vary strongly. The code can be quite hard to debug, because it is impossible to get 100 percent reproducible results, since every frame is unique, and you can't guarantee that the delta time remains the same.
Consider that a frame may sometimes take three times the average delta time. This can lead to severe mistakes in the game logic, for example, when a player moves three times the distance and passes through a wall he would normally collide with. This is why physics engines expect the delta time to be fixed.
The following is a figure describing the problem we are referring to:
What we will do now is use a technique called fixed time steps. We write code that guarantees that in any circumstances, we always give the same delta time to the update function, no matter what happens. If you find that sounding difficult, there is no big difference from what we already have. We just have to do some book-keeping in our code for how much time has passed since we last called the update()
function.
void Game::run() { sf::Clock clock; sf::Time timeSinceLastUpdate = sf::Time::Zero; while (mWindow.isOpen()) { processEvents(); timeSinceLastUpdate += clock.restart(); while (timeSinceLastUpdate > TimePerFrame) { timeSinceLastUpdate -= TimePerFrame; processEvents(); update(TimePerFrame); } render(); } }
The actual effect of this change is that we accumulate how much time has elapsed in a variable timeSinceLastUpdate
. When we are over the required amount for one frame, we subtract the desired length of this frame (namely TimePerFrame
), and update the game. We do this until we are below the required amount again. This solves the problem with variable delta times, as we are guaranteed that the same amount of frames is always run. In the application you can download, the logic frame rate will be set to 60 frames per second by having the TimePerFrame
constant equal to sf::seconds(1.f / 60.f)
.
Eventually, we have two while
loops. The outer one is the game loop as we know it, and calls the render()
method. The inner one collects user input, and computes the game logic; this loop is executed at a constant rate. If rendering is slow, it may happen that
processEvents()
and
update()
are called multiple times before one render()
call. As a result, the game occasionally stutters, since, not every update is rendered, but the game doesn't slow down. On the other hand, fast rendering can lead to render()
being called multiple times without a logic update in between. Rendering the same state multiple times does not change anything on the screen, but it allows for techniques such as interpolations between two states to smoothen the game flow.
If you are interested in the topic, there is a famous article with detailed explanations at http://gafferongames.com/game-physics/fix-your-timestep.
Other techniques related to frame rates
SFML provides a few utilities that are worth knowing with respect to time handling and frame updates. One of them is
sf::sleep()
, a function that interrupts the execution for a given time, which gives the processor an opportunity to work on other tasks. Sleeping is not very accurate, so you should not use it for exact timing purposes. The method sf::RenderWindow::setFramerateLimit()
tries to achieve the specified frame rate by calling sf::sleep()
internally. It is a nice function for testing purposes, but it also lacks precision.
Another important technique is vertical synchronization, also known as V-Sync. Enabled V-Sync adapts the rate of graphical updates (calls of sf::RenderWindow::display()
) to the refresh rate of the monitor, usually around 60Hz. This can avoid graphical artifacts such as screen tearing, where a part of your window shows the old frame, and another the new one. You can enable or disable V-Sync using the method sf::RenderWindow::setVerticalSyncEnabled()
.