HTML Canvas 和 JavaScript 带碰撞检测的旋转物体

HTML Canvas and JavaScript rotating objects with collision detection

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

移动生命值到本地space

先选一个

您可以通过多种方式做到这一点。最简单的方法。当您计算点和线之间的叉积时,如果该点位于线的右侧,则为负,如果位于线的左侧,则为正。如果你然后依次做四个边中的每一个并且它们都是相同的符号,那么点必须在里面。

得到一条线和一个点的叉积

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

更快的方法。

但是对于每个对象和每个项目符号来说,这是大量的数学运算。

最好的方法是将子弹转换到坦克局部坐标系中,然后它只是简单的测试边界,左,右,上,下。

为此,您需要反转坦克变换矩阵。不幸的是,目前浏览器 flags/prefixes 仍然支持最简单的方法,因此您需要在 javascript 中创建和操作转换。 (在全面实施 ctx.getTransform() 并填补 canvas 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

现在只需在坦克本地坐标中进行测试即可 space

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

Javascript方式

好吧,在这成为常态之前,您必须长期坚持下去。对坦克使用相同的 x,y,r,对子弹使用相同的 bx,by。

// 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 等让我无法查看我是否正确(结果我在行列式中输入了错误的符号)所以这里有一个快速演示来展示它的实际效果和正在工作。

用鼠标移到坦克身上,会显示被击中为红色。你可以很容易地延长它来击中坦克的运动部件。只需要对炮塔进行逆变换获取本地的子弹space即可进行测试

更新

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

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) * this.scale;
        var xdy = Math.sin(this.r) * this.scale;
        
        // 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,
            scale: 1,
            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 () {
        removeEventListener("resize", resizeCanvas)
        mouse && 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
        requestAnimationFrame(update);
    }
    setTimeout(function(){
        resizeCanvas();
        mouse.start(canvas, true);
        mouse.crashRecover = done;
        addEventListener("resize", resizeCanvas);
        requestAnimationFrame(update);
    },0);
})();