Javascript Canvas:旋转玩家时与敌人的碰撞不完全有效
Javascript Canvas: Collision against enemies not entirely working when rotating player
注意: *完整的JSFiddle可以在我的post*.[=16=的底部找到]
问题: 我试图消灭所有触及 canvas 中心蓝线的敌人。然而,事实并非如此,我的实现只有 "half working"。当一侧起作用时,另一侧不起作用。我该如何解决这个问题?
我尝试了什么:设置基本绘图函数后,我计算了碰撞对象的 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 下方:
我很乐意收到回复,因为我喜欢在 Javascript 中学习新事物:)
编辑 (03.11.2015): 如果可能,只有纯数学解决方案,但也可以 post 其他解决方案。为了学习新的技术,欢迎每一条信息。
您的代码的问题似乎是您比较角度的方式。不要忘记 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 );
}
}
简化了圆盘和圆弧之间的碰撞检测问题http://jsfiddle.net/crl/2rz296tf/31 (edit: with @markE suggestion http://jsfiddle.net/crl/2rz296tf/32/) (for debugging: 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 canvas 有一个非常好的命中测试方法: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:敌人的中心点是否在盾牌的扫掠角内(使用 Math.atan2)。测试#2:敌人的中心点是否位于屏蔽路径弧的内半径和外半径之间的距离处。如果两个测试都为真,那么敌人正在与盾牌路径相撞。
注意: *完整的JSFiddle可以在我的post*.[=16=的底部找到]
问题: 我试图消灭所有触及 canvas 中心蓝线的敌人。然而,事实并非如此,我的实现只有 "half working"。当一侧起作用时,另一侧不起作用。我该如何解决这个问题?
我尝试了什么:设置基本绘图函数后,我计算了碰撞对象的 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 下方:
我很乐意收到回复,因为我喜欢在 Javascript 中学习新事物:)
编辑 (03.11.2015): 如果可能,只有纯数学解决方案,但也可以 post 其他解决方案。为了学习新的技术,欢迎每一条信息。
您的代码的问题似乎是您比较角度的方式。不要忘记 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 );
}
}
简化了圆盘和圆弧之间的碰撞检测问题http://jsfiddle.net/crl/2rz296tf/31 (edit: with @markE suggestion http://jsfiddle.net/crl/2rz296tf/32/) (for debugging: 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 canvas 有一个非常好的命中测试方法: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:敌人的中心点是否在盾牌的扫掠角内(使用 Math.atan2)。测试#2:敌人的中心点是否位于屏蔽路径弧的内半径和外半径之间的距离处。如果两个测试都为真,那么敌人正在与盾牌路径相撞。