Advertisement

SAT-Collision detection and resolve

Started by July 04, 2020 12:25 PM
3 comments, last by C_Worm 4 years, 7 months ago

Hey, i'm trying to implement SAT-collision detection and response and was wondering if someone could take a look at it and come with ideas on how to improve the resolving of the entity and get it more clean. Right now it doesn't quite work because it detects collision when e1 is close to (but outside) the corners of e2. and the TranslateEntity() just sets e1 on top of e2.

e1 is a isometric tile.

e2 is a plain square AABB tile.

Entity struct:


struct Entity
{

    V4f_COL col;
    V2f texCoord;
    V2f texDimension;
    
    V2f points[4] = {};

    void CreateQuad(float x, float y, float w, float h)
    {
        
        points[pTop].x = x;
        points[pTop].y = y;
        
        points[pLeft].x = x;
        points[pLeft].y = y + h;
        
        points[pRight].x = x + w;
        points[pRight].y = y;
        
        points[pBottom].x = x + w;
        points[pBottom].y = y + h;
        
    }
    
    void CreateIsometricQuad(float x, float y, float w, float h)
    {
        
        points[pTop].x = x;
        points[pTop].y = y;
        
        points[pLeft].x = x - (w / 2.0f);
        points[pLeft].y = y + (h / 2.0f);
        
        points[pRight].x = x + (w / 2.0f);
        points[pRight].y = y + (h / 2.0f);
        
        points[pBottom].x = x;
        points[pBottom].y = y + h;
        
    }
    
    void Move(V2f movement)
    {
        for(i32 i = 0; i < 4; i++)
        {
            points[i] += movement;
        }
    }
    
    
    void SetSolidColor(float _r, float _g, float _b, float _a)
    {
        col.r = _r;
        col.g = _g;
        col.b = _b;
        col.a = _a;
    }
    
    void SetTextureCoordsToFullMap()
    {
        texCoord.x = 0.0f;
        texCoord.y = 0.0f;
        texDimension.x = 1.0f;
        texDimension.y = 1.0f;
    }
    
    
    void SelectTextureFragment(float x, float y, float w, float h)
    {
        texCoord.x = x;
        texCoord.y = y;
        texDimension.x = w;
        texDimension.y = h;
    }
    
};

CollisionInfo struct:


struct CollisionInfo
{
    bool collision;
    float minPenetration;
    V2f mtv;
};

Here's is where im using it:



    CollisionInfo ci_0 = {};
    CollisionInfo ci_1 = {};
   
    // COLLISION CHECKING
    ci_1 = Collision_Entity_SAT(e[tile_00], e[tile_00 + 1]);
    ci_0 = Collision_Entity_SAT(e[tile_00], e[ghostSorc]);
    
    if(ci_1.collision || ci_0.collision)
    {
        if(ci_1.collision)
        {
            printf("c_1.overlap: %f\n", ci_1.minPenetration);
            printf("c_1.mtv: ( %f, %f )\n\n", ci_1.mtv.x, ci_1.mtv.y);
            
            tilePos += ci_1.mtv;
            e[tile_00].SetSolidColor(0.0f, 0.5f, 0.0f, 1.0f);
            e[tile_00].Move(ci_1.mtv);
            
        }
        
        if(ci_0.collision)
        {
            printf("c_0.overlap: %f\n", ci_0.minPenetration);
            printf("c_0.mtv: ( %f, %f )\n\n", ci_0.mtv.x, ci_0.mtv.y);
            
            tilePos += ci_0.mtv;
            e[tile_00].SetSolidColor(0.0f, 0.5f, 0.0f, 1.0f);
            e[tile_00].Move(ci_0.mtv);
        }
    }
    else
    {
        e[tile_00].SetSolidColor(0.2f, 0.0f, 0.2f, 1.0f);
    }
    

Collision_Entity_SAT();

CollisionInfo Collision_Entity_SAT(Entity e1, Entity e2)
{
CollisionInfo result = {};
    
    float dot_e1[4] = {};
    float dot_e2[4] = {};
    
    V2f normals_e1[4] = {};
    V2f normals_e2[4] = {};
    V2f mtv = {};
    
    float minDot_e1 = 0.0f;
    float maxDot_e1 = 0.0f;
    
    float minDot_e2 = 0.0f;
    float maxDot_e2 = 0.0f;
    
    float overlap = 100000000.0f;
    float tempOverlap = 0.0f;
    
    // Get normals_e1 of all sides and make them unit vectors
    for(i32 i = 0; i < 4; i++)
    {
        
        if(i == 3)
        {
            normals_e1[i] = Unit_V2f(Normal_V2f(e1.points[i], e1.points[(i - 3)]));
            
        }
        else
        {
            normals_e1[i] = Unit_V2f(Normal_V2f(e1.points[i], e1.points[(i + 1)]));
            
        }
        
    }
    
    // Get normals_e2 of all sides and make them unit vectors
    for(i32 i = 0; i < 4; i++)
    {
        
        if(i == 3)
        {
            normals_e2[i] = Unit_V2f(Normal_V2f(e2.points[i], e2.points[(i - 3)]));
            
        }
        else
        {
            normals_e2[i] = Unit_V2f(Normal_V2f(e2.points[i], e2.points[(i + 1)]));
            
        }
        
    }
    
    
    
    for(i32 j = 0, i = 0; j < 4; j++)
    {
        
        for(i = 0; i < 4; i++)
        {
            
            // calc dot products of all points one normal at a time
            dot_e1[i] = DotProduct_V2f(normals_e1[j], e1.points[i]);
            dot_e2[i] = DotProduct_V2f(normals_e1[j], e2.points[i]);
            
        }
        
        // Get min/max Dotproducts of each entity against 1 normal at a time
        minDot_e1 = Min_f(dot_e1, sizeof(dot_e1)/sizeof(float));
        maxDot_e1 = Max_f(dot_e1, sizeof(dot_e1)/sizeof(float));
        
        minDot_e2 = Min_f(dot_e2, sizeof(dot_e2)/sizeof(float));
        maxDot_e2 = Max_f(dot_e2, sizeof(dot_e2)/sizeof(float));
        
        // Check for overlapp
        tempOverlap = OverLap(minDot_e1, maxDot_e1, minDot_e2, maxDot_e2);
        
        // No overlapp = no collision
        if(tempOverlap <= 0.0f)
        {
            
            result.collision = false;
            result.mtv = v2f(0.0f, 0.0f);
            result.minPenetration = 0.0f;
            return result;
        }
        // some overlapp occured = collision on the axis currently under control
        else
        {
            
            if(tempOverlap < overlap)
            {
                overlap = tempOverlap;
                mtv = normals_e1[j];
                result.minPenetration = overlap;
            }
            
        }
        
    }
    
    
    // Do it all again but check against the normals on the other entity
    for(i32 j = 0, i = 0; j < 4; j++)
    {
        
        for(i = 0; i < 4; i++)
        {
            
            // calc dot products of all points one normal at a time
            dot_e1[i] = DotProduct_V2f(normals_e2[j], e1.points[i]);
            dot_e2[i] = DotProduct_V2f(normals_e2[j], e2.points[i]);
            
        }
        
        // get min/max DP's
        minDot_e1 = Min_f(dot_e1, sizeof(dot_e1)/sizeof(float));
        maxDot_e1 = Max_f(dot_e1, sizeof(dot_e1)/sizeof(float));
        
        minDot_e2 = Min_f(dot_e2, sizeof(dot_e2)/sizeof(float));
        maxDot_e2 = Max_f(dot_e2, sizeof(dot_e2)/sizeof(float));
        
        // Check for overlapp
        tempOverlap = OverLap(minDot_e1, maxDot_e1, minDot_e2, maxDot_e2);
        if(tempOverlap <= 0.0f)
        {
            
            result.collision = false;
            result.mtv = v2f(0.0f, 0.0f);
            result.minPenetration = 0.0f;
            return result;
        }
        else
        {
            
            if(tempOverlap < overlap)
            {
                
                overlap = tempOverlap;
                mtv = normals_e2[j];
                result.minPenetration = overlap;
                
            }
            
        }
        
    }
    
    // Collision occured and we obtain the minimum penetration magnitude and the corresponding axis
    result.mtv = Scalar_V2f(result.minPenetration, mtv);
    result.collision = true;
    
    return result;
    
}

Although I can't help you with SAT, I would like to give you some advice about posting.

Currently your posted code is quite big. Some of it is redundant. I tend to write too much in my posts so I know your pain, but try to keep it short. There's a lot of functions which seem to be somewhat long in terms of lines of code, but maybe that's your style. Personally I almost never set individual members of say, vec3f or similar. Also redundant functions which are not related to the question.
This isn't necessarily a bad thing if you want clarity but setting every bit of a VectorNFloat will eat your development time as it's quite easy to make a mistake and hard to look at a lot of code. If your math library doesn't already provide scalar * VectorNFloat overloads as well as other things, code will be error prone. If you wrote it yourself, as you did above, it might be better to test it and make sure the error isn't there. For more complicated matters, having a well known robust library to use or compare to is beneficial. Once you know the rest is solid, post the function that executes the SAT test and people that know how it works will be able to tell you in a few seconds if it's good or not.

If you are just starting, I don't want to discourage you from posting here. It's an amazing forum with very knowledgeable and helpful people, worth it's … bytes?… in gold. By all means do, but if you are unsure of things like your vector math library, use an existing one, at least to test new things like these, it will help you and others.

Advertisement
 void CreateQuad(float x, float y, float w, float h)
    {
        
        points[pTop].x = x;
        points[pTop].y = y;
        
        points[pLeft].x = x;
        points[pLeft].y = y + h;
        
        points[pRight].x = x + w;
        points[pRight].y = y;
        
        points[pBottom].x = x + w;
        points[pBottom].y = y + h;
        
    }
    
    void CreateIsometricQuad(float x, float y, float w, float h)
    {
        
        points[pTop].x = x;
        points[pTop].y = y;
        
        points[pLeft].x = x - (w / 2.0f);
        points[pLeft].y = y + (h / 2.0f);
        
        points[pRight].x = x + (w / 2.0f);
        points[pRight].y = y + (h / 2.0f);
        
        points[pBottom].x = x;
        points[pBottom].y = y + h;
        
    }

This code has several issues.

  • You probably want to process general 2D convex polygons, not “quad” and “isometric quad”.
    Not only to increase generality (you are restricting algorithms that works for any convex polygon to quadrilaterals), but to get rid of the hardcoded array sizes (the number of vertices is a property of the polygon, not a constant) and the assumptions about vertex positions and edge orientations that are confusing you in the proper collision handling code.
  • Functions that create a polygon representing a rectangular or isometric tile should be part of a separate polygon factory; the polygon should have a simple constructor taking a sequence or an array of points, without assumptions about tile shapes or the like.
  • Top, left, right and bottom are good names for parameters of factory functions, not for the vertices of the polygon.
  • More importantly, x and y should be a V2f, to represent a point, and a factory of axis-aligned or isometric quads should know w, h and more generally tile shapes and conventions: it should create the polygons given only one reference point (e.g. the center).
  • What are pos and size?
  • What is V3f, if the game is 2D?
  • Edge normals should be stored in the polygon, not computed repeatedly. They don't even change in case the polygon is translated.

Vector operations should be V2f etc. operators and member functions.

Regarding the collision calculation, you should rewrite it for arbitrary polygons that don't necessarily have the same number of vertices. I estimate it's going to become half as large and twice as correct.

Also, a likely bug:

 printf("COLLISION\n");
        result.minPenetration = penetration[0];
        for(i32 i = 0; i < 4; i++)
        {
            if(result.minPenetration < penetration[i])
            {
                result.minPenetration = penetration[i];
            }
            
            result.minVec = ScalarVec_V2f(penetration[i], normals_e2[j]);
        }

result.minVec is set to ScalarVec_V2f(penetration[3], normals_e2[3]) irrespective of the i and j values that correspond to result.minPenetration.

Omae Wa Mou Shindeiru

Okay, thanks for your inputs!

I have tested the math/vector functions, and they do seem to work.

However i've update the SAT-function and the resolving of the collitions works only on to sides (left and lower).

when the incoming object collides with the right/upper side of one of the other (isomteric/AABB) “tile”, the object just teleports to the other side of the object it collided with.

Im testing a isometric tile against another isometric tile and against a simple axis alligned rectangle.

so my question is now, do you have any ideas on why only two side seem to resolve the collision correctly?

This topic is closed to new replies.

Advertisement