Hello,
Over the past couple of days, I've been struggling with getting collision detection to work for my game, a 2D sidescrolling platformer, made with SFML 2. This is example code using my most recent method of collision detection, although it has some flaws:

// main.h

#ifndef MAIN_H
#define MAIN_H

#include <SFML/Graphics.hpp>
#include <iostream>

extern sf::RenderWindow mainWin;

extern sf::Sprite player;
extern sf::Sprite platform[3];
extern sf::Texture tPlayer, tPlatform1, tPlatform2;
extern sf::Clock frameClock;

extern sf::Vector2f velocity;
extern bool falling;

enum DIRECTION { NORTH, EAST, SOUTH, WEST, NONE };

void getInput();
void update();
DIRECTION collisionDetection(sf::FloatRect source, sf::FloatRect collide);

#endif // MAIN_H




// main.cpp

#include "main.h"

sf::RenderWindow mainWin(sf::VideoMode(800, 600, 32), "SFML CD Test");
sf::Sprite player;
sf::Sprite platform[3];
sf::Texture tPlayer, tPlatform1, tPlatform2;
sf::Clock frameClock;
sf::Vector2f velocity;
bool falling;

int main()
{
    falling = true;
    tPlayer.loadFromFile("box2.png");
    tPlatform1.loadFromFile("box.png");
    tPlatform2.loadFromFile("box3.png");
    player.setTexture(tPlayer);
    player.setColor(sf::Color::Blue);
    for(int i = 0; i < 2; i++)
    {
        platform[i].setTexture(tPlatform1);
        platform[i].setColor(sf::Color::Red);
    }
    platform[2].setTexture(tPlatform2);
    platform[2].setColor(sf::Color::Yellow);
    platform[0].setPosition(400, 400);
    platform[1].setPosition(200, 300);
    platform[2].setPosition(600, 375);

    while(mainWin.isOpen())
    {
        sf::Event event;
        while(mainWin.pollEvent(event))
        {
            if(event.type == sf::Event::Closed)
                mainWin.close();
        }
        mainWin.clear();

        getInput();
        update();

        mainWin.draw(player);
        for(int i = 0; i < 3; i++)
            mainWin.draw(platform[i]);

        mainWin.display();

        frameClock.restart();
    }
}

void getInput()
{
    if(sf::Keyboard::isKeyPressed(sf::Keyboard::Left))
        velocity.x = -500;
    if(sf::Keyboard::isKeyPressed(sf::Keyboard::Right))
        velocity.x = 500;

    if(!sf::Keyboard::isKeyPressed(sf::Keyboard::Left) && !sf::Keyboard::isKeyPressed(sf::Keyboard::Right))
        velocity.x = 0;


    if(sf::Keyboard::isKeyPressed(sf::Keyboard::Up) && !falling)
    {
        velocity.y = -5000;
        player.move(0, -5);
    }
}

void update()
{
    bool left, right, up, down;
    left = right = up = down = false;
    DIRECTION cdDir;

    for(int i = 0; i < 3; i++)
    {
        cdDir = collisionDetection(player.getGlobalBounds(), platform[i].getGlobalBounds());
        if(cdDir == NORTH)
            up = true;
        else if(cdDir == SOUTH)
            down = true;
        else if(cdDir == EAST)
            right = true;
        else if(cdDir == WEST)
            left = true;
    }

    if(!down)
    {
        if(velocity.y < 200)
            velocity.y += 25;
        falling = true;
    }
    else
    {
        velocity.y = 0;
        falling = false;
    }

    if(right)
        player.move(-2, 0);
    if(left)
        player.move(2, 0);

    float elapsedTime = frameClock.getElapsedTime().asSeconds();

    player.move(velocity.x * elapsedTime, velocity.y * elapsedTime);
}

DIRECTION collisionDetection(sf::FloatRect source, sf::FloatRect collide)
{
    if(!source.intersects(collide))
        return NONE;

    // return the side of source which collide hit
    if(source.top < collide.top && collide.top < source.top + source.height &&
       source.top + source.height < collide.top + collide.height)
    {
        return SOUTH;
    }
    else if(collide.top < source.top && source.top < collide.top + collide.height &&
            collide.top + collide.height < source.top + source.height)
    {
        return NORTH;
    }
    if(source.left < collide.left && collide.left < source.left + source.width &&
            source.left + source.width < collide.left + collide.width)
    {
        return EAST;
    }
    else if(collide.left < source.left && source.left < collide.left + collide.width &&
            collide.left + collide.width < source.left + collide.width)
    {
        return WEST;
    }

    return NONE;
}

The issue with my method is that when there are 2 collisions (for example, LEFT and TOP), it can only return one. Thus, when the player sprite collides with the yellow platform (platform[2]), it usually doesn't detect the left or right collision, and you can "go inside" platform[2].
Excuse my greenness, but it appears to me that there must be some "simple" solution, or many platformers would run slow because of all the calculations collision detection must make each frame.
Thus, my question is, how do most platformers handle collision detection? Is there some "secret" to collision detection that I haven't grasped?

Thanks in advance.

Recommended Answers

All 8 Replies

You could set up some enums like this:

enum EDirection {
    eNone  = 0x0000,

    eNorth = 0x0001,
    eSouth = 0x0010,
    eEast  = 0x0100,
    eWest  = 0x1000,

    eNorthEast = eNorth | eEast,
    eNorthWest = eNorth | eWest,
    eSouthEast = eSouth | eEast,
    eSouthWest = eSouth | eWest
};

And the, check all of the conditions each time (which is only as slow as finding that there is no collision in your current code)

unsigned short collisions = eNone;
if(source.top < collide.top && collide.top < source.top + source.height && source.top + source.height < collide.top + collide.height)
{
    collisions |= eSouth;
}
else if(collide.top < source.top && source.top < collide.top + collide.height && collide.top + collide.height < source.top + source.height)
{
    collisions |= eNorth
}

if(source.left < collide.left && collide.left < source.left + source.width && source.left + source.width < collide.left + collide.width)
{
    collisions |= eEast;
}
else if(collide.left < source.left && source.left < collide.left + collide.width && collide.left + collide.width < source.left + collide.width)
{
    collisions |= eWest;
}

return collisions

Then you can use the enum as a mask to detect the collision directions in the return value:

switch ( unsigned short cols = collisionDetection( source, collide ) )
{
case eNone :
    // Do things
    break;
case eNorth :
    // Do things
    break;
case eSouth :
    // Do things
    break;
case eEast :
    // Do things
    break;
case eWest :
    // Do things
    break;
case eNorthEast :
    // Do things
    break;
case eNorthWest :
    // Do things
    break;
case eSouthEast :
    // Do things
    break;
case eSouthWest :
    // Do things
    break;
default :
    // Handle error!
    std::cerr << "Error! Invalid direction :" << cols << endl;
}

Thanks for the quick reply.
This code by itself works well. However, I think the way I handle the collision is a bit off. Here is the update() function now:

void update()
{
    bool left, right, up, down;
    left = right = up = down = false;

    for(int i = 0; i < 3; i++)
    {
        switch(unsigned short cols = collisionDetection(player.getGlobalBounds(), platform[i].getGlobalBounds()))
        {
            case eNone:
                break;
            case eNorth:
                up = true;
                break;
            case eSouth:
                down = true;
                break;
            case eEast:
                right = true;
                break;
            case eWest:
                left = true;
                break;
            case eNorthEast:
                up = true;
                right = true;
                break;
            case eNorthWest:
                up = true;
                left = true;
                break;
            case eSouthEast:
                down = true;
                right = true;
                break;
            case eSouthWest:
                down = true;
                left = true;
                break;
            default:
                std::cout << "Error! Invalid direction :" << cols << std::endl;
        }
    }

    if(!down)
    {
        if(velocity.y < 200)
            velocity.y += 25;
        falling = true;
    }
    else
    {
        velocity.y = 0;
        falling = false;
    }

    if(right)
        player.move(-2, 0);
    if(left)
        player.move(2, 0);

    float elapsedTime = frameClock.getElapsedTime().asSeconds();

    player.move(velocity.x * elapsedTime, velocity.y * elapsedTime);
}

When I run the program, the second the player sprite hits any platform, it immediately shoots right. I assume this has to do with this part of the code:

    if(right)
        player.move(-2, 0);
    if(left)
        player.move(2, 0);

Any ideas on how I could fix this?

You don't seem to set velocity.x anywhere? Could that be the reason?

Also, the values that you set things to seem to be a bit strange. Obviously, I don't have all the details of what you're trying to do; but in a collision I would imagine that you just want to reverse the velocity of the component affected by the collision. So, something like:

unsigned short cols = collisionDetection(player.getGlobalBounds(), platform[i].getGlobalBounds())

if ( cols != eNone )
{
    if ( cols & ( eNorth | eSouth ) )  // Collision in a vertical direction, reverse y
        velocity.y = -velocity.y;

    if ( cols & ( eEast | eWest ) )  // Collision in a horizontal direction, reverse x
        velocity.x = -velocity.x;
}
else
{
    // No collision, apply a gravitational acceleration (perhaps, I don't know what your plan is in this instance)
    velocity.y -= gravitationalAccn;
}

I'm not sure if I understand your code correctly. I inserted it into my update() function, now it looks like so:

void update()
{
    bool left, right, up, down;
    left = right = up = down = false;

    for(int i = 0; i < 3; i++)
    {
        unsigned short cols;
        switch(cols = collisionDetection(player.getGlobalBounds(), platform[i].getGlobalBounds()))
        {
            case eNone:
                break;
            case eNorth:
                up = true;
                break;
            case eSouth:
                down = true;
                break;
            case eEast:
                right = true;
                break;
            case eWest:
                left = true;
                break;
            case eNorthEast:
                up = true;
                right = true;
                break;
            case eNorthWest:
                up = true;
                left = true;
                break;
            case eSouthEast:
                down = true;
                right = true;
                break;
            case eSouthWest:
                down = true;
                left = true;
                break;
            default:
                std::cout << "Error! Invalid direction :" << cols << std::endl;
        }

        if ( cols != eNone )
        {
            if ( cols & ( eNorth | eSouth ) )  // Collision in a vertical direction, reverse y
                velocity.y = -velocity.y;
            if ( cols & ( eEast | eWest ) )  // Collision in a horizontal direction, reverse x
                velocity.x = -velocity.x;
        }
        else
        {
            // No collision, apply a gravitational acceleration
            velocity.y += .02;
        }
    }

    float elapsedTime = frameClock.getElapsedTime().asSeconds();

    player.move(velocity.x * elapsedTime, velocity.y * elapsedTime);
}

Though there are two problems with this:
1. When collisionDetection() returns eSouth, the player sprite bounces back up into the air.
2. I also tried changing the line velocity.y = -velocity.y to velocity.y = -velocity.y / 2. This stops my first problem. However, when the sprite is colliding eSouth with a platform, the controls are switched (left arrow key moves right, and vice versa).

That last fragment that I posted would be used to replace the switch...case statement, you can see that the up, down, left and right variables are no longer used anywhere.

  1. When collisionDetection() returns eSouth, the player sprite bounces back up into the air.

That is expected, since the collisions here are completely elastic. As you have done, you have to multiply by an additional factor if you don't want this to happen. This is a physically realistic situation. If you bounce a ball on the floor and it doesn't lose any energy in the collision, it will bounce for ever. The actual energy loss is dependent on the material that the platform is made of and the material that the object is made of. You could have this information carried in the platform and player objects:

if ( cols & ( eNorth | eSouth ) )  // Collision in a vertical direction, reverse y
    velocity.y = -velocity.y * player.GetReboundFactor() * platfor.GetReboundFactor();
if ( cols & ( eEast | eWest ) )  // Collision in a horizontal direction, reverse x
    velocity.x = -velocity.x * player.GetReboundFactor() * platfor.GetReboundFactor();

Now you can have some objects that bounce more than others and such.

when the sprite is colliding eSouth with a platform, the controls are switched (left arrow key moves right, and vice versa).

I don't know why this is since I don't think this part is shown in any of the code here. I would guess that it is something to do with the way that you're interpreting the keyboard inputs. Are you doing something like multiplying the velocity by a factor when the keys are pressed? If so, you could try adding to the velocity when the respective arrow keys are pressed.

Are you doing something like multiplying the velocity by a factor when the keys are pressed?

I do multiply the velocity by the frame rate, as seen here:

float elapsedTime = frameClock.getElapsedTime().asSeconds();
player.move(velocity.x * elapsedTime, velocity.y * elapsedTime);

I've tried removing elapsedTime from the code and tweaking the values for movement and gravity, but the problem remains.
I think the issue might be with this:

if ( cols & ( eEast | eWest ) )  // Collision in a horizontal direction, reverse x
    velocity.x = -velocity.x / 2;

Since collisionDetection() requires the two rects to intersect for a collision, I'm thinking that it might always return a two-direction value (eNorthEast, eNorthWest, etc.) If that was the case, perhaps the issue would be that the above if statement is always true, and so it reverses the controls.

I do multiply the velocity by the frame rate, as seen here:

Yes, but how does input from the arrow keys get involved?

If I were you I would print out the directions of the collisions that are detected on each time-step. Or, even better, put in some break-points and step through it in the debugger to see what's going on.

It looks like this is just straight debugging now, your original problem is solved?

Well, I changed my arrow keys code to this:

    if(sf::Keyboard::isKeyPressed(sf::Keyboard::Left))
    {
        if(!collidingX)
            velocity.x = -.5;
        else
            velocity.x = .5;
    }
    if(sf::Keyboard::isKeyPressed(sf::Keyboard::Right))
    {
        if(!collidingX)
            velocity.x = .5;
        else
            velocity.x = -.5;
    }

collisionX is true if cols includes eEast or eWest. This is the code:

        if ( cols != eNone )
        {
            if ( cols & ( eNorth | eSouth ) )
                velocity.y = -velocity.y / 2;
            if ( cols & (eEast | eWest) )
            {
                velocity.x = -velocity.x / 2;
                collidingX = true;
            }
        }

However, I've noticed two issues:
1. The player sprite, if it is colliding eSouth, cannot move off a platform. Consider this screenshot: Click Here
2. If I hold the right arrow key long enough while it's hitting the yellow platform, pressing the left arrow key will cause it to move into the yellow platform, instead of away from it.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.