Hi dear community,
I have added collision response for a dynamic entity colliding with a static one (at least that's what I spontaneously chose to call them). By static I mean it doesn't move and by dynamic I mean that it can move.
If I run into a static object and let go of the controls, my entity is positioned nicely at the outer bounds of the static once. (for reference see attachment)
However, if I, after colliding with the static object, continue to move towards the static object (which causes the dynamic object to slide btw), the dynamic one is further in than the outside bounds of the static object (for reference see attachment) Once I stop moving, it immediately jumps back to the correct position.
I think this is a bug (in my game of course!), and it rather annoys me. I mean, it still fulfills its purpose as preventing the player from running through a brick wall, but I believe it isn't correct.
I can't figure out what I have to change in my moving/collision response mechanics to solve this problem. I would be very thankful if someone could help me out and give me some hints.
I checked out this thread: http://en.sfml-dev.org/forums/index.php?topic=13766.0 and I really loved the little example program, but I couldn't really use it to fix my problem.
Another question I have, however it is even less related to SFML, is whether my method of collision response is appropriate. I have read quite many blog posts, tutorials and StackOverflow posts, yet frankly I only understood half of them at best, and from what I picked up, my method was once described (except that I don't use a collision normal (yep, don't exactly know what that is either) to determine the axis, but some ugly if-statements). Please consider this a low priority question, if it is not linked to my problem.
Here is the response code (added comments for hopefully clarification):
// pair is a pair of colliding Entities
else if (matches_categories(pair, Category::DynamicBody, Category::StaticBody)) {
// dynamic as in movable (e.g. a car), static as in non-movable (e.g. a wall)
auto& dynamicEnt = static_cast<Entity&>(*pair.first);
auto& staticEnt = static_cast<Entity&>(*pair.second);
auto dynamicBounds = dynamicEnt.getBoundingRect();
auto staticBounds = staticEnt.getBoundingRect();
// since the origins are centered, I need additional offset
// so setPosition() puts it to the outside of the static Entity
const float offset = dynamicBounds.width / 2.f;
sf::Vector2f position = dynamicEnt.getPosition();
// in some situations the dynamic entity would jump around (to the corners mostly), adding
// this tolerance value fixed the problem
float tol = 5;
if (bottom(dynamicBounds) > staticBounds.top + tol && dynamicBounds.top < bottom(staticBounds) - tol) {
// the collision if happening along the X-Achsis
// dynamicEnt.setVelocity(0, dynamicEnt.getVelocity().y);
if (position.x > staticEnt.getPosition().x)
// collision happened to the right of the static entity
position.x = right(staticBounds) + offset;
else
// collision happened to the left of the static entity
position.x = staticBounds.left - offset;
}
else if (right(dynamicBounds) > staticBounds.left && dynamicBounds.left < right(staticBounds)) {
// the collision is happending along the Y-Achsis
// dynamicEnt.setVelocity(dynamicEnt.getVelocity().x, 0);
if (position.y > staticEnt.getPosition().y)
// collision happened at rhe bottom of the static entity
position.y = bottom(staticBounds) + offset;
else
// collision happened at the top of the static entity
position.y = staticBounds.top - offset;
}
dynamicEnt.setPosition(position);
My update of the world is something like this (broken down):
update
set player velocity (handle input)
handle collisions
update entities (e.g. move player by player velocity * dt)
set player velocity to (0, 0)
My input handling for movement is done like this (the actual direction is determined by rotation):
if direction Forward
entity.setVelocity(0, -mech.get_max_speed());
else if direction Backward
entity.setVelocity(0, mech.get_max_speed());
};
I have created a minimal example. I know the positioning isn't perfect from each side, but it illustrates what I have described quite nicely:
#include <SFML/Graphics.hpp>
float right(const sf::FloatRect& rect) {
return rect.left + rect.width;
}
float bottom(const sf::FloatRect& rect) {
return rect.top + rect.height;
}
void check_handle_collision(sf::RectangleShape& dynamic,
const sf::RectangleShape& stat)
{
auto dynamicBounds = dynamic.getGlobalBounds();
auto staticBounds = stat.getGlobalBounds();
if(dynamicBounds.intersects(staticBounds)) {
const float offset = dynamicBounds.width / 2.f;
sf::Vector2f position = dynamic.getPosition();
float tol = 2;
if (bottom(dynamicBounds) > staticBounds.top + tol && dynamicBounds.top < bottom(staticBounds) - tol) {
if (position.x > stat.getPosition().x)
position.x = right(staticBounds) + offset;
else
position.x = staticBounds.left - offset;
}
else if (right(dynamicBounds) > staticBounds.left && dynamicBounds.left < right(staticBounds)) {
if (position.y > stat.getPosition().y)
position.y = bottom(staticBounds) + offset;
else
position.y = staticBounds.top - offset;
}
dynamic.setPosition(position);
}
}
int main() {
sf::RenderWindow window(sf::VideoMode(640, 480), "Minimal example");
sf::RectangleShape dynamic(sf::Vector2f(64, 64));
dynamic.setFillColor(sf::Color::Transparent);
dynamic.setOutlineColor(sf::Color::Green);
dynamic.setOutlineThickness(1);
dynamic.setOrigin(dynamic.getGlobalBounds().width / 2.f,
dynamic.getGlobalBounds().height / 2.f);
dynamic.setPosition(100, 100);
sf::RectangleShape stat = dynamic; // short for static
stat.setOutlineColor(sf::Color::Blue);
stat.setPosition(sf::Vector2f(window.getSize()) / 2.f);
sf::Clock clock;
sf::Time timeSinceLastUpdate = sf::Time::Zero;
const sf::Time TimePerFrame = sf::seconds(1.f / 60.f);
const float speed = 200.f;
sf::Vector2f vel(0, 0);
while (window.isOpen()) {
sf::Time dt = clock.restart();
timeSinceLastUpdate += dt;
while (timeSinceLastUpdate > TimePerFrame) {
timeSinceLastUpdate -= TimePerFrame;
sf::Vector2f vel(0, 0);
sf::Event event;
while(window.pollEvent(event)) {
if(event.type == sf::Event::Closed)
window.close();
}
if(sf::Keyboard::isKeyPressed(sf::Keyboard::Up))
vel.y += -speed;
if(sf::Keyboard::isKeyPressed(sf::Keyboard::Down))
vel.y += speed;
if(sf::Keyboard::isKeyPressed(sf::Keyboard::Left))
vel.x += -speed;
if(sf::Keyboard::isKeyPressed(sf::Keyboard::Right))
vel.x += speed;
check_handle_collision(dynamic, stat);
dynamic.move(vel * TimePerFrame.asSeconds());
}
window.clear();
window.draw(stat);
window.draw(dynamic);
window.display();
}
}
Sorry for the long post, I just really wanted to describe things precisely, and provide sufficient material.
The way dabbertorres mentioned already, you can just swap these two lines:
check_handle_collision(dynamic, stat);
dynamic.move(vel * TimePerFrame.asSeconds());
to become:
dynamic.move(vel * TimePerFrame.asSeconds());
check_handle_collision(dynamic, stat);
and that will fix the problem you described (I assume that your actual code tests for vertical collision too.)
On a separate note, when you use outlines on shapes, the zero position (0, 0) is the corner of the shape, not the outline (the outline extends outwards from the shape). To allow for this, you can either use negative outline values to extend inwards instead, or you will need to add .left to .width and .top to .height thus:
dynamic.setOrigin((dynamic.getGlobalBounds().width + dynamic.getGlobalBounds().left) / 2.f,
(dynamic.getGlobalBounds().height + dynamic.getGlobalBounds().top) / 2.f);
Although, I would prefer local bounds here instead.
p.s. When testing your code, I added the following code after the keyboard input to keep the speed consistent, even during diagonal movement:
if ((vel.x > 0.01f || vel.x < -0.01f) && (vel.y > 0.01f || vel.y < -0.01f))
vel *= 0.707f;
You're welcome :P
Hi,
uhm, I guess this was a dead simple solution, at least the described problem doesn't occur anymore...
I was thinking way too complicated, I should have tried that by myself. I was just sort of following the book with my design, but now that I think about it, this type of collision code should go where the other function 'adapt_player_pos()' lies, which keeps the player from moving outside of the world bounds. Quite logic actually..
In a nutshell, this is what I have now currently:
handle collisions
update
handle body collisions // stupid naming, I know
If I just moved the entire collision code behind the update function, things started happening like entire spritesheets of explosion animations popping up for a split second when they started playing... I'll just keep two seperate functions for know.
Anyway, thankyou for the wake-up call.
P.S.:
@hapax:
Thanks for the hint about the origin. I used negative OutlineThickness and it displays nicely on all sides equally now.
Forgive me, I simply overlooked correcting the diagonal movement, I was so occupied with that frustrating collision response :P