C++ SFML collision is not accurate

Viewed 526

I'm making a 2D game with SFML in C++ and I have a problem with collision. I have a player and a map made of tiles. Thing that doesn't work is that my collision detection is not accurate. When I move player up and then down towards tiles, it ends up differently.

Player very high above tile Player above tile Player standing on tile

I am aware that source of this problem may be calculating player movement with use of delta time between frames - so it is not constant. But it smooths movement, so I don't know how to do it other way. I tried with constant speed valuses and to make collision fully accurate - speed had to be very low and I am not satisfied with that.

void Player::move() {
    sf::Vector2f offsetVec;

    if (sf::Keyboard::isKeyPressed(sf::Keyboard::W))
        offsetVec += sf::Vector2f(0, -10);

    if (sf::Keyboard::isKeyPressed(sf::Keyboard::S))
        offsetVec += sf::Vector2f(0, 10);

    if (sf::Keyboard::isKeyPressed(sf::Keyboard::A))
        offsetVec += sf::Vector2f(-10, 0);

    if (sf::Keyboard::isKeyPressed(sf::Keyboard::D))
        offsetVec += sf::Vector2f(10, 0);

    this->moveVec += offsetVec;
}

void Player::update(float dt, Map *map) {
    sf::Vector2f offset = sf::Vector2f(this->moveVec.x * this->playerSpeed * dt,
                                       this->moveVec.y * this->playerSpeed * dt);
    sf::Sprite futurePos = this->sprite;
    futurePos.move(offset);
    if (map->isCollideable(this->pos.x, this->pos.y, futurePos.getGlobalBounds())) {
        this->moveVec = sf::Vector2f(0, 0);
        return;
    }
    this->sprite.move(offset);
    this->pos += offset;
    this->moveVec = sf::Vector2f(0, 0);
    return;
}

In player position update I create future sprite object, which is object after applying movement, to get it's boundaries and pass it to collision checker. To collision checker I also pass player pos, because my map is stored in 2d array of tile pointers, so I check only these in player range.

bool Map::isCollideable(float x, float y, const sf::FloatRect &playerBounds) {
    int startX = int(x) / Storage::tileSize;
    int startY = int(y) / Storage::tileSize;
    Tile *tile;
    for (int i = startX - 10; i <= startX + 10; ++i) {
        for (int j = startY - 10; j <= startY + 10; ++j) {
            if (i >= 0 && j >= 0) {
                tile = getTile(i, j);
                if (tile != nullptr && playerBounds.intersects(tile->getGlobalBounds()))
                    return true;
            }
        }
    }
    return false;
}

Full project on Github

My solution

I have changed if statement in update function to while statement, which decreases my offset vector till no collision is present. I still have to make some adjustments, but general idea is:

void Player::update(float dt, Map *map) {
    int repeats = 0;
    sf::Vector2f offset = sf::Vector2f(this->moveVec.x * this->playerSpeed * dt,
                                       this->moveVec.y * this->playerSpeed * dt);
    sf::Sprite futurePos = this->sprite;
    while (map->isCollideable(this->pos.x, this->pos.y, futurePos, offset)) {
        offset = 0.7f * offset;
        repeats++;
        if (repeats > 5) {
            this->moveVec = sf::Vector2f(0, 0);
            return;
        }
    }
    this->sprite.move(offset);
    this->pos += offset;
    this->moveVec = sf::Vector2f(0, 0);
    return;
}

I also had to rework isCollideable method a little, so it accepts sf::Sprite and offset vector so it can calculate boundaries on it's own.

2 Answers
Related