Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Collision detection through array

Hi everyone
I started to write a little game with ball and bricks and have some problem with collision detection. Here is my code http://jsbin.com/ibufux/9 . I know that detection works though array, but I can't figure how I can apply it to my code.

Here is what i have tried:

 bricksCollision: function() {
        for (var i = 0; i < $bricks.length; i++) {
                if ($ball.t == $bricks[i].offset().top) {
                    $bricks[i].splice(i, 1);
                }
        }

Every bricks in game are generated by for loop and then goes to $bricks array. Every brick after generating receive top and left position and have position absolute. I have tried to check if $ball.t (it's properties of my ball object which detects ball top position) reach the bricks and than remove bricks.

Thanks for any help. I'm only start to learn JS that's why my code is knotty.

like image 614
kuzyoy Avatar asked Jul 14 '13 11:07

kuzyoy


2 Answers

First of all, let'stalk about some code errors

  • $ball.t should be probably $ball.top
  • you do not need to have $ as a prefix, for your code, it's simply a variable and you are calling $ball instead of ball witch results in assumption errors!

for those assumption errors here is what you are doing wrong:

  • $ball is a dom element, not a jQuery element
  • the same as $bricks
  • $ball is an array

with those concluded from some console.log() let's try to fix the code:

the $ball should be called, as there's only one, by it's array element, as $ball[0] and because you have variables pointing to DOM elements and not jQuery elements, you need to wrap it in Jquery as:

if ( $($ball[0]).top === $($bricks[i]).offset().top ) { ...

a good idea not to get confused is only use $ in jQuery elements, prefixing it in a variable, does not make them a jQuery Element.

And everytime you see that you have an error such as "element x has no method y" always assume that you're calling a method from a DOM element, and not a jQuery element.

like image 54
balexandre Avatar answered Sep 21 '22 01:09

balexandre


Now, that @balexandre has nicely explained some points about your code, lets examine how we can compute the collision.

Imagine 2 Ranges overlapping each other (Range a partly overlaps Range b)

[100     .|..      300]
          [200      ..|.      400]

The part overlapping is from | to | -> 200 to 300, so the size of the overlap is 100

If you look at the numbers, you notice, that the overlap could be seen like,

  • Take the smaller number of the right side -> 300
  • Take the greate number of the left side -> 200
  • Subtract them from each other -> 300 - 200 = 100.

Lets take a look at 2 other situations. (Range b completely in Range a)

[50    ...    150]
    [75...125]

So the values we have are: Math.min (150,125) //125 for the end value and Math.max (50,75) // 75 for the start value, resulting in a value of 125 - 75 = 50 for the overlap

Let's take a look the last example (Range a not in Range b)

[50    ...    150]
                      [200    ...    300]

Using the above formula, yields the result Math.min (150 , 300 ) - Math.max (50,200) // -50 which absolutes' value is the gap between the 2 Ranges, 50

Now we can add a last condition, as you want to compute the collision, only values > 0 are of interest for us. Given this we can put it into one condition.

Math.min ((Brick["Right"],Ball["Right"]) - Math.max (Brick["Left"], Ball["Left"]) > 0)

Which will yield true if the elements' overlap and false if they don't.

Applying this to your code, we could compute the collision the following way

bricksCollision: function () {

    for (var i = 0; i < $bricks.length; i++) {
        var $brick = $($bricks[i]);
        var offset = $brick.offset();
        var brickBounds = [offset.left - field.l]; //brick left pos
        brickBounds[1] = brickBounds[0] + 40 //bricks right pos -> left pos + .bricks.width;
        var ballBounds = [ball.l]; //balls left pos
        ballBounds[1] = ballBounds[0] + 20 //balls right pos -> left pos + #ball.width;
        if (ball.t <= (offset.top + 20) && (Math.min(brickBounds[1], ballBounds[1]) - Math.max(brickBounds[0], ballBounds[0])) > 0) {

            $bricks[i].style.opacity = 0; //Make the brick opaque so it is not visible anymore
            $bricks.splice(i, 1) //remove the brick from the array -> splice on the array, not the element

            return true;
        }
    }
}

With this we could return true to the move function, when the Ball collides with a Brick.

But hey, we want it to Bounce off in the right direction, so we will face another problem. So rather then returning a Boolean value whether the Brick collides or not, we could return a new direction in which the Ball will should move.

To be able to easily change only the x or the y part of the direction, we should use something like a vector.

To do so, we could use 2 Bits of an Integer, where the bit b0 stays for the x direction and the bit b1 for the y direction. Such that.

Dec        Bin       Direction           

 0    ->    00    -> Down Left
             ^    -> Left
            ^     -> Down
 1    ->    01    -> Down Right
             ^    -> Right
            ^     -> Down
 2    ->    10    -> Up Left
             ^    -> Left
            ^     -> Up
 3    ->    11    -> Up Right
             ^    -> Right
            ^     -> Up

But to be able to change only a part of the direction, we need to pass the old direction to the collision function, and use bitwise & and | respectively to turn them off or on

Also we have to compute from which side the ball collides. Fortunatly we have overlap calculation from before, which already uses all values we need, to compute the direction of collision.

If it comes frome the

  • right
    • Brick ["Right"] - Ball["Left"] has to be the same value as the overlap.
  • left
    • Ball ["Right"] - Brick["Left"] has to be the same value as the overlap.

If none of them are true, it has to either come from the

  • bottom
    • if Ball["Top"] is more than ( Brick["Top"] plus half of the Brick["height"] )

or else from the top.

To reduce the range where the condition, for the collision from the side, evaluates to true we can add another condition that the overlap has to be less than e.g ... && overlap < 2

So if it collides with the edge it doesn't always bounce of to the side.


So enough the talking, in code this could look like something like this.

bricksCollision: function (direction) {
    var newDirection = direction

    var ballBounds = [ball.l]; //balls left pos
    ballBounds[1] = ballBounds[0] + 20 //balls right pos -> left pos + #ball.width;


    for (var i = 0; i < $bricks.length; i++) {
        var $brick = $($bricks[i]);
        var offset = $brick.offset();

        var brickBounds = [offset.left - field.l]; //brick left pos
        brickBounds[1] = brickBounds[0] + 40 //bricks right pos -> left pos + .bricks.width;


        var overlap = Math.min(brickBounds[1], ballBounds[1]) - Math.max(brickBounds[0], ballBounds[0]);

        if (ball.t <= ((offset.top - field.t) + 20) && overlap > 0) {

            $bricks[i].style.opacity = 0; //Make the brick opaque so it is not visible anymore
            $bricks.splice(i, 1) //remove the brick from the array -> splice on the array, not the element


            if (ballBounds[1] - brickBounds[0] == overlap && overlap < 2) { //ball comes from the left side
                newDirection &= ~(1); //Turn the right bit off -> set x direction to left
            } else if (brickBounds[1] - ballBounds[0] == overlap && overlap < 2) { //ball comes from the right side
                newDirection |= 1; // Turn the right bit on -> set x direction to right;
            } else {
                if (ball.t > (offset.top + (20 / 2))) //Ball comes from downwards
                    newDirection &= ~(2) // Turn the left bit off -> set y direction to down;
                else //Ball comes from upwards
                    newDirection |= 2; // Turn the left bit on -> set y direction to up;
            }
            //console.log("Coming from: %s  Going to: %s", field.directionsLkp[direction], field.directionsLkp[newDirection], direction)
            return newDirection;
        }
    }
    return direction;
}

To get that to work, we should also change the moveXX functions, to use the new direction, returned.

But if we are going to get the new direction from the collision function anyway, we could move the complete collision detection to the function, to simplify our move functions. But before that, we should have a look at the move functions and, add a lookup object to field which holds the numbers for the direction, to maintain readability.

var field = {
  directions: {
    uR : 3, // 11
    dR : 1, // 01
    dL : 0, // 00
    uL : 2  // 10

  },
  directionsLkp: [
    "dL","dR","uL","uR"
  ],
...
}

Now the move functions could then look like this,

    ballCondact: function () {
        var moves = [moveDl,moveDr,moveUl,moveUr]
        var timeout = 5;

        function moveUr() {
            var timer = setInterval(function () {
                $ball.css({
                    top: (ball.t--) + "px",
                    left: (ball.l++) + "px"
                })
                var newDirection = game.bricksCollision(field.directions.uR) //get the new direction from the collision function
                if (newDirection !== field.directions.uR) {
                    clearInterval(timer);
                    moves[newDirection](); //move in the new direction
                }
            }, timeout);
        }
...
}

Like this, the move function simply changes the direction if the collision function returns a direction which differs from the current one.

Now we can start moving the wall collisions to the collision function, to do this we could add another check at the beginning.

    bricksCollision: function (direction) {

      ...

      if (ball.t <= field.t)
        newDirection &= ~(2);  //Ball is at top, move down
      else if (ball.l <= 0)   //Ball is at the left, move right
        newDirection |= 1;
      else if (ball.t >= field.b - ball.height) //Ball is at the bottom, move up
        newDirection |= 2;
      else if (ball.l > field.width - ball.width) //Ball is at the right, move left
        newDirection &= ~(1);

      if (direction !== newDirection)
        return newDirection

      ...
}

Note, i left out the collision check for the platform, as the idea should be clear =)

Here is a Fiddle

like image 31
Moritz Roessler Avatar answered Sep 22 '22 01:09

Moritz Roessler