Verified Commit f0faa5c4 authored by Jonas Johan Solsvik's avatar Jonas Johan Solsvik 🎮
Browse files

Perferct 2d AABB collision. Jippi :)

parent b7f2b79c
# Devlog 03.09.2020 - 15:00
## Collision resolving @ gamedev stackexchange
https://gamedev.stackexchange.com/a/71123
```
Instead of using the above method, you resolve collision like so:
Move the player along the X axis.
Check for colliding tiles.
Resolve X collision.
Move the player along the Y axis.
Check for colliding tiles.
Resolve Y collision.
```
## Narrow phase collision detection
Computationally, it is desirable that all shapes are convex in a simulation, since we have a lot of powerful distance and intersection test algorithms that work with convex shapes.
## SAT - Separating axis theorem
The separating axis theorem (SAT) states that two convex shapes are not intersecting if and only if there exists at least one axis where the orthogonal projections of the shapes on this axis do not intersect.
## And more...
- If, for any edge, all vertices of the other polygon are in front of it – then the polygons do not intersect.
- Linear algebra provides an easy formula for this test: given an edge on the first shape with vertices a and b and a vertex v on the other shape, if (v - a) · n is greater than zero, then the vertex is in front of the edge.
- The Expanding Polytope Algorithm (EPA) allows us to obtain that information, starting where GJK left off: with a simplex that contains the origin.
# Devlog 03.09.2020 - 12:00
I found a great article!
@see https://www.toptal.com/game/video-game-physics-part-ii-collision-detection-for-solid-objects
## Newtons three laws of motion
- Inertia: If no force is applied on an object, its velocity (speed and direction of motion) shall not change.
- Force, Mass, and Acceleration: The force acting on an object is equal to the mass of the object multiplied by its acceleration (rate of change of velocity). This is given by the formula F = ma.
- Action and Reaction: “For every action there is an equal and opposite reaction.” In other words, whenever one body exerts a force on another, the second body exerts a force of the same magnitude and opposite direction on the first.
## axis-aligned bounding boxes (AABB)
To test if two AABBs intersect, we only have to find out if their projections intersect on all of the coordinate axes:
# Devlog 03.09.2020 - 09:00
- I am hitting a wall with my hacky off-the-top-of-my-head collision implementation. Time to study som literarture
\ No newline at end of file
# 03-aabb-collisions
- In this experiment, will try to implement a well-known collision-detection algorithm.
#include <cstdio>
#include <cmath>
#include <iostream>
#include <SFML/Audio.hpp>
#include <SFML/Graphics.hpp>
#define FLOAT_MAX 3.40282347e+38F
using ThingID = uint;
struct Thing
{
ThingID id{};
sf::Vector2f velocity{0, 0};
sf::RectangleShape shape{};
bool isColliding = false;
};
struct Controller
{
ThingID thingID{};
bool up{};
bool down{};
bool left{};
bool right{};
uint debounceUpMs{};
uint debounceDownMs{};
uint debounceLeftMs{};
uint debounceRightMs{};
};
using ThingVector = std::vector<Thing>;
using ThingIDVector = std::vector<ThingID>;
auto eventsUpdate(
sf::RenderWindow &window,
sf::View &camera,
std::vector<Controller> &controllers,
const sf::Clock gameClock) -> void;
auto physicsUpdate(
ThingVector &things,
const sf::Time deltaTime,
const ThingIDVector &dynamicThings) -> void;
auto AABBCollisionDetection(sf::RectangleShape A, sf::RectangleShape B) -> bool;
auto animationUpdate(
ThingVector &things,
std::vector<Controller> &controllers,
const sf::Time deltaTime) -> void;
auto renderUpdate(
sf::RenderWindow &window,
const ThingVector &things) -> void;
auto main() -> int
{
// # Create the main window and camera
const int WIDTH = 600;
const int HEIGHT = 600;
const int FRAMERATE_LIMIT = 90;
sf::RenderWindow window(sf::VideoMode(WIDTH, HEIGHT), "Double trouble");
window.setFramerateLimit(FRAMERATE_LIMIT);
sf::View camera(sf::FloatRect(0, 0, WIDTH, HEIGHT));
camera.setCenter(0, -200);
camera.setSize(WIDTH, HEIGHT);
window.setView(camera);
ThingVector things;
const ThingIDVector dynamicThings = {0, 1};
{
// # Create players
const auto PLAYER_SIZE = sf::Vector2f{50.0f, 50.0f};
Thing player1{0};
player1.shape.setFillColor(sf::Color::Red);
player1.shape.setSize(PLAYER_SIZE);
player1.shape.setOrigin(sf::Vector2f{PLAYER_SIZE.x * .5f, PLAYER_SIZE.y * .5f});
player1.shape.setPosition(sf::Vector2f{-100, -500});
things.push_back(player1);
Thing player2{1};
player2.shape.setFillColor(sf::Color::Blue);
player2.shape.setSize(PLAYER_SIZE);
player2.shape.setOrigin(sf::Vector2f{PLAYER_SIZE.x * .5f, PLAYER_SIZE.y * .5f});
player2.shape.setPosition(sf::Vector2f{100, -500});
things.push_back(player2);
// # Create ground
Thing ground{2};
ground.shape.setSize(sf::Vector2f{4000, 20});
ground.shape.setOrigin(2000, 10);
ground.shape.setPosition(0, 0);
ground.shape.setFillColor(sf::Color::White);
things.push_back(ground);
}
std::vector<Controller> controllers{
Controller{thingID : 0},
Controller{thingID : 1}}; // One controller for each player
sf::Clock gameClock{}; // Never restart
sf::Clock frameClock{}; // Restart every frame
while (window.isOpen())
{
const auto deltaTime = frameClock.getElapsedTime();
frameClock.restart();
eventsUpdate(window, camera, controllers, gameClock);
physicsUpdate(things, deltaTime, dynamicThings);
animationUpdate(things, controllers, deltaTime);
renderUpdate(window, things);
}
return 0;
}
auto eventsUpdate(
sf::RenderWindow &window,
sf::View &camera,
std::vector<Controller> &controllers,
const sf::Clock gameClock) -> void
{
auto &controller1 = controllers[0];
auto &controller2 = controllers[1];
// # Process events
sf::Event event;
while (window.pollEvent(event))
{
// ## Close window on exit
if (event.type == sf::Event::Closed ||
event.type == sf::Event::KeyPressed &&
event.key.code == sf::Keyboard::Escape)
{
window.close();
}
else if (event.type == sf::Event::KeyPressed)
{
const auto elapsedMs = gameClock.getElapsedTime().asMilliseconds();
const uint KEY_DEBOUNCE = 400;
/**
* Controller 1 - W + A + S + D
*/
if (
event.key.code == sf::Keyboard::W &&
controller1.debounceUpMs < elapsedMs)
{
controller1.up = true;
controller1.debounceUpMs = elapsedMs + KEY_DEBOUNCE;
}
else if (
event.key.code == sf::Keyboard::A &&
controller1.debounceLeftMs < elapsedMs)
{
controller1.left = true;
controller1.debounceLeftMs = elapsedMs + KEY_DEBOUNCE;
}
else if (
event.key.code == sf::Keyboard::S &&
controller1.debounceDownMs < elapsedMs)
{
controller1.down = true;
controller1.debounceDownMs = elapsedMs + KEY_DEBOUNCE;
}
else if (
event.key.code == sf::Keyboard::D &&
controller1.debounceRightMs < elapsedMs)
{
controller1.right = true;
controller1.debounceRightMs = elapsedMs + KEY_DEBOUNCE;
}
/**
* Controller 2 - Up + Left + Down + Right
*/
else if (
event.key.code == sf::Keyboard::Up &&
controller2.debounceUpMs < elapsedMs)
{
controller2.up = true;
controller2.debounceUpMs = elapsedMs + KEY_DEBOUNCE;
}
else if (
event.key.code == sf::Keyboard::Left &&
controller2.debounceLeftMs < elapsedMs)
{
controller2.left = true;
controller2.debounceLeftMs = elapsedMs + KEY_DEBOUNCE;
}
else if (
event.key.code == sf::Keyboard::Down &&
controller2.debounceDownMs < elapsedMs)
{
controller2.down = true;
controller2.debounceDownMs = elapsedMs + KEY_DEBOUNCE;
}
else if (
event.key.code == sf::Keyboard::Right &&
controller2.debounceRightMs < elapsedMs)
{
controller2.right = true;
controller2.debounceRightMs = elapsedMs + KEY_DEBOUNCE;
}
}
else if (event.type == sf::Event::Resized)
{
sf::FloatRect visibleArea(0, 0, event.size.width, event.size.height);
camera.setSize(event.size.width, event.size.height);
window.setView(camera);
}
}
}
auto physicsUpdate(
ThingVector &things,
const sf::Time deltaTime,
const ThingIDVector &dynamicThings) -> void
{
const auto dt = deltaTime.asSeconds();
std::cout << "----------------\n";
//
// 1. Rigidbody physics simulation
//
for (const auto dynamicThingID : dynamicThings)
{
auto &dynamicThing = things[dynamicThingID];
const float DRAG_COEFFISIENT = 1.0 - dt * 2;
const float GRAVITY = 10.0f * 50;
const float SPEED_OF_CAUSALITY_Y = 10.0f * 100;
const float SPEED_OF_CAUSALITY_X = 10.0f * 100;
dynamicThing.velocity.x = std::min(dynamicThing.velocity.x * DRAG_COEFFISIENT, SPEED_OF_CAUSALITY_X);
dynamicThing.velocity.y = std::min(dynamicThing.velocity.y + GRAVITY * dt * DRAG_COEFFISIENT, SPEED_OF_CAUSALITY_Y);
//
// 2. Move along the x-axis
//
{
const auto oldPosition = dynamicThing.shape.getPosition();
dynamicThing.shape.setPosition(
oldPosition.x + dynamicThing.velocity.x * dt,
oldPosition.y);
// 2.1. Collision-detection x-axis
for (const auto &otherThing : things)
{
if (dynamicThingID == otherThing.id)
{
continue; //... do not test collision with self.
}
if (AABBCollisionDetection(dynamicThing.shape, otherThing.shape))
{
// 2.2 Resolve collision x-axis
dynamicThing.shape.setPosition(oldPosition);
dynamicThing.velocity.x = 0;
break;
}
}
}
//
// 2. Move along the y-axis
//
{
const auto oldPosition = dynamicThing.shape.getPosition();
dynamicThing.shape.setPosition(
oldPosition.x,
oldPosition.y + dynamicThing.velocity.y * dt);
// 2.1. Collision-detection y-axis
for (const auto &otherThing : things)
{
if (dynamicThingID == otherThing.id)
{
continue; //... do not test collision with self.
}
if (AABBCollisionDetection(dynamicThing.shape, otherThing.shape))
{
// 2.2 Resolve collision y-axis
dynamicThing.shape.setPosition(oldPosition);
dynamicThing.velocity.y = 0;
break;
}
}
}
}
std::cout << "----------------\n";
}
/**
* AABBCollisionDetection() - Checks if two axis-aligned-bounding-boxes (AABB) are interescting
*
* @see https://www.toptal.com/game/video-game-physics-part-ii-collision-detection-for-solid-objects
*/
auto AABBCollisionDetection(sf::RectangleShape A, sf::RectangleShape B) -> bool
{
/**
* Offset all positions to make sure all calculations happen
* on positive numbers. This is a rquirement for aabb-algorithm.
*
* | |----------------------------------x
* | |
* | ----- | -----
* | | B | | | B |
* | ----- | -----
* | |
* -------------|--------------x Offset ->> |
* ----- | | -----
* | A | | | | A |
* ----- | | -----
* | |
* | |
* y y
*/
const auto WORLD_OFFSET = sf::Vector2f{1000000.0f, 1000000.0f}; // Offset +1 million in both directions
/**
* Get the aMin, aMax, bMin, bMax corners.
*
* |-----------------------------------------------x
* |
* |
* | [bMin]-------------------
* | | |
* | | |
* | | |
* | | |
* | | |
* | [aMin]----------------- |
* | | | | |
* | | | | |
* | | |---------|---------[bMax]
* | | |
* | | |
* | | |
* | | |
* | -----------------[aMax]
* |
* |
* y
*/
const auto a_pos = A.getPosition();
const auto b_pos = B.getPosition();
const auto a_size = A.getSize();
const auto b_size = B.getSize();
const auto a_min = a_pos - (a_size * 0.5f) + WORLD_OFFSET;
const auto a_max = a_pos - (a_size * 0.5f) + WORLD_OFFSET + a_size;
const auto b_min = b_pos - (b_size * 0.5f) + WORLD_OFFSET;
const auto b_max = b_pos - (b_size * 0.5f) + WORLD_OFFSET + b_size;
/**
* Compute all diffs between min and max, on both x-axis and y-axis.
*/
const auto diff_1 = a_min.x - b_max.x;
const auto diff_2 = a_min.y - b_max.y;
const auto diff_3 = b_min.x - a_max.x;
const auto diff_4 = b_min.y - a_max.y;
/**
* If all diffs are less than 0, we have a collision!
*/
const auto isColliding = diff_1 < 0 && diff_2 < 0 && diff_3 < 0 && diff_4 < 0;
return isColliding;
}
const float BOUNCINESS = 400.f;
const sf::Vector2f JUMP_UP{0, -BOUNCINESS};
const sf::Vector2f JUMP_DOWN{0, BOUNCINESS};
const sf::Vector2f JUMP_LEFT{-BOUNCINESS, 0};
const sf::Vector2f JUMP_RIGHT{BOUNCINESS, 0};
auto animationUpdate(
ThingVector &things,
std::vector<Controller> &controllers,
const sf::Time deltaTime) -> void
{
for (auto &controller : controllers)
{
if (controller.up)
{
things[controller.thingID].velocity += JUMP_UP;
controller.up = false; // Event consumed
}
if (controller.left)
{
things[controller.thingID].velocity += JUMP_LEFT;
controller.left = false; // Event consumed
}
if (controller.down)
{
things[controller.thingID].velocity += JUMP_DOWN;
controller.down = false; // Event consumed
}
if (controller.right)
{
things[controller.thingID].velocity += JUMP_RIGHT;
controller.right = false; // Event consumed
}
}
}
auto renderUpdate(
sf::RenderWindow &window,
const ThingVector &things) -> void
{
window.clear();
for (auto &thing : things)
{
window.draw(thing.shape);
}
window.display();
}
......@@ -10,8 +10,10 @@ set(
targets
01-red-circle
02-jumping-boxes
03-aabb-collision
)
foreach(target ${targets})
add_executable(${target} "${SRCDIR}/${target}/main.cpp")
......
.PHONY: 01 02 all
.PHONY: 01 02 03 all
all:
mkdir -p build;
......@@ -17,3 +17,9 @@ all:
make -C build;
./build/02-jumping-boxes;
03:
mkdir -p build;
cmake --target 03-aabb-collision -S . -B build;
make -C build;
./build/03-aabb-collision;
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment