Source: Simple Collision Detection for SFML 2
Shane Tran Whitmire edited this page Jul 7, 2023
·
13 revisions
This is an adaptation of the Simple Collision Detection code that was written for SFML 1. Now it works with SFML 2. Changes include:
- Refactored code to C++17 (
std::vector
,std::array
,uint32_t
, etc.), introduced AAA style, replaced pointers with references, and fixed naming inconsistencies. - The alpha values of a texture are now stored in a bitmask, since SFML no longer uses
sf::Image
s for rendering. Creating this bitmask for an already existing texture takes time (because of a call tosf::Texture::copyToImage
) so useCollision::CreateTextureAndBitmask
to load an image file into a texture and create the bitmask at the same time. - The helper functions of the original version are omitted.
sf::Sprite::getGlobalBounds
now does whatCollision::GetAABB
did before. -
sf::Sprite
no longer returns its size so a couple of extra calculations are needed to take scaling into account. - The
BoundingBoxTest
was rewritten completely. It now uses the Separating Axis Theorem.
To use this code save the following two files and include them in your current SFML project
/*
* File: collision.h
* Authors: Nick Koirala (original version), ahnonay (SFML2 compatibility), switchboy (single pixel test)
* Paweł Syska (C++17 refactor + naming convention)
*
* Collision Detection and handling class
* For SFML2.
Notice from the original version:
(c) 2009 - LittleMonkey Ltd
This software is provided 'as-is', without any express or
implied warranty. In no event will the authors be held
liable for any damages arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute
it freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented;
you must not claim that you wrote the original software.
If you use this software in a product, an acknowledgment
in the product documentation would be appreciated but
is not required.
2. Altered source versions must be plainly marked as such,
and must not be misrepresented as being the original software.
3. This notice may not be removed or altered from any
source distribution.
*
* Created on 30 January 2009, 11:02
*/
#ifndef COLLISION_H
#define COLLISION_H
#include <SFML/Graphics.hpp>
namespace Collision {
//////
/// Test for a collision between two sprites by comparing the alpha values of overlapping pixels
/// Supports scaling and rotation
/// alphaLimit: The threshold at which a pixel becomes "solid". If alphaLimit is 127, a pixel with
/// alpha value 128 will cause a collision and a pixel with alpha value 126 will not.
///
/// This functions creates bitmasks of the textures of the two sprites by
/// downloading the textures from the graphics card to memory -> SLOW!
/// You can avoid this by using the "createTextureAndBitmask" function
//////
bool pixelPerfectTest(const sf::Sprite& sprite1, const sf::Sprite& sprite2, sf::Uint8 alphaLimit = 0);
///////
/// Test if a single pixel collides By testing the alpha value at the given location.
/// Supports scaling and rotation
/// alphaLimit: The threshold at which a pixel becomes "solid". If alphaLimit is 127, a pixel with
/// alpha value 128 will cause a collision and a pixel with alpha value 126 will not.
///
/// This functions creates bitmasks of the textures of the sprite by
/// downloading the textures from the graphics card to memory -> SLOW!
/// You can avoid this by using the "createTextureAndBitmask" function
//////
bool singlePixelTest(const sf::Sprite& sprite, sf::Vector2f& mousePosition, sf::Uint8 alphaLimit);
//////
/// Replaces Texture::loadFromFile
/// Load an image file into the given texture and create a bitmask for it
/// This is much faster than creating the bitmask for a texture on the first run of "pixelPerfectTest"
///
/// The function returns false if the file could not be opened for some reason
//////
bool createTextureAndBitmask(sf::Texture &loadInto, const std::string& filename);
//////
/// Test for collision using circle collision detection
/// Radius is averaged from the dimensions of the sprite so
/// roughly circular objects will be much more accurate
//////
bool circleTest(const sf::Sprite& sprite1, const sf::Sprite& sprite2);
//////
/// Test for bounding box collision using the Separating Axis Theorem
/// Supports scaling and rotation
//////
bool boundingBoxTest(const sf::Sprite& sprite1, const sf::Sprite& sprite2);
}
#endif /* COLLISION_H */
/*
* File: collision.cpp
* Author: Nick (original version), ahnonay (SFML2 compatibility), Paweł Syska (C++17 refactor + naming convention)
*/
#include <SFML/Graphics.hpp>
#include <map>
#include <vector>
#include <array>
#include "Collision.h"
namespace Collision
{
using TextureMask = std::vector<sf::Uint8>;
static sf::Uint8 getPixel (const TextureMask& mask, const sf::Texture& tex, uint32_t x, uint32_t y) {
if (x > tex.getSize().x || y > tex.getSize().y)
return 0;
return mask[ x + y * tex.getSize().x ];
}
class BitmaskRegistry
{
public:
auto& get (const sf::Texture& tex) {
auto pair = bitmasks.find(&tex);
if (pair == bitmasks.end())
{
return create (tex, tex.copyToImage());
}
return pair->second;
}
auto& create (const sf::Texture& tex, const sf::Image& img) {
auto mask = TextureMask( tex.getSize().y * tex.getSize().x );
for (uint32_t y = 0; y < tex.getSize().y; ++y)
{
for (uint32_t x = 0; x < tex.getSize().x; ++x)
mask[ x + y * tex.getSize().x ] = img.getPixel(x,y).a;
}
// store and return ref to the mask
return (bitmasks[&tex] = std::move(mask));
}
private:
std::map<const sf::Texture*, TextureMask> bitmasks;
};
// Gets global instance of BitmaskRegistry.
// "static" to make sure this function doesn't leak to other source file
static BitmaskRegistry& bitmasks() {
static BitmaskRegistry& instance;
return instance;
}
bool singlePixelTest(const sf::Sprite& sprite, sf::Vector2f& mousePosition, sf::Uint8 alphaLimit) {
if (!sprite.getGlobalBounds().contains(mousePosition.x, mousePosition.y))
return false;
auto subRect = sprite.getTextureRect();
auto& mask = bitmasks().get(*sprite.getTexture());
auto sv = sprite.getInverseTransform().transformPoint(mousePosition.x, mousePosition.y);
// Make sure pixels fall within the sprite's subrect
if (sv.x > 0 && sv.y > 0 && sv.x < subRect.width && sv.y < subRect.height) {
return getPixel(mask, *sprite.getTexture(), static_cast<int>(sv.x) + subRect.left, static_cast<int>(sv.y) + subRect.top) > alphaLimit;
}
return false;
}
bool pixelPerfectTest(const sf::Sprite& sprite1, const sf::Sprite& sprite2, sf::Uint8 alphaLimit) {
sf::FloatRect intersection;
if (!sprite1.getGlobalBounds().intersects(sprite2.getGlobalBounds(), intersection))
return false;
auto s1SubRect = sprite1.getTextureRect();
auto s2SubRect = sprite2.getTextureRect();
auto& mask1 = bitmasks().get(*sprite1.getTexture());
auto& mask2 = bitmasks().get(*sprite2.getTexture());
// Loop through our pixels
for (auto i = intersection.left; i < intersection.left + intersection.width; ++i) {
for (auto j = intersection.top; j < intersection.top + intersection.height; ++j) {
auto s1v = sprite1.getInverseTransform().transformPoint(i, j);
auto s2v = sprite2.getInverseTransform().transformPoint(i, j);
// Make sure pixels fall within the sprite's subrect
if (s1v.x > 0 && s1v.y > 0 && s2v.x > 0 && s2v.y > 0 &&
s1v.x < s1SubRect.width && s1v.y < s1SubRect.height &&
s2v.x < s2SubRect.width && s2v.y < s2SubRect.height) {
if (getPixel(mask1, *sprite1.getTexture(), (int)(s1v.x)+s1SubRect.left, (int)(s1v.y)+s1SubRect.top) > alphaLimit &&
getPixel(mask2, *sprite2.getTexture(), (int)(s2v.x)+s2SubRect.left, (int)(s2v.y)+s2SubRect.top) > alphaLimit)
return true;
}
}
}
return false;
}
bool createTextureAndBitmask(sf::Texture &loadInto, const std::string& filename)
{
auto img = sf::Image();
if (!img.loadFromFile(filename))
return false;
if (!loadInto.loadFromImage(img))
return false;
bitmasks().create(loadInto, img);
return true;
}
sf::Vector2f getSpriteCenter (const sf::Sprite& sprite)
{
auto AABB = sprite.getGlobalBounds();
return sf::Vector2f (AABB.left + AABB.width / 2.f, AABB.top + AABB.height / 2.f);
}
sf::Vector2f getSpriteSize (const sf::Sprite& sprite)
{
auto originalSize = sprite.getTextureRect();
auto scale = sprite.getScale();
return sf::Vector2f (originalSize.width * scale.x, originalSize.height * scale.y);
}
bool circleTest(const sf::Sprite& sprite1, const sf::Sprite& sprite2) {
auto spr1Size = getSpriteSize(sprite1);
auto spr2Size = getSpriteSize(sprite2);
auto radius1 = (spr1Size.x + spr1Size.y) / 4.f;
auto radius2 = (spr2Size.x + spr2Size.y) / 4.f;
auto diff = getSpriteCenter(sprite1) - getSpriteCenter(sprite2);
return (diff.x * diff.x + diff.y * diff.y <= (radius1 + radius2) * (radius1 + radius2));
}
struct OrientedBoundingBox // Used in the BoundingBoxTest
{
std::array<sf::Vector2f, 4> points;
OrientedBoundingBox (const sf::Sprite& sprite) // Calculate the four points of the OBB from a transformed (scaled, rotated...) sprite
{
auto transform = sprite.getTransform();
auto local = sprite.getTextureRect();
points[0] = transform.transformPoint(0.f, 0.f);
points[1] = transform.transformPoint(local.width, 0.f);
points[2] = transform.transformPoint(local.width, local.height);
points[3] = transform.transformPoint(0.f, local.height);
}
// Project all four points of the OBB onto the given axis and return the dot products of the two outermost points
void projectOntoAxis (const sf::Vector2f& axis, float& min, float& max)
{
min = (points[0].x * axis.x + points[0].y * axis.y);
max = min;
for (int j = 1; j < points.size(); ++j)
{
auto projection = points[j].x * axis.x + points[j].y * axis.y;
if (projection < min)
min = projection;
if (projection > max)
max = projection;
}
}
};
bool boundingBoxTest(const sf::Sprite& sprite1, const sf::Sprite& sprite2) {
auto OBB1 = OrientedBoundingBox(sprite1);
auto OBB2 = OrientedBoundingBox(sprite2);
// Create the four distinct axes that are perpendicular to the edges of the two rectangles
auto axes = std::array<sf::Vector2f, 4>({
{ OBB1.points[1].x - OBB1.points[0].x, OBB1.points[1].y - OBB1.points[0].y },
{ OBB1.points[1].x - OBB1.points[2].x, OBB1.points[1].y - OBB1.points[2].y },
{ OBB2.points[0].x - OBB2.points[3].x, OBB2.points[0].y - OBB2.points[3].y },
{ OBB2.points[0].x - OBB2.points[1].x, OBB2.points[0].y - OBB2.points[1].y }
});
for (auto& axis : axes)
{
float minOBB1, maxOBB1, minOBB2, maxOBB2;
// Project the points of both OBBs onto the axis ...
OBB1.projectOntoAxis(axis, minOBB1, maxOBB1);
OBB2.projectOntoAxis(axis, minOBB2, maxOBB2);
// ... and check whether the outermost projected points of both OBBs overlap.
// If this is not the case, the Separating Axis Theorem states that there can be no collision between the rectangles
if (!((minOBB2 <= maxOBB1) && (maxOBB2 >= minOBB1)))
return false;
}
return true;
}
}