hundreds of lines: collision detection

I’m currently prototyping a game in which two players are drawing lines on the screen. This can result in hundreds of lines per player. The point is that no two lines may intersect. If they do, the player is ‘dead’.

I’m collision checking the lines in my game loop, so on every frame. Everything works great, until there are a few hundred lines in there, the performance starts degrading very rapidly.

I store every line’s old and new x and y position in an array, calculate the new X and Y position and test if they are intersecting.

It looks like this:
[lua]-- Collision, so stop game, or kill player
if isIntersecting( a, b, c, d ) then
print( v.name … " collided with "… playerData.name )
started = false
return false
end[/lua]

Does anybody know a faster way of checking the collisions? Maybe even try to avoid storing the x and y coordinates in an array? [import]uid: 86582 topic_id: 31572 reply_id: 331572[/import]

Yes, this is a tough one. Your looking at an issue that is generally solved by using a “B-Tree” or even “Octree” structure. You will have to explore those on your own if you wish to proceed down that road.

Another way may be to “attach” a physics body to each line drawn and let the speed of the Corona engine do it’s magic. This body would be a rectangle, of course, as lines cannot be physics bodies. :slight_smile:

Hope this helps!
[import]uid: 21331 topic_id: 31572 reply_id: 126141[/import]

Along the lines of TheRealTonyK’s suggestion, you can probably even get by with just a grid, and store a separate occupancy list in each grid cell.

You still run the small risk of saturating a small neighborhood of cells, but with any reasonable sizes you ought to get a collision first. :slight_smile:

Then you could do something like

[lua]local lines_found = {}

local function Visitor (cell, my_line)
for line in pairs(cell) do
lines_found[line] = true
end

cell[my_line] = true
end

GRID:LineIter(my_line, Visitor)

for line in pairs(lines_found) do
if isIntersecting(line.p1, line.p2, my_line.p1, my_line.p2) then
started = false
end
end[/lua]

I have some grid code from an older project, if it helps. It’s built on coroutines (to make it nice as an iterator), but it shouldn’t be too terrible to adapt:

[lua]— Partition of the underlying world as a grid, with some operations to operate on per-cell data.


– 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.

– [MIT license: http://www.opensource.org/licenses/mit-license.php]


– Imports

local yield = coroutine.yield
local abs, floor = math.abs, math.floor
local insert, remove = table.insert, table.remove
local AARectIter
local LineIter

– G: Grid handle
– x: x-value
– Returns: Column x-value occupies

local function GetColumn (G, x)
return floor((x - G.x) / G.cellw) + 1
end

– G: Grid handle
– row, col: Cell row, column
– Returns: Cell index

local function GetIndex (G, row, col)
return (row - 1) * G.cols + col
end

– G: Grid handle
– y: y-value
– Returns: Row y-value occupies

local function GetRow (G, y)
return floor((y - G.y) / G.cellh) + 1
end

– G: Grid handle
– index: Cell index
– bBuild: If true, build missing cell
– Returns: Cell

local function GetCell (G, index, bBuild)
local cell = G.cells[index]

if not cell and bBuild then
cell = {}

G.cells[index] = cell
end

return cell
end

– Yields a cell if available
– G: Grid handle
– area: Area, in cells
– index: Cell index
– cell: Cell handle
– row, col: Cell row, column
– bBuild: If true, build missing cell

local function YieldCell (G, area, index, row, col, bBuild)
if index >= 1 and index <= area then
local cell = GetCell(G, index, bBuild)

if cell then
yield(cell, row, col)
end
end
end


– Grid iterator

local Iter = coroutine_ex.Create(function(func, G)
func(G)
end)

do
local Center, W, H, bTemp

– Axis-aligned rect iterator body
– G: Grid handle

local function AuxIter (G)
local x, y, bBuild = Center.x, Center.y, bTemp
local c1, r1 = GetColumn(G, x - W / 2), GetRow(G, y - H / 2)
local c2, r2 = GetColumn(G, x + W / 2), GetRow(G, y + H / 2)
local area, index, iinc = G.cols * G.rows, GetIndex(G, r1, c1), G.cols - (c2 - c1 + 1)

for r = r1, r2 do
for c = c1, c2 do
YieldCell(G, area, index, r, c, bBuild)

index = index + 1
end

index = index + iinc
end
end

– Builds an iterator over cells on or within an axis-aligned rect
– G: Grid handle
– center: Center position
– w, h: Box dimensions
– bBuild: If true, build missing cells
– Returns: Iterator that supplies cell, row, and column

function AARectIter (G, center, w, h, bBuild)
Center, W, H, bTemp = center, w, h, bBuild

– Supply the iterator routine.
return Iter, AuxIter, G
end
end

do
local X1, Y1, X2, Y2, C1, R1, C2, R2, Area, Index, bTemp

– Column iterator body
– G: Grid handle

local function Column (G)
local bBuild, iinc, rinc = bTemp, R1 < R2 and G.cols or -G.cols, R1 < R2 and 1 or -1

for _ = 1, abs(R2 - R1) + 1 do
YieldCell(G, Area, Index, R1, C1, bBuild)

Index, R1 = Index + iinc, R1 + rinc
end
end

– Row iterator body
– G: Grid handle

local function Row (G)
local bBuild, cinc = bTemp, C1 < C2 and 1 or -1

for _ = 1, abs(C2 - C1) + 1 do
YieldCell(G, Area, Index, R1, C1, bBuild)

Index, C1 = Index + cinc, C1 + cinc
end
end

– Diagonal iterator body
– G: Grid handle

local function Diagonal (G)
local bBuild, cinc, rinc, iinc, xoffinc = bTemp, 1, 1, G.cols, G.cellw
local slope, xoff = (Y2 - Y1) / (X2 - X1), G.x + C1 * xoffinc - X1

– If the line tends left, adjust horizontal values.
if C2 < C1 then
cinc, xoff, xoffinc = -1, xoff - xoffinc, -xoffinc
end

– If the line tends down, adjust vertical values.
if R2 < R1 then
rinc, iinc = -1, -iinc
end

– On each column, go from the current to the final row. The final row
– becomes current at the end of each pass.
local rlast

while C1 ~= C2 do
rlast = GetRow(G, Y1 + slope * xoff)

while true do
YieldCell(G, Area, Index, R1, C1, bBuild)

if R1 == rlast then
break
end

Index, R1 = Index + iinc, R1 + rinc
end

Index, C1, xoff = Index + cinc, C1 + cinc, xoff + xoffinc
end

– Treat the final column separately, since the line could cover several
– more rows than desired while in this column.
while rlast ~= R2 do
YieldCell(G, Area, Index, rlast, C2, bBuild)

Index, rlast = Index + iinc, rlast + rinc
end
end

– Visits cells along a line
– G: Grid handle
– p1, p2: Start, end positions
– bBuild: If true, build missing cells

function LineIter (G, p1, p2, bBuild)
X1, Y1, X2, Y2, bTemp = p1.x, p1.y, p2.x, p2.y, bBuild

C1, R1 = GetColumn(G, X1), GetRow(G, Y1)
C2, R2 = GetColumn(G, X2), GetRow(G, Y2)

Area, Index = G.cols * G.rows, GetIndex(G, R1, C1)

– By default, traverse algorithmically. On column or row matches, do spans.
local func = Diagonal

if C1 == C2 then
func = Column

elseif R1 == R2 then
func = Row
end

return Iter, func, G
end
end


– WorldGrid class definition

class.Define(“WorldGrid”, {
AARectIter = AARectIter,
GetColumn = GetColumn,
GetIndex = GetIndex,
GetRow = GetRow,
LineIter = LineIter,

– Clears the grid

Clear = function(G)
local cells = {}

for i = 1, G.cols * G.rows do
cells[i] = false
end

G.cells = cells
end,

– row, col: Cell row, column
– bBuild: If true, build missing cell
– Returns: Cell matching row and column

GetCell = function(G, row, col, bBuild)
if row >= 1 and row <= G.rows and col >= 1 and col <= G.cols then
return GetCell(G, GetIndex(G, row, col), bBuild)
end
end,

– index: Cell index
– bBuild: If true, build missing cell
– Returns: Cell matching index

GetCellFromIndex = function(G, index, bBuild)
if index >= 1 and index <= G.cols * G.rows then
return GetCell(G, index, bBuild)
end
end,

– x, y: Coordinates in cell
– bBuild: If true, build missing cell
– Returns: Cell matching x and y

GetCellFromXY = function(G, x, y, bBuild)
return G:GetCell(GetRow(G, y), GetColumn(G, x), bBuild)
end
},

– Constructor
– x, y: Grid coordinates
– w, h: Grid dimensions
– cols: Column count
– rows: Row count

function(G, x, y, w, h, cols, rows)
G.cellw, G.cellh, G.x, G.y, G.cols, G.rows = w / cols, h / rows, x, y, cols, rows

G:Clear()
end)[/lua] [import]uid: 27791 topic_id: 31572 reply_id: 126162[/import]

Out of curiosity, do the lines that the players draw move, or do they stay in place after they’re drawn? The reason I ask is, if they stay in place, then it should be necessary to see if a line intersects any others only when it’s drawn – no need to check again every frame, since nothing will have changed.

I suppose the lines must be moving, since you mention the lines having “old” and “new” x,y coordinates, but I thought I’d ask anyway.

Also, how have you implemented your isIntersecting function? Some ways are certainly more efficient than others, so there may be room for optimization there.

Last, you might want to consider checking intersections only every, say, 5 frames (or check 1/5 the possibilities per frame). The player won’t notice the slight delay in declaring a loss, but you’ll get a performance boost.

  • Andrew [import]uid: 109711 topic_id: 31572 reply_id: 126178[/import]

Thank you all so much for taking time to help me!
I’ve rewritten the collision detection code to work with a grid for now. This seems to increase the performance a lot. I’m a little worried what happens when a line crosses a grid though, but the chance of that happening AND colliding (intersecting) with such a line is not going to happen that often. For now I’ll leave it as is, but when it turns out this method is not that tight, I’m probably going to switch to using a B-tree. Thanks for that suggestion!

I’ve never used B-trees before, so I’ll have to figure that one out sooner or later. But for now, thanks a lot, guys! [import]uid: 86582 topic_id: 31572 reply_id: 126250[/import]

Yes, this is a tough one. Your looking at an issue that is generally solved by using a “B-Tree” or even “Octree” structure. You will have to explore those on your own if you wish to proceed down that road.

Another way may be to “attach” a physics body to each line drawn and let the speed of the Corona engine do it’s magic. This body would be a rectangle, of course, as lines cannot be physics bodies. :slight_smile:

Hope this helps!
[import]uid: 21331 topic_id: 31572 reply_id: 126141[/import]

Along the lines of TheRealTonyK’s suggestion, you can probably even get by with just a grid, and store a separate occupancy list in each grid cell.

You still run the small risk of saturating a small neighborhood of cells, but with any reasonable sizes you ought to get a collision first. :slight_smile:

Then you could do something like

[lua]local lines_found = {}

local function Visitor (cell, my_line)
for line in pairs(cell) do
lines_found[line] = true
end

cell[my_line] = true
end

GRID:LineIter(my_line, Visitor)

for line in pairs(lines_found) do
if isIntersecting(line.p1, line.p2, my_line.p1, my_line.p2) then
started = false
end
end[/lua]

I have some grid code from an older project, if it helps. It’s built on coroutines (to make it nice as an iterator), but it shouldn’t be too terrible to adapt:

[lua]— Partition of the underlying world as a grid, with some operations to operate on per-cell data.


– 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.

– [MIT license: http://www.opensource.org/licenses/mit-license.php]


– Imports

local yield = coroutine.yield
local abs, floor = math.abs, math.floor
local insert, remove = table.insert, table.remove
local AARectIter
local LineIter

– G: Grid handle
– x: x-value
– Returns: Column x-value occupies

local function GetColumn (G, x)
return floor((x - G.x) / G.cellw) + 1
end

– G: Grid handle
– row, col: Cell row, column
– Returns: Cell index

local function GetIndex (G, row, col)
return (row - 1) * G.cols + col
end

– G: Grid handle
– y: y-value
– Returns: Row y-value occupies

local function GetRow (G, y)
return floor((y - G.y) / G.cellh) + 1
end

– G: Grid handle
– index: Cell index
– bBuild: If true, build missing cell
– Returns: Cell

local function GetCell (G, index, bBuild)
local cell = G.cells[index]

if not cell and bBuild then
cell = {}

G.cells[index] = cell
end

return cell
end

– Yields a cell if available
– G: Grid handle
– area: Area, in cells
– index: Cell index
– cell: Cell handle
– row, col: Cell row, column
– bBuild: If true, build missing cell

local function YieldCell (G, area, index, row, col, bBuild)
if index >= 1 and index <= area then
local cell = GetCell(G, index, bBuild)

if cell then
yield(cell, row, col)
end
end
end


– Grid iterator

local Iter = coroutine_ex.Create(function(func, G)
func(G)
end)

do
local Center, W, H, bTemp

– Axis-aligned rect iterator body
– G: Grid handle

local function AuxIter (G)
local x, y, bBuild = Center.x, Center.y, bTemp
local c1, r1 = GetColumn(G, x - W / 2), GetRow(G, y - H / 2)
local c2, r2 = GetColumn(G, x + W / 2), GetRow(G, y + H / 2)
local area, index, iinc = G.cols * G.rows, GetIndex(G, r1, c1), G.cols - (c2 - c1 + 1)

for r = r1, r2 do
for c = c1, c2 do
YieldCell(G, area, index, r, c, bBuild)

index = index + 1
end

index = index + iinc
end
end

– Builds an iterator over cells on or within an axis-aligned rect
– G: Grid handle
– center: Center position
– w, h: Box dimensions
– bBuild: If true, build missing cells
– Returns: Iterator that supplies cell, row, and column

function AARectIter (G, center, w, h, bBuild)
Center, W, H, bTemp = center, w, h, bBuild

– Supply the iterator routine.
return Iter, AuxIter, G
end
end

do
local X1, Y1, X2, Y2, C1, R1, C2, R2, Area, Index, bTemp

– Column iterator body
– G: Grid handle

local function Column (G)
local bBuild, iinc, rinc = bTemp, R1 < R2 and G.cols or -G.cols, R1 < R2 and 1 or -1

for _ = 1, abs(R2 - R1) + 1 do
YieldCell(G, Area, Index, R1, C1, bBuild)

Index, R1 = Index + iinc, R1 + rinc
end
end

– Row iterator body
– G: Grid handle

local function Row (G)
local bBuild, cinc = bTemp, C1 < C2 and 1 or -1

for _ = 1, abs(C2 - C1) + 1 do
YieldCell(G, Area, Index, R1, C1, bBuild)

Index, C1 = Index + cinc, C1 + cinc
end
end

– Diagonal iterator body
– G: Grid handle

local function Diagonal (G)
local bBuild, cinc, rinc, iinc, xoffinc = bTemp, 1, 1, G.cols, G.cellw
local slope, xoff = (Y2 - Y1) / (X2 - X1), G.x + C1 * xoffinc - X1

– If the line tends left, adjust horizontal values.
if C2 < C1 then
cinc, xoff, xoffinc = -1, xoff - xoffinc, -xoffinc
end

– If the line tends down, adjust vertical values.
if R2 < R1 then
rinc, iinc = -1, -iinc
end

– On each column, go from the current to the final row. The final row
– becomes current at the end of each pass.
local rlast

while C1 ~= C2 do
rlast = GetRow(G, Y1 + slope * xoff)

while true do
YieldCell(G, Area, Index, R1, C1, bBuild)

if R1 == rlast then
break
end

Index, R1 = Index + iinc, R1 + rinc
end

Index, C1, xoff = Index + cinc, C1 + cinc, xoff + xoffinc
end

– Treat the final column separately, since the line could cover several
– more rows than desired while in this column.
while rlast ~= R2 do
YieldCell(G, Area, Index, rlast, C2, bBuild)

Index, rlast = Index + iinc, rlast + rinc
end
end

– Visits cells along a line
– G: Grid handle
– p1, p2: Start, end positions
– bBuild: If true, build missing cells

function LineIter (G, p1, p2, bBuild)
X1, Y1, X2, Y2, bTemp = p1.x, p1.y, p2.x, p2.y, bBuild

C1, R1 = GetColumn(G, X1), GetRow(G, Y1)
C2, R2 = GetColumn(G, X2), GetRow(G, Y2)

Area, Index = G.cols * G.rows, GetIndex(G, R1, C1)

– By default, traverse algorithmically. On column or row matches, do spans.
local func = Diagonal

if C1 == C2 then
func = Column

elseif R1 == R2 then
func = Row
end

return Iter, func, G
end
end


– WorldGrid class definition

class.Define(“WorldGrid”, {
AARectIter = AARectIter,
GetColumn = GetColumn,
GetIndex = GetIndex,
GetRow = GetRow,
LineIter = LineIter,

– Clears the grid

Clear = function(G)
local cells = {}

for i = 1, G.cols * G.rows do
cells[i] = false
end

G.cells = cells
end,

– row, col: Cell row, column
– bBuild: If true, build missing cell
– Returns: Cell matching row and column

GetCell = function(G, row, col, bBuild)
if row >= 1 and row <= G.rows and col >= 1 and col <= G.cols then
return GetCell(G, GetIndex(G, row, col), bBuild)
end
end,

– index: Cell index
– bBuild: If true, build missing cell
– Returns: Cell matching index

GetCellFromIndex = function(G, index, bBuild)
if index >= 1 and index <= G.cols * G.rows then
return GetCell(G, index, bBuild)
end
end,

– x, y: Coordinates in cell
– bBuild: If true, build missing cell
– Returns: Cell matching x and y

GetCellFromXY = function(G, x, y, bBuild)
return G:GetCell(GetRow(G, y), GetColumn(G, x), bBuild)
end
},

– Constructor
– x, y: Grid coordinates
– w, h: Grid dimensions
– cols: Column count
– rows: Row count

function(G, x, y, w, h, cols, rows)
G.cellw, G.cellh, G.x, G.y, G.cols, G.rows = w / cols, h / rows, x, y, cols, rows

G:Clear()
end)[/lua] [import]uid: 27791 topic_id: 31572 reply_id: 126162[/import]

Out of curiosity, do the lines that the players draw move, or do they stay in place after they’re drawn? The reason I ask is, if they stay in place, then it should be necessary to see if a line intersects any others only when it’s drawn – no need to check again every frame, since nothing will have changed.

I suppose the lines must be moving, since you mention the lines having “old” and “new” x,y coordinates, but I thought I’d ask anyway.

Also, how have you implemented your isIntersecting function? Some ways are certainly more efficient than others, so there may be room for optimization there.

Last, you might want to consider checking intersections only every, say, 5 frames (or check 1/5 the possibilities per frame). The player won’t notice the slight delay in declaring a loss, but you’ll get a performance boost.

  • Andrew [import]uid: 109711 topic_id: 31572 reply_id: 126178[/import]

Thank you all so much for taking time to help me!
I’ve rewritten the collision detection code to work with a grid for now. This seems to increase the performance a lot. I’m a little worried what happens when a line crosses a grid though, but the chance of that happening AND colliding (intersecting) with such a line is not going to happen that often. For now I’ll leave it as is, but when it turns out this method is not that tight, I’m probably going to switch to using a B-tree. Thanks for that suggestion!

I’ve never used B-trees before, so I’ll have to figure that one out sooner or later. But for now, thanks a lot, guys! [import]uid: 86582 topic_id: 31572 reply_id: 126250[/import]