SFML Integration
learn how to combine SFML and Box2D
Source Code
- The code for this tutorial can be found on Github : Learn Box2D – SFML Integration
Integrate SFML and Box2D
In this tutorial you will learn how to combine Box2D and SFML, this will allows you to display the physical world of Box2D inside a nice window. The image below shows the result we’ll obtain. To be able to follow along you should read the SFML Tutorial, at least till Simple Shapes.
Step 1 : Create a Simple Scene
The first step is to create a simple Scene using our custom SFML Engine : Improved Engine v0.2. Let’s call that scene Box2DScene. There is nothing extraordinary here, if you’ve followed the SFML tutorial, you should be able to understand the code below.
//////////////////////////////////////////////////////////// // Nero Game Engine - Box2D Tutorials //////////////////////////////////////////////////////////// #ifndef BOX2DSCENE_H_INCLUDED #define BOX2DSCENE_H_INCLUDED ///////////////////////////HEADERS////////////////////////// //////////////////////////////////////////////////////////// class Box2DScene : public ng::Scene { public: typedef std::unique_ptr<Box2DScene> ptr; Box2DScene() { setSceneName("Box2D Scene v0.1"); } };
Step 2 : Create the Physical World and a Body
In the previous tutorial First Solid Body, you’ve learned how to create a physical world and a solid box. In this section, we are going to do the exact same thing but inside our new Box2DScene class.
- First : Create a physical world
//Box2D #include <Box2D/Box2D.h> //////////////////////////////////////////////////////////// class Box2DScene : public ng::Scene { private: b2World mPhysicWorld; public: typedef std::unique_ptr<Box2DScene> ptr; Box2DScene(): mPhysicWorld(b2Vec2(0.f, 9.8f)) { setSceneName("Box2D Scene v0.1"); } void init() { //print the physical world mPhysicWorld.Dump(); } };
- Second : Create a solid box
Using the exact same code as in the previous tutorial we create a solid box inside the Init() method. See the code below.
void init() { //create a rigid body createBody(); //print the physical world mPhysicWorld.Dump(); } void createBody() { //create an empty body b2BodyDef bodyDef; bodyDef.position = b2Vec2(0.f, 0.f); bodyDef.angle = 0.f; bodyDef.allowSleep = true; bodyDef.fixedRotation = false; bodyDef.gravityScale = 1.f; bodyDef.type = b2_dynamicBody; b2Body* boxBody = mPhysicWorld.CreateBody(&bodyDef); //create a box shape b2PolygonShape boxShape; boxShape.SetAsBox(1, 1); //values are in meter not pixel //create a fixture and provide the shape to the body b2FixtureDef fixtureDef; fixtureDef.shape = &boxShape; fixtureDef.isSensor = false; fixtureDef.density = 1.f; fixtureDef.friction = 0.1f; fixtureDef.restitution = 0.1f; boxBody->CreateFixture(&fixtureDef); //clean everything boxBody = nullptr; }
Step 3 : Debug Draw Callback Class
Till now, we have a black window, but that’s about to change. As learned in Physics World, the class b2World allows you to register four (4) callback classes, one of them is called Debug Draw. Using a Debug Draw class, Box2D can draw its internal state. Since Box2D can be used with multiple graphics libraries like SFML, SDL, OpenGL etc, the exact way the internal state is drawn is not part of Box2D, you have to implement it yourself.
The pseudo-code below shows how Debug Draw works :
- First, you create your own Debug Draw class by inheriting b2Draw.
- Then, you can configure your debug draw class and provide it to the physical world using the method SetDebugDraw().
- Finally, inside your game loop, during the render phase, you call the physical world method DrawDebugData().
//Step 1 : create your own debug draw class class MyDrawingClass : public b2Draw { //implement your drawing methods here }; MyDrawingClass myDrawingClass; //Step 2 : configure the debug draw class myDrawingClass.SetFlags(/***/) //Step 3 : provide the debug draw class to the physical world physicWorld.SetDebugDraw(myDrawingClass); //Step 4 : inside your game loop, render the physical world physicWorld.DrawDebugData();
Inside our Debug Draw class, we have to implement eight (8) methods. Six (6) of them are from the class b2Draw and their implementation is mandatory because they are declared virtual void. The two (2) others are our own addition, they won’t be useful now but is good to have them for later. The latest version of the Box2D Testbed on Github adds another two (2) methods to draw texts.
If you’ve read the SFML tutorial Simple Shapes you should be able to implement those methods yourself by adjusting the source code of the Box2D Testbed on Github.
//b2Draw methods to override void DrawPolygon(const b2Vec2* vertices, int32 vertexCount, const b2Color& color) override; void DrawSolidPolygon(const b2Vec2* vertices, int32 vertexCount, const b2Color& color) override; void DrawCircle(const b2Vec2& center, float radius, const b2Color& color) override; void DrawSolidCircle(const b2Vec2& center, float radius, const b2Vec2& axis, const b2Color& color) override; void DrawSegment(const b2Vec2& p1, const b2Vec2& p2, const b2Color& color) override; void DrawTransform(const b2Transform& xf) override; // Additional methods void DrawPoint(const b2Vec2& p, float size, const b2Color& color) override; void DrawAABB(b2AABB* aabb, const b2Color& color);
Step 4 : Conversions between SFML and Box2D
Before we can implement our Debug Draw class, there is a little issue to resolve. SFML and Box2D have certain differences that make mixing both not straightforward. The most important one is that Box2D uses Meters for measuring distance while SFML uses Pixels, so comes the question : How many pixels is one meter ?
The table below shows the fundamental differences between SFML and Box2D and how will convert between both. All those conversions are simple and obvious except the first one. The conversion between Meters and Pixels is arbitrary, we will be using 30 floats for 1 meter, but you can try other values.
Box2D | SFML | Conversion | |
---|---|---|---|
Distance | Meter | Pixel | 1 Meter = 30 Pixels |
Rotation | Radian | Degree | Radian = 3.14f / 180.f * Degree |
Y-Axis | Upward | Downward | Box2D y-axis = - (SFML y-axis) |
Color (RGB) | 0 to 1 | 0 to 255 | Box2D color = SFML Color / 255 |
To make those conversions easy, we’ve added a file name PhysicsUtil in our Engine (Expand the second below : Physics Utility). The constant variable SCALE will be used to convert distances between Box2D and SFML, the functions sf_to_b2 and b2_to_sf will be used to convert vectors and colors in both directions.
const float SCALE = 30.f; inline sf::Color b2_to_sf(const b2Color& color, int transparency = 255) { return sf::Color(color.r * 255, color.g * 255, color.b * 255, transparency); } inline sf::Vector2f b2_to_sf(b2Vec2 vect, float scale = 1.f) { return sf::Vector2f(vect.x * scale, vect.y * scale); } inline sf::Vector2f b2_to_sf(float x, float y, float scale = 1.f) { return sf::Vector2f(x * scale, y * scale); } inline b2Color sf_to_b2(const sf::Color& color) { return b2Color(color.r / 255, color.g / 255, color.b / 255); } inline b2Vec2 sf_to_b2(const sf::Vector2f& vect, float scale = 1.f) { return b2Vec2(vect.x / scale, vect.y / scale); } inline b2Vec2 sf_to_b2(float x, float y, float scale = 1.f) { return b2Vec2(x / scale, y / scale); } inline float vectLength(sf::Vector2f vect) { return std::sqrt(vect.x * vect.x + vect.y * vect.y); } inline float vectLength(b2Vec2 vect) { return std::sqrt(vect.x * vect.x + vect.y * vect.y); } inline float distance(sf::Vector2f vect1, sf::Vector2f vect2) { return vectLength(vect2 - vect1); } inline float distance(b2Vec2 vect1, b2Vec2 vect2) { return vectLength(vect2 - vect1); } float toDegree (float radian) { return 180.f / 3.141592653589793238462643383f * radian; } float toRadian(float degree) { return 3.141592653589793238462643383f / 180.f * degree; }
Step 5 : Debug Draw Implementation
Below you can see the implementation of our Debug Class. We create a new class called DebugDraw that inherits b2Draw. The class has access to the SFML Render window via a pointer. Expand the section Full Implementation to see the details of each drawing method.
///////////////////////////HEADERS////////////////////////// //Box2D #include <Box2D/Common/b2Draw.h> //SFML #include <SFML/Graphics.hpp> //Nero Games #include <PhysicsUtil.h> //////////////////////////////////////////////////////////// class DebugDraw : public b2Draw { private: sf::RenderWindow* mRenderWindow; float mThickness = -2.f; int mTranparency = 50.f; public: DebugDraw() { } void setRenderWindow(sf::RenderWindow* renderWindow) { mRenderWindow = renderWindow; } void DrawPolygon(const b2Vec2* vertices, int32 vertexCount, const b2Color& color) { sf::ConvexShape polygon; polygon.setOutlineThickness(mThickness); polygon.setOutlineColor(ng::b2_to_sf(color)); polygon.setFillColor(sf::Color::Transparent); polygon.setPointCount(vertexCount); for(int32 i = 0; i < vertexCount; i++) { polygon.setPoint(i, ng::b2_to_sf(vertices[i], ng::SCALE)); } mRenderWindow->draw(polygon); } void DrawSolidPolygon(const b2Vec2* vertices, int32 vertexCount, const b2Color& color) { sf::ConvexShape solidPolygon; solidPolygon.setOutlineThickness(mThickness); solidPolygon.setOutlineColor(ng::b2_to_sf(color)); solidPolygon.setFillColor(ng::b2_to_sf(color, mTranparency)); solidPolygon.setPointCount(vertexCount); for(int32 i = 0; i < vertexCount; i++) { solidPolygon.setPoint(i, ng::b2_to_sf(vertices[i], ng::SCALE)); } mRenderWindow->draw(solidPolygon); } void DrawCircle(const b2Vec2& center, float32 radius, const b2Color& color) { sf::CircleShape circle; circle.setOutlineThickness(mThickness); circle.setOutlineColor(ng::b2_to_sf(color)); circle.setFillColor(sf::Color::Transparent); float rad = radius * ng::SCALE; circle.setPosition(ng::b2_to_sf(center, ng::SCALE)); circle.setRadius(rad); circle.setOrigin(sf::Vector2f(rad, rad)); mRenderWindow->draw(circle); } void DrawSolidCircle(const b2Vec2& center, float32 radius, const b2Vec2& axis, const b2Color& color) { sf::CircleShape solidCircle; solidCircle.setOutlineThickness(mThickness); solidCircle.setOutlineColor(ng::b2_to_sf(color)); solidCircle.setFillColor(ng::b2_to_sf(color, mTranparency)); float rad = radius * ng::SCALE; solidCircle.setPosition(ng::b2_to_sf(center, ng::SCALE)); solidCircle.setRadius(rad); solidCircle.setOrigin(sf::Vector2f(rad, rad)); mRenderWindow->draw(solidCircle); b2Vec2 p = center + radius * axis; DrawSegment(center, p, color); } void DrawSegment(const b2Vec2& p1, const b2Vec2& p2, const b2Color& color) { sf::RectangleShape line; float length = ng::distance(ng::b2_to_sf(p1, ng::SCALE), ng::b2_to_sf(p2, ng::SCALE)); line.setSize(sf::Vector2f(length, mThickness)); line.setPosition(ng::b2_to_sf(p1, ng::SCALE)); line.setFillColor(ng::b2_to_sf(color)); line.setOrigin(sf::Vector2f(line.getOrigin().x, mThickness/2.f)); float delta_x = p2.x - p1.x; float delta_y = p2.y - p1.y; float angle = atan2(delta_y, delta_x); line.setRotation(ng::toDegree(angle)); mRenderWindow->draw(line); } void DrawTransform(const b2Transform& xf) { const float32 k_axisScale = 0.4f; b2Vec2 p1, p2; p1 = xf.p; p2 = p1 + k_axisScale * xf.q.GetXAxis(); DrawSegment(p1, p2, b2Color(1.0f, 0.0f, 0.0f)); p2 = p1 + k_axisScale * xf.q.GetYAxis(); DrawSegment(p1, p2, b2Color(0.0f, 1.0f, 0.0f)); } void DrawPoint(const b2Vec2& p, float32 size, const b2Color& color) { float s = size*1.5f; sf::RectangleShape point; point.setFillColor(ng::b2_to_sf(color)); point.setPosition(ng::b2_to_sf(p, ng::SCALE)); point.setSize(sf::Vector2f(s, s)); point.setOrigin(sf::Vector2f(s/2, s/2)); mRenderWindow->draw(point); } void DrawAABB(b2AABB* aabb, const b2Color& color) { sf::ConvexShape aabb_shape; aabb_shape.setOutlineColor(ng::b2_to_sf(color)); aabb_shape.setPointCount(4); aabb_shape.setPoint(0, ng::b2_to_sf(aabb->lowerBound, ng::SCALE)); aabb_shape.setPoint(1, sf::Vector2f(aabb->upperBound.x * ng::SCALE, aabb->lowerBound.y * ng::SCALE)); aabb_shape.setPoint(2, ng::b2_to_sf(aabb->upperBound, ng::SCALE)); aabb_shape.setPoint(3, sf::Vector2f(aabb->lowerBound.x * ng::SCALE, aabb->upperBound.y * ng::SCALE)); mRenderWindow->draw(aabb_shape); } };
class DebugDraw : public b2Draw { private: sf::RenderWindow* mRenderWindow; float mThickness = -2.f; int mTranparency = 50.f; public: DebugDraw() { //nothing } ~DebugDraw() { mRenderWindow = nullptr; } void setRenderWindow(sf::RenderWindow* renderWindow) { mRenderWindow = renderWindow; } /* Drawing methods here */ }
Step 6 : Use our Debug Draw Class
Now the magic part, let’s provide our Debug Draw class to the physical world. We add the debug draw as a new attribute called mDebugDraw. Inside the Init() method, we provide the SFML Render window to the debug draw, then we configure it with several flags. There are five (5) flags :
- b2Draw::e_shapeBit
- b2Draw::e_jointBit
- b2Draw::e_aabbBit
- b2Draw::e_pairBit
- b2Draw::e_centerOfMassBit
Each one of them indicates certain information to draw, since we want to draw everything we use all of them. These flags are binary values, the bitwise OR operator ( | ) behaves like an addition.
After configuring our debug draw, we give it to the physical world with SetDebugDraw().
The final step is to call the method DrawDebugData() at each Game Loop, this is done inside the Render() method of our Scene. If you compile the code now, you should see the body we’ve created earlier in the top-left corner of the window.
class Box2DScene : public ng::Scene { private: b2World mPhysicWorld; DebugDraw mDebugDraw; public: typedef std::unique_ptr<Box2DScene> ptr; Box2DScene(): mPhysicWorld(b2Vec2(0.f, 9.8f)) { setSceneName("Box2D Scene v0.1"); } void init() { //setup the DebugDraw mDebugDraw.setRenderWindow(&getRenderWindow()); mDebugDraw.SetFlags(b2Draw::e_shapeBit | b2Draw::e_jointBit | b2Draw::e_aabbBit | b2Draw::e_pairBit | b2Draw::e_centerOfMassBit); mPhysicWorld.SetDebugDraw(&mDebugDraw); //create a rigid body createBody(); } void render() { mPhysicWorld.DrawDebugData(); } };
Step 7 : Create More Bodies
Having one body in the corner of the window is not that nice, let’s create more bodies. We are going to adjust our method createBody() so a new body gets created each time we click on the window with the mouse.
First, we modify the method createBody() and add the body position as a parameter. Then we add the callback onMouseButton() to capture our mouse clicks.
Each time the Left mouse button is pressed down, we take the position of the mouse, convert it into Box2D dimensions using our utility function sf_to_b2 and create a new body at this exact position.
Now you can have as many boxes as you want !?
void onMouseButton(const sf::Mouse::Button& button, const bool& isPressed, const sf::Vector2f& position) { if(isPressed && button == sf::Mouse::Left) { createBody(ng::sf_to_b2(position, ng::SCALE)); } } void createBody(b2Vec2 position = b2Vec2(0.f, 0.f)) { //create an empty body b2BodyDef bodyDef; bodyDef.position = position; /* rest of the code */ }
Step 8 : A Last Touch - Simulate the Physical World
We’ve succeeded at integrating SFML and Box2D. Using our Debug Draw class we can now display Box2D physical world inside an SFML window. But as you can see, nothing is moving. That’s because the simulation of physics is not happening. Let’s fix that.
Box2D simulates physics when the physical world method Step() is called, we need to make this call inside the Update() method of our Scene. The method Step() takes three parameters. The first one is the time step in seconds, you will learn more about the two other parameters later.
After adding the code below to your Scene, you should see all your boxes falling under the effect of gravity.
void update(const sf::Time& timeStep) { mPhysicWorld.Step(timeStep.asSeconds(), 8.f, 3.f); }