javascriptgeometrycollision-detectiongame-physics

Swept AABB collision between circle and rectangle


I'm fairly new to javascript, and I've been trying to build a small game to learn new concepts. An issue I'm encountering is that projectiles in the game often have a higher velocity than the width of objects they should collide with, and so pass through without colliding when checking for collision every frame.

I'm working on implementing (what I think is called) swept AABB collision, where my program checks the path an object takes and determines if it should have hit something. My current code takes in a rectangle and a circle, along with their x any y velocities, and gets their relative velocity. It then inflates the area of the rectangle in the x and y by 2 times the circle's radius.

This is then fed into my current sweptAABB function, which I admit I'm not entirely sure how it works (I cobbled it together from several sources):

function sweptAABB(pointx, pointy, velx, vely, deltaTime, rectx, recty, rectw, recth){
  //pointx and pointy are the circle's center, velx and vely are the relative velocities 

  //how far the "point" (this is the circle, but it's a point since we're expanding our rectangle to account for it's radius) will move during the frame:
  const movementX = velx*(deltaTime);
  const movementY = vely*(deltaTime)
  
  const invMoveX = movementX !== 0 ? 1 / movementX : Infinity;
  const invMoveY = movementY !== 0 ? 1 / movementY : Infinity;
  
  //times that the point would enter/exit the rectangle
  const tEnterXY = {x: (rectx-pointx)*invMoveX, y:(recty - pointy) * invMoveY}
  const tExitXY = {x: (rectx+rectw-pointx)*invMoveX, y:(recty+recth - pointy) * invMoveY};

  
  //the earliest posible enter/exit times
  const enterTime = Math.max(Math.min(tEnterXY.x, tExitXY.x), Math.min(tEnterXY.y, tExitXY.y));
  const exitTime = Math.min(Math.max(tEnterXY.x, tExitXY.x), Math.max(tEnterXY.y, tExitXY.y));

  return enterTime <= exitTime && exitTime >= 0 && enterTime <= 1;
  
}

The issue I'm seeing is that occasionaly the function will return true if the corner of the rectangle is near but not touching the circle, like this: (I can't embed images yet lol)

I'm not sure whether the mistake is in the code or a logic error on my part, but any help understanding or fixing this problem would be greatly appreciated.


Solution

  • In my modest opinion, the idea on which you rely, is not satisfactory, since in that way, you will get “collisions” some pixels before or after the point of contact, the way to avoid this, is that the time between verifications is less or equal to what it takes any object at maximum speed to travel a pixel, in that case, the detection becomes simpler, you can use:

        function detectCollision() {
    
             // this “if” only allows you to execute your code if the object and 
             // the square containing the circle are overlapping.
           if( objA.getX() + objA.getWidth() > objB.getX() && objA.getX() < objB.getX() + objB.getWidth() &&
                   objA.getY() < objB.getY() + objB.getHeight() && objA.getY() + objA.getHeight() > objB.getY() ) {
    
                // deriving according to the direction of advance
              if( direction === "rigth" ) {
    
                   // this “if” returns true, if the center (of the “y”) of object 
                   // “B” is above the “y” of object “A” and below the “y” of 
                   // object “A” added to its height.
                 if( objB.getCenterY() < objA.getY() + objA.getHeight() && objB.getCenterY() > objA.getY() ) {
                    return true;
                 }
                 else {
    
                      // deriving according to the center of object "b" being 
                      // above or below the "y" of object "A"
                    if( objB.getCenterY() < objA.getY() ) {
    
                         // passing the appropriate parameters to "detectCircleCollision()"
                       return detectCircleCollision( objA.getY() - objB.getCenterY(), objA.getX() + objA.getWidth() - objB.getCenterX() );
                    }
                    else {
                       return detectCircleCollision( objA.getY() + objA.getHeight() - objB.getCenterY(), objA.getX() + objA.getWidth() - objB.getCenterX() );
                    }
                 }
              }
              else if( direction === "left" ) {
                 if( objB.getCenterY() < objA.getY() + objA.getHeight() && objB.getCenterY() > objA.getY() ) {
                    return true;
                 }
                 else {
                    if( objB.getCenterY() < objA.getY() ) {
                       return detectCircleCollision( objA.getY() - objB.getCenterY(), objA.getX() - objB.getCenterX() );
                    }
                    else {
                       return detectCircleCollision( objA.getY() + objA.getHeight() - objB.getCenterY(), objA.getX() - objB.getCenterX() );
                    }
    
                 }
              }
              else if( direction === "up" ) {
                 if( objB.getCenterX() < objA.getX() + objA.getWidth() && objB.getCenterX() > objA.getX() ) {
                    return true;
                 }
                 else {
                    if( objB.getCenterX() < objA.getX() ) {
                       return detectCircleCollision( objA.getY() - objB.getCenterY(), objA.getX() - objB.getCenterX() );
                    }
                    else {
                       return detectCircleCollision( objA.getY() - objB.getCenterY(), objA.getX() + objA.getWidth() - objB.getCenterX() );
                    }
                 }
              }
              else if( direction === "down" ) {
                 if( objB.getCenterX() < objA.getX() + objA.getWidth() && objB.getCenterX() > objA.getX() ) {
                    return true;
                 }
                 else {
                    if( objB.getCenterX() < objA.getX() ) {
                       return detectCircleCollision( objA.getY() + objA.getHeight() - objB.getCenterY(), objA.getX() - objB.getCenterX() );
                    }
                    else {
                       return detectCircleCollision( objA.getY() + objA.getHeight() - objB.getCenterY(), objA.getX() + objA.getWidth() - objB.getCenterX() );
                    }
                 }
              }
           }
           return false;
        }
    

    Auxiliary function to determine if there is a collision (the distance is less than the radius), this calculates the length of the hypotenuse of the triangle formed by the received distances.

        function detectCircleCollision( sideA, sideB ) {
            return Math.sqrt( sideA * sideA + sideB * sideB ) < ratio;
        }
    

    Here is a small code that shows how it works, for it to work, you must click on the button that says "Go!!!", and then change the forward direction with the other buttons.

    let iniciando = true
    let objetoBX = 40;
    let objetoBY = 40;
    let objetoBAlto = 140;
    let objetoBAncho = 140;
    let bCentroX = objetoBX + objetoBAncho / 2;
    let bCentroY = objetoBY + objetoBAlto / 2;
    let diametro = objetoBAlto;
    let radio = diametro / 2;
    
    let bicho;
    let arr;
    let aba;
    let der;
    let izq;  // botones
    
    let objetoAX = 10;
    let objetoAY = 10;
    let objetoAAlto = 20;
    let objetoAAncho = 20;
    let direccion = "derecha";
    
    function mover() {
      if( iniciando ) {
        iniciando = false;
        document.getElementById( "arr" ).addEventListener( "click", function() {
          direccion = "arriba";
        });
        document.getElementById( "aba" ).addEventListener( "click", function() {
          direccion = "abajo";
        });
        document.getElementById( "der" ).addEventListener( "click",function() {
          direccion = "derecha";
        });
        document.getElementById( "izq" ).addEventListener( "click", function() {
          direccion = "izquierda";
        });
        bicho = document.getElementById( "bicho" );
        ion();
      }
      if( direccion == "arriba" ) {
        objetoAY -= 1;
        bicho.style.marginTop = objetoAY + 'px';
      }
      else if( direccion == "abajo" ) {
        objetoAY += 1;
        bicho.style.marginTop = objetoAY + 'px';
      }
      else if( direccion == "derecha" ) {
        objetoAX += 1;
        bicho.style.marginLeft = objetoAX + 'px';
      }
      else if( direccion == "izquierda" ) {
        objetoAX -= 1;
        bicho.style.marginLeft = objetoAX + 'px';
      }
      if( detectaChoque() ) {
        bicho.style.background = "red";
      }
      else {
        bicho.style.background = "blue";
      }
    }
    
    function detectaChoque() {
       if( objetoAX + objetoAAncho > objetoBX && objetoAX < objetoBX + objetoBAncho &&
               objetoAY < objetoBY + objetoBAlto && objetoAY + objetoAAlto > objetoBY ) {
    
          if( direccion === "derecha" ) {
             if( bCentroY < objetoAY + objetoAAlto && bCentroY > objetoAY ) {
                return true;
             }
             else {
                if( bCentroY < objetoAY ) {
                   return detectaColisionCirculo( objetoAY - bCentroY, objetoAX + objetoAAncho - bCentroX );
                }
                else {
                   return detectaColisionCirculo( objetoAY + objetoAAlto - bCentroY, objetoAX + objetoAAncho - bCentroX );
                }
             }
          }
          else if( direccion === "izquierda" ) {
             if( bCentroY < objetoAY + objetoAAlto && bCentroY > objetoAY ) {
                return true;
             }
             else {
                if( bCentroY < objetoAY ) {
                   return detectaColisionCirculo( objetoAY - bCentroY, objetoAX - bCentroX );
                }
                else {
                   return detectaColisionCirculo( objetoAY + objetoAAlto - bCentroY, objetoAX - bCentroX );
                }
    
             }
          }
          else if( direccion === "arriba" ) {
             if( bCentroX < objetoAX + objetoAAncho && bCentroX > objetoAX ) {
                return true;
             }
             else {
                if( bCentroX < objetoAX ) {
                   return detectaColisionCirculo( objetoAY - bCentroY, objetoAX - bCentroX );
                }
                else {
                   return detectaColisionCirculo( objetoAY - bCentroY, objetoAX + objetoAAncho - bCentroX );
                }
             }
          }
          else if( direccion === "abajo" ) {
             if( bCentroX < objetoAX + objetoAAncho && bCentroX > objetoAX ) {
                return true;
             }
             else {
                if( bCentroX < objetoAX ) {
                   return detectaColisionCirculo( objetoAY + objetoAAlto - bCentroY, objetoAX - bCentroX );
                }
                else {
                   return detectaColisionCirculo( objetoAY + objetoAAlto - bCentroY, objetoAX + objetoAAncho - bCentroX );
                }
             }
          }
       }
       return false;
    }
    
    function choco() {
        bicho.style.background = "red";
    }
    
    function detectaColisionCirculo( ladoA, ladoB ) {
        return Math.sqrt( ladoA * ladoA + ladoB * ladoB ) < radio;
    }
    
    function ion() {
        var c = document.getElementById( "myCanvas" );
        var ctx = c.getContext( "2d" );
        ctx.arc( 75, 75,  74, 0, Math.PI * 2, false );
        ctx.fill();
    }
    *{ padding: 0px;  position: absolute;} .bot { width:20px; height:20px; } #escenario { width: 440px; height: 220px; margin-left: 0px; margin-top: 0px; background-color: yellow; } #a { margin-left: 230px; margin-top: 130px; } #arr { margin-left: 270px; margin-top: 60px; } #izq { margin-left: 250px; margin-top: 80px; } #der { margin-left: 290px; margin-top: 80px; } #aba {  margin-left: 270px; margin-top: 100px; } #bicho {  margin-left: 10px; margin-top: 10px; background-color: blue; } canvas { width: 280px; height: 140px; margin-left: 40px; margin-top: 40px; }
    <body> <section id="escenario"> <canvas id="myCanvas">  Canvi  </canvas> <div class="bot" id= "bicho"> </div> <button id="a" onclick="setInterval( mover, 100 )"> Go!!! </button> <br> </section> <footer id="control"> <button class="bot" id="arr" onclick="ion()" > </button> <br> <button class="bot" id="der" > </button> <br> <button class="bot" id="izq" > </button> <br> <button class="bot" id="aba" > </button> <br> </footer> </body>