Game Collision Detection - Separating Axis Theorem Collision Resolution Issue

19 Views Asked by At

As you'll see in the loom and fiddle, the collision detection works, but there seems to be a nuanced issue with triangles/3 vert polygons. Maybe I didn't implement SAT completely or correctly. The code is still a pre rewrite prototyped mess.

WASD to move (kinda) Click to shoot (kinda)

Very primitive demo

https://www.loom.com/share/8bb2b16982304d68b11048c6f4584459?sid=6a296885-4b22-4ddf-82ae-2f225e3701aa

https://jsfiddle.net/isaacrector/dj6bm37z/

<div id="applicationContainer">

  <canvas id="applicationCanvas" width="1000" height="600"></canvas>
  
  
 <div id="FPS"></div>
  
  
</div>
class Vector2 {

        constructor (x = 0, y = 0)
    {
            this.x = x;
        this.y = y;
    }

}

class Entity {

        constructor ()
    {
            Entity.entities.push(this);
        
        /* if (Entity.entities[] == undefined)
            Entity.entities[Object.getPrototypeOf (this.constructor).name] = [];
        
        if (Entity.entities[Object.getPrototypeOf (this.constructor).name][this.constructor.name] == undefined && [Object.getPrototypeOf (this.constructor).name] != this.constructor.name)
                Entity.entities[Object.getPrototypeOf (this.constructor).name][this.constructor.name] = {}; */
        
        //Entity.entities[Object.getPrototypeOf (this.constructor).name][this.constructor.name].push(this);
        
        /* console.log(Object.getPrototypeOf (this.constructor).name);
                console.log(this.constructor.name);
        console.log (Entity.entities); */
    }
    
        update () {
            /* on update position send request to udpate network data */
        
    }
    
   /*  networkUpdate() {
        application requests server update
        object (entity) has unique id application.entities.networkdata
        move local position towards network position
    } */
    
    draw () {
            
    }
    
} /* Entity */

Entity.entities = [];

class Polygon extends Entity {

        constructor (vertices, moveable = false) {
        
        super();
        
        this.position = new Vector2(0, 0);
            this.vertices = vertices;
        /* this.offsetVertices();
        console.log("new vertices");
        console.log(this.newVertices);
        this.movingVertices = [];
        this.movingVerticesTimer = 3; */
            this.edges = [];
        
        this.moveable = moveable;
        
        this.velocity = { 65: { x: -25, y: 0 }, 68: { x: 25, y: 0 }, 87: { x: 0, y: -25 }, 83: { x: 0, y: 25 } }
        
        this.buildEdges();
            
        Polygon.polygons.push(this);
        
    }
    
    update () {
    
            super.update();
        
        //this.moveVertices();
        
        if (this.moveable)
        {
            let fps = (Application.activeFrameCount / ((Application.currentFrameTimestampMS - Application.activeFrameCountTimestampMS) / 1000));
            let deltaTime = fps / 1000;

            if (Input.keysDown[65])
            {
                this.position.x += this.velocity[65].x * deltaTime;
                this.position.y += this.velocity[65].y * deltaTime;
            }

            if (Input.keysDown[68])
            {
                this.position.x += this.velocity[68].x * deltaTime;
                this.position.y += this.velocity[68].y * deltaTime;
            }

            if (Input.keysDown[87])
            {
                this.position.x += this.velocity[87].x * deltaTime;
                this.position.y += this.velocity[87].y * deltaTime;
            }

            if (Input.keysDown[83])
            {
                this.position.x += this.velocity[83].x * deltaTime;
                this.position.y += this.velocity[83].y * deltaTime;
            }
         }
    
    }
    
    draw () {
            
        Application.canvasContext.lineWidth = 1;
        if (this.colliding)
                Application.canvasContext.strokeStyle = "#FF0000";
        else
                Application.canvasContext.strokeStyle = "#000000";
        Application.canvasContext.beginPath();
        Application.canvasContext.moveTo(
            this.position.x + this.vertices[0].x,
            this.position.y + this.vertices[0].y
        );//  + (Math.random() * (0.25 - 0.01) + 0.01)
        for (let i = 1; i < this.vertices.length; i++) {
            Application.canvasContext.lineTo(
                this.position.x + this.vertices[i].x,
                this.position.y + this.vertices[i].y
            );
        }
        Application.canvasContext.closePath();
        Application.canvasContext.stroke();
    
    }
    
    buildEdges ()  {
            
        this.edges = [];
        
        if (this.vertices.length < 3) {
            console.error("Only polygons supported.");
        }
        
        for (let i = 0; i < this.vertices.length; i++) {
            const a = this.vertices[i];
            let b = this.vertices[0];
            if (i + 1 < this.vertices.length) {
                b = this.vertices[i + 1];
            }
            this.edges.push({
                x: (b.x - a.x),
                y: (b.y - a.y),
            });
        } 
        
    }
    
    projectInAxis (x, y) {
        let min = 10000000000;
        let max = -10000000000;
        for (let i = 0; i < this.vertices.length; i++) {
            let px = this.position.x + this.vertices[i].x;
            let py = this.position.y + this.vertices[i].y;
            var projection = (px * x + py * y) / (Math.sqrt(x * x + y * y));
            if (projection > max) {
                max = projection;
            }
            if (projection < min) {
                min = projection;
            }
        }
        return { min, max };
    }
        
    testWith (otherPolygon) {
        // get all edges
        const edges = [];
        for (let i = 0; i < this.edges.length; i++) {
            edges.push(this.edges[i]);
        }
        for (let i = 0; i < otherPolygon.edges.length; i++) {
            edges.push(otherPolygon.edges[i]);
        }
        
        let depth = Number.MAX_VALUE;
        let normal = new Vector2();
        
        // build all axis and project
        for (let i = 0; i < edges.length; i++) {
            // get axis
            const length = Math.sqrt(edges[i].y * edges[i].y + edges[i].x * edges[i].x);
            const axis = {
                x: -edges[i].y / length,
                y: edges[i].x / length,
            };
            // project polygon under axis
            const { min: minA, max: maxA } = this.projectInAxis(axis.x, axis.y);
            const { min: minB, max: maxB } = otherPolygon.projectInAxis(axis.x, axis.y);
            if (Polygon.intervalDistance(minA, maxA, minB, maxB) > 0) {
                return false;
            }
            
            let axisDepth = Math.min(maxB - minA, maxA - minB);
            
            if (axisDepth < depth)
            {
                    depth = axisDepth;
                normal = axis;
            }
        }
        
        depth /= 2;
        
        let magnitude = Math.sqrt((normal.x * normal.x) + (normal.y * normal.y));
        normal.x /= magnitude;
        normal.y /= magnitude;
        
        this.position.x += -normal.x * depth / 2;
        this.position.y += -normal.y * depth / 2;
        
        otherPolygon.position.x += normal.x * depth / 2;
        otherPolygon.position.y += normal.y * depth / 2;
        
        let centerA = Polygon.findArithmeticMean(this.vertices);
        let centerB = Polygon.findArithmeticMean(otherPolygon.vertices);

        let direction = new Vector2(centerB.x - centerA.x, centerB.y - centerA.y);

                // dot
        if(direction.x * normal.x + direction.x * normal.x < 0)
        {
          normal = -normal;
        }
        
        return true;
    }
    
    onSA(){
    
    }
    
    static findArithmeticMean( vertices )
    {
        let sumX = 0;
        let sumY = 0;

        for(var i = 0; i < vertices.length; i++)
        {
            let v = vertices[i];
            sumX += v.X;
            sumY += v.Y;
        }

        return new Vector2(sumX / vertices.length, sumY / vertices.length);
    }
    
    moveVertices() {
            let fps = (Application.activeFrameCount / ((Application.currentFrameTimestampMS - Application.activeFrameCountTimestampMS) / 1000));
        
        let deltaTime = fps / 1000;
        
        for(var i = 0; i < this.vertices.length; i++)
        {
                
                if (this.movingVertices[i] == undefined) {
                            
                                this.movingVertices[i] = new Vector2();
                            this.movingVertices[i].x = this.vertices[i].x;
                             this.movingVertices[i].y = this.vertices[i].y;
                            }
                                this.movingVertices[i].x += (this.newVertices[i].x - this.movingVertices[i].x) * 0.1 * deltaTime;
                            this.movingVertices[i].y += (this.newVertices[i].y - this.movingVertices[i].y) * 0.1 * deltaTime;
                           // } 
            
           /*  if (this.movingVertices[i] == undefined)
                    {
                this.movingVertices[i] = new Vector2();
                                this.movingVertices[i].x = this.newVertices[i].x;
                                this.movingVertices[i].y = this.newVertices[i].y; 
            } */
        }
        
        
            /* for(var i = 0; i < this.vertices.length; i++) {
                        if (this.movingVertices[i] == undefined)
                        { */
                    /* this.movingVertices[i] = new Vector2();
                                    this.movingVertices[i].x = this.newVertices[i].x;
                                    this.movingVertices[i].y = this.newVertices[i].y; 
                }
                
                /* this.movingVertices[i].x += (this.newVertices[i].x - this.movingVertices[i].x) * 0.001 * deltaTime;
                            this.movingVertices[i].y += (this.newVertices[i].y - this.movingVertices[i].y) * 0.001 * deltaTime; 
            
            /* if (Math.abs(this.movingVertices[i].x - this.newVertices[i].x) < 1 && Math.abs(this.movingVertices[i].y - this.newVertices[i].y) < 1)
            {
                this.offsetVertices(i);
            }  
        }*/
        
       /*  this.movingVerticesTimer -= deltaTime;
        if (this.movingVerticesTimer <= 0) {
            this.movingVerticesTimer = 10;
            this.offsetVertices();
            this.movingVertices = this.newVertices;
        } */
    }
    
    offsetVertices(index = undefined) {
    
            let singleIndex = true;
                    
            if (index == undefined)
                    {
                        index = 0
                        singleIndex = false;
                    }
                
        this.newVertices = [];
        
            for(; index < this.vertices.length; index++) {
                        this.newVertices[index] = new Vector2();
                
                        this.newVertices[index].x = this.vertices[index].x + (Math.random() * ((50 - 1) + 1));
                                            this.newVertices[index].y = this.vertices[index].y + (Math.random() * ((50 - 1) + 1));
                    
                    
                        if (singleIndex)
                            break;
                    }
         
         console.log("Set new vertices offset from original positions");
         console.log(this.newVertices);
    }
    
    static intervalDistance(minA, maxA, minB, maxB) {
        if (minA < minB) {
            return (minB - maxA);
        }
        return (minA - maxB);
    }
    
    static testCollisions () {
        for (let i = 0; i < Polygon.polygons.length; i++) {
                for (let ii = 0; ii < Polygon.polygons.length; ii++) {
                if (i == ii)
                    continue;
                if (Polygon.polygons[i].testWith(Polygon.polygons[ii])) {
                    //console.log("Tested with index: ", i);
                    Polygon.polygons[i].colliding = true;
                    Polygon.polygons[ii].colliding = true;
                    
                    // Add collisions array. If removing this collision results in 0 count then toggle colliding bool
                }
                else {
                        Polygon.polygons[i].colliding = false;
                    Polygon.polygons[ii].colliding = false;
                }
           }
        }
    }
    
}

Polygon.polygons = [];


class Shot extends Polygon {
        constructor (vertices) {
            super(vertices);
        //this.position = { x: 10, y: 10 };
        //this.direction = {};
    }
    
    update() {
            //let fps = (Application.activeFrameCount / ((Application.currentFrameTimestampMS - Application.activeFrameCountTimestampMS) / 1000));
        let deltaTime = Application.progressMS / 1000;
        
            this.position.x += this.direction.x * 0.25 * deltaTime;
        this.position.y += this.direction.y * 0.25 * deltaTime;
    }
    
    draw() {
            super.draw();
        
        /* Application.canvasContext.lineWidth = 5;
        Application.canvasContext.strokeStyle = "#000000";
        
        Application.canvasContext.beginPath();
        
        Application.canvasContext.moveTo(this.position.x, this.position.y);
        Application.canvasContext.lineTo(this.position.x + 5, this.position.y + 5);
            
        Application.canvasContext.closePath();
        
        Application.canvasContext.stroke(); */
    }
}

/* class Event {

} */

class Input {
  constructor () {

  }
}

Input.keysDown = {};

document.addEventListener("keydown", function (event) {
  Input.keysDown[event.which] = true;
  console.log (Input.keysDown);
});

document.addEventListener("keyup", function (event) {
  delete Input.keysDown[event.which];
  // console.log (Input.keysDown);
});

class Application 
{
        static start () {
            
        //Application.lineObject = new LineObject();
        Application.player = new Polygon ([
            { x: 0, y: 0 },
            { x: 50, y: 0 },
            { x: 50, y: 50 },
            { x: 0, y: 50 },
        ], true);
        new Polygon ([
            { x: 0, y: 0 },
            { x: 50, y: 0 },
            { x: 50, y: 50 },
            { x: 0, y: 50 },
        ]);
        new Polygon ([
            { x: 0, y: 0 },
            { x: 50, y: 0 },
            { x: 50, y: 50 },
            { x: 0, y: 50 },
        ]);
        new Polygon ([
                { x: 12.5, y: 0 },
            { x: 62.5, y: 100 },
            { x: 0, y: 100 },
        ]); 
        Application.nextScreenFrame(); 
                                                            
    }

    static nextScreenFrame () {
        requestAnimationFrame (Application.screenFrame);
    }
    
    static screenFrame (frameTimestamp)
    {
        Application.currentFrameTimestampMS = new Date().getTime();

        Application.progressMS = Application.currentFrameTimestampMS - Application.lastFrameTimestampMS;

        if (Application.progressMS >= Application.frameIntervalMS * Application.progressVariance) 
        {
            Application.updateFrame();
            Application.drawFrame();

            Application.progressPerSecondMS += Application.progressMS;
            Application.progressVariance = 1 + (1 - (Application.progressMS / (Application.progressPerSecondMS / Application.activeFrameCount)) * 1.8);
            
            // In the event of a decrease in average progress, progress will already be less than the frame interval. No need to increase the frame interval. This does not really matter in execution, but is good practice.
            if (Application.progressVariance > 1)
                    Application.progressVariance = 1;

            Application.lastFrameTimestampMS = Application.currentFrameTimestampMS;

            Application.activeFrameCount++;

            if (Application.activeFrameCount == Application.fps * 5)
            {
                Application.activeFrameCountTimestampMS = Application.currentFrameTimestampMS;
                Application.activeFrameCount = 1;
                Application.maxFrameCount = 1;
                Application.progressPerSecondMS = Application.progressMS;
            }

            if (Application.activeFrameCount > Application.fps)
            {
                Application.FPS.innerHTML = "FPS: " + (Application.activeFrameCount / ((Application.currentFrameTimestampMS - Application.activeFrameCountTimestampMS) / 1000)).toFixed (2) + "<br>Hertz: " + (Application.maxFrameCount / ((Application.currentFrameTimestampMS - Application.activeFrameCountTimestampMS) / 1000)).toFixed (2);
            }
            else
            {
                Application.FPS.innerHTML = "FPS: ...<br>Hertz: ...";
            }

            /* if (Math.floor ((timestamp - startTime) / 1000) % 6 == 0)
                $(".log").html((actualFramesPerSecond / ((progressPerSecond) / 1000)).toFixed(1) + " / " + (possibleFramesPerSecond / ((progressPerSecond) / 1000)).toFixed(1));
            
            if ((fillType == '-' && newFillPerc == 100) ||
                (fillType == '+' && newFillPerc == 0))
            {
                $(".log").html(timestamp - meterEmptyStartTimestamp);
                meterEmptyStartTimestamp = timestamp;
            } */
        }

        Application.maxFrameCount++;

        Application.nextScreenFrame();
    }
    
    static updateFrame ()
    {
            for (var i = 0; i < Entity.entities.length; i++)
                    Entity.entities[i].update();
            
        Polygon.testCollisions();
    }
    
    static drawFrame ()
    {
            Application.canvasContext.clearRect(0, 0, Application.canvas.width, Application.canvas.height);
        
                for (var i = 0; i < Entity.entities.length; i++)
                Entity.entities[i].draw();
    }

}

/*
*/
Application.startTimestampMS = new Date().getTime();

Application.fps = 60; // Max is normally 60 (screen refresh rate, also the interval at which requestAnimationFrame is called)
Application.frameIntervalMS = 1000 / Application.fps;
Application.activeFrameCount = 0; // rename to updated or active FramesPerSecond
Application.activeFrameCountTimestampMS = Application.startTimestampMS;
Application.maxFrameCount = 0; // Screen refresh rate (hertz)

Application.currentFrameTimestampMS = Application.startTimestampMS;
Application.lastFrameTimestampMS = Application.currentFrameTimestampMS;
Application.progressMS = 0;
Application.progressPerSecondMS = 0;
Application.progressVariance = 1; // When progress is just under framerate interval, slightly lower framerate

/* $(window).resize (function ()
                  {
  Game.HUD.resize();
}); */

Application.canvas = document.getElementById("applicationCanvas");
Application.canvasContext = Application.canvas.getContext("2d");

Application.FPS = document.getElementById("FPS");
Application.start();

/* ["mousedown", "touchdown"] */
Application.canvas.addEventListener("mousedown", function (event) {
  //if (app.developmentMode)
    //console.log(event);
    
  // Calculate start point (considering screen event cords and target element margins, padding, positioning)
  const rect = event.target.getBoundingClientRect();
  let startX = event.clientX - rect.left; //x position within the element.
  let startY = event.clientY - rect.top;
  
});

/* ["mousemove", "touch...?"] */
Application.canvas.addEventListener("mousemove", function (event) {
  // Calculate hover point (considering screen event cords and target element margins, padding, positioning)
  const rect = event.target.getBoundingClientRect();
  let hoverX = event.clientX - rect.left; //x position within the element.
  let hoverY = event.clientY - rect.top;
  
});

/* ["mouseup", "touchup"] */
Application.canvas.addEventListener("mouseup", function (event) {
  // Calculate end point (considering screen event cords and target element margins, padding, positioning)
  const rect = event.target.getBoundingClientRect();
  let endX = event.clientX - rect.left; //x position within the element.
  let endY = event.clientY - rect.top;
  
  const shot = new Shot ([  
                { x: 0, y: 0 },
                { x: 5, y: 0 },
                { x: 10, y: 10 },
                { x: 0, y: 10 },
            ]);
  shot.position = { x: Application.player.position.x, y: Application.player.position.y };
  shot.direction = { x: endX - Application.player.position.x, y: endY - Application.player.position.y };
});


0

There are 0 best solutions below