Cylinder Cube Collision Detection

Home --> Programming Projects --> Block Engine --> Cylinder Cube Collision Detection

Collision Detection: The Right Way

In a block game with gravity, it makese sense to model the player as a cylinder when it comes to moving the player through the world. One reason is so that the player can stand on the edge of clifs and look slightly over. There are other reasons as well.

So we want code to detect a collision between a moving cylinder and a cube. The cylinder is parallel to the z-axis. The cube is also always axis-aligned. The clyinder should be moving in a straight line. We want our code to find the first time the cylinder intersects the cube. Also, we would like to know information about the time of intersection so that after the intersection the cylinder will move parallel to the cube.

That is the right way to do it.

Collision Correction: The Hippie Way

But in addition to the right way to do it, there is also the "hippie way". Cubes are 1 unit wide, so the player's cylinder radius is perhaps 0.3 or something like that. We can move the player's cylinder 0.1 units at a time IGNORING any collision. Then after each 0.1 length move we can see if the cylinder intersects a cube. If it does, then we can move the cylinder slightly so it no longer intersects a cube. However we do not backtrack when there is a collision: we move the cylinder in a possibly new direction. We translate the cylinder the shortest length possible so that it no longer intersects a cube. Call this method Collision Correction.

In this page we describe this "hippie method" of collision correction.

This hippie collision correction method is likely slower than traditional collision detection. However it is simple. In a game where we mainly intersect the player's cyllinder with cubes, it works well and is robust.

What About Changing The Velocity?

Another downside of the collision correction method is that when there is a collision, the collision correction code does NOT generate a velocity vector which we can use to continue to move the cylinder.

However Block Arena and Fractal Block World use the approach of not storing the player's velocity. Instead, we have the position P_0 of the player at the previous time step and the position P_1 of the player at the current time step. To calculate the position P_2 at the next time step, we just pretend the velocity is some multiple (by a fixed constant) of P_1 minus P_0. Then we take this velocity (and modify it because of drag and forces) and combine it with P_1 to get P_2'. Then we do collision correction as we attempt to move the player's cyllinder from P_1 to P_2'. After the collision correction we get the position P_2 of the player. This is sort of like Verlet integration.

With this approach for updating the player's position, momentum works automatically. If the player is moving fast horizontally and hits the ground, he will slide along the ground naturally (ignoring drag effects, which will slow him down). This is what should happen.

The Code

Here is the function CollisionCorrectCylinderCube to move a cylinder which intersects a cube in the smallest way possible so that the cylinder no longer intersects the cube. It uses two helper functions: ColCorHelper1 and ColCorHelper2. The class Vector is a class with floating-point members x,y, and z.

Certainly this code could be made much faster by not pushing onto a vector.
//This is C++ code.

//A "possible correction" is a pair (diff,trans),
//where diff is a float and trans is a Vector.
//Given a possible correction (diff,trans),
//if diff > 0.0 then if we translate the cylinder
//by trans then there will no longer be an intersection.
//If diff <= 0.0, then we can ignore the possible correction pair.

//Given the cylinder and the cube,
//we want to form the list of all possible corrections.
//
//If there are either no possible corrections
//(or every possible correction's diff values are negative)
//then no correction needs to be made.
//
//If there is at least one possible correction (diff,trans)
//where diff is positive, then considering the pair (diff,trans) with
//the least such positive diff, translating the cylinder
//by trans will make it no longer intersect the cylinder.

//To collision correct the cylinder from the cube,
//we have 3 possible corrections:
//1) (Pushing up): moving the cylinder up so the bottom of
//   the cylinder >= the max z value of the cube.
//2) (Pushing down): moving the cylinder down so the top of
//   the cylinder is <= the min z value of the cube.
//3) (Pushing laterally): moving the cylinder
//   laterally (changing the x and y coordinates without
//   changing the z coordinates).
//   Here, we break into 8 cases depending on
//   where the (x,y) position of the cender of the cylinder is
//   relative to the (x,y) coordinates of the 4 vertical edges
//   of the cube.

//This function takes a list of possible corrections
//and returns the possible correction from the list
//with the smallest diff value.
void ColCorHelper1(
    svector< pair > diffs,
    float & best_diff,
    Vector & best_trans)
{
    if( diffs.size() == 0 ) {
        best_diff = 0.0f;
        best_trans = Vector(0.0f, 0.0f, 0.0f);
        return;
    }

    best_diff = diffs[0].first;
    int best_index = 0;
    for(int i = 1; i < diffs.size(); ++i) {
        float diff = diffs[i].first;
        if( diff < best_diff ) {
            best_diff = diff;
            best_index = i;
        }
    }
    best_trans = diffs[best_index].second;
}

//This generates a possible collision pair (pdiff,ptrans)
//for when the cylinder intersects an edge of the cube.
//More precisely, we let mover_plane_p be the center
//of the cylinder projected to the xy plane
//and we let solid_plane_p be the cube edge projected
//to the xy plane.
//Then we calculate how to translate mover_plane_p
//(by the translation vector ptrans) so that
//the resulting vector mover_plane_p + ptrans
//is at least mover_radius away from solid_plane_p.
//
//Just to reiterate, mover_plane_p and solid_plane_p
//should both be in the xy plane.
//If their dist apart is < mover_radius,
//then we set pdiff to be > 0.0 and 
//so a correction needs to be made.
void ColCorHelper2(
    const Vector & mover_plane_p, //Should be in xy plane.
    float mover_radius,           //The radius of the cylinder.
    const Vector & solid_plane_p, //Should be in xy plane.
    float & pdiff,    //Output: the distance required move out of collision.
    Vector & ptrans)  //Output: translation vector to move out of collision.
{
    ptrans = mover_plane_p - solid_plane_p;
    pdiff = mover_radius - Length(ptrans);
    Normalize(ptrans); //Could do faster.
    ptrans *= pdiff;
}

//Returns true iff there is a collision.
//If there is a collision, then trans
//determines which translation should be made
//to move the cylinder so that it no longer
//intersects the cube.
//Morever, this will be the smallest such
//translation that does the trick.
bool CollisionCorrectCylinderCube(
    const Vector & p,     //Bottom of cylinder
    float radius,         //Radius of cylinder.
    float height,         //Height of cylinder.
    const Vector & c_min, //Min position of cube.
    const Vector & c_max, //Max position of cube.
    Vector & trans)       //Output: translation to move cylinder.
{
    float cyl_min_z = p.z;
    float cyl_max_z = p.z + height;

    //The bottom of the cylinder projected
    //to the xy plane.
    Vector plane_p = Vector(p.x, p.y, 0.0f);

    //When the smallest diff is positive,
    //a correction will be made.

    svector< pair > diffs;

    //Pushing up.
    float diff1 = c_max.z - cyl_min_z;
    Vector trans1(0.0f, 0.0f, diff1);
    diffs.push_back( make_pair(diff1, trans1) );

    //Pushing down.
    float diff2 = cyl_max_z - c_min.z;
    Vector trans2(0.0f, 0.0f, -diff2);
    diffs.push_back( make_pair(diff2, trans2) );

    //Pushing laterally.
    //Breaking into 8 cases for the plane.
    float pdiff;
    Vector ptrans;
    if( p.x >= c_max.x ) {
        //Right.
        if( p.y >= c_max.y) {
            //Front right.
            //Pushing away from a vertal cube edge.
            ColCorHelper2(
                plane_p, radius,
                Vector(c_max.x, c_max.y, 0.0f),
                pdiff, ptrans);
            diffs.push_back( make_pair(pdiff, ptrans) );
        } else if( p.y <= c_min.y ) {
            //Back right.
            //Pushing away from a vertal cube edge.
            ColCorHelper2(
                plane_p, radius,
                Vector(c_max.x, c_min.y, 0.0f),
                pdiff, ptrans);
            diffs.push_back( make_pair(pdiff, ptrans) );
        } else {
            //Middle right.
            //Pushing away from a vertical cube face.
            pdiff = radius - (p.x - c_max.x);
            ptrans = Vector(pdiff, 0.0f, 0.0f);
            diffs.push_back( make_pair(pdiff, ptrans) );
        }
    } else if( p.x <= c_min.x ) {
        //Left.
        if( p.y >= c_max.y ) {
            //Front left.
            //Pushing away from a vertal cube edge.
            ColCorHelper2(
                plane_p, radius,
                Vector(c_min.x, c_max.y, 0.0f),
                pdiff, ptrans);
            diffs.push_back( make_pair(pdiff, ptrans) );
        } else if( p.y <= c_min.y ) {
            //Back left.
            //Pushing away from a vertal cube edge.
            ColCorHelper2(
                plane_p, radius,
                Vector(c_min.x, c_min.y, 0.0f),
                pdiff, ptrans);
            diffs.push_back( make_pair(pdiff, ptrans) );
        } else {
            //Middle left.
            //Pushing away from a vertical cube face.
            pdiff = radius - (c_min.x - p.x);
            ptrans = Vector(-pdiff, 0.0f, 0.0f);
            diffs.push_back( make_pair(pdiff, ptrans) );
        }
    } else {
        //Middle.
        if( p.y >= c_max.y ) {
            //Front middle.
            //Pushing away from a vertical cube face.
            pdiff = radius - (p.y - c_max.y);
            ptrans = Vector(0.0f, pdiff, 0.0f);
            diffs.push_back( make_pair(pdiff, ptrans) );
        } else if( p.y <= c_min.y ) {
            //Back middle.
            //Pushing away from a vertical cube face.
            pdiff = radius - (c_min.y - p.y);
            ptrans = Vector(0.0f, -pdiff, 0.0f);
            diffs.push_back( make_pair(pdiff, ptrans) );
        } else {
            //Middle middle.
            //Should not have to worry about!
        }
    }

    //Finding the possible correction
    //with the smallest diff value.
    float best_diff;
    Vector best_trans;
    ColCorHelper1(
        diffs, best_diff, best_trans);

    if( best_diff <= 0.0f ) {
        //No collision.
        return false;
    } else {
        trans = best_trans;
        return true;
    }
}