0
votes

Here is my game plnkr.

(Edit: another plnkr with one static monster instead of multiple dynamic ones)

Enter or the button will restart the game.

Can anyone tell why the collision detection algorithm taken from here is not working? It seems to detect a hit not accurately (too widely). The demo on their site works great but I'm not sure what I'm doing wrong.

Most relevant piece of code (inside update function):

// Are they touching?


if (heroImage.width) {
    var heroImageData = ctx.getImageData(heroImage.x, heroImage.y, heroImage.width, heroImage.height);
    var monsterImageData;
    for (var i = 0; i < monsters.length; i++) {
        var monster = monsters[i];

        monster.x += monster.directionVector.x;
        monster.y += monster.directionVector.y;

        monsterImageData = ctx.getImageData(monster.monsterImage.x, monster.monsterImage.y, monster.monsterImage.width, monster.monsterImage.height);
        if (isPixelCollision(heroImageData, hero.x, hero.y, monsterImageData, monster.x, monster.y)) {
            stop();
        }
    }
}
4

4 Answers

2
votes

Your hero image is 71x68px and has a lot of transparent space around the outside. I'm guessing if you crop this to just fit the image it will reduce the space between collisions.

enter image description here

2
votes

You are taking the imageData on the game's drawing context, so since you have a background, there's no transparent pixel at all, so your pixel collision detection returns always true - > you are just doing a bounding box check, in fact.
The idea of the algorithm is to compare two static imageData that only need to be computed once (getImageData is a costly operation).

A few advices :
• load your images before launching the game.
• redim (crop) your image, it has a lot of void, as @Quantumplate noticed.
• compute only once the imageData of your sprites on the context before the launch of the game. Do not forget to clearRect() the canvas before the drawImage + getImageData. This is the way to solve your bug.
• get rid of the

if (xDiff < 4 && yDiff < 4) {

and the corresponding else. This 'optimisation' is pointless. The point of using pixel detection is to be precise. Redim (crop) your image is more important to win a lot of time (but do you need to ... ?? )
• Rq : How poorly written is the pixel detection algorithm !!! 1) To round a number, it's using !! 5 different methods (round, <<0, ~~, 0 |, ? : ) !!! 2) It loops on X first when CPU cache prefers on Y first, and many other things... But now if that works...

2
votes

As @GameAlchemist pointed out you're taking ImageData for monster and hero from the canvas background, which has already been painted with the background image. Thus will always have alpha value 255 (Opaque).

Which is being checked in the collision function

if (
    ( pixels [((pixelX - x ) + (pixelY - y ) * w ) * 4 + 3 /*RGBA, alpha @ 4*/] !== 0/*alpha zero expected*/ ) &&
    ( pixels2[((pixelX - x2) + (pixelY - y2) * w2) * 4 + 3 /*RGBA, alpha @ 4*/] !== 0/*alpha zero expected*/ )
) {
    return true;
}

Instead both the ImageData should be generated by drawing these images to a canvas with nothing painted. Even after doing that collision algorithm doesn't seem to work too well.

I have created two variables monsterImageData and heroImageData to hold the imageData these variable are loaded only once.

There's a new canvas in HTML file id=testCanvas. This is used to get image data values for monster and heroes.

Here is the plunker link for modified code.

1
votes

Here's an alternate (more efficient) pixel perfect collision test...

Preparation: For each image you want to test for collisions

  • As mentioned, trim any excess transparent pixels off the edges of your image,
  • Resize a canvas to the image size, (you can reuse 1 canvas for multiple images)
  • Draw the image on the canvas,
  • Get all the pixel info for the canvas: context.getImageData,
  • Make an array containing only alpha information: false if transparent, otherwise true.

To do a pixel-perfect collision test

  • Do a quick test to see if the image rects are colliding. If not, you're done.

    // r1 & r2 are rect objects {x:,y:,w:.h:}
    function rectsColliding(r1,r2){
        return(!(
            r1.x       > r2.x+r2.w ||
            r1.x+r1.w  < r2.x      ||
            r1.y       > r2.y+r2.h ||
            r1.y+r1.h  < r2.y
        ));
    }
    
  • Calculate the intersecting rect of the 2 images

    // r1 & r2 are rect objects {x:,y:,w:.h:}
    function intersectingRect(r1,r2){
      var x=Math.max(r1.x,r2.x);
      var y=Math.max(r1.y,r2.y);
      var xx=Math.min(r1.x+r1.w,r2.x+r2.w);
      var yy=Math.min(r1.y+r1.h,r2.y+r2.h);
      return({x:x,y:y,w:xx-x,h:yy-y});
    }
    
  • Compare the intersecting pixels in both alpha arrays. If both arrays have a non-transparent pixel at the same location then there is a collision. Be sure to normalize against the origin (x=0,y=0) by offsetting your comparisons.

    // warning untested code -- might need tweaking
    var i=intersectingRect(r1,r2);
    var offX=Math.min(r1.x,r2.x);
    var offY=Math.min(r1.y,r2.y);
    for(var x=i.x-offX; x<=(i.x-offX)+i.w; x++{
    for(var y=i.y-offY; y<=(i.y-offY)+i.h; y++{
        if(
            // x must be valid for both arrays
            x<alphaArray1[y].length && x<alphaArray2[y].length &&
            // y must be valid for both arrays
            y<alphaArray1.length && y<alphaArray2.length &&
            // collision is true if both arrays have common non-transparent alpha
            alphaArray1[x,y] && alphaArray2[x,y]
        ){
            return(true);
        }
    }}
    return(false);