Javascript画布:旋转玩家时与不完全工作的敌人发生碰撞

Javascript Canvas: Collision against enemies not entirely working when rotating player

本文关键字:工作 敌人 碰撞 不完全 画布 旋转 玩家 Javascript      更新时间:2023-09-26

注意:*完整的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)

这只适用于--和+-部分。更改<=to>=的作用与预期相反。我需要找到一种从-1循环到1的方法。

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

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

所以最后我完全弄不明白为什么我的逻辑不起作用,但我渴望学习如何实现这一点:


在JSFiddle下方:

http://jsfiddle.net/mzg635p9/

我很高兴收到回复,因为我喜欢用Javascript学习新东西:)

编辑(2015年11月3日):如果可能,只发布纯数学解决方案,但也可以发布其他解决方案。为了学习新技术,每一条信息都是受欢迎的。

简化了磁盘和圆弧之间的碰撞检测问题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;
        }
    }
}

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

你可以用这个方法来测试一个圆是否与你的盾牌碰撞。它可以在屏蔽的所有角度工作。

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

mousemove内,只需绘制(不划)盾牌路径的2条弧线,并使用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:敌人的中心点是否在盾牌的扫掠角内(使用数学atan2)。测试#2:敌人的圆心是否在盾牌路径弧的内半径和外半径之间。如果这两个测试都是真的,那么敌人正在与盾牌路径碰撞。

代码的问题似乎是比较角度的方式。别忘了2Pi和0是完全一样的。看看这个例子:你有两个角度,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 );
    }
}