├── LICENSE ├── README.md └── shash.lua /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 rxi 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shash.lua 2 | A simple, lightweight spatial hash for Lua. 3 | 4 | 5 | ## Functions 6 | 7 | #### shash.new([cellsize]) 8 | Creates a new spatial hash; if `cellsize` is not specified a default value of 9 | `64` is used. 10 | 11 | #### :add(obj, x, y, w, h) 12 | Adds an object with the given bounding box to the spatial hash. 13 | 14 | #### :update(obj, x, y [, w, h]) 15 | Updates the object's bounding box. 16 | 17 | #### :remove(obj) 18 | Removes the object from the spatial hash. 19 | 20 | #### :clear() 21 | Removes all objects from the spatial hash. 22 | 23 | #### :each(x, y, w, h, fn, ...) 24 | #### :each(obj, fn, ...) 25 | For each object which overlaps with the given bounding box or object, the 26 | function `fn` is called. The first argument passed to `fn` is the overlapping 27 | object, followed by any additional arguments passed to `each()`. 28 | 29 | #### :info(opt, ...) 30 | Returns information about the spatial hash which can be useful for debugging. 31 | Available options and their arguments are as follows: 32 | 33 | Opt | Args | Description 34 | ------------|-----------|------------------------------------------------------- 35 | `entities` | | Returns the total number of entities 36 | `cells` | | Returns the total number of cells 37 | `cell` | `x`, `y` | Returns the number of entities in the cell 38 | 39 | 40 | ## License 41 | This library is free software; you can redistribute it and/or modify it under 42 | the terms of the MIT license. See [LICENSE](LICENSE) for details. 43 | -------------------------------------------------------------------------------- /shash.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- shash.lua 3 | -- 4 | -- Copyright (c) 2017 rxi 5 | -- 6 | -- This library is free software; you can redistribute it and/or modify it 7 | -- under the terms of the MIT license. See LICENSE for details. 8 | -- 9 | 10 | local shash = { _version = "0.1.1" } 11 | shash.__index = shash 12 | 13 | 14 | function shash.new(cellsize) 15 | local self = setmetatable({}, shash) 16 | cellsize = cellsize or 64 17 | self.cellsize = cellsize 18 | self.tablepool = {} 19 | self.cells = {} 20 | self.entities = {} 21 | return self 22 | end 23 | 24 | 25 | local function coord_to_key(x, y) 26 | return x + y * 1e7 27 | end 28 | 29 | 30 | local function cell_position(cellsize, x, y) 31 | return math.floor(x / cellsize), math.floor(y / cellsize) 32 | end 33 | 34 | 35 | local function each_overlapping_cell(self, e, fn, ...) 36 | local cellsize = self.cellsize 37 | local sx, sy = cell_position(cellsize, e[1], e[2]) 38 | local ex, ey = cell_position(cellsize, e[3], e[4]) 39 | for y = sy, ey do 40 | for x = sx, ex do 41 | local idx = coord_to_key(x, y) 42 | fn(self, idx, ...) 43 | end 44 | end 45 | end 46 | 47 | 48 | local function add_entity_to_cell(self, idx, e) 49 | if not self.cells[idx] then 50 | self.cells[idx] = { e } 51 | else 52 | table.insert(self.cells[idx], e) 53 | end 54 | end 55 | 56 | 57 | local function remove_entity_from_cell(self, idx, e) 58 | local t = self.cells[idx] 59 | local n = #t 60 | -- Only one entity? Remove entity from cell and remove cell 61 | if n == 1 then 62 | self.cells[idx] = nil 63 | return 64 | end 65 | -- Find and swap-remove entity 66 | for i, v in ipairs(t) do 67 | if v == e then 68 | t[i] = t[n] 69 | t[n] = nil 70 | return 71 | end 72 | end 73 | end 74 | 75 | 76 | function shash:add(obj, x, y, w, h) 77 | -- Create entity. The table is used as an array as this offers a noticable 78 | -- performance increase on LuaJIT; the indices are as follows: 79 | -- [1] = left, [2] = top, [3] = right, [4] = bottom, [5] = object 80 | local e = { x, y, x + w, y + h, obj } 81 | -- Add to main entities table 82 | self.entities[obj] = e 83 | -- Add to cells 84 | each_overlapping_cell(self, e, add_entity_to_cell, e) 85 | end 86 | 87 | 88 | function shash:remove(obj) 89 | -- Get entity of obj 90 | local e = self.entities[obj] 91 | -- Remove from main entities table 92 | self.entities[obj] = nil 93 | -- Remove from cells 94 | each_overlapping_cell(self, e, remove_entity_from_cell, e) 95 | end 96 | 97 | 98 | function shash:update(obj, x, y, w, h) 99 | -- Get entity from obj 100 | local e = self.entities[obj] 101 | -- No width/height specified? Get width/height from existing bounding box 102 | w = w or e[3] - e[1] 103 | h = h or e[4] - e[2] 104 | -- Check the entity has actually changed cell-position, if it hasn't we don't 105 | -- need to touch the cells at all 106 | local cellsize = self.cellsize 107 | local ax1, ay1 = cell_position(cellsize, e[1], e[2]) 108 | local ax2, ay2 = cell_position(cellsize, e[3], e[4]) 109 | local bx1, by1 = cell_position(cellsize, x, y) 110 | local bx2, by2 = cell_position(cellsize, x + w, y + h) 111 | local dirty = ax1 ~= bx1 or ay1 ~= by1 or ax2 ~= bx2 or ay2 ~= by2 112 | -- Remove from old cells 113 | if dirty then 114 | each_overlapping_cell(self, e, remove_entity_from_cell, e) 115 | end 116 | -- Update entity 117 | e[1], e[2], e[3], e[4] = x, y, x + w, y + h 118 | -- Add to new cells 119 | if dirty then 120 | each_overlapping_cell(self, e, add_entity_to_cell, e) 121 | end 122 | end 123 | 124 | 125 | function shash:clear() 126 | -- Clear all cells and entities 127 | for k in pairs(self.cells) do 128 | self.cells[k] = nil 129 | end 130 | for k in pairs(self.entities) do 131 | self.entities[k] = nil 132 | end 133 | end 134 | 135 | 136 | local function overlaps(e1, e2) 137 | return e1[3] > e2[1] and e1[1] < e2[3] and e1[4] > e2[2] and e1[2] < e2[4] 138 | end 139 | 140 | 141 | local function each_overlapping_in_cell(self, idx, e, set, fn, ...) 142 | local t = self.cells[idx] 143 | if not t then 144 | return 145 | end 146 | for i, v in ipairs(t) do 147 | if e ~= v and overlaps(e, v) and not set[v] then 148 | fn(v[5], ...) 149 | set[v] = true 150 | end 151 | end 152 | end 153 | 154 | 155 | local function each_overlapping_entity(self, e, fn, ...) 156 | -- Init set for keeping track of which entities have already been handled 157 | local set = table.remove(self.tablepool) or {} 158 | -- Do overlap checks 159 | each_overlapping_cell(self, e, each_overlapping_in_cell, e, set, fn, ...) 160 | -- Clear set and return to pool 161 | for v in pairs(set) do 162 | set[v] = nil 163 | end 164 | table.insert(self.tablepool, set) 165 | end 166 | 167 | 168 | function shash:each(x, y, w, h, fn, ...) 169 | local e = self.entities[x] 170 | if e then 171 | -- Got object, use its entity 172 | each_overlapping_entity(self, e, y, w, h, fn, ...) 173 | else 174 | -- Got bounding box, make temporary entity 175 | each_overlapping_entity(self, { x, y, x + w, y + h }, fn, ...) 176 | end 177 | end 178 | 179 | 180 | function shash:info(opt, ...) 181 | if opt == "cells" or opt == "entities" then 182 | local n = 0 183 | for k in pairs(self[opt]) do 184 | n = n + 1 185 | end 186 | return n 187 | end 188 | if opt == "cell" then 189 | local t = self.cells[ coord_to_key(...) ] 190 | return t and #t or 0 191 | end 192 | error( string.format("invalid opt '%s'", opt) ) 193 | end 194 | 195 | 196 | return shash 197 | --------------------------------------------------------------------------------