【问题标题】:HTML Canvas and JavaScript rotating objects with collision detection带有碰撞检测的 HTML Canvas 和 JavaScript 旋转对象
【发布时间】:2017-05-19 02:19:17
【问题描述】:

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

【问题讨论】:

    标签: javascript html canvas collision detection


    【解决方案1】:

    将命中点移动到本地空间

    第一个替代方案

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

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

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

    更快的方法。

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

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

    为此,您需要反转 tank 变换矩阵。不幸的是,目前实现这一点的简单方法仍然落后于浏览器标志/前缀,因此您需要在 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
    

    现在只需在坦克本地坐标空间中进行测试

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

    用鼠标在坦克体上移动,它会以红色显示它被击中。您可以轻松地扩展它以撞击水箱的移动部件。你只需要炮塔的逆变换,让子弹在局部空间进行测试。

    更新

    添加代码以阻止坦克的视觉弹出作为交叉的画布边缘。这是通过在显示时从每个罐中减去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);
    })();

    【讨论】:

    • 哇!我完全没想到有人会走这么远来回答我的问题!先生,您的回答超越了您!感谢您解释所有这些。现在更有意义了。我会尝试实现你的编码,我会让你知道它是怎么回事。非常感谢,我希望你有一个美好的一天!
    • 有史以来最好的答案! :)
    • 你真的很棒。到目前为止,您已经做出了这个答案并且它绝对完美。
    猜你喜欢
    • 2022-08-19
    • 2016-03-22
    • 2014-09-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多