Glusoft

Create a Hitbox Editor

In this tutorial the goal is to create a hitbox editor. Hitbox are polygons use to detect collisions in video games.

HitBox Editor

In a point and click game we need to check the collision between one point and a polygon which is simpler than the collision between two polygon. This tutorial is not about collisions so we will only check if a point is in the polygon. The coordinates of the point will be the same as the cursor like point and click games. Only one method is enough to achieve that. The code for this algorithm is very short:

/*
nvert: Number of vertices in the polygon. Whether to repeat the first vertex at the end has been discussed in 
the article referred above.
vertx, verty: Arrays containing the x- and y-coordinates of the polygon's vertices.
testx, testy: X- and y-coordinate of the test point.
*/
int pnpoly(int nvert, float *vertx, float *verty, float testx, float testy) {
	int i, j, c = 0;
	for (i = 0, j = nvert - 1; i < nvert; j = i++) {
		if (((verty[i]>testy) != (verty[j]>testy)) &&
			(testx < (vertx[j] - vertx[i]) * (testy - verty[i]) / 
			(verty[j] - verty[i]) + vertx[i]))
			c = !c;
	}
	return c;
}

If you want to have more complex collisions you will need to use SAT (Separate Axis Theorem) or maybe use a 2D physics library to have an efficient implementation of collisions.

Create the project

To make this editor I will use SFML but you can do the same thing with SDL if you want
We will use this image as a background in the editor : machinarium.jpgSource : machinarium

We will need to display polygons and the only class available in SFML to do that is sf::ConvexShape. Next, we need something to draw concave polygon, we can simply divide the concave polygon into multiple convex polygon. To do that we have to use some library to split the polygon : Efficient Polygon Triangulation

Hitbox Editor

Once the project is created and configured we can see what do we need ?

Here is the main of the editor, everything should be simple to understand if you know a little bit of SFML :).

bool testMode = false;
float *vertx = NULL;
float *verty = NULL;

sf::Vector2f pos = sf::Vector2f(0, 0);
sf::Texture tex;
tex.loadFromFile("machinarium.jpg");

sf::Sprite spr(tex);
spr.setPosition(pos);

std::vector<sf::ConvexShape> polyShapes;
std::vector<sf::CircleShape> circles;

Vector2dVector polygon;

sf::RenderWindow window(sf::VideoMode(1280, 800), "Hitbox Editor");

while (window.isOpen()) {
	sf::Event event;

	while (window.pollEvent(event)) {
		if (event.type == sf::Event::Closed)
			window.close();
		if (event.type == sf::Event::MouseButtonPressed) {
			if (event.key.code == sf::Mouse::Left) {
				sf::Vector2i mousePos = sf::Mouse::getPosition(window);
				if (testMode) {
					std::cout << "result : " << pnpoly(polygon.size(), vertx, verty, 
					(float) mousePos.x, (float) mousePos.y) << "\n";
				}
				else {
					polygon.push_back(Vector2d(mousePos.x, mousePos.y));

					sf::CircleShape circle;
					circle.setRadius(2);
					circle.setOutlineColor(sf::Color::White);
					circle.setFillColor(sf::Color::White);
					circle.setPosition((float) mousePos.x - 2, (float) mousePos.y - 2);
					circles.push_back(circle);
				}
			}
		}
		if (event.type == sf::Event::KeyPressed) {
			if (event.key.code == sf::Keyboard::Escape)
				window.close();

			// Remove the last point added
			if (event.key.code == sf::Keyboard::BackSpace) {
				polygon.pop_back();
				circles.pop_back();
			}
			// Clear the current hitbox
			if (event.key.code == sf::Keyboard::C) {
				polyShapes.clear();
				polygon.clear();
				circles.clear();

				if (vertx != NULL) {
					delete(vertx);
					delete(verty);
					vertx = NULL;
					verty = NULL;
				}
			}

			// Enable the test mode
			if (event.key.code == sf::Keyboard::T) {
				testMode = !testMode;
			}

			// Save the hitbox into a file and clear
			if (event.key.code == sf::Keyboard::S) {
				save_file("out.txt", polygon, pos);

				// Clear the hitbox
				polyShapes.clear();
				polygon.clear();
				circles.clear();
				if (vertx != NULL) {
					delete(vertx);
					delete(verty);
					vertx = NULL;
					verty = NULL;
				}
			}

			if (event.key.code == sf::Keyboard::F) {
				vertx = new float[polygon.size()];
				verty = new float[polygon.size()];

				for (size_t i = 0; i < polygon.size(); i++) {
					vertx[i] = polygon[i].GetX();
					verty[i] = polygon[i].GetY();
				}

				Vector2dVector result;

				// Split the polygon into triangles
				Triangulate::Process(polygon, result);

				size_t tcount = result.size() / 3; // number of triangles

				for (size_t i = 0; i < tcount; i++) {
					const Vector2d &p1 = result[i * 3 + 0];
					const Vector2d &p2 = result[i * 3 + 1];
					const Vector2d &p3 = result[i * 3 + 2];

					// Create a triangle with the coordinates
					sf::ConvexShape poly;
					poly.setPointCount(3);
					poly.setPoint(0, sf::Vector2f(p1.GetX(), p1.GetY()));
					poly.setPoint(1, sf::Vector2f(p2.GetX(), p2.GetY()));
					poly.setPoint(2, sf::Vector2f(p3.GetX(), p3.GetY()));
					poly.setOutlineColor(sf::Color::White);
					poly.setFillColor(sf::Color::White);
					poly.setPosition(0, 0);
					polyShapes.push_back(poly);
				}
			}
		}
	}

	window.clear();
	window.draw(spr);

	for (auto& x : circles) {
		window.draw(x);
	}

	for (auto& x : polyShapes) {
		window.draw(x);
	}

	window.display();
}

if (vertx != NULL) {
	delete(vertx);
	delete(verty);
	vertx = NULL;
	verty = NULL;
}

return 0;

We need the helper function save_file for this main to compile:

void save_file(const char* filename, Vector2dVector vect, sf::Vector2f pos) {
	std::string seq = "";
	for (size_t i = 0; i < vect.size(); i++) {
		seq.append(std::to_string((int)(vect[i].GetX() - pos.x)));
		seq.push_back(':');
		seq.append(std::to_string((int)(vect[i].GetY() - pos.y)));
		if (i != vect.size() - 1) {
			seq.push_back('-');
		}
	}

	std::ofstream file;
	file.open(filename, std::ios::out | std::ios::app);
	file << seq << "\n";
	std::cout << seq << "\n";
	file.close();
}

If you have some errors you can download the project for visual studio with libraries : HitboxEditor.7z

Do not forget to change the include path and lib path in the configuration !