0

I'm currently working on a Pinball game using the HTML5 Canvas and JavaScript. Right now I'm getting a hard time with the pixel by pixel collision, which is fundamental because of the flippers.

Right now my Bounding Box Collision seems to be working

checkCollision(element) {
    if (this.checkCollisionBoundingBox(element)) {
        console.log("colision with the element bounding box");

        if (this.checkCollisionPixelByPixel(element)) {
            return true;
        } else {
            return false;
        }
    } else {
        return false;
    }
}

checkCollisionBoundingBox(element) {
    if (this.pos.x < element.pos.x + element.width && this.pos.x + this.width > element.pos.x && this.pos.y < element.pos.y + element.height && this.pos.y + this.height > element.pos.y) {
          return true;
    } else {
          return false;
      }
    }

I've tried several ways of implementing the pixel by pixel one but for some reason it does not work perfectly (on walls, on images, on sprites etc). I'll leave them here:

checkCollisionPixelByPixel(element) {
        var x_left = Math.floor(Math.max(this.pos.x, element.pos.x));
        var x_right = Math.floor(Math.min(this.pos.x + this.width, element.pos.x + element.width));
        var y_top = Math.floor(Math.max(this.pos.y, element.pos.y));
        var y_bottom = Math.floor(Math.min(this.pos.y + this.height, element.pos.y + element.height));

        for (var y = y_top; y < y_bottom; y++) {
            for (var x = x_left; x < x_right; x++) {
                var x_0 = Math.round(x - this.pos.x);
                var y_0 = Math.round(y - this.pos.y);
                var n_pix = y_0 * (this.width * this.total) + (this.width * (this.actual-1)) + x_0; //n pixel to check
                var pix_op = this.imgData.data[4 * n_pix + 3]; //opacity (R G B A)

                var element_x_0 = Math.round(x - element.pos.x);
                var element_y_0 = Math.round(y - element.pos.y);
                var element_n_pix = element_y_0 * (element.width * element.total) + (element.width * (element.actual-1)) + element_x_0; //n pixel to check
                var element_pix_op = element.imgData.data[4 * element_n_pix + 3]; //opacity (R G B A)
                console.log(element_pix_op);
                if (pix_op == 255 && element_pix_op == 255) {

                    console.log("Colision pixel by pixel");
                    /*Debug*/
                    /*console.log("This -> (R:" + this.imgData.data[4 * n_pix] + ", G:" + this.imgData.data[4 * n_pix + 1] + ", B:" + this.imgData.data[4 * n_pix + 2] + ", A:" + pix_op + ")");
                    console.log("Element -> (R:" + element.imgData.data[4 * element_n_pix] + ", G:" + element.imgData.data[4 * element_n_pix + 1] + ", B:" + element.imgData.data[4 * element_n_pix + 2] + ", A:" + element_pix_op + ")");
                    console.log("Collision -> (x:" + x + ", y:" + y +")");
                    console.log("This(Local) -> (x:" + x_0 + ", y:" + y_0+")");
                    console.log("Element(Local) -> (x:" + element_x_0 + ", y:" + element_y_0+")");*/
                    /*ball vector*/
                    var vector = {
                        x: (x_0 - Math.floor(this.imgData.width / 2)),
                        y: -(y_0 - Math.floor(this.imgData.height / 2))
                    };
                    //console.log("ball vector -> ("+vector.x+", "+vector.y+") , Angulo: "+ Math.atan(vector.y/vector.x)* 180/Math.PI);

                     // THIS WAS THE FIRST TRY, IT DIDN'T WORK WHEN THE BALL WAS GOING NORTHEAST AND COLLIDED WITH A WALL. DIDN'T WORK AT ALL WITH SPRITES
                    //this.angle = (Math.atan2(vector.y, vector.x) - Math.PI) * (180 / Math.PI);


                     // THIS WAS THE SECOND ATTEMPT, WORKS WORSE THAN THE FIRST ONE :/
                    //normal vector
                    var normal = {
                        x: (x_0 - (this.imgData.width / 2)),
                        y: -(y_0 - (this.imgData.height / 2))
                    };
                    //Normalizar o vetor
                    var norm = Math.sqrt(normal.x * normal.x + normal.y * normal.y);
                    if (norm != 0) {
                        normal.x = normal.x / norm;
                        normal.y = normal.y / norm;
                    }
                    var n_rad = Math.atan2(normal.y, normal.x);
                    var n_deg = (n_rad + Math.PI) * 180 / Math.PI;
                    console.log("Vetor Normal -> (" + normal.x + ", " + normal.y + ") , Angulo: " + n_deg);
                    //Vetor Velocidade
                    var velocity = {
                        x: Math.cos((this.angle * Math.PI / 180) - Math.PI),
                        y: Math.sin((this.angle * Math.PI / 180) - Math.PI)
                    };
                    console.log("Vetor Velocidade -> (" + velocity.x + ", " + velocity.y + ") , Angulo: " + this.angle);
                    //Vetor Reflexao
                    var ndotv = normal.x * velocity.x + normal.y * velocity.y;
                    var reflection = {
                        x: -2 * ndotv * normal.x + velocity.x,
                        y: -2 * ndotv * normal.y + velocity.y
                    };
                    var r_rad = Math.atan2(reflection.y, reflection.x);
                    var r_deg = (r_rad + Math.PI) * 180 / Math.PI;
                    console.log("Vetor Reflexao -> (" + reflection.x + ", " + reflection.y + ") , Angulo: " + r_deg);

                    this.angle = r_deg;


                    return true;
                }
            }
        }
        return false;
    }
}

The ball class

class Ball extends Element {
    constructor(img, pos, width, height, n, sound, angle, speed) {
        super(img, pos, width, height, n, sound);
        this.angle = angle; //direction [0:360[
        this.speed = speed;
    }
    move(ctx, cw, ch) {
        var rads = this.angle * Math.PI / 180
        var vx = Math.cos(rads) * this.speed / 60;
        var vy = Math.sin(rads) * this.speed / 60;

        this.pos.x += vx;
        this.pos.y -= vy;

        ctx.clearRect(0, 0, cw, ch);
        this.draw(ctx, 1);
    }
}
Pedro Caseiro
  • 161
  • 1
  • 16
  • 1
    This answer http://stackoverflow.com/a/36026906/3877726 has a method that is very fast and adapts to various irregular sprite shapes. It is limited to shapes that do not bend in on themselves. It requires a little init code but the collision test is many time faster than any software based pixel collision test, comparable to a circle/box collision in performance – Blindman67 May 12 '16 at 21:07

1 Answers1

1

Assuming a "flipper" is composed of 2 arcs and 2 lines it would be much faster to do collision detection mathematically rather than by the much slower pixel-test method. Then you just need 4 math collision tests.

Even if your flippers are a bit more complicated than arcs+lines, the math hit tests would be "good enough" -- meaning in your fast-moving game, the user cannot visually notice the approximate math results vs the pixel-perfect results and the difference between the 2 types of tests will not affect gameplay at all. But the pixel-test version will take magnitudes more time and resources to accomplish. ;-)

First two circle-vs-circle collision tests:

function CirclesColliding(c1,c2){
    var dx=c2.x-c1.x;
    var dy=c2.y-c1.y;
    var rSum=c1.r+c2.r;
    return(dx*dx+dy*dy<=rSum*rSum);
}

Then two circle-vs-line-segment collision tests:

// [x0,y0] to [x1,y1] define a line segment
// [cx,cy] is circle centerpoint, cr is circle radius 
function isCircleSegmentColliding(x0,y0,x1,y1,cx,cy,cr){

    // calc delta distance: source point to line start
    var dx=cx-x0;
    var dy=cy-y0;

    // calc delta distance: line start to end
    var dxx=x1-x0;
    var dyy=y1-y0;

    // Calc position on line normalized between 0.00 & 1.00
    // == dot product divided by delta line distances squared
    var t=(dx*dxx+dy*dyy)/(dxx*dxx+dyy*dyy);

    // calc nearest pt on line
    var x=x0+dxx*t;
    var y=y0+dyy*t;

    // clamp results to being on the segment
    if(t<0){x=x0;y=y0;}
    if(t>1){x=x1;y=y1;}

    return( (cx-x)*(cx-x)+(cy-y)*(cy-y) < cr*cr );
}
markE
  • 102,905
  • 11
  • 164
  • 176
  • This is a good idea, but, unfortunately one of the requirements is to make a pixel-by-pixel collision approach that works with all type of objects that compose the Pinball machine. Sorry, I should have mentioned that earlier! – Pedro Caseiro May 12 '16 at 20:21
  • **Trying again one last time...** But aren't all bumpers, etc composed of either lines or arcs. The math tests are just as effective as pixel tests -- maybe even more considering anti-aliasing that affects pixel hit tests. – markE May 12 '16 at 20:31
  • How can i adapt that to a sprite? – Pedro Caseiro May 12 '16 at 20:49
  • @PedroCaseiro. Just bound the sprite with lines, rects and/or arcs. :-) BTW, that's how many physics engines (including box2d) do it: Simplifiy testing to lines, rects and circles. Again, don't over-stress pixel-perfect because the reality is your player won't recognize a pixel or two difference in a fast paced game. Also, be sure to check Blindman67 linked solution in your question's comments. I haven't double-checked his math, but the concept looks solid (and innovative!)-- even for very irregularly shaped sprites. – markE May 12 '16 at 23:04