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

3

14

Note: *A complete JSFiddle can be found at the bottom of my post*.

Problem: I am trying to destroy all enemies that touch the blue line in the center of the canvas. However, this is not the case and my implementation is only "half working". When one side works, the other doesnt. How can I fix this problem?


What I tried: Once I set up the basic drawing functions, I calculated the difference between x and y of the colliding objects. Used the pythagorean distance to calculate the distance between the two points. Finally checked if the distance is less or equal to the combined radius of the two objects. Using arctangent I calculated the rotation of the movement of the objects.


Alternative Solution I Thought Of: Using a loop to creating various invisible circles or dots along the blue line that act as a collision receptor. Problem is: it eats more resources and it wouldnt be elegant at all.


The Javascript function of most interest to you would be:

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

The angle is the angle of the rotating blue line (which is a semi circle with a stoke).

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

This works only for the -- and +- sections. Changing <= to >= does the opposite as expected. I would need to find a way to loop from -1 to 1.

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

works for --, -+, ++ but not for +- (bottom right).

So in the end im utterly confused why my logic doesnt work but I am eager to learn how this can be achieved:


Below the JSFiddle:

http://jsfiddle.net/mzg635p9/

I would be happy for a response as I love learning new things in Javascript : )

Edit (03.11.2015): If possible only purely mathematical solutions but feel free to also post other solutions. For the sake of learning new techniques, every piece of information is welcome.

Kylstra answered 31/10, 2015 at 18:9 Comment(0)
M
6

made something simplified on the problem of collision detection between a disk and an arc http://jsfiddle.net/crl/2rz296tf/31 (edit: with @markE suggestion http://jsfiddle.net/crl/2rz296tf/32/) (for debugging: http://jsfiddle.net/crl/2rz296tf/27/)

some util functions for comparing angles:

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

collision detection:

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;
        }
    }
}
Mouthwatering answered 31/10, 2015 at 20:51 Comment(5)
Nice solution -- upvote. For completeness, you might want to account for the shield linewidth. F.ex, see how the enemy slips by the edge of the shield in this slightly modified demo: jsfiddle.net/m1erickson/72ct5gopHarmsworth
thanks for this tip @Harmsworth (also I was sad you removed your answer :( )Mouthwatering
You're welcome! :-) Also, yes...it was a bit aggravating that I posted a correct answer and then the questioner changed the question so that my answer was no longer applicable (they now want a "math only" answer). Oh well...Harmsworth
@Harmsworth im so sorry man : ( Ya, I will clarify more in future questions. Im learning canvas by experimenting with stuff so I need to learn it the hard way. Your solution worked perfectly thoughKylstra
@Harmsworth put up your answer again man!. I will edit my question. Regardless of math or not it was really useful.Kylstra
S
2

The problem with your code seems to be the way you compare angles. Don't forget that 2Pi is exactly the same thing as 0. Take a look at this example: You have 2 angles, a and b.

a = 0.1 * Pi

b = 1.9 * Pi

a is slightly above the x axis, while b is slightly bellow it.

It seems that a is ahead of b when looking at both, so you'd expect a > b to be true. But wait! Look at the numbers, b is way bigger than a! When you want to check if an angle is between an interval, you have to make sure that your interval is continuous, which in this case is false for angle = 0.

Here's my solution. I tested it as best as I could, but you can never know if you missed something.

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

To use it, change your shield function to:

shield: 
    function (player, spawn) {
        return (this.distance(player, spawn) <= player.radius + spawn.radius) && 
            is_angle_between(this.tangent(player, spawn), angle , angle - Math.PI );
    }
}
Sen answered 31/10, 2015 at 19:26 Comment(15)
But why is my old version with only one circle working? jsfiddle.net/2rz296tf/20 where im looping from -1 to 1Kylstra
@Kylstra The other version has the "jump" at Pi instead of 0, and the example you've set there never triggers the case where the answer is wrong. Try multiple example and you'll see it fails for some.Sen
Putting the trigger aside. I mean the collision on the shield works at every angle, the same formula on my new version doesnt work at all thoughKylstra
I could need some picture or some visual aid. I think im utterly failing to understand the concept eventhough I understand how a circle works.Kylstra
@Kylstra Don't worry, I'm writing a function that solves the problem. In fact, it's written already, I'm testing it to make sure it always works.Sen
Thanks, crl has posted something recently too. What do you say? Im worried that eventhough it works as he is testing for the extremities using cos and sin it might be somewhat too long for collision detection. I like the logic behind it thoughKylstra
@Kylstra Updated with code. I prefer the version you had, it's cleaner. I don't like those multiple chained if else clauses crl used. But it's up to you ; )Sen
I got a problem with it too. It is not very mantainable. I understand what he is doing but I cant follow the code / read it very wellKylstra
Got difficulty understanding how this works. Man, I feel really small right now. : (Kylstra
Let us continue this discussion in chat.Sen
Check out my bounty ; )Kylstra
@Kylstra I though I had make it clear : ( See my edit ; ) Still I think you should spend some time trying to understand how it works. As a plus you'd learn some maths xDSen
Do you know the site math for fun? Its really great. It helped me understand trigonometry basics.Kylstra
I really hate libraries like jquery, bootstrap, or prebuilt functions that do the math for you so im always glad when I can learn the math behind things like this. As things stand now Im only able to think "simple" in math but I hope that changes. I wonder if it is an experience thing or if it just comes naturally.Kylstra
Hey slysherz. There is an error in the code. 1) Math.PI should be the total max of the rotation which is 360 degrees and instead of angle + Math.Pi its angle - Math.PI and then it worksKylstra
H
2

Html5 canvas has a very nice hit-testing method: context.isPointInPath.

You can use this method to test if a circle is collides with your shield. It will work at all angles of the shield.

In your case, the path would be an inner arc with radius of player.shield.radius-enemy.radius and an outer arc with radius of player.shield.radius+enemy.radius.

Inside mousemove, just draw (without stroking) the 2 arcs of the shield-path and test each enemy's centerpoint using context.isPointInside( enemy.centerX, enemy.centerY ).

For better accuracy, extend the shield-path sweep by the radius of the enemy on both ends.

Here's example code and a Demo:

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>

For optimum performance, you can alternatively do the same isPointInPath hit test mathematically.

It's a 2 part test. Test#1: Is the enemy centerpoint is within the sweep angle of the shield (using Math.atan2). Test#2: Is the enemy centerpoint at a distance between the inner and outer radii of the shield-path arcs. If both tests are true then the enemy is colliding with the shield-path.

Harmsworth answered 31/10, 2015 at 22:22 Comment(13)
would you explain how context.isPointInPath under the hood? Im a learning freak, sorry :D. Your solution is really cool though. Probably the cleanest of all right nowKylstra
isPointInPath will test whether any given [x,y] is inside the last path that was defined (last defined path == the path that was created with the last .beginPath). In your case the path is a "semi-donut" shield-path. The shield-path is the black stroked path in my demo, not the blue shield stroke. BTW, you don't actually have to display the last path in order to use .isPointInPath. It's enough to do .beginPath plus the .arc's without doing .stroke. :-)Harmsworth
Would you say something like this is stupid? flockdraw.com/gallery/view/2095396Kylstra
That is creating a for loop to place invisible (transparent) circles along the shields arc and check for a collision. Im not going to do it as it is against the purpose of learning the math but it works and the code is mantainable isnt it?Kylstra
The image kind of reminds me of American football where the defense lines up to protect the quaterback. :-) No, it's not stupid -- just inefficient. That solution would give a rough estimate of the collision but would require more cpu processing than the other answers provided here. With that solution, the more & smaller circles you put on the shield line the more accurate the solution becomes (also the more resources it uses to get the more accurate solution). Cheers!Harmsworth
oh and mark where do I learn how to do those collisions? What book did you read?Kylstra
Most collision algorithms are extensions of my old math classes in school -- just the basics of geometry and trigonometry. I can recommend studying Kevin Lindsey's collection of intersection algorithms: kevlindev.com/gui/math/intersectionHarmsworth
In practice you could want to have some weird shield shape, so isPointInPath is so much easier, and also weird enemies shape, so the defineShieldHitPath done by MarkE would be too hard, so you could just assimilate the enemies to one point, its 'head' for exampleMouthwatering
@Harmsworth im having some trouble implementing the isPointInPath function solution you suggested. The detection is only working randomly for some reason when applied to the game : ( Could I show you the fiddle? I could just need a hintKylstra
@Asperger. Sure, send the fiddle (or code) to MarksStackoverflowAddress on gmail. I'm going to be quite busy this week so I can't promise when I'll get a chance to examine a whole game project.Harmsworth
@crl, For collision testing genuinely "weird" shapes, there is another pixel perfect solution -- but it's cpu intensive. What you do is use .getImageData to make an array of all the non-transparent pixels in your weird-enemy & weird-shield shapes. To hit-test, you use deltaX & deltaY offset values to "overlay" the enemy array directly over the shield array (just as it visually appears on the canvas). Then you just iterate through the enemy array and find a corresponding non-transparent pixel in the shield array. If a corresponding shield pixel is found you have a collision. ;-)Harmsworth
It works now : ) Those two suggestions are / were really new to me and the mozilla documentation doesnt mention it in their 2d collision documentation.Kylstra
It actually seems as if im forgetting a small detail here. isPointInPath() does the entire magic. It checks if the coordinates supplied are within the last open path which is the shield, yet it somehow fails. What am I missing?Kylstra

© 2022 - 2024 — McMap. All rights reserved.