user14039007
user14039007

Reputation:

Collision Detection on HTML5 Canvas JS

I looked at other posts on this topic and I couldnt find any that actually worked for my scenario

Basically I am making a 2d sword game that where players fight each other. The "player" is just a circle and the sword is just attached on the circle and points where the mouse is pointing...

enter image description here

Basically I have to detect when the sword is touching another player.

Here is my code for rendering the Player on canvas...

function drawImageLookat(img, sword, x, y, lookx, looky, mouseisdown){
   ctx.setTransform(1, 0, 0, 1, x, y);  
   ctx.rotate(Math.atan2(looky - y, lookx - x));
   ctx.drawImage(img,-img.width / 2, -img.height / 2);
   ctx.save();

   // draw second image
   ctx.transform(1, 0, 0, 1, img.width / 2, img.height / 2); 
   if(mouseisdown) {
     ctx.rotate(30 * Math.PI / 180);
 
   ctx.drawImage(sword, -sword.width / 2+30, -sword.height / 2-20);
   } else {

ctx.drawImage(sword, -sword.width / 2+30, -sword.height / 2-20);
   }

   ctx.restore();  // get the parent transform
ctx.setTransform(1, 0, 0, 1, 0, 0); 
}
imgready = false
    const smiley = new Image()
 const cupcake = new Image()

    const sword1 = new Image()

smiley.src = "./assets/images/smiley.png"
cupcake.src = "./assets/images/cupcake.png"

sword1.src = "./assets/images/sword1.png"
smiley.onload = () => {
imgready = true;
}


const drawPlayer = (player) => {
if(imgready) {
if(player.show) {
if(player.msg) {
  ctx.font = "20px Arial";
ctx.fillText(player.msgcontent, player.x, player.y-50);
}

drawImageLookat(eval(player.image), eval(player.sword), player.x, player.y, player.lookx, player.looky, player.mousedown)
}
}
  };
  
  socket.on('state', (gameState) => {
      ctx.clearRect(0,0, 1200, 999999999999999999)
    for (let player in gameState.players) {
      drawPlayer(gameState.players[player])
    }
  })

Every 1000/60 milliseconds, 'state' is emitted by the server and the client draws all of the Players that gameState has. Now basically I want if mouse is down, check if sword is touching other player. And it can be a boolean result. How to check if sword is touching?

Any help appreciated thanks! Also my full code is here: https://repl.it/@CoderGautamYT/FunChat-1

Upvotes: 0

Views: 196

Answers (1)

Blindman67
Blindman67

Reputation: 54026

Using distance point from line segment

I assume its the sword (as a line segment) that you want to check if contacting the character (circle)

The following function will return true if a line segment contacts a circle

// x1, y1, x2, y2  line segement
// cx, cy  center of circle
// r radius of circle
function isLineTouchingCircle(x1, y1, x2, y2, cx, cy, r){
    const self = isLineTouchingCircle;  // expecting that you will want the closest point
    const v1x = x2 - x1;
    const v1y = y2 - y1;
    var u = ((cx - x1) * v1x + (cy - y1) * v1y) / (v1y * v1y + v1x * v1x);
    u = u < 0 ? 0 : u > 1 ? 1 : u; 
    const x = (self.x = x1 + v1x * u) - cx;
    const y = (self.y = y1 + v1y * u) - cy;
    
    return (x * x + y * y) - r * r < 0;
}

To convert the transformed sword sprite to a line segment

   const angle = Math.atan2(looky - y, lookx - x);
   const angle1 = 30 * Math.PI / 180; // You should use radians

    const swordLength = sword.height
    // base of sword relative to player
    var x1 = img.width / 2;       
    var y1 = img.height / 2;      
    const ax = Math.cos(angle);
    const ay = Math.sin(angle);

    // find base of sword rotated to player
    var x2 = x1 * ax - y1 * ay + x;
    var y2 = x1 * ay + y1 * ax + y;
    
    // find tip of sword (I dont know which way the sword points 
    // so rotated -90 deg  "the (- Math.PI / 2) part" )
    var x3 = Math.cos(angle + angle1 - Math.PI / 2) * swordLength + x2;
    var y3 = Math.sin(angle + angle1 - Math.PI / 2) * swordLength + y2;

    // check contact
    if (isLineTouchingCircle(x2, y2, x3, y3, player2.x, player2.y, player2.r)) {

    }

Please note I am a little unsure as to how the sword image is oriented.

Smiley V Smiley

To make sure it works

const player = (x, y, r, dir, col="green") => ({x,y,r, dir, col, dx: 0, dy: 0, strikeCount: 0, lefty: false, sDir: 0, hit: false, home: false});
const SIZE = 400;

const PLY1 = player(100,SIZE/2, 30, 0);
const PLY2 = player(SIZE - 100,SIZE/2, 30, 0);
PLY2.lefty = true;
const ctx = canvas.getContext("2d");
canvas.height = canvas.width = SIZE;
const blood = document.createElement("canvas");
blood.height = blood.width = SIZE;
const ctxB = blood.getContext("2d");
const playerImg = {
    w: 60,
    h: 60,
    path: ((p=new Path2D())=> (p.addPath(new Path2D("M16 3a23 23 0 0112-2h5a28 28 0 0118 9h1a28 28 0 017 20l-1 7a21 21 0 01-4 13h-1a26 26 0 01-9 7v1a23 23 0 01-12 1h-5a28 28 0 01-18-9 28 28 0 01-8-20l1-7a21 21 0 015-13 26 26 0 019-7zm-2 9a7 7 0 00-5 2 7 7 0 00-2 5v4a7 7 0 002 5 7 7 0 005 2h4a7 7 0 005-2 7 7 0 002-5v-4a7 7 0 00-2-5 7 7 0 00-5-2zm28 0a7 7 0 00-5 2 7 7 0 00-2 5v4a7 7 0 002 5 7 7 0 005 2h4a7 7 0 005-2 7 7 0 002-5v-4a7 7 0 00-2-5 7 7 0 00-5-2zm0 6a3 3 0 014-1 2 2 0 011 2v2a3 3 0 01-4 3 2 2 0 01-2-4zm-29 1a3 3 0 014-1 2 2 0 011 2l-1 2a2 2 0 01-3 1 2 2 0 01-1-3zm1 22a1 1 0 00-2 2 7 7 0 004 4l4 2a20 20 0 0012 2h1a19 19 0 009-4l2-3a2 2 0 001-2 2 2 0 00-3 0l-2 1a14 14 0 01-7 4h-1a15 15 0 01-10-1z"), new DOMMatrix([1,0,0,1,-30, -30])), p))(),
}
const swordImg = {
    w: 29,
    h: 126,
    path: ((p=new Path2D())=> (p.addPath(new Path2D("M14 0l4 7a30 30 0 014 15v74h6v8h-8l-1 22-9-1v-21l-9-1v-7h7V22a37 37 0 013-15zm0 8l-2 88h6zm-1 95l-1 20 5 1 1-20z"),new DOMMatrix([1,0,0,1,-14.5, -120])), p))(),
}
function isLineTouchingCircle(x1, y1, x2, y2, cx, cy, r){
    const self = isLineTouchingCircle;  
    const v1x = x2 - x1;
    const v1y = y2 - y1;
    var u = ((cx - x1) * v1x + (cy - y1) * v1y) / (v1y * v1y + v1x * v1x);
    u = u < 0 ? 0 : u > 1 ? 1 : u; 
    const x = (self.x = x1 + v1x * u) - cx;
    const y = (self.y = y1 + v1y * u) - cy;
    return (x * x + y * y) - r * r < 0;
}
function updatePlayer(p, lookTo) {
    p.dir = Math.atan2(p.y - lookTo.y, p.x - lookTo.x);
    const dist = Math.hypot(p.y - lookTo.y, p.x - lookTo.x);
    if (Math.random() < (p.hit ? 0.1 : 0.01)) {
        const move = p.dir + Math.random() ** 2 * Math.sign(Math.random() -  0.5) +  (p.hit ? Math.PI : 0);
        const hs = p.home ? 0.5 : 1;
        p.dx = Math.cos(move) * (p.hit ? 1 + Math.random() : hs);
        p.dy = Math.sin(move) * (p.hit ? 1 + Math.random() : hs);
    }
    if (p.strikeCount < 0 && dist < 120 && Math.random() < 0.05) {
        p.strike = true;
        p.strikeCount = 30;
    } else { 
        if (p.strikeCount < 20) {  p.strike = false }
        p.strikeCount --;
    }
    p.x += p.dx;
    p.y += p.dy;
    p.x = (p.x % SIZE + SIZE) % SIZE;
    p.y = (p.y % SIZE + SIZE) % SIZE;
}

function drawPlayer(p) {
   const ax = Math.cos(p.dir);
   const ay = Math.sin(p.dir);
   ctx.setTransform(ax, ay, -ay, ax, p.x, p.y);
   if (p.hit) {
       ctx.strokeStyle = "#800";
       ctx.lineWidth = 8;
       ctx.stroke(playerImg.path);
   }
   ctx.fillStyle =  p.col;
   ctx.fill(playerImg.path, "evenodd");


   if(p.lefty) {
      ctx.transform(1, 0, 0, 1, playerImg.w / 2, playerImg.h / 2);
      if (p.strike) {
          ctx.rotate(Math.PI * 0.4);
          p.sDir = Math.PI * 0.4;
       } else { p.sDir = 0 }
   } else {
      ctx.transform(1, 0, 0, 1, -playerImg.w / 2, playerImg.h / 2);
       if (p.strike) {
          ctx.rotate(-Math.PI * 0.4);
          p.sDir = -Math.PI * 0.4;
       } else { p.sDir = 0 }
   }
   ctx.fillStyle = p.home ? "#F00" : "#000";
   ctx.fill(swordImg.path, "evenodd");
   p.home = p.hit = false;
}
function testHit(p, p1) {
    const left = p.lefty ? 1 : -1;
    const x1 = playerImg.w / 2 * left;       
    const y1 = playerImg.h / 2;      
    const ax = Math.cos(p.dir)
    const ay = Math.sin(p.dir)
    const x2 = x1 * ax - y1 * ay + p.x;
    const y2 = x1 * ay + y1 * ax + p.y;
    const x3 = Math.cos(p.dir + p.sDir - Math.PI / 2) * swordImg.h + x2;
    const y3 = Math.sin(p.dir + p.sDir - Math.PI / 2) * swordImg.h + y2;
    if (isLineTouchingCircle(x2, y2, x3, y3, p1.x, p1.y, p1.r)) {
       p.home = p1.hit = true;
       drawBlood(isLineTouchingCircle.x, isLineTouchingCircle.y, Math.random() * 4 + 1);
    }
}
const bCol = ["#F00","#E02","#D04"];
function drawBlood(x, y, count) {
   const d = Math.random() * Math.PI * 2;
   const s =  Math.random() ** 3 * 40;
   const r =  Math.random() ** 4 * 6 + 1
   ctxB.setTransform(1,0,0,1,Math.cos(d) * s + x, Math.sin(d) * s + y);
   ctxB.beginPath();
   ctxB.fillStyle = bCol[Math.random() * 3  | 0];
   ctxB.arc(0,0, r, 0, Math.PI * 2);
   ctxB.fill();
   count > 0 && drawBlood(x, y, count - 1);
}
requestAnimationFrame(mainLoop);
var frame = 0;
function mainLoop() {
    ctx.setTransform(1,0,0,1,0,0);
    ctx.clearRect(0,0,SIZE,SIZE);
    ctx.drawImage(blood, 0, 0);
    if (frame++ % 8 === 0) {
        ctxB.setTransform(1,0,0,1,0,0);
        ctxB.fillStyle = "white";
        ctxB.globalAlpha = 0.1;
        ctxB.fillRect(0,0,SIZE,SIZE);
        ctxB.globalAlpha = 1;
    }
    updatePlayer(PLY1, PLY2);
    updatePlayer(PLY2, PLY1);
    drawPlayer(PLY1);
    drawPlayer(PLY2);
    PLY1.strike && testHit(PLY1, PLY2);
    PLY2.strike && testHit(PLY2, PLY1);
    requestAnimationFrame(mainLoop);
}
div {
   border: 2px solid black;
   overflow: hidden;
   width: 150px;
   height: 150px;
}
canvas {
   width: 170px;
   height: 170px;
   transform: matrix(1, 0, 0, 1, -10, -10);
}
<div><canvas id="canvas"></canvas></div>

Upvotes: 3

Related Questions