Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

2d Square to rectangle collision detection and action(physics)

so I made this way to detect and react to squares moving and touching other squares. It uses Pythagorean theorem to make a third parameter so you don't get two true if statements when squares touch. I recently have been attempting to use this method on a square to a rectangle, and cannot seem to get it working. I have drawn lines to help visualize what the code is doing. Anybody have any suggestions on how to get this collision working properly?

const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

let gravity = 1.5;
let friction = 0.9;

//CHARACTER:
class Player {
    constructor(x, y, w, h, vx, vy, c, j) {
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
        this.vx = vx;
        this.vy = vy;
        this.color = c;
        this.jumping = j;
    }
    draw() {
      context.fillStyle = this.color;
      context.fillRect(this.x, this.y, this.w, this.h);
    }
    canvasCollision() {
        if (this.x <= 0) this.x = 0;
        if (this.y <= 0) this.y = 0;
        if (this.x + this.w >= canvas.width) this.x = canvas.width - this.w;
        if (this.y + this.h >= canvas.height) {this.y = canvas.height - this.h; this.vy = 0; this.jumping = false};
    }
    update() {
        this.draw(); 
        this.vy += gravity;
        this.x += this.vx;
        this.y += this.vy;
        this.vx *= friction;
        this.vy *= friction;
        this.canvasCollision() //must be after other updates
    }
}

let player1 = new Player(75, canvas.height/2 + 75, 75, 75, 0, 0, '#8DAA9D', false); 

function controlPlayer1(obj) {
    //this order matters. If update is before jump then obj won't jump when on top of other block.
    if (controller1.up1 && !obj.jumping) { obj.vy -= 25; obj.jumping = true };
    if (controller1.left1) { obj.vx -= 0.5 };
    if (controller1.right1) { obj.vx += 0.5 };
    obj.update();
}

//MOVEMENT:
class Controller {
    constructor() {
        this.left1  = false;
        this.up1    = false;
        this.right1 = false;

        this.down1  = false;

        let controller1 = (e) => {
              if (e.code === 'KeyD')   { this.right1 = e.type === 'keydown' }
            if (e.code === 'KeyA')   { this.left1 = e.type === 'keydown' }
            if (e.code === 'KeyW')   { this.up1 = e.type === 'keydown' }    
            if (e.code === 'KeyS')  { this.down1 = e.type === 'keydown' }       
        }
        
    window.addEventListener('keydown', controller1);
    window.addEventListener('keyup', controller1);

    }
}

let controller1 = new Controller();

//PLATFORM
class Platform {
  constructor(x, y, w, h, yv, c) {
      this.x = x;
      this.y = y;
      this.w = w;
      this.h = h;
      this.yv = yv;
      this.color = c;
  }
  draw(){
    context.fillStyle = this.color;
    context.fillRect(this.x, this.y, this.w, this.h);
  }
  update(){
    this.draw();
    this.y += this.yv;
    this.yv *= 0.9;// friction
  }
}

let platform1 = new Platform(canvas.width/2, canvas.height - 150, 100, 30, 0, '#202020');
let platform2 = new Platform (canvas.width/4, canvas.height/2, 75, 75, 0, '#202020');

//COLLISION DETECTION
function platformDetection(obj, obj2){
 //center point of each side of obj1
 let objLeft = {x: obj.x,  y: obj.y + obj.h/2};
 let objTop = {x: obj.x + obj.w/2, y: obj.y};
 let objRight = {x: obj.x + obj.w, y: obj.y + obj.h/2};
 let objBottom = {x: obj.x + obj.w/2, y: obj.y + obj.h};
 //center point of each side a obj2
 let obj2Left = {x: obj2.x, y: obj2.y + obj2.h/2};
 let obj2Top = {x: obj2.x + obj2.w/2, y: obj2.y};
 let obj2Right = {x: obj2.x + obj2.w, y: obj2.y + obj2.h/2};
 let obj2Bottom = {x: obj2.x + obj2.w/2, y: obj2.y + obj2.h};
 //distance between obj1 and obj2 opposing sides
 let rightDistX = objRight.x - obj2Left.x;
 let rightDistY = objRight.y - obj2Left.y;
 let leftDistX = objLeft.x - obj2Right.x;
 let leftDistY = objLeft.y - obj2Right.y;
 let topDistX =  objTop.x - obj2Bottom.x;
 let topDistY = objTop.y - obj2Bottom.y;
 let bottomDistX = objBottom.x - obj2Top.x;
 let bottomDistY = objBottom.y - obj2Top.y;
 //pythagorean theorem for distance. dRight is from the right side of obj1 to the left of obj2. the rest follow suit.
 let dRight = Math.sqrt(rightDistX*rightDistX + rightDistY*rightDistY);
 let dLeft = Math.sqrt(leftDistX*leftDistX + leftDistY*leftDistY);
 let dTop = Math.sqrt(topDistX*topDistX + topDistY*topDistY);
 let dBottom = Math.sqrt(bottomDistX*bottomDistX + bottomDistY*bottomDistY);
 //Math.min return the smallest value thus variable minimum will be which ever sides are closest together
 let minimum = Math.min(dRight, dLeft, dBottom, dTop);
 let val = 0;
 //compare minimum to pythagorean theorem and set val based on which ever side is closest
 if (dTop == minimum) {
  val = 1;
  //the context stuff can be deleted. It's just here for visual. The if statements can be one line each.
  context.lineWidth = 2;
  context.strokeStyle = 'blue';
  context.beginPath();
  context.moveTo(objTop.x, objTop.y); 
  context.lineTo(obj2Bottom.x, obj2Bottom.y);
  context.stroke();
}
else if (dRight == minimum) {
  val = 2;
  context.strokeStyle = 'orange';
  context.beginPath();
  context.moveTo(objRight.x, objRight.y); 
  context.lineTo(obj2Left.x, obj2Left.y);
  context.stroke();
}
else if (dBottom == minimum) {
  val = 3;
  context.strokeStyle = 'green';
  context.beginPath();
  context.moveTo(objBottom.x, objBottom.y); 
  context.lineTo(obj2Top.x, obj2Top.y);
  context.stroke();
}
else if (dLeft == minimum) {
  val = 4;
  context.strokeStyle = 'pink';
  context.beginPath();
  context.moveTo(objLeft.x, objLeft.y); 
  context.lineTo(obj2Right.x, obj2Right.y);
  context.stroke();
}
 //pass the objects and val 
 platformAction(obj, obj2, val);
}

//ACTION
function platformAction(obj, obj2, val){
//player1 top to player2 bottom
if (obj.y <= obj2.y + obj2.h && obj2.y + obj2.h >= obj.y && val == 1) {
  obj2.y = obj.y - obj2.h; 
  obj.y = obj2.y + obj2.h;
  obj2.vy = 0;
  obj2.jumping = false;
  obj.jumping = true;
}
//player1 right to player2 left
if (obj.x + obj.w >= obj2.x && obj2.x <= obj.x + obj.w && val == 2) {
  obj2.x = obj.x + obj.w;
  obj.x = obj2.x - obj.w - 1;
  obj2.vx = 0;
}
//player1 bottom to player2 top
if (obj.y + obj.h >= obj2.y && obj2.y <= obj.y + obj.h && val == 3) {
  obj.y = obj2.y - obj.h;
  obj2.y = obj.y + obj.h;
  obj.vy = 0;
  obj.jumping = false;
  obj2.jumping = true;
}
//player1 left to player2 right
if (obj.x <= obj2.x + obj2.w && obj2.x + obj2.w >= obj.x && val == 4) {
  obj2.x = obj.x - obj2.w;
  obj.x = obj2.x + obj2.w + 1;
  obj.vx = 0;
  obj2.vx = 0;
}
}

function initObj(obj){
obj.update();
}

function loop() {
  context.clearRect(0, 0, canvas.width, canvas.height); 
  context.fillStyle = 'grey';
  context.fillRect(0, 0, canvas.width, canvas.height);

  //PLATFORM
  initObj(platform1);
  initObj(platform2);

  //PLATFORM DETECTION
  platformDetection(player1, platform1);
  platformDetection(player1, platform2);

  //PLAYER
  controlPlayer1(player1); 

  requestAnimationFrame(loop)
}
loop();
<!DOCTYPE html>
<html>
  <head>

    <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi">
    <title>Tone.io</title>
    <style>
    body {
      height:100vh;
      width:100vh;
      margin: 0;
    }
    </style>
  </head>
 
  <body>

    <canvas id="canvas"></canvas>

    <script src = "Js/Tone.js" type = "text/javascript"></script>

  </body>

</html>
like image 727
MPdoor2 Avatar asked May 07 '21 19:05

MPdoor2


2 Answers

@MPdoor2 "I made...". Anywho, when I gave you that code I did specifically say I had hacked it from a method I created to be used on tilemaps. The method works flawless for that purpose although there is more to the code and yes it is built for squares since that's what tiles map mainly are.

I have been playing with alternate methods of doing CD. Here's a shorter method that (so far) seems to be working well. This method still determines the distance between each side but in a different way. Once the broadphase determines a collision has occurred it calls the narrow phase and whichever side has the shortest distance is the side being penetrated. i.e. when you collide with another block from the player right to the object left we know that even the Y axis penetrates (top and bottom corners of player). This calculates the distance between all three and since the distance between X would be 0 it is the shortest and the CD for moving the player in the Y direction does not get called.

Try the snippet below and see if this works for you.

const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');

canvas.width = 700;
canvas.height = 500;

let gravity = 1.5;
let friction = 0.9;

//CHARACTER:
class Player {
    constructor(x, y, vx, vy, c, j) {
        //each player must have separate values for control purposes i.e. velocity/jump/attack etc.
        this.x = x;
        this.y = y;
        this.w = 50;
        this.h = 50;
        this.vx = vx;
        this.vy = vy;
        this.color = c;
        this.jumping = j;
        this.center = {x: this.x + this.w/2,  y: this.y + this.h/2};
        this.topLeft = {x: this.x, y: this.y};
        this.topRight = {x: this.x + this.w, y: this.y};
        this.bottomRight = {x: this.x + this.w, y: this.y + this.h};
        this.bottomLeft = {x: this.x, y: this.y + this.h};
    }
    draw() {
       context.fillStyle = this.color;
       context.fillRect(this.x, this.y, this.w, this.h);
    }
    canvasCollision() {
        if (this.x <= 0) this.x = 0;
        if (this.y <= 0) this.y = 0;
        if (this.x + this.w >= canvas.width) this.x = canvas.width - this.w;
        if (this.y + this.h >= canvas.height) {this.y = canvas.height - this.h; this.vy = 0; this.jumping = false};
    }
    update() {
        this.draw(); 
        this.vy += gravity;
        this.x += this.vx;
        this.y += this.vy;
        this.vx *= friction;
        this.vy *= friction;
        this.center = {x: this.x + this.w/2,  y: this.y + this.h/2};
        this.topLeft = {x: this.x, y: this.y};
        this.topRight = {x: this.x + this.w, y: this.y};
        this.bottomRight = {x: this.x + this.w, y: this.y + this.h};
        this.bottomLeft = {x: this.x, y: this.y + this.h};
        this.canvasCollision() //must be after other updates
    }
}
let player = new Player(0, 0, 0, 0, 'red', false); 

function controlPlayer1(obj) {
    if (controller1.up1 && !obj.jumping) { obj.vy -= 25; obj.jumping = true };
    if (controller1.left1) { obj.vx -= 0.5 };
    if (controller1.right1) { obj.vx += 0.5 };
    obj.update();
}

class Platform {
    constructor(x,y,w,h) {
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
        this.center = {x: this.x + this.w/2,  y: this.y + this.h/2};
        this.topLeft = {x: this.x, y: this.y};
        this.topRight = {x: this.x + this.w, y: this.y};
        this.bottomRight = {x: this.x + this.w, y: this.y + this.h};
        this.bottomLeft = {x: this.x, y: this.y + this.h};
    }
    draw() {
        context.fillStyle = 'blue';
        context.fillRect(this.x, this.y, this.w, this.h);
    }
}

let platforms = [];
function createPlatforms() {
    for (let i=0; i<4; i++) {
       platforms.push(new Platform(200 * i, 450 - (i * 75), 100, 25));
    }
}
createPlatforms();

let platform1 = platforms.push(new Platform(300, 400, 50, 100));
let platform2 = platforms.push(new Platform(400, 400, 50, 50));

//MOVEMENT:
class Controller {
    constructor() {
        this.left1  = false;
        this.up1    = false;
        this.right1 = false;
        this.left2  = false;
        this.up2    = false;
        this.right2 = false;

        let controller1 = (e) => {
            if (e.code === 'ArrowRight') { this.right1 = e.type === 'keydown' }
            if (e.code === 'ArrowLeft')  { this.left1 = e.type === 'keydown' }
            if (e.code === 'ArrowUp')    { this.up1 = e.type === 'keydown' }           
        }
    
    window.addEventListener('keydown', controller1);
    window.addEventListener('keyup', controller1);
    }
}
let controller1 = new Controller();

function collisionDetection(obj) {
    if (player.x + player.w < obj.x ||
        player.x > obj.x + obj.w ||
        player.y + player.h < obj.y ||
        player.y > obj.y + obj.h) {
            return
        }
        narrowPhase(obj);
}

function narrowPhase(obj) {
    let playerTop_ObjBottom = Math.abs(player.y - (obj.y + obj.h));
    let playerRight_ObjLeft = Math.abs((player.x + player.w) - obj.x);
    let playerLeft_ObjRight = Math.abs(player.x - (obj.x + obj.w));
    let playerBottom_ObjTop = Math.abs((player.y + player.h) - obj.y);

    if ((player.y <= obj.y + obj.h && player.y + player.h > obj.y + obj.h) && (playerTop_ObjBottom < playerRight_ObjLeft && playerTop_ObjBottom < playerLeft_ObjRight)) {
        player.y = obj.y + obj.h;
        player.vy = 0;
    }
    if ((player.y + player.h >= obj.y && player.y < obj.y) && (playerBottom_ObjTop < playerRight_ObjLeft && playerBottom_ObjTop < playerLeft_ObjRight)) {
        player.y = obj.y - player.h; 
        player.jumping = false;
        player.vy = 0;
    }
    if ((player.x + player.w >= obj.x && player.x < obj.x) && (playerRight_ObjLeft < playerTop_ObjBottom && playerRight_ObjLeft < playerBottom_ObjTop)) {
        player.x = obj.x - player.w;
        player.vx = 0; 
    }
    if ((player.x <= obj.x + obj.w && player.x + player.w > obj.x + obj.w) && (playerLeft_ObjRight < playerTop_ObjBottom && playerLeft_ObjRight < playerBottom_ObjTop)) {
        player.x = obj.x + obj.w;
        player.vx = 0; 
    }
}

function animate() {
    context.clearRect(0, 0, canvas.width, canvas.height); 
    context.fillStyle = 'grey';
    context.fillRect(0, 0, canvas.width, canvas.height);
    controlPlayer1(player); 
    for (let i=0;i<platforms.length;i++) {
        platforms[i].draw();
        collisionDetection(platforms[i])
    };
    requestAnimationFrame(animate)
}
animate();
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height">
    <title>CD</title>
</head>
<body>
<canvas id="canvas"></canvas>
<script src="squareRectCollision2.js"></script>
</body>
</html>
like image 130
Justin Avatar answered Nov 16 '22 22:11

Justin


To detect collisions between rectangles, use the position of their sides. For instance, they "collide vertically" if:

  • object1's bottom side is below object2's bottom, and object1's top is above object2's bottom
  • or object1's top side is above object2's top, and object1's bottom is below object2's top

Consider they do collide if they collide both "vertically" and "horizontally".

Now once you've determined that, you need to detect from which edge. I'm using the closest one here, evaluating the distance for all that "collide vertically" or "horizontally".

const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');

canvas.width = Math.min(window.innerWidth - 3, 800);
canvas.height = Math.min(window.innerHeight - 3, 200);

let gravity = 1.5;
let friction = 0.9;

//CHARACTER:
class Player {
  constructor(x, y, w, h, vx, vy, c, j) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.vx = vx;
    this.vy = vy;
    this.color = c;
    this.jumping = j;
  }
  draw() {
    context.fillStyle = this.color;
    context.fillRect(this.x, this.y, this.w, this.h);
  }
  canvasCollision() {
    if (this.x <= 0) this.x = 0;
    if (this.y <= 0) this.y = 0;
    if (this.x + this.w >= canvas.width) this.x = canvas.width - this.w;
    if (this.y + this.h >= canvas.height) {
      this.y = canvas.height - this.h;
      this.vy = 0;
      this.jumping = false
    };
  }
  update() {
    this.draw();
    this.vy += gravity;
    this.x += this.vx;
    this.y += this.vy;
    this.vx *= friction;
    this.vy *= friction;
    this.canvasCollision() //must be after other updates
  }
}

let player1 = new Player(75, canvas.height / 2 + 75, 75, 75, 0, 0, '#8DAA9D', false);

function controlPlayer1(obj) {
  //this order matters. If update is before jump then obj won't jump when on top of other block.
  if (controller1.up1 && !obj.jumping) {
    obj.vy -= 25;
    obj.jumping = true
  };
  if (controller1.left1) {
    obj.vx -= 0.5
  };
  if (controller1.right1) {
    obj.vx += 0.5
  };
  obj.update();
}

//MOVEMENT:
class Controller {
  constructor() {
    this.left1 = false;
    this.up1 = false;
    this.right1 = false;

    this.down1 = false;

    let controller1 = (e) => {
      if (e.code === 'KeyD') {
        this.right1 = e.type === 'keydown'
      }
      if (e.code === 'KeyA') {
        this.left1 = e.type === 'keydown'
      }
      if (e.code === 'KeyW') {
        this.up1 = e.type === 'keydown'
      }
      if (e.code === 'KeyS') {
        this.down1 = e.type === 'keydown'
      }
    }

    window.addEventListener('keydown', controller1);
    window.addEventListener('keyup', controller1);

  }
}

let controller1 = new Controller();

//PLATFORM
class Platform {
  constructor(x, y, w, h, yv, c) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.yv = yv;
    this.color = c;
  }
  draw() {
    context.fillStyle = this.color;
    context.fillRect(this.x, this.y, this.w, this.h);
  }
  update() {
    this.draw();
    this.y += this.yv;
    this.yv *= 0.9; // friction
  }
}

let platform1 = new Platform(canvas.width / 2, canvas.height - 150, 100, 30, 0, '#202020');
let platform2 = new Platform(canvas.width / 4, canvas.height / 2, 75, 75, 0, '#202020');

//COLLISION DETECTION
function platformDetection(obj, obj2) {

  let ol1 = obj.x;
  let ot1 = obj.y;
  let or1 = ol1 + obj.w;
  let ob1 = ot1 + obj.h;
  let ol2 = obj2.x;
  let ot2 = obj2.y;
  let or2 = ol2 + obj2.w;
  let ob2 = ot2 + obj2.h;

  let [collideTop, collideBottom, collideRight, collideLeft] = [-1, -1, -1, -1];

  if (ob1 >= ob2) {
    if (ot1 <= ob2) {
      collideTop = ob2 - ot1;
    }
  }
  if (ol1 <= ol2) {
    if (or1 >= ol2) {
      collideRight = or1 - ol2;
    }
  }
  if (ot1 <= ot2) {
    if (ob1 >= ot2) {
      collideBottom = ob1 - ot2;
    }
  }
  if (or1 >= or2) {
    if (ol1 <= or2) {
      collideLeft = or2 - ol1;
    }
  }
  let min = minOfVals(collideTop, collideRight, collideBottom, collideLeft);
  let val = 0;
  if (min >= 0) {
    if (min == collideTop && (collideLeft >= 0 || collideRight >= 0)) {
      val = 1;
    } else if (min == collideRight && (collideTop >= 0 || collideBottom >= 0)) {
      val = 2;
    } else if (min == collideBottom && (collideLeft >= 0 || collideRight >= 0)) {
      val = 3;
    } else if (min == collideLeft && (collideTop >= 0 || collideBottom >= 0)) {
      val = 4;
    }
  }
  if (val) console.log(val, min, collideTop, collideRight, collideBottom, collideLeft);
  //pass the objects and val 
  platformAction(obj, obj2, val);
}

function minOfVals(...vals) {
  let min = -1;

  function isBelowMin(v) {
    return v >= 0 && (min < 0 || v < min);
  }
  for (v of vals) {
    if (isBelowMin(v)) min = v;
  }
  return min;
}

//ACTION
function platformAction(obj, obj2, val) {
  //player1 top to player2 bottom
  if (val == 1) {
    console.log("colliding from top");
    obj2.y = obj.y - obj2.h;
    //obj.y = obj2.y + obj2.h; //useless
    obj2.vy = 0;
    obj2.jumping = false;
    obj.jumping = true;
  }
  //player1 right to player2 left
  if (val == 2) {
    console.log("colliding from right");
    obj2.x = obj.x + obj.w;
    obj.x = obj2.x - obj.w - 1;
    obj2.vx = 0;
  }
  //player1 bottom to player2 top
  if (val == 3) {
    console.log("colliding from bottom");
    obj.y = obj2.y - obj.h;
    //obj2.y = obj.y + obj.h; //useless
    obj.vy = 0;
    obj.jumping = false;
    obj2.jumping = true;
  }
  //player1 left to player2 right
  if (val == 4) {
    console.log("colliding from left");
    obj2.x = obj.x - obj2.w;
    obj.x = obj2.x + obj2.w + 1;
    obj.vx = 0;
    obj2.vx = 0;
  }
}

function initObj(obj) {
  obj.update();
}

function loop() {
  context.clearRect(0, 0, canvas.width, canvas.height);
  context.fillStyle = 'grey';
  context.fillRect(0, 0, canvas.width, canvas.height);

  //PLATFORM
  initObj(platform1);
  initObj(platform2);

  //PLATFORM DETECTION
  platformDetection(player1, platform1);
  platformDetection(player1, platform2);

  //PLAYER
  controlPlayer1(player1);

  requestAnimationFrame(loop)
}
loop();
<!DOCTYPE html>
<html>

<head>

  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi">
  <title>Tone.io</title>
  <style>
    html {
      margin: 0;
      padding: 0;
    }
    body {
      height: 100vh;
      width: 100vh;
      margin: 0;
      padding: 0;
    }
  </style>
</head>

<body>

  <canvas id="canvas"></canvas>

  <script src="Js/Tone.js" type="text/javascript"></script>

</body>

</html>

There's still a different behavior when your controlled player comes from the left or from the right: push speed is not the same. But that's not due to collisions, so I'm letting you figure out this one (maybe it's on purpose too).

like image 33
julien.giband Avatar answered Nov 16 '22 23:11

julien.giband