From 6a6a7dea85b3136aef0ed34e5cd9e966fe68f633 Mon Sep 17 00:00:00 2001 From: rxi Date: Sat, 20 Aug 2016 09:13:52 +0100 Subject: [PATCH] Initial commit --- LICENSE | 20 ++++++ README.md | 42 ++++++++++++ shash.lua | 197 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 259 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 shash.lua diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5818e8d --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2016 rxi + + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..84b49e4 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# shash.lua +A simple, lightweight spatial hash for Lua. + + +## Functions + +#### shash.new([cellsize]) +Creates a new spatial hash; if `cellsize` is not specified a default value of +`64` is used. + +#### :add(obj, x, y, w, h) +Adds an object with the given bounding box to the spatial hash. + +#### :update(obj, x, y [, w, h]) +Updates the object's bounding box. + +#### :remove(obj) +Removes the object from the spatial hash. + +#### :clear() +Removes all objects from the spatial hash. + +#### :each(x, y, w, h, fn, ...) +#### :each(obj, fn, ...) +For each object which overlaps with the given bounding box or object, the +function `fn` is called. The first argument passed to `fn` is the overlapping +object, followed by any additional arguments passed to `each()`. + +#### :info(opt, ...) +Returns information about the spatial hash which can be useful for debugging. +Available options and their arguments are as follows: + + Opt | Args | Description +------------|-----------|------------------------------------------------------- + `entities` | | Returns the total number of entities + `cells` | | Returns the total number of cells + `cell` | `x`, `y` | Returns the number of entities in the cell + + +## License +This library is free software; you can redistribute it and/or modify it under +the terms of the MIT license. See [LICENSE](LICENSE) for details. diff --git a/shash.lua b/shash.lua new file mode 100644 index 0000000..a6953bc --- /dev/null +++ b/shash.lua @@ -0,0 +1,197 @@ +-- +-- shash.lua +-- +-- Copyright (c) 2016 rxi +-- +-- This library is free software; you can redistribute it and/or modify it +-- under the terms of the MIT license. See LICENSE for details. +-- + +local shash = { _version = "0.1.0" } +shash.__index = shash + + +function shash.new(cellsize) + local self = setmetatable({}, shash) + cellsize = cellsize or 64 + self.cellsize = cellsize + self.tablepool = {} + self.cells = {} + self.entities = {} + return self +end + + +local function coord_to_key(x, y) + return x + y * 1e7 +end + + +local function cell_position(cellsize, x, y) + return math.floor(x / cellsize), math.floor(y / cellsize) +end + + +local function each_overlapping_cell(self, e, fn, ...) + local cellsize = self.cellsize + local sx, sy = cell_position(cellsize, e[1], e[2]) + local ex, ey = cell_position(cellsize, e[3], e[4]) + for y = sy, ey do + for x = sx, ex do + local idx = coord_to_key(x, y) + fn(self, idx, ...) + end + end +end + + +local function add_entity_to_cell(self, idx, e) + if not self.cells[idx] then + self.cells[idx] = { e } + else + table.insert(self.cells[idx], e) + end +end + + +local function remove_entity_from_cell(self, idx, e) + local t = self.cells[idx] + local n = #t + -- Only one entity? Remove entity from cell and remove cell + if n == 1 then + self.cells[idx] = nil + return + end + -- Find and swap-remove entity + for i, v in ipairs(t) do + if v == e then + t[i] = t[n] + t[n] = nil + return + end + end +end + + +function shash:add(obj, x, y, w, h) + -- Create entity. The table is used as an array as this offers a noticable + -- performance increase on LuaJIT; the indices are as follows: + -- [1] = left, [2] = top, [3] = right, [4] = bottom, [5] = object + local e = { x, y, x + w, y + h, obj } + -- Add to main entities table + self.entities[obj] = e + -- Add to cells + each_overlapping_cell(self, e, add_entity_to_cell, e) +end + + +function shash:remove(obj) + -- Get entity of obj + local e = self.entities[obj] + -- Remove from main entities table + self.entities[obj] = nil + -- Remove from cells + each_overlapping_cell(self, e, remove_entity_from_cell, e) +end + + +function shash:update(obj, x, y, w, h) + -- Get entity from obj + local e = self.entities[obj] + -- No width/height specified? Get width/height from existing bounding box + w = w or e[3] - e[1] + h = h or e[4] - e[2] + -- Check the entity has actually changed cell-position, if it hasn't we don't + -- need to touch the cells at all + local cellsize = self.cellsize + local ax1, ay1 = cell_position(cellsize, e[1], e[2]) + local ax2, ay2 = cell_position(cellsize, e[3], e[4]) + local bx1, by1 = cell_position(cellsize, x, y) + local bx2, by2 = cell_position(cellsize, x + w, y + h) + local dirty = ax1 ~= bx1 or ay1 ~= by1 or ax2 ~= bx2 or ay2 ~= by2 + -- Remove from old cells + if dirty then + each_overlapping_cell(self, e, remove_entity_from_cell, e) + end + -- Update entity + e[1], e[2], e[3], e[4] = x, y, x + w, y + h + -- Add to new cells + if dirty then + each_overlapping_cell(self, e, add_entity_to_cell, e) + end +end + + +function shash:clear() + -- Clear all cells and entities + for k in pairs(self.cells) do + self.cells[k] = nil + end + for k in pairs(self.entities) do + self.entities[k] = nil + end +end + + +local function overlaps(e1, e2) + return e1[3] > e2[1] and e1[1] < e2[3] and e1[4] > e2[2] and e1[2] < e2[4] +end + + +local function add_each_overlapping_in_cell(self, idx, e, set) + local t = self.cells[idx] + if not t then + return + end + for i, v in ipairs(t) do + if e ~= v and overlaps(e, v) then + set[v] = true + end + end +end + + +local function each_overlapping_entity(self, e, fn, ...) + -- Init set + local set = table.remove(self.tablepool) or {} + -- Do overlap checks and add overlapping to set + each_overlapping_cell(self, e, add_each_overlapping_in_cell, e, set) + -- Do callback for each entity in set and clear set + for v in pairs(set) do + fn(v[5], ...) + set[v] = nil + end + -- Return set to pool + table.insert(self.tablepool, set) +end + + +function shash:each(x, y, w, h, fn, ...) + local e = self.entities[x] + if e then + -- Got object, use its entity + each_overlapping_entity(self, e, y, w, h, fn, ...) + else + -- Got bounding box, make temporary entity + each_overlapping_entity(self, { x, y, x + w, y + h }, fn, ...) + end +end + + +function shash:info(opt, ...) + if opt == "cells" or opt == "entities" then + local n = 0 + for k in pairs(self[opt]) do + n = n + 1 + end + return n + end + if opt == "cell" then + local t = self.cells[ coord_to_key(...) ] + return t and #t or 0 + end + error( string.format("invalid opt '%s'", opt) ) +end + + +return shash