HTML Canvas和JavaScript通过碰撞检测旋转对象

时间:2017-01-04 17:24:52

标签: javascript html canvas collision detection

我正在使用JavaScript和HTML Canvas创建游戏。这是一款多人2D游戏,坦克试图互相攻击。坦克既可以移动也可以旋转。如何通过旋转矩形物体找出碰撞检测?我知道,我可以把它们做成方形并使用圆形检测,但是当一个坦克撞到一堵墙时它看起来很乱。感谢所有试图提供帮助的人:)

1 个答案:

答案 0 :(得分:2)

将生命值移动到本地空间

首先是另一种选择

有很多方法可以做到。最简单的方法。当您计算点和线之间的叉积时,如果该点位于该线的右侧,则该值将为负,如果为左侧则为正。如果你依次对四个边中的每个边做,并且它们都是相同的符号,则必须在内部。

获得直线和点的叉积

//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
}

更快捷的方式。

但对于每个物体和每个子弹,这都是很多数学。

最好的方法是将子弹转换为坦克局部坐标系,然后只需测试边界,左,右,顶部,底部就可以了。

为此,您需要反转储罐转换矩阵。不幸的是,这样做的简单方法目前仍然落后于浏览器标志/前缀,因此您需要在javascript中创建和操作转换。 (不应该太长,直到ctx.getTransform()全面实施并填充画布2d API中非常需要的性能漏洞)

如果ctx.getTransform可用

所以你有一个坦克在x,y和旋转r,你用

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

转换包含我们进行计算所需的一切,我们需要做的就是反转它然后将子弹与倒置矩阵相乘

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

子弹是bx,所以创建一个DOMPoint

var bullet = new DOMPoint(bx,by);

然后为每个坦克改造子弹DOMMatrix.transformPoint

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

现在只需在坦克局部坐标空间进行测试

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

Javascript方式

直到成为常态,你必须做很长的路。使用相同的x,y,r表示坦克,bx,表示子弹。

// 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 
}

测试以确保其正常工作

太多的负片,xdx,xdy等让我能够看到我是否正确(事实证明我把错误的标志放在行列式中)所以这里是一个快速演示,以显示它在行动和工作。

使用鼠标移动坦克身体,它会显示它被击中红色。您可以轻松地将其延伸到水箱移动部件。你只需要使用炮塔的逆变换来获得当地空间的子弹来进行测试。

更新

添加代码以阻止坦克在视觉上以交叉的画布边缘弹出。这是通过在显示时从每个坦克中减去OFFSET来完成的。通过将OFFSET添加到测试坐标来进行命中测试时,必须考虑此偏移量。

&#13;
&#13;
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);
})();
&#13;
&#13;
&#13;