├── .babelrc ├── .gitignore ├── LICENSE.md ├── Nearby.js ├── README.md ├── package-lock.json ├── package.json ├── performance-test.html ├── rollup.config.js └── test └── test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015" ] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Oğuz Eroğlu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Nearby.js: -------------------------------------------------------------------------------- 1 | var Nearby = function (width, height, depth, binSize){ 2 | this.limitBox = this.createBox(0, 0, 0, width, height, depth); 3 | this.binSize = binSize; 4 | 5 | this.bin = new Map(); 6 | 7 | this.reusableResultMap = new Map(); 8 | } 9 | 10 | Nearby.prototype.createBox = function(x, y, z, width, height, depth){ 11 | var bb = {}; 12 | 13 | bb.containsBox = function(box){ 14 | return this.minX <= box.minX && box.maxX <= this.maxX && 15 | this.minY <= box.minY && box.maxY <= this.maxY && 16 | this.minZ <= box.minZ && box.maxZ <= this.maxZ; 17 | }; 18 | 19 | bb.setFromCenterAndSize = function(x, y, z, width, height, depth){ 20 | var halfWidth = width / 2; 21 | var halfHeight = height / 2; 22 | var halfDepth = depth / 2; 23 | 24 | this.minX = x - halfWidth; 25 | this.maxX = x + halfWidth; 26 | this.minY = y - halfHeight; 27 | this.maxY = y + halfHeight; 28 | this.minZ = z - halfDepth; 29 | this.maxZ = z + halfDepth; 30 | }; 31 | 32 | bb.setFromCenterAndSize(x, y, z, width, height, depth); 33 | 34 | return bb; 35 | } 36 | 37 | Nearby.prototype.createObject = function(id, box){ 38 | var self = this; 39 | 40 | var obj = { 41 | id: id, 42 | box: box, 43 | binInfo: new Map(), 44 | }; 45 | 46 | return obj; 47 | } 48 | 49 | Nearby.prototype.insert = function(obj){ 50 | if (!this.limitBox.containsBox(obj.box)){ 51 | return; 52 | } 53 | 54 | var BIN_SIZE = this.binSize; 55 | 56 | var box = obj.box; 57 | var minX = box.minX; 58 | var minY = box.minY; 59 | var minZ = box.minZ; 60 | var maxX = box.maxX; 61 | var maxY = box.maxY; 62 | var maxZ = box.maxZ; 63 | 64 | var round = Math.round(minX / BIN_SIZE) * BIN_SIZE; 65 | var minXLower, minXUpper; 66 | if (round <= minX){ 67 | minXLower = round; 68 | minXUpper = minXLower + BIN_SIZE; 69 | }else{ 70 | minXUpper = round; 71 | minXLower = round - BIN_SIZE; 72 | } 73 | 74 | round = Math.round(maxX / BIN_SIZE) * BIN_SIZE; 75 | var maxXLower, maxXUpper; 76 | if (round < maxX){ 77 | maxXLower = round; 78 | maxXUpper = maxXLower + BIN_SIZE; 79 | }else{ 80 | maxXUpper = round; 81 | maxXLower = round - BIN_SIZE; 82 | } 83 | if (minXLower > maxXLower){ 84 | maxXLower = minXLower; 85 | } 86 | 87 | round = Math.round(minY/BIN_SIZE) * BIN_SIZE; 88 | var minYLower, minYUpper; 89 | if (round <= minY){ 90 | minYLower = round; 91 | minYUpper = minYLower + BIN_SIZE; 92 | }else{ 93 | minYUpper = round; 94 | minYLower = round - BIN_SIZE; 95 | } 96 | 97 | round = Math.round(maxY/BIN_SIZE) * BIN_SIZE; 98 | var maxYLower, maxYUpper; 99 | if (round < maxY){ 100 | maxYLower = round; 101 | maxYUpper = maxYLower + BIN_SIZE; 102 | }else{ 103 | maxYUpper = round; 104 | maxYLower = round - BIN_SIZE; 105 | } 106 | if (minYLower > maxYLower){ 107 | maxYLower = minYLower; 108 | } 109 | 110 | round = Math.round(minZ/BIN_SIZE) * BIN_SIZE; 111 | var minZLower, minZUpper; 112 | if (round <= minZ){ 113 | minZLower = round; 114 | minZUpper = minZLower + BIN_SIZE; 115 | }else{ 116 | minZUpper = round; 117 | minZLower = round - BIN_SIZE; 118 | } 119 | 120 | round = Math.round(maxZ/BIN_SIZE) * BIN_SIZE; 121 | var maxZLower, maxZUpper; 122 | if (round < maxZ){ 123 | maxZLower = round; 124 | maxZUpper = maxZLower + BIN_SIZE; 125 | }else{ 126 | maxZUpper = round; 127 | maxZLower = round - BIN_SIZE; 128 | } 129 | if (minZLower > maxZLower){ 130 | maxZLower = minZLower; 131 | } 132 | 133 | for (var x = minXLower; x<= maxXLower; x+= BIN_SIZE){ 134 | if (!this.bin.has(x)){ 135 | this.bin.set(x, new Map()); 136 | } 137 | if (!obj.binInfo.has(x)){ 138 | obj.binInfo.set(x, new Map()); 139 | } 140 | for (var y = minYLower; y<= maxYLower; y+= BIN_SIZE){ 141 | if (!this.bin.get(x).has(y)){ 142 | this.bin.get(x).set(y, new Map()); 143 | } 144 | if (!obj.binInfo.get(x).has(y)){ 145 | obj.binInfo.get(x).set(y, new Map()); 146 | } 147 | for (var z = minZLower; z <= maxZLower; z+= BIN_SIZE){ 148 | if (!this.bin.get(x).get(y).has(z)){ 149 | this.bin.get(x).get(y).set(z, new Map()); 150 | } 151 | this.bin.get(x).get(y).get(z).set(obj, true); 152 | 153 | obj.binInfo.get(x).get(y).set(z, true); 154 | } 155 | } 156 | } 157 | } 158 | 159 | Nearby.prototype.query = function(x, y, z){ 160 | var BIN_SIZE = this.binSize; 161 | 162 | var rX = Math.round(x / BIN_SIZE) * BIN_SIZE; 163 | var rY = Math.round(y / BIN_SIZE) * BIN_SIZE; 164 | var rZ = Math.round(z / BIN_SIZE) * BIN_SIZE; 165 | 166 | var minX, maxX; 167 | if (rX <= x){ 168 | minX = rX; 169 | maxX = rX + BIN_SIZE; 170 | }else{ 171 | maxX = rX; 172 | minX = rX - BIN_SIZE; 173 | } 174 | var minY, maxY; 175 | if (rY <= y){ 176 | minY = rY; 177 | maxY = rY + BIN_SIZE; 178 | }else{ 179 | maxY = rY; 180 | minY = rY - BIN_SIZE; 181 | } 182 | var minZ, maxZ; 183 | if (rZ <= z){ 184 | minZ = rZ; 185 | maxZ = rZ + BIN_SIZE; 186 | }else{ 187 | maxZ = rZ; 188 | minZ = rZ - BIN_SIZE; 189 | } 190 | 191 | var result = this.reusableResultMap; 192 | result.clear(); 193 | 194 | for (var xDiff = -BIN_SIZE; xDiff <= BIN_SIZE; xDiff += BIN_SIZE){ 195 | var keyX = (minX + xDiff); 196 | for (var yDiff = -BIN_SIZE; yDiff <= BIN_SIZE; yDiff += BIN_SIZE){ 197 | var keyY = (minY + yDiff); 198 | for (var zDiff = -BIN_SIZE; zDiff <= BIN_SIZE; zDiff += BIN_SIZE){ 199 | var keyZ = (minZ + zDiff); 200 | if (this.bin.has(keyX) && this.bin.get(keyX).has(keyY)){ 201 | var res = this.bin.get(keyX).get(keyY).get(keyZ); 202 | if (!res) continue; 203 | 204 | for (var obj of res.keys()){ 205 | result.set(obj, true); 206 | } 207 | } 208 | } 209 | } 210 | } 211 | 212 | return result; 213 | } 214 | 215 | Nearby.prototype.delete = function(obj){ 216 | var binInfo = obj.binInfo; 217 | 218 | for (var x of binInfo.keys()){ 219 | for (var y of binInfo.get(x).keys()){ 220 | for (var z of binInfo.get(x).get(y).keys()){ 221 | if (this.bin.has(x) && this.bin.get(x).has(y) && this.bin.get(x).get(y).has(z)){ 222 | this.bin.get(x).get(y).get(z).delete(obj); 223 | if (this.bin.get(x).get(y).get(z).size == 0){ 224 | this.bin.get(x).get(y).delete(z); 225 | } 226 | if (this.bin.get(x).get(y).size == 0){ 227 | this.bin.get(x).delete(y); 228 | } 229 | if (this.bin.get(x).size == 0){ 230 | this.bin.delete(x); 231 | } 232 | } 233 | } 234 | } 235 | } 236 | 237 | for (var x of binInfo.keys()){ 238 | binInfo.delete(x); 239 | } 240 | } 241 | 242 | Nearby.prototype.update = function(obj, x, y, z, width, height, depth){ 243 | obj.box.setFromCenterAndSize(x, y, z, width, height, depth); 244 | 245 | this.delete(obj); 246 | this.insert(obj); 247 | } 248 | 249 | export default Nearby; 250 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nearby 2 | Nearby is an easy-to-use lightweight JS library for your 2D/3D games that helps you get the nearby objects in a constant time `O(1)` instead of simple brute force algorithms that run on `O(n)`. 3 | 4 | Ported from the [WorldBinHandler class of ROYGBIV engine](https://github.com/oguzeroglu/ROYGBIV/blob/master/js/handler/WorldBinHandler.js). 5 | 6 | # The Problem 7 | In most of the games (2D/3D), collision checks are essential both for physics or AI (steering behaviors such as obstacle avoidance, collision avoidance). Many naive implementations depend on comparing a certain bounding box with the bounding box of every other object of the scene: 8 | 9 | forEach object of the scene 10 | checkIfCollided(box, object.box) 11 | 12 | This naive approach runs on `O(n)` and this is a problem especially for rather crowded scenes with many dynamic/static objects. 13 | 14 | In order to overcome this problem, game engines use [Octree](https://en.wikipedia.org/wiki/Octree) data structure. However Octree is not so easy to implement for dynamic objects. A tree needs to be reconstructed in that case, which triggers GC activity and slows down the main thread in Javascript. 15 | # The Solution 16 | 17 | While working on [ROYGBIV engine](https://github.com/oguzeroglu/ROYGBIV) particle collisions, I experimented with couple of solutions and ended up implementing a binning algorithm that splits the world into bins, insert the object into different bins based on their bounding boxes. This helps us finding nearby objects of a given point in constant time `O(1)`. This library is a standalone version of the same algorithm. 18 | # Performance Comparison 19 | Run the [performance-test](https://github.com/oguzeroglu/Nearby/blob/master/performance-test.html) in your browser. In order to test the efficiency of Nearby, a defined amount of objects are created and put into random positions. Then the closest object to the point `(0, 0, 0)` is searched first with Nearby algorithm and then with the naive approach (brute forcing). 20 | 21 | Here are the results: 22 | | Number of objects | Nearby | Naive approach | 23 | |--|--|--| 24 | | 1000000 | 1.33 ms | 51 ms | 25 | | 100000 | 0.2 ms | 11 ms | 26 | | 10000 | 0.18 ms | 2 ms | 27 | 28 | As you can see Nearby offers a much faster solution. 29 | 30 | # Usage 31 | 32 | Get the latest release. Include the Nearby.min.js in your HTML 33 | ```HTML 34 |
35 | 36 | 37 | ``` 38 | 39 | For NodeJS: 40 | 41 | ``` 42 | npm install --save nearby-js 43 | ``` 44 | 45 | ```Javascript 46 | var Nearby = require("nearby-js"); 47 | ``` 48 | 49 | Then with Javascript: 50 | ```javascript 51 | // INITIALIZE 52 | var sceneWidth = 1000, sceneHeight = 1000, sceneDepth = 1000; 53 | var binSize = 50; 54 | // Creates a world centered in (0, 0, 0) of size (1000x1000x1000) 55 | // The world is splitted into cubes of (50x50x50). 56 | var nearby = new Nearby(sceneWidth, sceneHeight, sceneDepth, binSize); 57 | 58 | // CREATE AN OBJECT REPRESENTATION 59 | var objectPosX = 0, objectPosY = 100, objectPosZ = -100; 60 | var objectWidth = 10, objectHeight = 50, objectDepth = 100; 61 | 62 | // Creates a new bounding box of (10x50x100) size, located at 63 | // the position (x: 0, y: 100, z: -100) 64 | var box = nearby.createBox( 65 | objectPosX, objectPosY, objectPosZ, 66 | objectWidth, objectHeight, objectDepth 67 | ); 68 | 69 | var objectID = "my_collidable_object"; 70 | var object = nearby.createObject(objectID, box); 71 | 72 | // INSERT THE OBJECT INTO THE WORLD 73 | nearby.insert(object); 74 | ``` 75 | 76 | To find Nearby objects: 77 | ```javascript 78 | var searchX = 0, searchY = 0, searchZ = 0; 79 | 80 | // Find the nearby objects from (searchX, searchY, searchZ) 81 | // 82 | // Nearby returns the object within range (3 * binSize) / 2 83 | // So for this example the max distance that makes an object "nearby" 84 | // is (50 * 3) / 2 = 75 85 | 86 | // returns a Map having keys: inserted objects 87 | var result = nearby.query(searchX, searchY, searchZ); 88 | 89 | for (var object of result.keys()){ 90 | console.log(object.id + " is found nearby!"); 91 | } 92 | ``` 93 | 94 | To update an object: 95 | ```javascript 96 | var newPosX = -500, newPosY = 100, newPosZ = 100; 97 | var newWidth = 1000, newHeight = 1000, newDepth = 1000; 98 | nearby.update( 99 | object, newPosX, newPosY, newPosZ, 100 | newWidth, newHeight, newDepth 101 | ); 102 | ``` 103 | 104 | To delete an object: 105 | ```javascript 106 | nearby.delete(object); 107 | ``` 108 | # In Action 109 | This algorithm is used by ROYGBIV engine in many demos. 110 | For instance in [this demo](https://oguzeroglu.github.io/ROYGBIV/demo/blaster/application.html) and [this demo](https://oguzeroglu.github.io/ROYGBIV/demo/plasmaGun/application.html) Nearby algorithm is used to check if a ParticleSystem is collided with walls or objects. In [this demo](https://oguzeroglu.github.io/ROYGBIV/demo/shooter/application.html) the algorithm is used to perform Ray checks from the weapon (when the user shoots). 111 | 112 | # License 113 | Nearby uses MIT license. 114 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nearby-js", 3 | "version": "1.0.0", 4 | "description": "Find nearby 3D objects in constant time O(1).", 5 | "main": "build/Nearby.js", 6 | "module": "build/Nearby-module.js", 7 | "es": "build/Nearby-es.js", 8 | "directories": { 9 | "test": "test" 10 | }, 11 | "scripts": { 12 | "build": "rollup --config rollup.config.js --environment BUILD:production && minify ./build/Nearby.js -o ./build/Nearby.min.js", 13 | "test": "npm run build && mocha test" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/oguzeroglu/Nearby.git" 18 | }, 19 | "keywords": [ 20 | "game-development", 21 | "3d", 22 | "spatial-hashing" 23 | ], 24 | "author": "Oguz Eroglu", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/oguzeroglu/Nearby/issues" 28 | }, 29 | "homepage": "https://github.com/oguzeroglu/Nearby#readme", 30 | "dependencies": {}, 31 | "devDependencies": { 32 | "babel-eslint": "^7.1.1", 33 | "babel-minify": "^0.5.1", 34 | "babel-plugin-external-helpers": "^6.18.0", 35 | "babel-preset-es2015": "^6.18.0", 36 | "babel-register": "^6.18.0", 37 | "babelrc-rollup": "^3.0.0", 38 | "eslint": "^4.1.1", 39 | "expect.js": "^0.3.1", 40 | "istanbul": "^0.4.5", 41 | "mocha": "^3.2.0", 42 | "rollup": "^0.43.0", 43 | "rollup-plugin-babel": "^2.7.1", 44 | "rollup-plugin-istanbul": "^1.1.0", 45 | "rollup-watch": "^4.3.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /performance-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |