简体   繁体   中英

Optimization of JavaScript collision detection

First of all - please do not remove this post. It's not a duplicate.

I know it covers a problem that was mentioned here multiple times but this time it's not "how to detect collisions" because as you will see later, it's already done. It's more about "how to write" this in as much optimized way as possible, because below detection will be triggered multiple times in a short delay of time.

Here's my fiddle: http://jsfiddle.net/slick/81y70h1f/

I generate random squares and detect if they collide with each other.

HTML is generated using below way. No rocket science:

<?php for ($i=1; $i<=$amount; $i++) { ?>
    <div id="square_<?= $i; ?>" class="square" style="top: <?= rand(0, 800); ?>px; left: <?= rand(0, 800); ?>px;">
        <div>square_<?= $i; ?></div>
    </div>
<?php } ?>

In the fiddle, $amount is set to 16. As you can imagine, the possible amount of unique pair combination is equal to:

在此处输入图片说明

In the fiddle you will see that I perform the uniqueness calculation twice. Second time just for squares that don't collide.

var squares_without_collision = $(squares).not(garbage).get();
pairs_cleaned = get_unique_pairs(squares_without_collision);

The pairs_cleaned is my final array when I will perform the secret operation that is not a part of this problem. This array will be always slightly reduced with unnecessary crap.

When I will increase $amount to 100 I will get 4950 possible combination. When I refresh page it still works fine but I can observe the speed drops down. I even didn't try to set it to 200 because I don't want my browser to crash.

Question - is here still any space of the improvement and optimization? Because now I will reveal that these squares will be Google Map markers and my collision calculation will be triggered on events when:

  1. Tiles are loaded
  2. Map is dragged
  3. Zoom is changed

In the final version, instead of changing background from green to red, I will be showing or hiding markers. I'm worried, that with more markers I will do a turtle script. I would like to keep it extra fast.

Ok had a look and you have way over complicated it. No need to find the pairs, you are querying the DOM way to often. You should only touch the DOM once for each element. The garbage array is redundant use a semaphore. Never use each() in time critical code as it is very slow.

Always keep variables in function scope (inside the main function) because leaving them in global scope will half the access speed.

Arrays are slow and should be avoided at all costs. Reuse array items if you can. Always ask do you really need a new array? is there a way not to use an array?

Dont test where not needed. You have some garbage but you retest those squares.

Avoid function calls inside loops of time critical code. Calling a function is CPU intensive it is way better to have code inline.

Avoid indexing into arrays. Reference the array item once and use the reference.

Avoid JQuery unless you have a clear and justified reason. JQuery is VERY slow and encourages excessive DOM manipulation.

Think that's it. Below is your Fiddle modified that will run a lot faster.

$(function () {
    var squares = [];  // keep arrays in function scope as out side the function
    var pairs_cleaned = []; // they are in global scope and run at half the speed.
    var x1,y1;
    squares = $('.square'); // get the squares
    var len = squares.length;
    console.log('----- Squares away ' + len + '------');
    console.log(squares);


    var width = 80+10;  // you can do this get the size and padding from the first square
    var height = 80+10; // if each square is a different size then you will have to change the code a little
    for(var i = 0; i < len; i += 1){ // itterate them. Avoid using Each in time critical code as it is slow       
        var div = squares[i];
        squares[i] = {  // replace the existing array with a new object containing all we will need. This reuses the array and avoids overheads when growing an array.
            square:div, // save the square. Not sure if you need it?
            garbage:false,     // flage as not garbage
            x: x1 = Number(div.offsetLeft),  // get the squares location
            y: y1 = Number(div.offsetTop),   // and ensure all values are Numbers
            b: y1 + height,  // I have only included the static height and width.
            r: x1 + width,  
        };                       

    }
    var s1,s2;
    for (var i = 0; i < len; i++) { // instead of calling the function to get an array of pairs, just pair them on the fly. this avoid a lot of overhead.
        s1 = squares[i]; // reference the item once outside the loop rather than many times inside the next loop
        for (var j = i + 1; j < len; j++) {
            if(!squares[j].garbage){ // ignore garbage
                s2 = squares[j];
                // do the test inside the loop rather than call a function. This avoids a lot of overhead
                if (s1.x > s2.r || s1.y > s2.b || s1.r < s2.x || s1.b < s2.y){ // do test
                    pairs_cleaned.push([s1,s2]); // if passed save unique pairs
                }else{
                    s2.square.style.backgroundColor = '#ff0040';  // this should not be here is Very very slowwwwwwwww
                    s2.garbage = true;  // garbage
                }
            }
        }
    }

    console.log('----- all pairs without garbage ------');
    console.log(pairs_cleaned);
});

OK. Hope that helps. It's been run and works on chrome. You will need to look at the querying of the elements for position and size but I did not think it important for this example.

There are other optimizations you can do but this should see you to around 1000 squares in realtime if you get rid of the s2.square.style.backgroundColor = '#ff0040'; from the inner loop. It is the slowest part of the whole collision test loop. DOM is death for fast code requirements. Always keep all DOM contact out of critical code sections.

One last thing. To get the best performance always use strict mode, it will give you 20%+ increased performance on most code.

You may consider implementing a simple collision grid for the task. That is, take a conceptual 2D grid spanning over the whole collision field, where each grid cell has a size greater than or equal to the maximum size of the colliding nodes, and bin the center points of each collision node in a data structure representing the grid.

From there, for each given collision node, you only need to check for collisions against other nodes placed in any of the adjacent grid cells to the current collision node's grid cell.

For example:

Say the width and height of your map is 1000px, and the collision nodes are represented in squares of 50x50 pixels. You choose to implement a 100px by 100px grid.

So you would first create a data structure that consists of a 2D array where each cell holds an array that will store collision objects:

var gridSize = { w: 1000, h: 1000 }; // The predefined grid size
var blockSize = { w: 100, h: 100 }; // The predefined block size

var collisionGrid = [];

// Initialize a grid of blockSize blocks to fill the gridSize
var x, y, gridX, gridY;
for (x = 0; x < gridSize.w; x += blockSize.w) {
    gridX = x/blockSize.w;
    collisionGrid[gridX] = [];
    for (y = 0; y < gridSize.h; y += blockSize.h) {
        gridY = x/blockSize.h;
        collisionGrid[gridX][gridY] = [];
    }
}

Then, as you learn about the locations of collision nodes (fetched data from some API, for instance), you would populate the data structure with references to each of the collision nodes according to where it's center point is placed on the grid.

So a square collision node with { x: 726, y:211, w: 50, h:50 } would be placed like this:

var placeNode = function(node) {
    var mid = {
        x: node.x + node.w/2,
        y: node.y + node.h/2
    };
    var cell = {
        x: Math.floor(mid.x/blockSize.w),
        y: Math.floor(mid.y/blockSize.h)
    };
    collisionGrid[cell.x][cell.y].push(node);
};

var node = { x: 726, y:211, w: 50, h:50 } // ...fetched from some API
placeNode(node);

After a few hundred or thousand nodes are placed in the grid (which takes very little overhead for each - just a division or two and pushing a reference to an array), checking for collisions for a given node is greatly reduced, since you only need to check for collisions against nodes in the current node's cell as well as the 8 adjacent cells.

In this example, nodes only within a 300x300px block will be checked against for a given node, but as the collision field size increases and the grid size/collision node sizes decrease, this technique can really shine.

In this blog post, I lightly explained the implementation of this kind of grid for collision for a game I was working on: http://blog.cheesekeg.com/prototype-just-the-basics-v0-2/

One thing to note is that there is a trade off here - when collision nodes move around, their corresponding references to moved from grid cell to grid cell as they travel over the grid. However, in the post I linked above, this fact doesn't cause any noticeable problems with performance when hundreds of collision nodes are moving about the grid.

As Brandon mentioned you are best to create some kind of grid to decrease the number of collisions that you actually detect.

I would suggest using plain javascript rather than jQuery for this if you really want the most performance but here is a jQuery solution I created.

 var gridDimensions = { x: 800, y: 800 }; var boxDimensions = { x: 80, y: 80 }; var hashes = hashSquares($('.square'), gridDimensions, boxDimensions); function hashSquares($squares, dimensions, squaresDimensions) { var squaresHash = []; for (var i = 0; i < Math.floor(dimensions.x / squaresDimensions.x); i++) { var yHashes = Array(Math.floor(dimensions.y / squaresDimensions.y)); for (var j = 0; j < yHashes.length; j++) { yHashes[j] = []; } squaresHash.push(yHashes); } $squares.each(function() { var $this = $(this); squaresHash[Math.floor($this.position().left / squaresDimensions.x)][Math.floor($this.position().top / squaresDimensions.y)].push($this); }); return squaresHash; } function checkSameSquare(x, y, hash) { //if they are both in the same hash square they definitely overlap if (hash[x][y].length > 1) { $.each(hash[x][y], function(i, $el) { //skip the first element if (i !== 0) { $el.addClass('collided'); } }); } } function checkSquareBelow(x, y, hash) { $.each(hash[x][y], function(i, $el) { $.each(hash[x][y + 1], function(i2, $el2) { if (detectCollision($el, $el2)) { $el2.addClass('collided'); } }); }); } function checkSquareRight(x, y, hash) { $.each(hash[x][y], function(i, $el) { $.each(hash[x + 1][y], function(i2, $el2) { if (detectCollision($el, $el2)) { $el2.addClass('collided'); } }); }); } function checkSquareDiagonalRightBelow(x, y, hash) { $.each(hash[x][y], function(i, $el) { $.each(hash[x + 1][y + 1], function(i2, $el2) { if (detectCollision($el, $el2)) { $el2.addClass('collided'); } }); }); } function detectCollision($div1, $div2) { var x1 = $div1.offset().left; var y1 = $div1.offset().top; var h1 = $div1.outerHeight(true); var w1 = $div1.outerWidth(true); var b1 = y1 + h1; var r1 = x1 + w1; var x2 = $div2.offset().left; var y2 = $div2.offset().top; var h2 = $div2.outerHeight(true); var w2 = $div2.outerWidth(true); var b2 = y2 + h2; var r2 = x2 + w2; if (b1 < y2 || y1 > b2 || r1 < x2 || x1 > r2) return false; return true; } for (var i = 0; i < hashes.length; i++) { for (var j = 0; j < hashes[i].length; j++) { checkSameSquare(j, i, hashes); if (j < hashes[i].length - 1) { checkSquareRight(j, i, hashes); } if (i < hashes.length - 1) { checkSquareBelow(j, i, hashes); } if (j < hashes[i].length - 1 && i < hashes.length - 1) { checkSquareDiagonalRightBelow(j, i, hashes); } } } 
 body { margin: 10px; font-family: Arial, sans-serif; } #container { background-color: #cccccc; height: 880px; position: relative; width: 880px; } .square { background-color: lawngreen; height: 80px; position: absolute; width: 80px; z-index: 10; } .square > div { font-size: 12px; padding: 5px; } .square:hover { background-color: forestgreen; z-index: 11; cursor: pointer; } .collided { background-color: red; } 
 <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <div id="container"> <div id="square_1" class="square" style="top: 31px; left: 141px;"> <div>square_1</div> </div> <div id="square_2" class="square" style="top: 56px; left: 726px;"> <div>square_2</div> </div> <div id="square_3" class="square" style="top: 555px; left: 391px;"> <div>square_3</div> </div> <div id="square_4" class="square" style="top: 725px; left: 330px;"> <div>square_4</div> </div> <div id="square_5" class="square" style="top: 398px; left: 642px;"> <div>square_5</div> </div> <div id="square_6" class="square" style="top: 642px; left: 794px;"> <div>square_6</div> </div> <div id="square_7" class="square" style="top: 521px; left: 187px;"> <div>square_7</div> </div> <div id="square_8" class="square" style="top: 621px; left: 455px;"> <div>square_8</div> </div> <div id="square_9" class="square" style="top: 31px; left: 549px;"> <div>square_9</div> </div> <div id="square_10" class="square" style="top: 677px; left: 565px;"> <div>square_10</div> </div> <div id="square_11" class="square" style="top: 367px; left: 120px;"> <div>square_11</div> </div> <div id="square_12" class="square" style="top: 536px; left: 627px;"> <div>square_12</div> </div> <div id="square_13" class="square" style="top: 691px; left: 312px;"> <div>square_13</div> </div> <div id="square_14" class="square" style="top: 93px; left: 757px;"> <div>square_14</div> </div> <div id="square_15" class="square" style="top: 507px; left: 720px;"> <div>square_15</div> </div> <div id="square_16" class="square" style="top: 251px; left: 539px;"> <div>square_16</div> </div> </div> 

http://jsfiddle.net/81y70h1f/13/

Notice you only have to test collisions against the same square the square immediately to the right to the bottom right and below as all other collisions are already handled as you move along the grid.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM