This is part of an ongoing series where we write a complete 2D game engine in C++ and SFML. A new tutorial is released every Monday. You can find the complete list of tutorials here and download the source code from the projects GitHub page.This week we’ll begin work on a debug class, which will contain useful functions that will help us test the code we have written. We’ll start by outputting information, warning, and error messages; and drawing shapes to the window but we’ll expand the class as we develop more features that we want to test.

I have not spoken about testing before as I’m unsure whether I will discuss unit tests (or any other testing methodologies). I would like to focus on implementing features but of course I know that we do need a testing strategy. For now, we’ll rely on playtesting new features and then introduce other more structured tests at a later date.

Before we jump into the code it’s good to be aware that the debug class will be disabled in the final release, so we have to make sure we don’t include any features that we want to keep in the released game. If we decide that a debug feature should for some reason be kept in the final game we will need to find it a new home.To enable us to draw lines we need to add a helper function to the Window class that accepts vertices and draws them to the window.

Window.hpp
class Window
{
public:
...
void Draw(const sf::Vertex* vertices,
std::size_t vertexCount, sf::PrimitiveType type);
...
};

Window.cpp
void Window::Draw(const sf::Vertex* vertices, 
std::size_t vertexCount, sf::PrimitiveType type)
{
window.draw(vertices, vertexCount, type);
}

This will be called in the debug class that we are about to write. The class will start simple: we want to draw a line and a rectangle to the window and have the option of logging text to the console.

Window.cpp
#ifndef Debug_hpp
#define Debug_hpp

#include <array>
#include <iostream>
#include <functional>
#include <SFML/Graphics.hpp>

#include "Window.hpp"

class Debug
{
public:
static void Draw(Window& window);

static void DrawRect(const sf::FloatRect& rect,
sf::Color colour = sf::Color::White);

static void DrawLine(const sf::Vector2f& from,
const sf::Vector2f& to,
sf::Color colour = sf::Color::White);

static void Log(const std::string& msg);
static void LogWarning(const std::string& msg);
static void LogError(const std::string& msg);

private:
static std::vector<std::array<sf::Vertex, 2>> lines;
static std::vector<sf::RectangleShape> rects;
};

#endif /* Debug_hpp */

I’ve designed the Debug class to be static as it is going to be used by many different systems and we won’t actually be including it in the final release. Static global classes are very rarely the correct choice, which may be the case this time. However, it will help us quickly implement it and it’s something we can always change later if need be.

To draw the shapes and lines we need a dedicated Draw function, which will loop through the vectors of lines and shapes and pass them to the Draw function in the window class.

Debug.cpp
#include "Debug.hpp"

std::vector<sf::RectangleShape> Debug::rects = {};
std::vector<std::array<sf::Vertex, 2>> Debug::lines = {};

void Debug::Draw(Window& window)
{
for (auto& r : rects)
{
window.Draw(r);
}
rects.clear();

for (auto& l : lines)
{
sf::Vertex line[2] = { l.at(0), l.at(1) };
window.Draw(line, 2, sf::Lines);
}
lines.clear();
}

Once we’ve drawn the primitives, their vectors are cleared; so we’ll need to continue to call the relevant debugs function every frame that we want something drawn.

The DrawRect function accepts a rectangle area and a colour and creates an SFML RectangleShape. This is added to the shape collection for drawing in the next call to the Draw function.

Debug.cpp
void Debug::DrawRect(const sf::FloatRect& rect, sf::Color colour)
{
sf::Vector2f size(rect.width, rect.height);
sf::Vector2f pos(rect.left, rect.top);
sf::RectangleShape shape(size);
shape.setPosition(pos);
shape.setOutlineColor(colour);
shape.setOutlineThickness(3.f);
shape.setFillColor(sf::Color::Transparent);
rects.push_back(shape);
}

DrawLine is similarly straightforward: it creates a line consisting of two vertices and adds it to the vector for drawing during the next call to the draw function.

Debug.cpp
void Debug::DrawLine(const sf::Vector2f& from, 
const sf::Vector2f& to, sf::Color colour)
{
lines.push_back({ sf::Vertex(from, colour),
sf::Vertex(to, colour) });
}

Currently, logging will write to the console using std::cout (LINK) as we have not yet created a method of displaying text in-game. We’ll change this in future but for now it’s a quick way to output messages.

Debug.cpp
void Debug::Log(const std::string& msg)
{
std::cout << msg << std::endl;
}

void Debug::LogWarning(const std::string& msg)
{
std::cout << "WARNING: " << msg << std::endl;
}

void Debug::LogError(const std::string& msg)
{
std::cout << "ERROR: " << msg << std::endl;
}

We can’t forget to call our Debugs draw function, we’ll do that straight after we draw our objects in SceneGame.

SceneGame.hpp
#include “Debug.hpp"

SceneGame.cpp
void SceneGame::Draw(Window& window)
{
objects.Draw(window);

Debug::Draw(window);
}

With that done we can now easily draw lines and rectangles to the window. The first thing I would like to draw is the bounds for the quadtree we recently wrote. That way we can see exactly where the quadtree has split. We’ll create a function to do just that in the Quadtree class.

QuadTree.hpp
...
#include "Debug.hpp"

class Quadtree
{
public:
...

void DrawDebug();

...
};

QuadTree.cpp
void QuadTree::DrawDebug()
{
if(children[0] != nullptr)
{
for (int i = 0; i < 4; i++)
{
children[i]->DrawDebug();
}
}

Debug::DrawRect(bounds, sf::Color::Red);
}

This loops through any children nodes and calls their DrawDebug function and then draw this nodes bounds. We’ll call this new function in the collidable systems update method.

S_Collidable.cpp
void S_Collidable::Update()
{
collisionTree.DrawDebug(); // New line.

collisionTree.Clear();
for (auto maps = collidables.begin(); maps != collidables.end(); ++maps)
{
for (auto collidable : maps->second)
{
collisionTree.Insert(collidable);
}
}

Resolve();
}

Now when you run the game the bounds nodes will be outlined in red. I’ve added an extra hundred or so players in the images below to show you how the quadtree splits.
Quadtree with nodes drawn
Quadtree with nodes drawn.

You’ll notice that the more collidable objects in an area the smaller the quadtrees nodes will be as they will have split.
Quadtree with nodes drawn.
You may also notice from the image above that when a player object leaves the quadtrees bounds (top of the quadtree in the last image) they no longer collide. So we need to ensure that the quadtrees rectangle contains the whole play area.

Next I’ll draw the bounds of objects when they collide to make sure that they are colliding correctly (spoilers they’re not).Drawing the collision boxes is nice and easy, we’ll do it in the Resolve function of our collidable system.

S_Collidable.cpp
void S_Collidable::Resolve()
{


if(m.colliding)
{
Debug::DrawRect(collision->GetCollidable(), sf::Color::Red);
Debug::DrawRect(collidable->GetCollidable(), sf::Color::Red);

if(collision->owner->transform->isStatic())
{
collidable->ResolveOverlap(m);
}
else
{
collidable->ResolveOverlap(m);
}
}

...
}

We draw a rect for both of the colliding objects just before we resolve the overlap.If you run the game now, you’ll see that something is not quite right when we collide with a tile.
The collisions are not quite right.
The collision rectangles do not line up with the sprites.

This happens because we are positioning an objects sprite based on its top-left rather than its centre point. This also means that if we position the player at say {10, 20} then it will actually be the top left of the player that is in this position. This could cause problems further on so its good that we’ve caught it now.

We’ll fix it be moving the sprite to the correct position in its LateUpdate function.

C_Sprite.cpp
void C_Sprite::LateUpdate(float deltaTime)
{
sf::Vector2f pos = owner->transform->GetPosition();
const sf::IntRect& spriteBounds = sprite.getTextureRect();
const sf::Vector2f& spriteScale = sprite.getScale();
sprite.setPosition(
pos.x - ((abs(spriteBounds.width) * 0.5f) * spriteScale.x),
pos.y - ((abs(spriteBounds.height) * 0.5f) * spriteScale.y)
);
}

We retrieve the absolute width and height of the sprite because we may set them to a negative value, for example: when we flip a sprites direction we multiply the sprite’s width by -1. Currently, we don’t set the height to a negative but I’m still checking for the absolute value here to stop any hard to find bugs in future if we do decide to flip the sprite horizontally.

Now when you run the game and collide with a tile everything looks as it should.

Using this visual feedback we can tweak the size of the player collider. We want to make it smaller than the texture size because the texture contains a transparent border.We’ll create a function to set the size of the collider and also a new function that allows us to set an offset (a vector we had already created but were not using).

C_BoxCollider.hpp
class C_BoxCollider : public C_Collider
{
public:
...

void SetOffset(const sf::Vector2f& offset);
void SetOffset(float x, float y);

void SetSize(const sf::Vector2f& size);
void SetSize(float width, float height);


};

C_BoxCollider.cpp
void C_BoxCollider::SetOffset(const sf::Vector2f& offset) 
{
this->offset = offset;
}

void C_BoxCollider::SetOffset(float x, float y)
{
offset.x = x;
offset.y = y;
}

void C_BoxCollider::SetSize(const sf::Vector2f& size)
{
AABB.width = size.x;
AABB.height = size.y;
}

void C_BoxCollider::SetSize(float width, float height)
{
AABB.width = width;
AABB.height = height;
}

We’ll set the size and offset of the collider when we create the player object in SceneGame.

SceneGame.cpp
void SceneGame::OnCreate()
{

player->transform->SetPosition(100, 700);

auto collider = player->AddComponent<C_BoxCollider>();
collider->SetSize(frameWidth * 0.4f, frameHeight * 0.5f);
collider->SetOffset(0.f, 14.f);
collider->SetLayer(CollisionLayer::Player);

objects.Add(player);


}

We’ll set the size and offset of the collider when we create the player object in SceneGame.


Smaller player collision box.

That’s it for this week. As always, if you have any suggestions for what you would like covered or are having any trouble implementing a feature, then let me know in the comments and I’ll get back to you as soon as I can. 

Thank you for reading 🙂