【问题标题】:Javascript Canvas: Collision against enemies not entirely working when rotating playerJavascript Canvas:旋转玩家时与敌人的碰撞不完全有效
【发布时间】:2016-01-31 23:53:03
【问题描述】:

注意: *完整的JSFiddle可以在我的帖子底部找到*。

问题:我正在尝试消灭所有触及画布中心蓝线的敌人。但是,情况并非如此,我的实现只是“一半工作”。当一侧起作用时,另一侧不起作用。我该如何解决这个问题?


我尝试了什么:一旦我设置了基本的绘图功能,我计算了碰撞对象的 x 和 y 之间的差异。使用毕达哥拉斯距离来计算两点之间的距离。最后检查距离是否小于或等于两个对象的组合半径。我使用反正切计算了物体运动的旋转。


我想到的替代解决方案:使用循环沿着蓝线创建各种不可见的圆圈或点,作为碰撞受体。问题是:它消耗更多资源,而且一点也不优雅。


您最感兴趣的 Javascript 函数是:

function (player, spawn) {
    return (this.distance(player, spawn) <= player.radius + spawn.radius) && (this.tangent(player, spawn) <= angle - Math.PI * 1);
}

角度是旋转的蓝线的角度(这是一个带有斯托克的半圆)。

this.tangent(player, spawn) <= angle - Math.PI * 1)

这仅适用于 -- 和 +- 部分。将 = 与预期相反。我需要找到一种从 -1 循环到 1 的方法。

this.tangent(player, spawn) >= angle - Math.PI * 2 && this.tangent(player, spawn) >= angle

适用于 --、-+、++,但不适用于 +-(右下角)。

所以最后我完全困惑为什么我的逻辑不起作用,但我很想知道如何实现这一点:


JSFiddle 下方:

http://jsfiddle.net/mzg635p9/

我很乐意收到回复,因为我喜欢在 Javascript 中学习新事物:)

编辑 (03.11.2015): 如果可能,仅提供纯数学解决方案,但也可以随意发布其他解决方案。为了学习新技术,欢迎提供每一条信息。

【问题讨论】:

    标签: javascript canvas collision


    【解决方案1】:

    您的代码的问题似乎在于您比较角度的方式。别忘了 2Pi 和 0 是一回事。看看这个例子: 你有 2 个角度,a 和 b。

    a = 0.1 * Pi

    b = 1.9 * Pi

    a 略高于 x 轴,而 b 略低于 x 轴。

    看起来 a 似乎在 b 之前,所以你会期望 a > b 是真的。可是等等!看数字,b比a大! 当您想检查一个角度是否在一个区间之间时,您必须确保您的区间是连续的,在这种情况下,对于角度 = 0,这是错误的。

    这是我的解决方案。我已尽我所能对其进行了测试,但您永远无法知道自己是否遗漏了什么。

    // Gets the equivalent angle between 0 and MAX
    var normalize_angle = function( angle )
    {
        var MAX = Math.PI * 2;  // Value for a full rotation. Should be 360 in degrees
        angle %= MAX;
        return angle < 0 ? angle + MAX : angle;
    };
    
    var is_angle_between = function( alpha, min, max )
    {
        // Convert all the angles to be on the same rotation, between 0 and MAX
        alpha = normalize_angle( alpha );
        min = normalize_angle( min );
        max = normalize_angle( max );
    
        if( max > min )
        {   // Check if the equal case fits your needs. It's a bit pointless for floats
            return max >= alpha && min <= alpha;    // Traditional method works
        } else {    // This happens when max goes beyond MAX, so it starts from 0 again
            return max >= alpha || min <= alpha;    // alpha has to be between max and 0 or
                                                    //                 between min and MAX
        }
    };
    

    要使用它,请将您的屏蔽功能更改为:

    shield: 
        function (player, spawn) {
            return (this.distance(player, spawn) <= player.radius + spawn.radius) && 
                is_angle_between(this.tangent(player, spawn), angle , angle - Math.PI );
        }
    }
    

    【讨论】:

    • 但是为什么我只有一个圆圈的旧版本可以工作? jsfiddle.net/2rz296tf/20 我从 -1 循环到 1
    • @Asperger 另一个版本在 Pi 处有“跳跃”而不是 0,并且您在那里设置的示例永远不会触发答案错误的情况。尝试多个示例,您会发现它失败了一些。
    • 把触发器放在一边。我的意思是盾牌上的碰撞在各个角度都有效,但我的新版本中的相同公式根本不起作用
    • 我可能需要一些图片或一些视觉帮助。我认为我完全无法理解这个概念,尽管我了解圆的工作原理。
    • @Asperger 别担心,我正在编写一个解决问题的函数。事实上,它已经写好了,我正在对其进行测试以确保它始终有效。
    【解决方案2】:

    对磁盘和弧http://jsfiddle.net/crl/2rz296tf/31之间的碰撞检测问题做了一些简化(编辑:@markE 建议http://jsfiddle.net/crl/2rz296tf/32/)(用于调试:http://jsfiddle.net/crl/2rz296tf/27/

    一些比较角度的实用函数:

    function mod(x, value){ // Euclidean modulo http://jsfiddle.net/cLvmrs6m/4/
        return x>=0 ? x%value : value+ x%value;
    }
    
    function angularize(x){
        return mod(x+pi, 2*pi)-pi;
    }
    

    碰撞检测:

    var d_enemy_player = dist(enemy.pos, player.pos)
    if (d_enemy_player>player.shieldradius-enemy.radius && d_enemy_player<player.shieldradius+enemy.radius){ 
        //only worth checking when we are approaching the shield distance
        var angle_enemy = atan2(enemy.pos.y-player.pos.y, enemy.pos.x-player.pos.x)
    
        var delta_with_leftofshield = angularize(angle_enemy-player.angle-player.shieldwidth)
        var delta_with_rightofshield = angularize(angle_enemy-player.angle+player.shieldwidth)
        var delta_with_shield = angularize(angle_enemy-player.angle)
    
        if (delta_with_leftofshield<0 && delta_with_rightofshield>0){
            console.log('boom')
            enemy.destroyed = true;
        } else if(delta_with_shield>=0 ){
            // check distance with right extremety of shield's arc
            console.log('right')
            var d_rightofshield_enemy = dist(enemy.pos, {x:player.pos.x+player.shieldradius*cos(player.angle+player.shieldwidth), y:player.pos.y+player.shieldradius*sin(player.angle+player.shieldwidth)});
            if (d_rightofshield_enemy<enemy.radius){
                console.log('right boom')
                enemy.destroyed = true;
            }
        } else {
            console.log('left')
            var d_leftofshield_enemy = dist(enemy.pos, {x:player.pos.x+player.shieldradius*cos(player.angle-player.shieldwidth), y:player.pos.y+player.shieldradius*sin(player.angle-player.shieldwidth)});
            if (d_leftofshield_enemy<enemy.radius){
                console.log('left boom')
                enemy.destroyed = true;
            }
        }
    }
    

    【讨论】:

    • 不错的解决方案——点赞。为了完整起见,您可能需要考虑屏蔽线宽。 F.ex,在这个稍作修改的演示中看看敌人是如何从盾牌边缘滑过的:jsfiddle.net/m1erickson/72ct5gop
    • 感谢这个提示@markE(我也很伤心你删除了你的答案:()
    • 不客气! :-) 另外,是的……我发布了一个正确的答案,然后提问者更改了问题,因此我的答案不再适用(他们现在想要一个“仅数学”的答案),这有点令人恼火。哦,好吧...
    • @markE 我很抱歉,伙计:(是的,我会在以后的问题中澄清更多。我通过试验东西来学习画布,所以我需要努力学习它。不过你的解决方案效果很好跨度>
    • @markE 再次提出你的答案!我将编辑我的问题。不管数学与否,它真的很有用。
    【解决方案3】:

    Html5 画布有一个非常好的命中测试方法:context.isPointInPath

    您可以使用此方法测试一个圆圈是否与您的盾牌相撞。它适用于盾牌的各个角度。

    在您的情况下,路径将是半径为player.shield.radius-enemy.radius 的内圆弧和半径为player.shield.radius+enemy.radius 的外圆弧。

    mousemove 内,只需绘制(不抚摸)盾牌路径的两条弧线,并使用context.isPointInside( enemy.centerX, enemy.centerY ) 测试每个敌人的中心点。

    为了获得更好的准确度,将盾牌路径扫描扩大到两端敌人的半径。

    这是示例代码和演示:

    function log() {
      console.log.apply(console, arguments);
    }
    
    var canvas = document.getElementById("canvas");
    var ctx = canvas.getContext("2d");
    var cw = canvas.width;
    var ch = canvas.height;
    
    function reOffset() {
      var BB = canvas.getBoundingClientRect();
      offsetX = BB.left;
      offsetY = BB.top;
    }
    var offsetX, offsetY;
    reOffset();
    window.onscroll = function(e) {
      reOffset();
    }
    window.onresize = function(e) {
      reOffset();
    }
    
    var isDown = false;
    var startX, startY;
    
    
    var cx = cw / 2;
    var cy = ch / 2;
    var radius = 100;
    var startAngle = Math.PI/6;
    var enemyRadius = 15;
    var shieldStrokeWidth = 8;
    var endRadians = enemyRadius / (2 * Math.PI * radius) * (Math.PI * 2);
    
    defineShieldHitPath(cx, cy, radius, enemyRadius, startAngle);
    drawShield(cx, cy, radius, startAngle, shieldStrokeWidth);
    
    $("#canvas").mousemove(function(e) {
      handleMouseMove(e);
    });
    
    
    function defineShieldHitPath(cx, cy, r, enemyRadius, startAngle) {
      ctx.beginPath();
      ctx.arc(cx, cy, r - enemyRadius - shieldStrokeWidth / 2, startAngle - endRadians, startAngle + Math.PI + endRadians);
      ctx.arc(cx, cy, r + enemyRadius + shieldStrokeWidth / 2, startAngle + Math.PI + endRadians, startAngle - endRadians, true);
      ctx.closePath();
      ctx.lineWidth = 1;
      ctx.strokeStyle = 'black';
      // stroked just for the demo.
      // you don't have to stroke() if all you're doing is 'isPointInPath'
      ctx.stroke();
    }
    
    function drawShield(cx, cy, r, startAngle, strokeWidth) {
      ctx.beginPath();
      ctx.arc(cx, cy, r, startAngle, startAngle + Math.PI);
      ctx.lineWidth = strokeWidth;
      ctx.strokeStyle = 'blue';
      ctx.stroke();
    }
    
    function drawEnemy(cx, cy, r, fill) {
      ctx.beginPath();
      ctx.arc(cx, cy, r, 0, Math.PI * 2);
      ctx.fillStyle = fill;
      ctx.fill();
    }
    
    
    
    function handleMouseMove(e) {
    
      // tell the browser we're handling this event
      e.preventDefault();
      e.stopPropagation();
    
      mouseX = parseInt(e.clientX - offsetX);
      mouseY = parseInt(e.clientY - offsetY);
    
      ctx.clearRect(0, 0, cw, ch);
    
      drawShield(cx, cy, radius, startAngle, shieldStrokeWidth);
    
      defineShieldHitPath(cx, cy, radius, enemyRadius, startAngle);
      if (ctx.isPointInPath(mouseX, mouseY)) {
        drawEnemy(mouseX, mouseY, enemyRadius, 'red');
      } else {
        drawEnemy(mouseX, mouseY, enemyRadius, 'green');
      }
    
    }
    body{ background-color: ivory; }
    #canvas{border:1px solid red; }
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
    <h4>The shield is the blue arc.<br>The filled circle that moves with the mouse is the enemy.<br>The black stroked arc is the shield perimiter.<br>The enemy turns red when colliding with the blue shield.<br>Test by moving the mouse-enemy in / out of the shield perimiter.</h4>
    <canvas id="canvas" width=400 height=400></canvas>

    为了获得最佳性能,您也可以在数学上执行相同的isPointInPath 命中测试。

    这是一个两部分的测试。测试#1:敌人的中心点是否在盾牌的扫掠角内(使用 Math.atan2)。测试#2:敌人的中心点是否在盾牌路径弧的内半径和外半径之间的距离处。如果两个测试都为真,那么敌人正在与盾牌路径相撞。

    【讨论】:

    • 您能解释一下 context.isPointInPath 是如何在幕后工作的吗?我是一个学习狂,对不起:D。不过,您的解决方案确实很酷。可能是目前最干净的
    • isPointInPath 将测试任何给定的 [x,y] 是否在定义的最后一个路径内(最后定义的路径 == 使用最后一个 .beginPath 创建的路径)。在您的情况下,路径是“半甜甜圈”屏蔽路径。盾牌路径是我演示中的黑色描边路径,而不是蓝色盾牌描边。顺便说一句,您实际上不必显示最后一条路径即可使用 .isPointInPath。做 .beginPath 加上 .arc 而不做 .stroke 就足够了。 :-)
    • 你会说这样的事情很愚蠢吗? flockdraw.com/gallery/view/2095396
    • 那是创建一个 for 循环来沿着屏蔽弧放置不可见(透明)的圆圈并检查碰撞。我不打算这样做,因为这违背了学习数学的目的,但它可以工作并且代码是可维护的,不是吗?
    • @crl,对于真正“奇怪”形状的碰撞测试,还有另一个像素完美的解决方案——但它是 CPU 密集型的。你要做的是使用.getImageData 来制作一个包含怪异敌人和怪异盾牌形状中所有不透明像素的数组。要进行命中测试,您可以使用 deltaX 和 deltaY 偏移值将敌人阵列直接“覆盖”在盾牌阵列上(就像它在画布上直观显示一样)。然后你只需遍历敌人数组并在盾牌数组中找到对应的不透明像素。如果找到相应的屏蔽像素,则表示发生了碰撞。 ;-)
    猜你喜欢
    • 2023-03-21
    • 2021-05-10
    • 2013-04-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多