[libgdx]Collision detection not working

Ive been trying to build a collision detection algorithm that would work for my needs, since I had to pull box2d out of the project I was working on, since there were issues that were creating more grief than was being solved.

So, I looked at a few different options, I thought AABB collisions would be simplest, but I intend to have ramps / slopes in a platform game and so simple AABB collisions won’t cut it.

After, I tried to use the intersector class with LibGDX, except that, for some reason it would not detect the collision until after the player was contained in the terrain polygon.

So, I looked around and found a bunch of sources, which I tried to convert to implement a SAT algorithm, its not 100% complete, but it seems complete enough to determine IF a collision is being detected.

http://www.dyn4j.org/2010/01/sat/


http://www.metanetsoftware.com/technique/tutorialA.html
http://www.gamedev.net/topic/619029-separating-axis-theorem/

These all seem to work out to the same…

A few things, is there a way to ensure that the polygons vectors are actually alligned to be able to compare overlap?

Could it be that I’m not getting the correct normal?

Anyway, here’s the relevant code, I’ve been trying with the debugger, and was hoping to get some insights into anything that sticks out that I’m not seeing.

In the meantime, I’ll be making an effort to draw out the tests because just looking at the numbers spitting out doesn’t seem very helpful since no matter what’s going on its not registering an overlap…

    public boolean collides(Polygon poly1, Polygon poly2){
        boolean check = true;
        minMax1.isZero();
        minMax2.isZero();

        Array<Vector2> points1 = getPoints(poly1);
        Array<Vector2> points2 = getPoints(poly2);
        Array<Vector2> normal1 = getNormals(points1);
        Array<Vector2> normal2 = getNormals(points2);
        Vector2 edgeNormal;

        int edgeCountA = points1.size;
        int edgeCountB = points2.size;
        for (int edgeIndex = 0; edgeIndex < edgeCountA + edgeCountB; edgeIndex++) {
            if (edgeIndex < edgeCountA) {
                edgeNormal = normal1.get(edgeIndex);
            } else {
                edgeNormal = normal2.get(edgeIndex - edgeCountA);
            }
//            edgeNormal.nor();

            projA = ProjectPolygon(edgeNormal, points1);
            projB = ProjectPolygon(edgeNormal, points2);

            if (!projA.overlap(projB)) {
                Gdx.app.log("Gap detected", "no collision");
                check = false;
                break;
            }

            Gdx.app.log("If you made it this far", "There is a overlap");
        }
        return check;
    }

    private Projection ProjectPolygon(Vector2 edge, Array<Vector2> polyPoints){
        // To project a point on an axis use the dot product
        float dotProduct = edge.dot(polyPoints.first());

        float min = dotProduct;
        float max = dotProduct;
        for (int i = 0; i < polyPoints.size; i++) {
            dotProduct = polyPoints.get(i).dot(edge);
            if (dotProduct < min) {
                min = dotProduct;
            } else {
                if (dotProduct > max) {
                    max = dotProduct;
                }
            }
        }
        return new Projection(min, max);
    }

    public Array<Vector2> getPoints(Polygon shape){
        float[] vertices = shape.getVertices();
//        float[] vertices = shape.getTransformedVertices();
        int numVertices = shape.getVertices().length;

        Array<Vector2> points = new Array<Vector2>();
        for (int i = 0; i < numVertices / 2; i++){
            float x = vertices[i * 2];
            float y = vertices[i * 2 + 1];
            points.add(new Vector2(x,y));
        }
        return points;
    }

    public Array<Vector2> getNormals(Array<Vector2> edges) {
        for (int j = 0; j < edges.size; j++){
            //get the current vertex
            Vector2 p1 = edges.get(j);
            //get the next vertex
            Vector2 p2 = edges.get(j + 1 == edges.size ? 0 : j + 1);
            //subtract the two to get the edge between them
            Vector2 edge = p1.sub(p2);
            //The perpendicular vector is the normal
            Vector2 normal = new Vector2(-edge.y, edge.x); //edge.rotate90(1);
            //if the polygons are determined counter clockwise, then get the normal by shifting the
            //edge 90 degrees clockwise
            edges.set(j, normal);
        }
        return edges;
    }

this is the main code, it gets used by sending the player polygon and iterating through the terrain polygons defined in tiled and loaded in the map parser.

Let me know if anything extra might be needed to help you help me, any pointers would be appreciated.

My suggestion is that you test your algorithm with some simple polygons first (maybe a square), which should be much easier to debug than complex convex shapes.

Another thing, beware that SAT only works with CONVEX polygons, if you send in a concave shape, it will break.

I’ve been working with SAT collision detection only recently, but anyway here is my code (im also using LibGDX). If you run it you should see two ugly-looking convex polygons, use the arrow keys to move the left one, which should turn red if the two polygons collide.


package com.tofugeki.sandbox;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class CollisionDemo extends ApplicationAdapter {

    private List<Abomination> objects = new ArrayList<>();

    private ShapeRenderer renderer;
    private OrthographicCamera camera;

    @Override
    public void create() {
        renderer = new ShapeRenderer();
        camera = new OrthographicCamera(1920, 1080);

        // create two abomination objects
        final Abomination objectA = new Abomination();
        final Abomination objectB = new Abomination();
        objectA.position.set(-200, 0);
        objectB.position.set(200, 0);
        objects.addAll(Arrays.asList(objectA, objectB));
    }

    @Override
    public void render() {
        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        if (Gdx.input.isKeyPressed(Keys.UP))
            objects.get(0).position.add(0, 5);
        if (Gdx.input.isKeyPressed(Keys.DOWN))
            objects.get(0).position.add(0, -5);
        if (Gdx.input.isKeyPressed(Keys.LEFT))
            objects.get(0).position.add(-5, 0);
        if (Gdx.input.isKeyPressed(Keys.RIGHT))
            objects.get(0).position.add(5, 0);

        renderer.setProjectionMatrix(camera.combined);
        renderer.begin(ShapeType.Line);
        renderer.setColor(Color.WHITE);
        if (collides(objects.get(0), objects.get(1)))
            renderer.setColor(Color.RED);
        for (Abomination object : objects)
            object.render(renderer);
        renderer.end();
    }

    private static boolean collides(Abomination objectA, Abomination objectB) {
        // project both objects on all normals
        // if the projections on any normal are separate
        // then object A and B do not collide
        for (int i = 0; i < objectA.vertices.size(); i++) {
            final Vector2 A = objectA.vertices.get(i);
            final Vector2 B = objectA.vertices.get((i + 1) % objectA.vertices.size());
            final Vector2 normal = new Vector2(B.x - A.x, B.y - A.y).rotate90(-1);
            if (!intersects(objectA, objectB, normal))
                return false;
        }
        for (int i = 0; i < objectB.vertices.size(); i++) {
            final Vector2 A = objectB.vertices.get(i);
            final Vector2 B = objectB.vertices.get((i + 1) % objectB.vertices.size());
            final Vector2 normal = new Vector2(B.x - A.x, B.y - A.y).rotate90(-1);
            if (!intersects(objectA, objectB, normal))
                return false;
        }
        return true;
    }

    private static boolean intersects(Abomination objectA, Abomination objectB, Vector2 axis) {
        // checks if the projection of object A
        // and object B intersects each other on an axis
        float minA = Float.MAX_VALUE;
        float maxA = Float.MIN_VALUE;
        for (Vector2 vertex : objectA.vertices) {
            final Vector2 pos = new Vector2(
                    vertex.x + objectA.position.x,
                    vertex.y + objectA.position.y
            );
            final float value = pos.dot(axis);
            if (value < minA)
                minA = value;
            if (value > maxA)
                maxA = value;
        }

        float minB = Float.MAX_VALUE;
        float maxB = Float.MIN_VALUE;
        for (Vector2 vertex : objectB.vertices) {
            final Vector2 pos = new Vector2(
                    vertex.x + objectB.position.x,
                    vertex.y + objectB.position.y
            );
            final float value = pos.dot(axis);
            if (value < minB)
                minB = value;
            if (value > maxB)
                maxB = value;
        }

        return !(minA > maxB || minB > maxA);
    }

    private static class Abomination {

        private final Vector2 position = new Vector2();
        private final List<Vector2> vertices = new ArrayList<>();

        private Abomination() {
            final float ratioX = MathUtils.random(0.8f, 1.2f);
            final float ratioY = MathUtils.random(0.8f, 1.2f);
            for (float angle = 45; angle < 360; ) {
                final Vector2 vertex = new Vector2(
                        ratioX * MathUtils.cosDeg(angle) * 100,
                        ratioY * MathUtils.sinDeg(angle) * 100
                );
                vertices.add(vertex);
                angle += MathUtils.random(20f, 60f);
            }
        }

        private void render(ShapeRenderer renderer) {
            for (int i = 0; i < vertices.size(); i++) {
                final Vector2 A = vertices.get(i);
                final Vector2 B = vertices.get((i + 1) % vertices.size());
                renderer.line(
                        position.x + A.x, position.y + A.y,
                        position.x + B.x, position.y + B.y
                );
            }
        }

    }

}


This is the tutorial I learned from:

I hope this can be of some help to you. :slight_smile:

@shingekinolinus

Thanks a lot for that, I actually wound up discovering that the Intersector class in libgdx uses a SAT algorithm, and I found that it worked quite well once I ensured that I had wrapped all the polygons in a counter clockwise direction.

Actually, that article you put up was one of the many explanations I had read, but reading how you did it gives me a better grasp of how I was doing things wrong.

A bit of a later response, so I had given up on this topic a while ago, but thanks for the response. I would recommend looking at the intersector class because it also handles the collision response, which has been quite helpful to me at least.

It also contains functions to keep the player “grounded”, and collisions between a variety of shapes.

The only issue is the documentation is somewhat minimal, so its a bit of trial and error to be sure that you are sending the correct inputs.