ungroup/ungroup_game

View on GitHub
src/common/physics/CollisionUtil.cpp

Summary

Maintainability
Test Coverage
#include "CollisionUtil.hpp"

#include <cmath>

#include "VectorUtil.hpp"

bool CollisionUtil::areIntersecting(const CircleRigidBody& circle_a,
                                    const CircleRigidBody& circle_b) {
    if (circle_a.getId() == circle_b.getId()) {
        return false;
    }
    float dist = VectorUtil::distance(circle_a.getCenter(), circle_b.getCenter());
    return dist < circle_a.getRadius() + circle_b.getRadius();
}

/**
 * Implementation of https://stackoverflow.com/a/402019
 */
bool CollisionUtil::areIntersecting(const CircleRigidBody& circle, const sf::FloatRect& rectangle) {
    sf::Vector2f top_left(rectangle.left, rectangle.top);
    sf::Vector2f top_right(rectangle.left + rectangle.width, rectangle.top);
    sf::Vector2f bottom_left(rectangle.left, rectangle.top + rectangle.height);
    sf::Vector2f bottom_right(rectangle.left + rectangle.width, rectangle.top + rectangle.height);

    return rectangle.contains(circle.getCenter()) ||
           areIntersecting(circle, std::make_pair(top_left, top_right)) ||
           areIntersecting(circle, std::make_pair(top_right, bottom_right)) ||
           areIntersecting(circle, std::make_pair(bottom_right, bottom_left)) ||
           areIntersecting(circle, std::make_pair(bottom_left, top_left));
}

bool CollisionUtil::areIntersecting(const CircleRigidBody& circle,
                                    std::pair<sf::Vector2f, sf::Vector2f> line) {
    return CollisionUtil::minimumDistance(line.first, line.second, circle.getCenter()) <=
           circle.getRadius();
}

/**
 * Implementation of https://stackoverflow.com/a/1501725
 */
float CollisionUtil::minimumDistance(const sf::Vector2f& v, const sf::Vector2f& w,
                                     const sf::Vector2f& p) {
    const float l2 = VectorUtil::lengthSquared(v, w); // i.e. |w-v|^2 -  avoid a sqrt
    if (l2 == 0.0)
        return VectorUtil::distance(p, v); // v == w case
    // Consider the line extending the segment, parameterized as v + t (w - v).
    // We find projection of point p onto the line.
    // It falls where t = [(p-v) . (w-v)] / |w-v|^2
    // We clamp t from [0,1] to handle points outside the segment vw.
    const float t = std::max(0.f, std::min(1.f, VectorUtil::dot(p - v, w - v) / l2));
    const sf::Vector2f projection = v + t * (w - v); // Projection falls on the segment
    return VectorUtil::distance(p, projection);
}

Orientation CollisionUtil::getOrientation(const sf::Vector2f p, const sf::FloatRect& rectangle) {
    if (rectangle.contains(p)) {
        return Orientation::inside;
    } else if (p.y <= rectangle.top) {
        return Orientation::above;
    } else if (p.x >= rectangle.left + rectangle.width) {
        return Orientation::right;
    } else if (p.y >= rectangle.top + rectangle.height) {
        return Orientation::below;
    } else {
        return Orientation::left;
    }
}

Collision CollisionUtil::getCollision(const CircleRigidBody& circle_a,
                                      const CircleRigidBody& circle_b) {
    bool collided = true;
    sf::Vector2f between = VectorUtil::getVector(circle_b.getCenter(), circle_a.getCenter());
    float overlap = circle_a.getRadius() + circle_b.getRadius() - VectorUtil::length(between);
    sf::Vector2f normal = VectorUtil::normalize(between);

    sf::Vector2f relative_velocity = circle_a.getVelocity() - circle_b.getVelocity();
    float velocity_along_normal = VectorUtil::dot(relative_velocity, normal);

    // Do not consider collision if velocities are seperating
    if (velocity_along_normal > 0) {
        collided = false;
    }

    std::pair<sf::Vector2f, sf::Vector2f> resolution;
    if (circle_a.isMovable() && circle_b.isMovable()) {
        resolution = std::make_pair(normal * overlap / 2.f, -normal * overlap / 2.f);
    } else if (circle_a.isMovable() && !circle_b.isMovable()) {
        resolution = std::make_pair(normal * overlap, sf::Vector2f(0.f, 0.f));
    } else if (!circle_a.isMovable() && circle_b.isMovable()) {
        resolution = std::make_pair(sf::Vector2f(0.f, 0.f), -normal * overlap / 2.f);
    } else {
        resolution = std::make_pair(sf::Vector2f(0.f, 0.f), sf::Vector2f(0.f, 0.f));
    }

    sf::Vector2f collision_point =
        circle_b.getCenter() + resolution.second + (normal * circle_b.getRadius());

    Collision collision = {
        .ids = std::pair<uint32_t, uint32_t>(circle_a.getId(), circle_b.getId()),
        .position = collision_point,
        .normal = normal,
        .resolution = resolution,
        .collided = collided,
    };

    return collision;
}

std::pair<Impulse, Impulse> CollisionUtil::getImpulses(const CircleRigidBody& circle_a,
                                                       const CircleRigidBody& circle_b,
                                                       const Collision& collision) {
    sf::Vector2f relative_velocity = circle_a.getVelocity() - circle_b.getVelocity();
    float impulse = -2.f * VectorUtil::dot(relative_velocity, collision.normal) /
                    (1 / circle_a.getMass() + 1 / circle_b.getMass());
    return std::make_pair((Impulse){impulse, collision.normal},
                          (Impulse){impulse, -collision.normal});
}

bool CollisionUtil::isInBounds(const CircleRigidBody& circle, sf::Vector2f bounds_center,
                               float bounds_radius) {
    return VectorUtil::distance(bounds_center, circle.getCenter()) + circle.getRadius() <=
           bounds_radius;
}

sf::Vector2f CollisionUtil::getBoundsCorrection(const CircleRigidBody& circle,
                                                sf::Vector2f bounds_center, float bounds_radius) {
    sf::Vector2f between = VectorUtil::getVector(circle.getCenter(), bounds_center);
    sf::Vector2f normal = VectorUtil::normalize(between);
    float overlap = circle.getRadius() + bounds_radius - VectorUtil::length(between);
    sf::Vector2f move_circle = normal * (2.f * circle.getRadius() - overlap);
    return move_circle;
}