Page 1 of 1

enemy/projectile collisions stops working when colliding with 2 or more bullets

Posted: Fri Dec 13, 2024 5:52 am
by tektonic99
This is the code I'm using to detect collisions for projectiles in my game:
code.png
code.png (66.99 KiB) Viewed 1155 times
From the windfield physics library, I'm using its collider:enter('name') function which is basically just the beginContact callback for box2D, and it works perfectly fine until multiple bullets try to hit an enemy at the same time. In that case, one of them hits the enemy, and the others simply phases through without colliding. I have this collision function inside the main enemy loop because it allows me to create AoE projectiles that collide with multiple enemies for explosions etc. Is there anyway to still use box2D physics for my projectiles while also fixing the phasing issue?

Here's the function in the enemy main loop:
code2.png
code2.png (64.35 KiB) Viewed 1155 times
I'd really appreciate any ideas, I've been lost for a while now, thanks :awesome:

Re: enemy/projectile collisions stops working when colliding with 2 or more bullets

Posted: Sat Dec 14, 2024 1:37 am
by RNavega
tektonic99 wrote: Fri Dec 13, 2024 5:52 am In that case, one of them hits the enemy, and the others simply phases through without colliding.
I don't have experience with Windfield, but if I'm reading the source right, the library only keeps the enter data for the 'latest' contact with a certain collision class.
It's this line here: https://github.com/a327ex/windfield/blo ... t.lua#L816
(edit: which means, only the latest bullet to hit on that frame would occupy the enter data, not all of them)

However, one line above that, it does keep track of multiple contacts in the stay data.

So instead of...

Code: Select all

local collision_data = ec:getEnterCollisionData('projectile')
local b = collision_data.collider:getObject()
(...)
You'd do something like this, as long as you delete the projectile right after contact so this doesn't happen each frame that the projectile is passing through the object:

Code: Select all

local all_stay_data = ec:getStayCollisionData('projectile')
for stay_index = 1, #all_stay_data do
    local collision_data = all_stay_data[stay_index]
    local b = collision_data.collider:getObject()
    (...)
end

Re: enemy/projectile collisions stops working when colliding with 2 or more bullets

Posted: Sat Dec 14, 2024 8:33 am
by RNavega
...By the way, I think callbacks would be the ideal pattern for this projectile hit behavior, where you only need to react once when the event happens.
So you could be processing the projectile hits inside the callback you give to Windfield's :setPreSolve(), instead of polling every frame with enemyCollisionManager().

Docs link:
https://github.com/a327ex/windfield?tab ... vecallback

Re: enemy/projectile collisions stops working when colliding with 2 or more bullets

Posted: Sat Dec 14, 2024 11:32 pm
by tektonic99
RNavega wrote: Sat Dec 14, 2024 8:33 am ...By the way, I think callbacks would be the ideal pattern for this projectile hit behavior, where you only need to react once when the event happens.
So you could be processing the projectile hits inside the callback you give to Windfield's :setPreSolve(), instead of polling every frame with enemyCollisionManager().

Docs link:
https://github.com/a327ex/windfield?tab ... vecallback
Thanks for the feedback. I moved everything to :setPreSolve() and changed the AoEs to queries instead of physical bodies. It's all working as expected now. Thanks a lot for the idea and explanation, helped me a lot.

Re: enemy/projectile collisions stops working when colliding with 2 or more bullets

Posted: Wed Dec 18, 2024 4:29 pm
by Divinity
also, careful with organized tables loops (such as ipairs), because when you remove an item while iterating (like on your line 25), the next iteration index has been moved 1 down. I would recommend iterating end-to-start

https://stackoverflow.com/questions/123 ... -iterating

Re: enemy/projectile collisions stops working when colliding with 2 or more bullets

Posted: Fri Dec 20, 2024 1:52 am
by RNavega
Divinity wrote: Wed Dec 18, 2024 4:29 pm also, careful with organized tables loops (such as ipairs), because when you remove an item while iterating (like on your line 25), the next iteration index has been moved 1 down. I would recommend iterating end-to-start
After reading your comment (great catch btw), I was going to suggest "use a linked list, it's very fast for removing arbitrary items".
But after profiling a bit, there's some considerations re: performance:

Code: Select all

Starting...
0.0088169407157843      <-- LINKED-LIST (REMOVE MIDDLE)
0.015085826617797       <-- TABLE (REMOVE MIDDLE)
0.0037212258294996      <-- TABLE (REMOVE LAST)

0.017078480483006       <-- LINKED-LIST (ITERATE)
0.0058362494843696      <-- TABLE (ITERATE)
Finished.
- It's faster to remove the first item in the sequence using a linked list, than removing the first item with table.remove().
- Removing the last item is by far the fastest with table.remove() than with a linked list (like when you don't use an index, it pops the last item).
- Iterating the entire sequence is faster when done with an index FOR loop and the array part of a table, than using a WHILE loop and iterating all nodes in the linked list.

The code I used for those timings is this, the double-linked list implementation is from me:

Code: Select all

io.stdout:setvbuf('no')

-- Used for making DOUBLY-LINKED LISTS of nodes.
--
-- The list itself is a table that stores a reference to the first
-- and last nodes, if the list isn't empty:
-- {[1] = FIRST NODE, [2] = LAST NODE}
--
-- For maximum speed, the nodes are array-only tables in this format:
-- {[1] = VALUE, [2] = PREVIOUS NODE, [3] = NEXT NODE}
local LinkedList = {}

function LinkedList.new(self, ...)
    local newList = {
        nil, -- [1] = Reference to the first node, if any.
        nil, -- [2] = Reference to the last node, if any.
        -- Functions.
        add        = LinkedList.add,
        disconnect = LinkedList.disconnect,
        clear      = LinkedList.clear,
    }
    return newList
end
setmetatable(LinkedList, {__call = LinkedList.new})


function LinkedList.add(list, value)
    local newNode = {value, list[2], nil}
    if list[2] then
        list[2][3] = newNode
    else
        -- If there's no last node then there's no first node either, because
        -- a one-node list has the same node in both first and last places.
        list[1] = newNode
    end
    -- Set the new node as the last.
    list[2] = newNode
    return newNode
end


function LinkedList.disconnect(list, node)
    -- Reconnect the neighboring nodes, if any, so as to remove the
    -- input node from the list.
    --
    if node[2] then -- If the node has another behind it (so this isn't the first node).
        node[2][3] = node[3]
        if node[3] then -- If the node has another ahead of it (so this isn't the last node).
            node[3][2] = node[2]
        else
            list[2] = node[2]
        end
    elseif node[3] then -- If this (first node) has another in front of it.
        node[3][2] = nil
        list[1] = node[3]
    else -- Otherwise this was both the first and last node, the list is now empty.
        list[1] = nil
        list[2] = nil
    end
    -- For garbage-collection safety, remove references to other nodes.
    node[2] = nil
    node[3] = nil
end


function LinkedList.clear(list)
    local node = list[1]
    local oldNode
    while node do
        oldNode = node
        node = node[3]
        -- Remove references to other nodes.
        oldNode[2] = nil
        oldNode[3] = nil
    end
    list[1] = nil
    list[2] = nil
end


-- =====================
-- MEASURE TIMINGS
-- =====================

function deltaTime(func)
    local shortestTime = math.huge
    for x = 1, 3 do
        local startTime = love.timer.getTime()
        for y = 1, 100000 do
            func()
        end
        local endTime = love.timer.getTime() - startTime
        if endTime < shortestTime then
            shortestTime = endTime
        end
    end
    return shortestTime
end


local list = LinkedList()
local middleNode
for x = 1, 26 do
    local node = list:add(x)
    if x == 13 then
        middleNode = node
    end
end
local listDisconnect = list.disconnect
local listAdd = list.add


function workListMiddle()
    local middleValue = middleNode[1]
    listDisconnect(list, middleNode)
    middleNode = listAdd(list, middleValue)
end


function workListIterate()
    local result = 0
    local node = list[1]
    while node do
        result = result + node[1]
        node = node[3]
    end
    return result
end


local array = {}
for x = 1, 26 do
    array[x] = x
end
local tableRemove = table.remove
local tableInsert = table.insert


function workTableMiddle()
    local lastValue = array[13]
    tableRemove(array, 13)
    tableInsert(array, 13, lastValue)
end


function workTableIterate()
    local result = 0
    for x = 1, 26 do
        result = result + array[x]
    end
    return result
end


function workTableLast()
    local lastValue = array[26]
    tableRemove(array)
    tableInsert(array, lastValue)
end


print('Starting...')
love.timer.sleep(2)
print(deltaTime(workListMiddle), '<-- LINKED-LIST (REMOVE MIDDLE)')
love.timer.sleep(1)
print(deltaTime(workTableMiddle), '<-- TABLE (REMOVE MIDDLE)')
love.timer.sleep(1)
print(deltaTime(workTableLast), '<-- TABLE (REMOVE LAST)')
print()
love.timer.sleep(1)
print(deltaTime(workListIterate), '<-- LINKED-LIST (ITERATE)')
love.timer.sleep(1)
print(deltaTime(workTableIterate), '<-- TABLE (ITERATE)')
print('Finished.')


function love.keypressed(key)
    if key == 'escape' then
        love.event.quit()
    end
end
So I don't know, iterating the sequence vs removing items seem to cancel each other out in terms of time taken on each structure, and it ends up not making a difference? Or rather, since most of the time you'll only be iterating the sequence to update projectiles, and any deletions will be occasional (only when a projectile hits something etc), then that makes the "end-to-start looping" on a raw array table the fastest solution on average.
But this is stuff that probably won't make a perceptible difference, you're not gonna have tens of thousands of projectiles on screen.