HTML Canvas and JavaScript rotating objects with c

2019-06-10 00:58发布

I'm creating a game with JavaScript and HTML Canvas. It's a multiplayer 2D game with tanks that try to hit each other. The tanks can move as well as rotate. How can you figure out collision detection with rotating rectangular objects? I know, I could make them square and use circular detection, but it looks very messy when a tank runs into a wall. Thanks for all who try to help :)

1条回答
劳资没心,怎么记你
2楼-- · 2019-06-10 01:26

Move hit point to local space

First an alternative

There are many ways you can do it. The simplest way. When you calculate the cross product between a point and a line it will be negative if the point is right of the line and positive if left. If you then do each of the four sides in turn and they are all the same sign the point must be inside.

To get the cross product of a line and a point

//x1,y1,x2,y2   is a line
// px,py is a point
// first move line and point relative to the origin
// so that the line and point is a vector
px -= x1;
py -= y1;
x2 -= x1;
y2 -= y1;
var cross = x2 * py - y2 * px; 
if(cross < 0){ 
     // point left of line
}else if(cross > 0) {
    // point right of line
}else {
    // point on the line
}

A Quicker way.

But that is a lot of math for each object and each bullet.

The best way is to transform the bullet into the tanks local coordinate system then its just a simple matter of testing the bounds, left, right, top, bottom.

To do that you need to invert the tanks transformation matrix. Unfortunately the easy way to do that is currently still behind browser flags/prefixes so you need to create and manipulate the transformations in javascript. (Should not be too long till ctx.getTransform() is implemented across the board and fill a very needed performance hole in the canvas 2d API)

If ctx.getTransform is available

So you have a tank at x,y and rotated r and you draw it with

ctx.translate(x,y);
ctx.rotate(r);
// render the tank
ctx.fillRect(-20,-10,40,20); // rotated about center

The transform hold everything we need to do the calcs, all we need to do is invert it and then multiply the bullet with the inverted matrix

var tankInvMatrix = ctx.getTransform().invertSelf(); // get the inverted matrix

The bullet is at bx,by so create a DOMPoint

var bullet = new DOMPoint(bx,by);

Then for each tank transform the bullet with DOMMatrix.transformPoint

var relBullet = tankInvMatrix.transformPoint(bullet); // transform the point 
                                                      // returning the bullet 
                                                      // relative to the tank

Now just do the test in the tanks local coord space

if(relBullet.x > -20 && relBullet.x < 20 && relBullet.x > -10 && relBullet.x < 10){
      /// bullet has hit the tank
}

The Javascript way

Well until the becomes the norm you have to do it the long way. Using the same x,y,r for tank, bx,by for bullet.

// create a vector aligned to the tanks direction
var xdx = Math.cos(r);
var xdy = Math.sin(r);

// set the 2D API to the tank location and rotation
ctx.setTransform(xdx,xdy,-xdy,xdx,x,y);  // create the transform for the tank

// draw the tank
ctx.fillRect(-20,-10,40,20); // rotated about center

// create inverted matrix for the tank 
// Only invert the tank matrix once per frame

var d =  xdx * xdx - xdy * -xdy;
var xIx  = xdx / d;
var xIy  = -xdy / d;
// I am skipping c,d of the matrix as it is perpendicular to a,b
// thus c = -b and d = a
var ix = (-xdy * y - xdx * x) / d;
var iy = -(xdx * y - xdy * x) / d;

// For each bullet per tank
// multiply the bullet with the inverted tank matrix
// bullet local x & y
var blx = bx * xIx - by * xIy + ix;
var bly = bx * xIy + by * xIx + iy;

// and you are done.
if(blx > -20 && blx < 20 && bly > -10 && bly < 10){
      // tank and bullet are one Kaaboommmm 
}

Test to make sure it works

Too many negatives, xdx,xdy etc etc for me to be able to see if I got it correct (Turned out I put the wrong sign in the determinant) so here is a quick demo to show it in action and working.

Use the mouse to move over the tank body and it will show that it is hit in red. You could extend it easily to also hit the tank moving parts. You just need the inverse transform of the turret to get the bullet in local space to do the test.

UPDATE

Add code to stop tank's visually popping in and out as the crossed canvas edge. This is done by subtracting an OFFSET from each tank when displayed. This offset must be factored in when doing the hit test by adding OFFSET to the test coordinates.

const TANK_LEN = 40;
const TANK_WIDTH = 20;
const GUN_SIZE = 0.8; // As fraction of tank length
// offset is to ensure tanks dont pop in and out as the cross screen edge
const OFFSET = Math.sqrt(TANK_LEN * TANK_LEN + TANK_WIDTH * TANK_WIDTH ) + TANK_LEN * 0.8;
// some tanks
var tanks = {
    tanks : [], // array of tanks
    drawTank(){  // draw tank function
        this.r += this.dr;
        this.tr += this.tdr;
        if(Math.random() < 0.01){
            this.dr = Math.random() * 0.02 - 0.01;
        }
        if(Math.random() < 0.01){
            this.tdr = Math.random() * 0.02 - 0.01;
        }
        if(Math.random() < 0.01){
            this.speed = Math.random() * 2 - 0.4;
        }
        var xdx = Math.cos(this.r);
        var xdy = Math.sin(this.r);
        
        // move the tank forward
        this.x += xdx * this.speed;
        this.y += xdy * this.speed;

        this.x = ((this.x + canvas.width + OFFSET * 2) % (canvas.width + OFFSET * 2));
        this.y = ((this.y + canvas.height + OFFSET * 2) % (canvas.height + OFFSET * 2)) ;


        ctx.setTransform(xdx,xdy,-xdy,xdx,this.x - OFFSET, this.y - OFFSET);
        ctx.lineWidth = 2;

        
        ctx.beginPath();
        if(this.hit){
            ctx.fillStyle = "#F00";
            ctx.strokeStyle = "#800";
            this.hit = false;
        }else{
            ctx.fillStyle = "#0A0";
            ctx.strokeStyle = "#080";
        }
        ctx.rect(-this.w / 2, -this.h / 2, this.w, this.h);
        ctx.fill();
        ctx.stroke();
        ctx.translate(-this.w /4, 0)
        ctx.rotate(this.tr);
        ctx.fillStyle = "#6D0";
        ctx.beginPath();
        ctx.rect(-8, - 8, 16, 16);

        ctx.rect(this.w / 4, - 2, this.w * GUN_SIZE, 4);
        ctx.fill()
        ctx.stroke()
        // invert the tank matrix
        var d =  (xdx * xdx) - xdy * -xdy;
        this.invMat[0] = xdx / d;
        this.invMat[1] = -xdy / d;
        // I am skipping c,d of the matrix as it is perpendicular to a,b
        // thus c = -b and d = a
        this.invMat[2] = (-xdy * this.y - xdx * this.x) / d;
        this.invMat[3] = -(xdx * this.y - xdy * this.x) / d;        
    },
    hitTest(x,y){ // test tank against x,y
        x += OFFSET;
        y += OFFSET;
        var blx = x * this.invMat[0] - y * this.invMat[1] + this.invMat[2];
        var bly = x * this.invMat[1] + y * this.invMat[0] + this.invMat[3];
        if(blx > -this.w / 2 && blx < this.w / 2 && bly > -this.h / 2 && bly < this.h / 2){
            this.hit = true;
        }        
    },
    eachT(callback){ // iterator
        for(var i = 0; i < this.tanks.length; i ++){ callback(this.tanks[i],i); }
    },
    addTank(x,y,r){  // guess what this does????
        this.tanks.push({
            x,y,r,
            dr : 0,  // turn rate
            tr : 0,  // gun direction
            tdr : 0, // gun turn rate
            speed : 0, // speed
            w : TANK_LEN,
            h : TANK_WIDTH,
            invMat : [0,0,0,0],
            hit : false,
            hitTest : this.hitTest,
            draw : this.drawTank,
        })
    },
    drawTanks(){ this.eachT(tank => tank.draw()); },
    testHit(x,y){ // test if point x,y has hit a tank
        this.eachT(tank => tank.hitTest(x,y));
    }
}


// this function is called from a requestAnimationFrame call back
function display() { 
    if(tanks.tanks.length === 0){
        // create some random tanks
        for(var i = 0; i < 100; i ++){
            tanks.addTank(
                Math.random() * canvas.width,
                Math.random() * canvas.height,
                Math.random() * Math.PI * 2
            );
        }
    }
    
    ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
    ctx.globalAlpha = 1; // reset alpha
    ctx.clearRect(0, 0, w, h);
    
    // draw the mouse
    ctx.fillStyle = "red";
    ctx.strokeStyle = "#F80";
    ctx.beginPath();
    ctx.arc(mouse.x,mouse.y,3,0,Math.PI * 2);
    ctx.fill();
    ctx.stroke();


    // draw the tanks    
    tanks.drawTanks();
    // test for a hit (Note there should be a update, then test hit, then draw as is the tank is hit visually one frame late)
    tanks.testHit(mouse.x,mouse.y);
}



//====================================================================================================
// Boilerplate code not part of answer ignore all code from here down
//====================================================================================================

var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0, firstRun = true;
;(function(){
    const RESIZE_DEBOUNCE_TIME = 100;
    var  createCanvas, resizeCanvas, setGlobals, resizeCount = 0;
    createCanvas = function () {
        var c,cs;
        cs = (c = document.createElement("canvas")).style;
        cs.position = "absolute";
        cs.top = cs.left = "0px";
        cs.zIndex = 1000;
        document.body.appendChild(c);
        return c;
    }
    resizeCanvas = function () {
        if (canvas === undefined) {
            canvas = createCanvas();
        }
        canvas.width = innerWidth;
        canvas.height = innerHeight;
        ctx = canvas.getContext("2d");
        if (typeof setGlobals === "function") {
            setGlobals();
        }
        if (typeof onResize === "function") {
            if(firstRun){
                onResize();
                firstRun = false;
            }else{
                resizeCount += 1;
                setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
            }
        }
    }
    function debounceResize() {
        resizeCount -= 1;
        if (resizeCount <= 0) {
            onResize();
        }
    }
    setGlobals = function () {
        cw = (w = canvas.width) / 2;
        ch = (h = canvas.height) / 2;
    }
    mouse = (function () {
        function preventDefault(e) {
            e.preventDefault();
        }
        var mouse = {
            x : 0,y : 0,w : 0,
            alt : false,
            shift : false,
            ctrl : false,
            buttonRaw : 0,
            over : false,
            bm : [1, 2, 4, 6, 5, 3],
            active : false,
            bounds : null,
            crashRecover : null,
            mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
        };
        var m = mouse;
        function mouseMove(e) {
            var t = e.type;
            m.bounds = m.element.getBoundingClientRect();
            m.x = e.pageX - m.bounds.left + scrollX;
            m.y = e.pageY - m.bounds.top + scrollY;
            m.alt = e.altKey;
            m.shift = e.shiftKey;
            m.ctrl = e.ctrlKey;
            if (t === "mousedown") {
                m.buttonRaw |= m.bm[e.which - 1];
            } else if (t === "mouseup") {
                m.buttonRaw &= m.bm[e.which + 2];
            } else if (t === "mouseout") {
                m.buttonRaw = 0;
                m.over = false;
            } else if (t === "mouseover") {
                m.over = true;
            } else if (t === "mousewheel") {
                m.w = e.wheelDelta;
            } else if (t === "DOMMouseScroll") {
                m.w = -e.detail;
            }
            if (m.callbacks) {
                m.callbacks.forEach(c => c(e));
            }
            if ((m.buttonRaw & 2) && m.crashRecover !== null) {
                if (typeof m.crashRecover === "function") {
                    setTimeout(m.crashRecover, 0);
                }
            }
            e.preventDefault();
        }
        m.addCallback = function (callback) {
            if (typeof callback === "function") {
                if (m.callbacks === undefined) {
                    m.callbacks = [callback];
                } else {
                    m.callbacks.push(callback);
                }
            }
        }
        m.start = function (element) {
            if (m.element !== undefined) {
                m.removeMouse();
            }
            m.element = element === undefined ? document : element;
            m.mouseEvents.forEach(n => {
                m.element.addEventListener(n, mouseMove);
            });
            m.element.addEventListener("contextmenu", preventDefault, false);
            m.active = true;
        }
        m.remove = function () {
            if (m.element !== undefined) {
                m.mouseEvents.forEach(n => {
                    m.element.removeEventListener(n, mouseMove);
                });
                m.element.removeEventListener("contextmenu", preventDefault);
                m.element = m.callbacks = undefined;
                m.active = false;
            }
        }
        return mouse;
    })();
    // Clean up. Used where the IDE is on the same page.
    var done = function () {
        window.removeEventListener("resize", resizeCanvas)
        if(mouse !== undefined){
            mouse.remove();
        }
        document.body.removeChild(canvas);
        canvas = ctx = mouse = undefined;
    }
    function update(timer) { // Main update loop
        if(ctx === undefined){
            return;
        }
        globalTime = timer;
        display(); // call demo code
        //if (!(mouse.buttonRaw & 2)) {
            requestAnimationFrame(update);
        //} else {
        //    done();
        //}
    }
    setTimeout(function(){
        resizeCanvas();
        mouse.start(canvas, true);
        mouse.crashRecover = done;
        window.addEventListener("resize", resizeCanvas);
        requestAnimationFrame(update);
    },0);
})();

查看更多
登录 后发表回答