26 min read

In this article by Maxime Barbier, author of the book SFML Blueprints, we will add physics into this game and turn it into a new one. By doing this, we will learn:

  • What is a physics engine
  • How to install and use the Box2D library
  • How to pair the physics engine with SFML for the display
  • How to add physics in the game

In this article, we will learn the magic of physics. We will also do some mathematics but relax, it’s for conversion only. Now, let’s go!

(For more resources related to this topic, see here.)

A physics engine – késako?

We will speak about physics engine, but the first question is “what is a physics engine?” so let’s explain it.

A physics engine is a software or library that is able to simulate Physics, for example, the Newton-Euler equation that describes the movement of a rigid body. A physics engine is also able to manage collisions, and some of them can deal with soft bodies and even fluids.

There are different kinds of physics engines, mainly categorized into real-time engine and non-real-time engine. The first one is mostly used in video games or simulators and the second one is used in high performance scientific simulation, in the conception of special effects in cinema and animations.

As our goal is to use the engine in a video game, let’s focus on real-time-based engine. Here again, there are two important types of engines. The first one is for 2D and the other for 3D. Of course you can use a 3D engine in a 2D world, but it’s preferable to use a 2D engine for an optimization purpose. There are plenty of engines, but not all of them are open source.

3D physics engines

For 3D games, I advise you to use the Bullet physics library. This was integrated in the Blender software, and was used in the creation of some commercial games and also in the making of films. This is a really good engine written in C/C++ that can deal with rigid and soft bodies, fluids, collisions, forces… and all that you need.

2D physics engines

As previously said, in a 2D environment, you can use a 3D physics engine; you just have to ignore the depth (Z axes). However, the most interesting thing is to use an engine optimized for the 2D environment. There are several engines like this one and the most famous ones are Box2D and Chipmunk. Both of them are really good and none of them are better than the other, but I had to make a choice, which was Box2D. I’ve made this choice not only because of its C++ API that allows you to use overload, but also because of the big community involved in the project.

Physics engine comparing game engine

Do not mistake a physics engine for a game engine. A physics engine only simulates a physical world without anything else. There are no graphics, no logics, only physics simulation. On the contrary, a game engine, most of the time includes a physics engine paired with a render technology (such as OpenGL or DirectX). Some predefined logics depend on the goal of the engine (RPG, FPS, and so on) and sometimes artificial intelligence. So as you can see, a game engine is more complete than a physics engine. The two mostly known engines are Unity and Unreal engine, which are both very complete. Moreover, they are free for non-commercial usage.

So why don’t we directly use a game engine? This is a good question. Sometimes, it’s better to use something that is already made, instead of reinventing it. However, do we really need all the functionalities of a game engine for this project? More importantly, what do we need it for? Let’s see the following:

  • A graphic output
  • Physics engine that can manage collision

Nothing else is required. So as you can see, using a game engine for this project would be like killing a fly with a bazooka. I hope that you have understood the aim of a physics engine, the differences between a game and physics engine, and the reason for the choices made for the project.

Using Box2D

As previously said, Box2D is a physics engine. It has a lot of features, but the most important for the project are the following (taken from the Box2D documentation):

  • Collision: This functionality is very interesting as it allows our tetrimino to interact with each other
    • Continuous collision detection
    • Rigid bodies (convex polygons and circles)
    • Multiple shapes per body
  • Physics: This functionality will allow a piece to fall down and more
    • Continuous physics with the time of impact solver
    • Joint limits, motors, and friction
    • Fairly accurate reaction forces/impulses

As you can see, Box2D provides all that we need in order to build our game. There are a lot of other features usable with this engine, but they don’t interest us right now so I will not describe them in detail. However, if you are interested, you can take a look at the official website for more details on the Box2D features (http://box2d.org/about/).

It’s important to note that Box2D uses meters, kilograms, seconds, and radians for the angle as units; SFML uses pixels, seconds, and degrees. So we will need to make some conversions. I will come back to this later.

Preparing Box2D

Now that Box2D is introduced, let’s install it. You will find the list of available versions on the Google code project page at https://code.google.com/p/box2d/downloads/list. Currently, the latest stable version is 2.3. Once you have downloaded the source code (from compressed file or using SVN), you will need to build it.

Install

Once you have successfully built your Box2D library, you will need to configure your system or IDE to find the Box2D library and headers. The newly built library can be found in the /path/to/Box2D/build/Box2D/ directory and is named libBox2D.a. On the other hand, the headers are located in the path/to/Box2D/Box2D/ directory. If everything is okay, you will find a Box2D.h file in the folder.

On Linux, the following command adds Box2D to your system without requiring any configuration:

sudo make install

Pairing Box2D and SFML

Now that Box2D is installed and your system is configured to find it, let’s build the physics “hello world”: a falling square.

It’s important to note that Box2D uses meters, kilograms, seconds, and radian for angle as units; SFML uses pixels, seconds, and degrees. So we will need to make some conversions.

Converting radians to degrees or vice versa is not difficult, but pixels to meters… this is another story. In fact, there is no way to convert a pixel to meter, unless if the number of pixels per meter is fixed. This is the technique that we will use.

So let’s start by creating some utility functions. We should be able to convert radians to degrees, degrees to radians, meters to pixels, and finally pixels to meters. We will also need to fix the pixel per meter value. As we don’t need any class for these functions, we will define them in a namespace converter. This will result as the following code snippet:

namespace converter
{
   constexpr double PIXELS_PER_METERS = 32.0;
   constexpr double PI = 3.14159265358979323846;
 
   template<typename T>
   constexpr T pixelsToMeters(const T& x){return x/PIXELS_PER_METERS;};
 
   template<typename T>
   constexpr T metersToPixels(const T& x){return x*PIXELS_PER_METERS;};
 
   template<typename T>
   constexpr T degToRad(const T& x){return PI*x/180.0;};
 
   template<typename T>
   constexpr T radToDeg(const T& x){return 180.0*x/PI;}
}

As you can see, there is no difficulty here. We start to define some constants and then the convert functions. I’ve chosen to make the function template to allow the use of any number type. In practice, it will mostly be double or int. The conversion functions are also declared as constexpr to allow the compiler to calculate the value at compile time if it’s possible (for example, with constant as a parameter). It’s interesting because we will use this primitive a lot.

Box2D, how does it work?

Now that we can convert SFML unit to Box2D unit and vice versa, we can pair Box2D with SFML. But first, how exactly does Box2D work?

Box2D works a lot like a physics engine:

  1. You start by creating an empty world with some gravity.
  2. Then, you create some object patterns. Each pattern contains the shape of the object position, its type (static or dynamic), and some other characteristics such as its density, friction, and energy restitution.
  3. You ask the world to create a new object defined by the pattern.
  4. In each game loop, you have to update the physical world with a small step such as our world in the games we’ve already made.

Because the physics engine does not display anything on the screen, we will need to loop all the objects and display them by ourselves.

Let’s start by creating a simple scene with two kinds of objects: a ground and square. The ground will be fixed and the squares will not. The square will be generated by a user event: mouse click.

This project is very simple, but the goal is to show you how to use Box2D and SFML together with a simple case study. A more complex one will come later.

We will need three functionalities for this small project to:

  • Create a shape
  • Display the world
  • Update/fill the world

Of course there is also the initialization of the world and window. Let’s start with the main function:

  1. As always, we create a window for the display and we limit the FPS number to 60. I will come back to this point with the displayWorld function.
  2. We create the physical world from Box2D, with gravity as a parameter.
  3. We create a container that will store all the physical objects for the memory clean purpose.
  4. We create the ground by calling the createBox function (explained just after).
  5. Now it is time for the minimalist game loop:
    •    Close event managements
    •    Create a box by detecting that the right button of the mouse is pressed
  6. Finally, we clean the memory before exiting the program:
int main(int argc,char* argv[])
{
   sf::RenderWindow window(sf::VideoMode(800, 600, 32), "04_Basic");
   window.setFramerateLimit(60);
   b2Vec2 gravity(0.f, 9.8f);
   b2World world(gravity);
   std::list<b2Body*> bodies;
   bodies.emplace_back(book::createBox(world,400,590,800,20,b2_staticBody));
 
   while(window.isOpen()) {
       sf::Event event;
       while(window.pollEvent(event)) {
           if (event.type == sf::Event::Closed)
               window.close();
       }
       if (sf::Mouse::isButtonPressed(sf::Mouse::Left)) {
           int x = sf::Mouse::getPosition(window).x;
           int y = sf::Mouse::getPosition(window).y;
           bodies.emplace_back(book::createBox(world,x,y,32,32));
       }
       displayWorld(world,window);
   }
 
   for(b2Body* body : bodies) {
       delete static_cast<sf::RectangleShape*>(body->GetUserData());
       world.DestroyBody(body);
   }
   return 0;
}

For the moment, except the Box2D world, nothing should surprise you so let’s continue with the box creation.

This function is under the book namespace.

b2Body* createBox(b2World& world,int pos_x,int pos_y, int size_x,int size_y,b2BodyType type = b2_dynamicBody)
{
   b2BodyDef bodyDef;
   bodyDef.position.Set(converter::pixelsToMeters<double>(pos_x),
                        converter::pixelsToMeters<double>(pos_y));
   bodyDef.type = type;
   b2PolygonShape b2shape;
   b2shape.SetAsBox(converter::pixelsToMeters<double>(size_x/2.0),
                   converter::pixelsToMeters<double>(size_y/2.0));
 
   b2FixtureDef fixtureDef;
   fixtureDef.density = 1.0;
   fixtureDef.friction = 0.4;
   fixtureDef.restitution= 0.5;
   fixtureDef.shape = &b2shape;
 
   b2Body* res = world.CreateBody(&bodyDef);
   res->CreateFixture(&fixtureDef);
 
   sf::Shape* shape = new sf::RectangleShape(sf::Vector2f(size_x,size_y));
   shape->setOrigin(size_x/2.0,size_y/2.0);
   shape->setPosition(sf::Vector2f(pos_x,pos_y));
                                              
   if(type == b2_dynamicBody)
       shape->setFillColor(sf::Color::Blue);
   else
       shape->setFillColor(sf::Color::White);
 
   res->SetUserData(shape);
 
   return res;
}

This function contains a lot of new functionalities. Its goal is to create a rectangle of a specific size at a predefined position. The type of this rectangle is also set by the user (dynamic or static). Here again, let’s explain the function step-by-step:

  1. We create b2BodyDef. This object contains the definition of the body to create. So we set the position and its type. This position will be in relation to the gravity center of the object.
  2. Then, we create b2Shape. This is the physical shape of the object, in our case, a box. Note that the SetAsBox() method doesn’t take the same parameter as sf::RectangleShape. The parameters are half the size of the box. This is why we need to divide the values by two.
  3. We create b2FixtureDef and initialize it. This object holds all the physical characteristics of the object such as its density, friction, restitution, and shape.
  4. Then, we properly create the object in the physical world.
  5. Now, we create the display of the object. This will be more familiar because we will only use SFML. We create a rectangle and set its position, origin, and color.
  6. As we need to associate and display SFML object to the physical object, we use a functionality of Box2D: the SetUserData() function. This function takes void* as a parameter and internally holds it. So we use it to keep track of our SFML shape.
  7. Finally, the body is returned by the function. This pointer has to be stored to clean the memory later. This is the reason for the body’s container in main().

Now, we have the capability to simply create a box and add it to the world. Now, let’s render it to the screen. This is the goal of the displayWorld function:

void displayWorld(b2World& world,sf::RenderWindow& render)
{
   world.Step(1.0/60,int32(8),int32(3));
   render.clear();
   for (b2Body* body=world.GetBodyList(); body!=nullptr; body=body->GetNext())
   {  
       sf::Shape* shape = static_cast<sf::Shape*>(body->GetUserData());
       shape->setPosition(converter::metersToPixels(body->GetPosition().x),
       converter::metersToPixels(body->GetPosition().y));
       shape->setRotation(converter::radToDeg<double>(body->GetAngle()));
       render.draw(*shape);
   }
   render.display();
}

This function takes the physics world and window as a parameter. Here again, let’s explain this function step-by-step:

  1. We update the physical world. If you remember, we have set the frame rate to 60. This is why we use 1,0/60 as a parameter here. The two others are for precision only. In a good code, the time step should not be hardcoded as here. We have to use a clock to be sure that the value will always be the same. Here, it has not been the case to focus on the important part: physics. We reset the screen, as usual.
  2. Here is the new part: we loop the body stored by the world and get back the SFML shape. We update the SFML shape with the information taken from the physical body and then render it on the screen.
  3. Finally, we render the result on the screen.

    SFML Blueprints

As you can see, it’s not really difficult to pair SFML with Box2D. It’s not a pain to add it. However, we have to take care of the data conversion. This is the real trap. Pay attention to the precision required (int, float, double) and everything should be fine.

Now that you have all the keys in hand, let’s build a real game with physics.

Adding physics to a game

Now that Box2D is introduced with a basic project, let’s focus on the real one. We will modify our basic Tetris to get Gravity-Tetris alias Gravitris. The game control will be the same as in Tetris, but the game engine will not be. We will replace the board with a real physical engine.

With this project, we will reuse a lot of work previously done. As already said, the goal of some of our classes is to be reusable in any game using SFML. Here, this will be made without any difficulties as you will see. The classes concerned are those you deal with user event Action, ActionMap, ActionTarget—but also Configuration and ResourceManager. There are still some changes that will occur in the Configuration class, more precisely, in the enums and initialization methods of this class because we don’t use the exact same sounds and events that were used in the Asteroid game. So we need to adjust them to our needs.

Enough with explanations, let’s do it with the following code:

class Configuration
{
   public:
       Configuration() = delete;
       Configuration(const Configuration&) = delete;
       Configuration& operator=(const Configuration&) = delete;
      
       enum Fonts : int {Gui};
       static ResourceManager<sf::Font,int> fonts;
      
       enum PlayerInputs : int { TurnLeft,TurnRight, MoveLeft, MoveRight,HardDrop};
       static ActionMap<int> playerInputs;
      
       enum Sounds : int {Spawn,Explosion,LevelUp,};
       static ResourceManager<sf::SoundBuffer,int> sounds;
      
       enum Musics : int {Theme};
       static ResourceManager<sf::Music,int> musics;
      
       static void initialize();
      
   private:
       static void initTextures();
       static void initFonts();
       static void initSounds();
       static void initMusics();
       static void initPlayerInputs();
};

As you can see, the changes are in the enum, more precisely in Sounds and PlayerInputs. We change the values into more adapted ones to this project. We still have the font and music theme. Now, take a look at the initialization methods that have changed:

void Configuration::initSounds()
{
   sounds.load(Sounds::Spawn,"media/sounds/spawn.flac");
   sounds.load(Sounds::Explosion,"media/sounds/explosion.flac");
   sounds.load(Sounds::LevelUp,"media/sounds/levelup.flac");
}
void Configuration::initPlayerInputs()
{
   playerInputs.map(PlayerInputs::TurnRight,Action(sf::Keyboard::Up));
   playerInputs.map(PlayerInputs::TurnLeft,Action(sf::Keyboard::Down));
   playerInputs.map(PlayerInputs::MoveLeft,Action(sf::Keyboard::Left));
   playerInputs.map(PlayerInputs::MoveRight,Action(sf::Keyboard::Right));
 playerInputs.map(PlayerInputs::HardDrop,Action(sf::Keyboard::Space,
   Action::Type::Released));
}

No real surprises here. We simply adjust the resources to our needs for the project. As you can see, the changes are really minimalistic and easily done. This is the aim of all reusable modules or classes. Here is a piece of advice, however: keep your code as modular as possible, this will allow you to change a part very easily and also to import any generic part of your project to another one easily.

The Piece class

Now that we have the configuration class done, the next step is the Piece class. This class will be the most modified one. Actually, as there is too much change involved, let’s build it from scratch. A piece has to be considered as an ensemble of four squares that are independent from one another. This will allow us to split a piece at runtime. Each of these squares will be a different fixture attached to the same body, the piece.

We will also need to add some force to a piece, especially to the current piece, which is controlled by the player. These forces can move the piece horizontally or can rotate it.

Finally, we will need to draw the piece on the screen.

The result will show the following code snippet:

constexpr int BOOK_BOX_SIZE = 32;
constexpr int BOOK_BOX_SIZE_2 = BOOK_BOX_SIZE / 2;
class Piece : public sf::Drawable
{
   public:
       Piece(const Piece&) = delete;
       Piece& operator=(const Piece&) = delete;
 
       enum TetriminoTypes {O=0,I,S,Z,L,J,T,SIZE};
       static const sf::Color TetriminoColors[TetriminoTypes::SIZE];
 
       Piece(b2World& world,int pos_x,int pos_y,TetriminoTypes type,float rotation);
       ~Piece();
       void update();
       void rotate(float angle);
       void moveX(int direction);
       b2Body* getBody()const;
 
   private:
       virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const override;
       b2Fixture* createPart((int pos_x,int pos_y,TetriminoTypes type); ///< position is relative to the piece int the matrix coordinate (0 to 3)
       b2Body * _body;
       b2World& _world;
};

Some parts of the class don’t change such as the TetriminoTypes and TetriminoColors enums. This is normal because we don’t change any piece’s shape or colors. The rest is still the same.

The implementation of the class, on the other side, is very different from the precedent version. Let's see it:
Piece::Piece(b2World& world,int pos_x,int pos_y,TetriminoTypes type,float rotation) : _world(world)
{
   b2BodyDef bodyDef;
   bodyDef.position.Set(converter::pixelsToMeters<double>(pos_x),
   converter::pixelsToMeters<double>(pos_y));
   bodyDef.type = b2_dynamicBody;
   bodyDef.angle = converter::degToRad(rotation);
   _body = world.CreateBody(&bodyDef);
 
   switch(type)
   {
       case TetriminoTypes::O : {
           createPart((0,0,type); createPart((0,1,type);
           createPart((1,0,type); createPart((1,1,type);
       }break;
       case TetriminoTypes::I : {
           createPart((0,0,type); createPart((1,0,type);
            createPart((2,0,type); createPart((3,0,type);
       }break;
       case TetriminoTypes::S : {
           createPart((0,1,type); createPart((1,1,type);
           createPart((1,0,type); createPart((2,0,type);
       }break;
       case TetriminoTypes::Z : {
           createPart((0,0,type); createPart((1,0,type);
           createPart((1,1,type); createPart((2,1,type);
       }break;
       case TetriminoTypes::L : {
           createPart((0,1,type); createPart((0,0,type);
           createPart((1,0,type); createPart((2,0,type);
       }break;
       case TetriminoTypes::J : {
           createPart((0,0,type); createPart((1,0,type);
           createPart((2,0,type); createPart((2,1,type);
       }break;
       case TetriminoTypes::T : {
           createPart((0,0,type); createPart((1,0,type);
           createPart((1,1,type); createPart((2,0,type);
       }break;
       default:break;
   }
   body->SetUserData(this);
   update();
}

The constructor is the most important method of this class. It initializes the physical body and adds each square to it by calling createPart(). Then, we set the user data to the piece itself. This will allow us to navigate through the physics to SFML and vice versa. Finally, we synchronize the physical object to the drawable by calling the update() function:

Piece::~Piece()
{
   for(b2Fixture* fixture=_body->GetFixtureList();fixture!=nullptr;
   fixture=fixture->GetNext())
{
       sf::ConvexShape* shape = static_cast<sf::ConvexShape*>(fixture->GetUserData());
       fixture->SetUserData(nullptr);
       delete shape;
   }
   _world.DestroyBody(_body);
}

The destructor loop on all the fixtures attached to the body, destroys all the SFML shapes and then removes the body from the world:

b2Fixture* Piece::createPart((int pos_x,int pos_y,TetriminoTypes type)
{
   b2PolygonShape b2shape;
   b2shape.SetAsBox(converter::pixelsToMeters<double>(BOOK_BOX_SIZE_2),
   converter::pixelsToMeters<double>(BOOK_BOX_SIZE_2)
   ,b2Vec2(converter::pixelsToMeters<double>(BOOK_BOX_SIZE_2+(pos_x*BOOK_BOX_SIZE)),
converter::pixelsToMeters<double>(BOOK_BOX_SIZE_2+(pos_y*BOOK_BOX_SIZE))),0);
 
   b2FixtureDef fixtureDef;
   fixtureDef.density = 1.0;
   fixtureDef.friction = 0.5;
   fixtureDef.restitution= 0.4;
   fixtureDef.shape = &b2shape;
 
   b2Fixture* fixture = _body->CreateFixture(&fixtureDef);
 
   sf::ConvexShape* shape = new sf::ConvexShape((unsigned int) b2shape.GetVertexCount());
   shape->setFillColor(TetriminoColors[type]);
   shape->setOutlineThickness(1.0f);
   shape->setOutlineColor(sf::Color(128,128,128));
   fixture->SetUserData(shape);
  
   return fixture;
}

This method adds a square to the body at a specific place. It starts by creating a physical shape as the desired box and then adds this to the body. It also creates the SFML square that will be used for the display, and it will attach this as user data to the fixture. We don’t set the initial position because the constructor will do it.

void Piece::update()
{
   const b2Transform& xf = _body->GetTransform();
  
   for(b2Fixture* fixture = _body->GetFixtureList(); fixture != nullptr;
   fixture=fixture->GetNext())
{
       sf::ConvexShape* shape = static_cast<sf::ConvexShape*>(fixture->GetUserData());
       const b2PolygonShape* b2shape = static_cast<b2PolygonShape*>(fixture->GetShape());
       const uint32 count = b2shape->GetVertexCount();
       for(uint32 i=0;i<count;++i)
{
           b2Vec2 vertex = b2Mul(xf,b2shape->m_vertices[i]);
           shape->setPoint(i,sf::Vector2f(converter::metersToPixels(vertex.x),
           converter::metersToPixels(vertex.y)));
       }
   }
}

This method synchronizes the position and rotation of all the SFML shapes from the physical position and rotation calculated by Box2D. Because each piece is composed of several parts—fixture—we need to iterate through them and update them one by one.

void Piece::rotate(float angle) {
   body->ApplyTorque((float32)converter::degToRad(angle),true);
}
void Piece::moveX(int direction) {
   body->ApplyForceToCenter(b2Vec2(converter::pixelsToMeters(direction),0),true);
}

These two methods add some force to the object to move or rotate it. We forward the job to the Box2D library.

b2Body* Piece::getBody()const {return _body;}
 
void Piece::draw(sf::RenderTarget& target, sf::RenderStates states) const
{
   for(const b2Fixture* fixture=_body->GetFixtureList();fixture!=nullptr; fixture=fixture->GetNext())
{
       sf::ConvexShape* shape = static_cast<sf::ConvexShape*>(fixture->GetUserData());
       if(shape)
           target.draw(*shape,states);
   }
}

This function draws the entire piece. However, because the piece is composed of several parts, we need to iterate on them and draw them one by one in order to display the entire piece. This is done by using the user data saved in the fixtures.

Summary

Since the usage of a physics engine has its own particularities such as the units and game loop, we have learned how to deal with them. Finally, we learned how to pair Box2D with SFML, integrate our fresh knowledge to our existing Tetris project, and build a new funny game.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here