-
Notifications
You must be signed in to change notification settings - Fork 83
Various Garbage Generation Optimizations #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
27886eb
899bc92
8687554
fecce4a
62767b6
c06e0bc
28fdc3f
53b475f
45b50fe
d01d827
af767f4
100641a
392da11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,39 @@ local bump = { | |
| ]] | ||
| } | ||
|
|
||
| ------------------------------------------ | ||
| -- Table Pool | ||
| ------------------------------------------ | ||
| local Pool = {} | ||
| do | ||
| local ok, tabelClear = pcall(require, 'table.clear') | ||
| if not ok then | ||
| tabelClear = function (t) | ||
| for k, _ in pairs(t) do | ||
| t[k] = nil | ||
| end | ||
| end | ||
| end | ||
|
|
||
| local pool = {} | ||
| local len = 0 | ||
|
|
||
| function Pool.fetch() | ||
| if len == 0 then | ||
| Pool.free({}) | ||
| end | ||
| local t = table.remove(pool, len) | ||
| len = len - 1 | ||
| return t | ||
| end | ||
|
|
||
| function Pool.free(t) | ||
| tabelClear(t) | ||
| len = len + 1 | ||
| pool[len] = t | ||
| end | ||
| end | ||
|
|
||
| ------------------------------------------ | ||
| -- Auxiliary functions | ||
| ------------------------------------------ | ||
|
|
@@ -189,13 +222,15 @@ local function rect_detectCollision(x1,y1,w1,h1, x2,y2,w2,h2, goalX, goalY) | |
| end | ||
|
|
||
| return { | ||
| overlaps = overlaps, | ||
| ti = ti, | ||
| move = {x = dx, y = dy}, | ||
| normal = {x = nx, y = ny}, | ||
| touch = {x = tx, y = ty}, | ||
| itemRect = {x = x1, y = y1, w = w1, h = h1}, | ||
| otherRect = {x = x2, y = y2, w = w2, h = h2} | ||
| overlaps = overlaps, | ||
| ti = ti, | ||
| distance = rect_getSquareDistance(x1,y1,w1,h1, x2,y2,w2,h2), | ||
| moveX = dx, | ||
| moveY = dy, | ||
| normalX = nx, | ||
| normalY = ny, | ||
| touchX = tx, | ||
| touchY = ty, | ||
| } | ||
| end | ||
|
|
||
|
|
@@ -266,55 +301,57 @@ end | |
| -- Responses | ||
| ------------------------------------------ | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding |
||
| local touch = function(world, col, x,y,w,h, goalX, goalY, filter) | ||
| return col.touch.x, col.touch.y, {}, 0 | ||
| local touch = function(world, col, x,y,w,h, goalX, goalY, filter, alreadyVisited) | ||
| return col.touchX, col.touchY, {}, 0 | ||
| end | ||
|
|
||
| local cross = function(world, col, x,y,w,h, goalX, goalY, filter) | ||
| local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter) | ||
| local cross = function(world, col, x,y,w,h, goalX, goalY, filter, alreadyVisited) | ||
| local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter, alreadyVisited) | ||
| return goalX, goalY, cols, len | ||
| end | ||
|
|
||
| local slide = function(world, col, x,y,w,h, goalX, goalY, filter) | ||
| local slide = function(world, col, x,y,w,h, goalX, goalY, filter, alreadyVisited) | ||
| goalX = goalX or x | ||
| goalY = goalY or y | ||
|
|
||
| local tch, move = col.touch, col.move | ||
| if move.x ~= 0 or move.y ~= 0 then | ||
| if col.normal.x ~= 0 then | ||
| goalX = tch.x | ||
| if col.moveX ~= 0 or col.moveY ~= 0 then | ||
| if col.normalX ~= 0 then | ||
| goalX = col.touchX | ||
| else | ||
| goalY = tch.y | ||
| goalY = col.touchY | ||
| end | ||
| end | ||
|
|
||
| col.slide = {x = goalX, y = goalY} | ||
| col.slideX, col.slideY = goalX, goalY | ||
|
|
||
| x,y = tch.x, tch.y | ||
| local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter) | ||
| x, y = col.touchX, col.touchY | ||
| local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter, alreadyVisited) | ||
| return goalX, goalY, cols, len | ||
| end | ||
|
|
||
| local bounce = function(world, col, x,y,w,h, goalX, goalY, filter) | ||
| local bounce = function(world, col, x,y,w,h, goalX, goalY, filter, alreadyVisited) | ||
| goalX = goalX or x | ||
| goalY = goalY or y | ||
|
|
||
| local tch, move = col.touch, col.move | ||
| local tx, ty = tch.x, tch.y | ||
| local tx, ty = col.touchX, col.touchY | ||
|
|
||
| local bx, by = tx, ty | ||
|
|
||
| if move.x ~= 0 or move.y ~= 0 then | ||
| if col.moveX ~= 0 or col.moveY ~= 0 then | ||
| local bnx, bny = goalX - tx, goalY - ty | ||
| if col.normal.x == 0 then bny = -bny else bnx = -bnx end | ||
| if col.normalX == 0 then | ||
| bny = -bny | ||
| else | ||
| bnx = -bnx | ||
| end | ||
| bx, by = tx + bnx, ty + bny | ||
| end | ||
|
|
||
| col.bounce = {x = bx, y = by} | ||
| x,y = tch.x, tch.y | ||
| col.bounceX, col.bounceY = bx, by | ||
| x, y = col.touchX, col.touchY | ||
| goalX, goalY = bx, by | ||
|
|
||
| local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter) | ||
| local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter, alreadyVisited) | ||
| return goalX, goalY, cols, len | ||
| end | ||
|
|
||
|
|
@@ -331,10 +368,7 @@ local function sortByWeight(a,b) return a.weight < b.weight end | |
|
|
||
| local function sortByTiAndDistance(a,b) | ||
| if a.ti == b.ti then | ||
| local ir, ar, br = a.itemRect, a.otherRect, b.otherRect | ||
| local ad = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, ar.x,ar.y,ar.w,ar.h) | ||
| local bd = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, br.x,br.y,br.w,br.h) | ||
| return ad < bd | ||
| return a.distance < b.distance | ||
| end | ||
| return a.ti < b.ti | ||
| end | ||
|
|
@@ -365,7 +399,8 @@ local function removeItemFromCell(self, item, cx, cy) | |
| end | ||
|
|
||
| local function getDictItemsInCellRect(self, cl,ct,cw,ch) | ||
| local items_dict = {} | ||
| local items_dict = Pool.fetch() | ||
|
|
||
| for cy=ct,ct+ch-1 do | ||
| local row = self.rows[cy] | ||
| if row then | ||
|
|
@@ -444,17 +479,21 @@ function World:addResponse(name, response) | |
| self.responses[name] = response | ||
| end | ||
|
|
||
| function World:project(item, x,y,w,h, goalX, goalY, filter) | ||
| local EMPTY_TABLE = {} | ||
|
|
||
| function World:project(item, x,y,w,h, goalX, goalY, filter, alreadyVisited) | ||
| assertIsRect(x,y,w,h) | ||
|
|
||
| goalX = goalX or x | ||
| goalY = goalY or y | ||
| filter = filter or defaultFilter | ||
| filter = filter or defaultFilter | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't create the table for |
||
| local collisions, len = {}, 0 | ||
| local collisions, len = nil, 0 | ||
|
|
||
| local visited = {} | ||
| if item ~= nil then visited[item] = true end | ||
| local visited = Pool.fetch() | ||
| if item ~= nil then | ||
| visited[item] = true | ||
| end | ||
|
|
||
| -- This could probably be done with less cells using a polygon raster over the cells instead of a | ||
| -- bounding rect of the whole movement. Conditional to building a queryPolygon method | ||
|
|
@@ -467,7 +506,7 @@ function World:project(item, x,y,w,h, goalX, goalY, filter) | |
| local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch) | ||
|
|
||
| for other,_ in pairs(dictItemsInCellRect) do | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, see World:check() / World:projectMove() for more details on how alreadyVisited works. |
||
| if not visited[other] then | ||
| if not visited[other] and (alreadyVisited == nil or not alreadyVisited[other]) then | ||
| visited[other] = true | ||
|
|
||
| local responseName = filter(item, other) | ||
|
|
@@ -481,15 +520,23 @@ function World:project(item, x,y,w,h, goalX, goalY, filter) | |
| col.type = responseName | ||
|
|
||
| len = len + 1 | ||
| if collisions == nil then | ||
| collisions = {} | ||
| end | ||
| collisions[len] = col | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
||
| table.sort(collisions, sortByTiAndDistance) | ||
| Pool.free(visited) | ||
| Pool.free(dictItemsInCellRect) | ||
|
|
||
| return collisions, len | ||
| if collisions ~= nil then | ||
| table.sort(collisions, sortByTiAndDistance) | ||
| end | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. EMPTY_TABLE is by far the newest change here, everything else I changed back in march. The idea is that we reuse one static It's ever so slightly dangerous though, because if the user does something crazy and inserts into EMPTY_TABLE, that could cause some incredibly strange behavior. |
||
| return collisions or EMPTY_TABLE, len | ||
| end | ||
|
|
||
| function World:countCells() | ||
|
|
@@ -560,6 +607,8 @@ function World:queryRect(x,y,w,h, filter) | |
| end | ||
| end | ||
|
|
||
| Pool.free(dictItemsInCellRect) | ||
|
|
||
| return items, len | ||
| end | ||
|
|
||
|
|
@@ -580,6 +629,8 @@ function World:queryPoint(x,y, filter) | |
| end | ||
| end | ||
|
|
||
| Pool.free(dictItemsInCellRect) | ||
|
|
||
| return items, len | ||
| end | ||
|
|
||
|
|
@@ -696,19 +747,23 @@ function World:move(item, goalX, goalY, filter) | |
| end | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a new method I've found this incredibly practical for optimization, because the fewer things which are stored in the world, the faster collision detection is. An example of how you could use this: Bullets need to collide with Actors; however, Actors don't necessarily need to collide with Bullets. So, we insert actors into the bump world, but we keep track of the bullets some other way. We can still have the bullets interact with the actors by using Again, I'll document this in README if you're okay with this change. |
||
| function World:check(item, goalX, goalY, filter) | ||
| local x,y,w,h = self:getRect(item) | ||
| return self:projectMove(item, x, y, w, h, goalX,goalY, filter) | ||
| end | ||
|
|
||
| function World:projectMove(item, x, y, w, h, goalX, goalY, filter) | ||
| filter = filter or defaultFilter | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, so here's the big Before: Now: Instead of wrapping |
||
| local visited = {[item] = true} | ||
| local visitedFilter = function(itm, other) | ||
| if visited[other] then return false end | ||
| return filter(itm, other) | ||
| local projected_cols, projected_len = self:project(item, x,y,w,h, goalX,goalY, filter) | ||
|
|
||
| if projected_len == 0 then | ||
| return goalX, goalY, EMPTY_TABLE, 0 | ||
| end | ||
|
|
||
| local cols, len = {}, 0 | ||
|
|
||
| local x,y,w,h = self:getRect(item) | ||
|
|
||
| local projected_cols, projected_len = self:project(item, x,y,w,h, goalX,goalY, visitedFilter) | ||
| local visited = Pool.fetch() | ||
| visited[item] = true | ||
|
|
||
| while projected_len > 0 do | ||
| local col = projected_cols[1] | ||
|
|
@@ -724,14 +779,16 @@ function World:check(item, goalX, goalY, filter) | |
| col, | ||
| x, y, w, h, | ||
| goalX, goalY, | ||
| visitedFilter | ||
| filter, | ||
| visited | ||
| ) | ||
| end | ||
|
|
||
| Pool.free(visited) | ||
|
|
||
| return goalX, goalY, cols, len | ||
| end | ||
|
|
||
|
|
||
| -- Public library functions | ||
|
|
||
| bump.newWorld = function(cellSize) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| local RNG_SEED = 69420731 | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's a the benchmark I was using during development. Not sure if it makes sense to merge it in. Let me know if you want this removed or moved to a subfolder or something. |
||
| local WORLD_SIZE = 10 | ||
| local OBJECT_COUNT = 10 | ||
| local MOVEMENT_GENERATIONS = 250 | ||
| local MOVE_RANGE = 1 | ||
| local TEST_COUNT = 10 | ||
|
|
||
| local function clamp(lower, val, upper) | ||
| if lower > upper then lower, upper = upper, lower end | ||
| return math.max(lower, math.min(upper, val)) | ||
| end | ||
|
|
||
| local function doTest(world) | ||
| -- seed RNG | ||
| math.randomseed(RNG_SEED) | ||
|
|
||
| -- create OBJECT_COUNT random entities | ||
| local entities = {} | ||
| for i = 1, OBJECT_COUNT do | ||
| local x = math.random() * WORLD_SIZE | ||
| local y = math.random() * WORLD_SIZE | ||
| local w = (math.random() * 1.5) + 0.5 | ||
| local h = (math.random() * 1.5) + 0.5 | ||
| local entity = {name = i} | ||
| table.insert(entities, entity) | ||
| world:add(entity, x, y, w, h) | ||
| end | ||
|
|
||
| -- Collect garbage and stop GC. | ||
| collectgarbage('collect') | ||
| collectgarbage('stop') | ||
|
|
||
| -- move all entities for MOVEMENT_GENERATIONS generations. | ||
| local collisions = 0 | ||
| for i = 1, MOVEMENT_GENERATIONS do | ||
| for _, entity in ipairs(entities) do | ||
| local x, y = world:getRect(entity) | ||
| local goalX = clamp(0, x - MOVE_RANGE + (math.random() * MOVE_RANGE * 2), WORLD_SIZE) | ||
| local goalY = clamp(0, y - MOVE_RANGE + (math.random() * MOVE_RANGE * 2), WORLD_SIZE) | ||
| local _, _, _, len = world:move(entity, goalX, goalY) | ||
| collisions = collisions + len | ||
| end | ||
| end | ||
|
|
||
| -- restart GC and measure memory difference before and after. | ||
| local kbBefore = collectgarbage('count') | ||
| collectgarbage('restart') | ||
| collectgarbage('collect') | ||
| local kbAfter = collectgarbage('count') | ||
| local kbGarbage = kbBefore - kbAfter | ||
|
|
||
| -- -- Print stats. | ||
| -- print(("Collisions: %d"):format(collisions)) | ||
| -- print(("Garbage: %.2fkB"):format(kbGarbage)) | ||
|
|
||
| return kbGarbage | ||
| end | ||
|
|
||
| local function doTests(label, bump) | ||
| print(("============= %s ============="):format(label)) | ||
| local totalGarbage = 0 | ||
| for i = 1, TEST_COUNT do | ||
| local world = bump.newWorld(1) | ||
| local garbage = doTest(world) | ||
| totalGarbage = totalGarbage + garbage | ||
| end | ||
| local averageGarbage = totalGarbage / TEST_COUNT | ||
| print(("Garbage: %.2f kB"):format(averageGarbage)) | ||
| print(("(Average after %d tests.)"):format(TEST_COUNT)) | ||
| end | ||
|
|
||
| doTests('Original', require 'bump-original') | ||
| print('') | ||
| doTests('Modded', require 'bump') | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, the first change here is to add a table pool so that we can reuse temporary tables instead of throwing them away. This is used for a few operations:
World:project().getDictItemsInCellRectWorld:projectMove()(formerlyWorld:check()). More on this later.