通过数组实现碰撞检测

6

大家好
我开始写一个关于球和砖块的小游戏,但在碰撞检测方面遇到了一些问题。这是我的代码http://jsbin.com/ibufux/9 。我知道检测是通过数组实现的,但我不知道该如何将其应用到我的代码中。

下面是我尝试过的方法:

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

游戏中的每一个砖块都是通过for循环生成,并进入$bricks数组。每个砖块在生成后都会接收到顶部和左侧位置,并拥有绝对位置。我尝试检查$ball.t(我的球体对象的属性,它检测球的顶部位置)是否到达砖块,然后删除砖块。

感谢任何帮助。我刚开始学习JS,所以我的代码很费解。


7
请注意,JavaScript不像某些其他语言那样强制使用$varName命名约定。您可以将您的砖块数组称为bricks而不是$bricks - Benjamin Gruenbaum
如果我理解正确的话,您只比较了顶部值和左侧值。这意味着两个对象必须具有完全相同的值。我想当您进行比较时,至少需要考虑宽度,可能还要考虑高度。 - bestprogrammerintheworld
问题在于我无法检测到$bricks数组中元素的顶部位置。我尝试使用jQuery函数**$bricks[i].offset().top,但控制台显示Uncaught TypeError: Object 0 has no method 'offset'**。 - kuzyoy
$bricks[i]是DOM中的元素吗? - bestprogrammerintheworld
$bricks是一个包含元素的数组 - 每个生成的砖块都会被添加到DOM中**$field.append(b),并且被添加到数组中$bricks.push(b)**。 - kuzyoy
2个回答

5

首先,让我们谈谈一些代码错误

  • $ball.t 可能应该是 $ball.top
  • 您无需将 $ 作为前缀,对于您的代码,它只是一个变量,而您正在调用 $ball 而不是 ball,这会导致假设错误

对于那些假设错误,以下是您犯的错误:

  • $ball 是一个 DOM 元素,而不是 jQuery 元素
  • $bricks 同样也是
  • $ball 是一个数组

通过一些console.log()得出结论后,我们尝试修复代码:

由于只有一个球,应该使用其数组元素来调用$ball[0],同时由于您有指向 DOM 元素而不是 jQuery 元素的变量,因此需要将其包装在 jQuery 中:

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

为了避免混淆,一个好的想法是仅在jQuery元素中使用$,在变量中加前缀并不会使它们成为jQuery元素。

并且每次当你看到像“元素x没有方法y”的错误时,总是假设你正在从DOM元素中调用方法,而不是从jQuery元素中调用。


谢谢@balexandre。看来我理解和($bricks[i]).offset().top)完美地工作了。 - kuzyoy

2
现在,@balexandre已经很好地解释了一些关于您的代码的要点,让我们来看看如何计算碰撞。

想象一下两个重叠的范围(范围a部分重叠范围b)

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

重叠部分是从|到|,即200到300,因此重叠的大小为100。
如果你看这些数字,你会注意到可以这样看待重叠部分,
- 取右侧较小的数字-> 300 - 取左侧较大的数字-> 200 - 将它们相减-> 300-200 = 100。
让我们看看另外两种情况。(范围b完全在范围a内)
[50    ...    150]
    [75...125]

所以我们拥有的值是:Math.min (150,125) //125 作为结束值,Math.max (50,75) // 75 作为起始值,这导致重叠值为125 - 75 = 50

让我们看一下最后一个例子(范围a不在范围b中)。
[50    ...    150]
                      [200    ...    300]

使用上述公式,得到结果Math.min(150, 300) - Math.max(50, 200) // -50,其绝对值为两个范围之间的差距,即50。
现在我们可以添加一个最后的条件,因为你想要计算碰撞,所以只有> 0的值对我们有用。鉴于此,我们可以将其放入一个条件中。 Math.min((Brick["Right"], Ball["Right"]) - Math.max(Brick["Left"], Ball["Left"]) > 0) 如果元素重叠,则将产生true,否则将产生false
将此应用于您的代码,我们可以按以下方式计算碰撞。
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;
        }
    }
}

通过这个方法,当球碰到砖块时,我们可以在移动函数中返回true。

但是,我们希望它以正确的方向弹开,所以我们将面临另一个问题。因此,我们可以返回一个新的方向,让球应该移动,而不是返回一个布尔值,表示砖块是否发生碰撞。

为了能够轻松地仅更改方向的x或y部分,我们应该使用类似于向量的东西。

为此,我们可以使用整数的2位,其中位 b0 表示x方向,位 b1 表示y方向。如下所示。

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

但是,如果只想改变方向的一部分,我们需要将旧方向传递给碰撞函数,并使用位运算符 & 和 | 分别关闭或打开它们。
另外,我们还需要计算球碰撞的哪一侧。幸运的是,我们之前已经进行了重叠计算,这已经使用了我们需要的所有值来计算碰撞方向。
如果来自右侧:
- 砖块 ["Right"] - 球 ["Left"] 的值必须与重叠相同。
如果来自左侧:
- 球 ["Right"] - 砖块 ["Left"] 的值必须与重叠相同。
如果以上都不是,则必须来自底部:
- 如果 Ball ["Top"] 大于(Brick ["Top"] 加上 Brick ["height"] 的一半),则为底部。
否则就是从顶部来的。
为了减少侧面碰撞条件的真实性范围,我们可以添加另一个条件,即重叠必须小于例如... && overlap < 2
因此,如果它与边缘发生碰撞,它不会总是反弹到侧面。
足够说了,代码可能看起来像这样。
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;
}

要使其正常工作,我们还应更改moveXX函数,以使用返回的新方向。
但是,如果我们无论如何都要从碰撞函数中获取新方向,我们可以将完整的碰撞检测移动到该函数中,以简化我们的移动函数。但在此之前,我们应该查看移动函数,并添加一个查找对象到field中,该对象保存方向的数字,以保持可读性。
var field = {
  directions: {
    uR : 3, // 11
    dR : 1, // 01
    dL : 0, // 00
    uL : 2  // 10

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

现在,移动功能可以看起来像这样,
    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);
        }
...
}

就像这样,如果碰撞函数返回的方向与当前方向不同,移动函数就会简单地改变方向。
现在我们可以将墙壁碰撞移动到碰撞函数中,为此我们可以在开头添加另一个检查。
    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

      ...
}

请注意,我省略了平台的碰撞检查,因为这个想法应该很清楚 =)
这里是一个演示

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接