Trail Collision Detection

How can they not be straight segments? Even Roblox’ Trail object uses straight segments.

A*O(log(N)+k) and B*O(N) and here in this case A is much bigger than B. I think my method would be faster if you don’t put more than 10 thousand objects and at that point probably both of the equations would lag too much anyway. I suppose you would want to run this on mobile. There is also the point of FindPartsInRegion3 being not actually an accurate way of collision detection since the region orientation is fixed. There is also the point of you actually creating and deleting those parts which is a cause of lag on its own. With my method, you wouldn’t need to put any parts.

1 Like

My bad about the trail segments, I thought you were storing one segment per trail not multiple segments

idk I still think it might be difficult to get good segments though because of non straight player movement and you run the risk of it turning into OP’s scenario 2 (perhaps even more inefficient though because of the math you are doing)

For your collision detection you can use region3 to get an estimate and then something more specific to get more accurate collision detection (although your k would increase) - but you have flexibility with this solution and you can tune it to whatever is optimal for your use case, idk if you really have that for an inherent O(n) solution

Yes it’s nearly the same as scenario 2 but since we’re checking a whole segment not points, you can just listen to it every 0.5 seconds or something and create the segments requiring much less calculations total.

Actually you don’t have to listen to it. Just do previous position to the new position that’s already known. You don’t actually have to send trail info as it is already available to you by knowing just the current position of all the other characters which Roblox already makes readily available.

If a player moves zig zag it will be practically like solution 2 unless you want to completely lose accuracy

I’m not sure what you mean, you still have to look back at previous trail segments that haven’t expired (been in existance for longer than trail.lifetime)

The whole point of it is to get rid of region3.

I’m trying to say that you don’t have to fire remote events for trials at all.

sorry I meant for the region3 idea, you use region3

why would you need remote events for any scenario?

You don’t mean using remote events to send player position for trails, right?

I mean I would imagine either the server handling collisions and telling clients or clients handling their own collision and telling server (telling = sending through remote events)

But that is only when there is a collision and would be the exact same as your scenario

Checking position would obviously not be done via remote events lol

Alright yeah, that’d be the good way to do it. There’s also the fact that you can combine segments. While adding a new segment, check if their lookvectors are very close (product the two) to each other and if they are don’t create a new entry but just update their tick time and length. Even though you would lose disappearance accuracy doing it maximum of twice per segment wouldn’t cause any trouble and it might even give you a 20% increase in performance on average.

1 Like

Merging segments sounds like a good idea
I’m assuming by disappearance accuracy you mean trail lifetime thing? If so then you could augment your segment by adding a queue of {originPosition,tick} and when you are checking lifetime, you check against queue:Top() and if it is < time you remove just that part of the segment and iterate through etc

But how do you get the specific 20% number? :wink:

You could do that but we’re trying to be very efficient here. If you wanted to do that, you can combine every straight line and get better performance.

If the player was always to walk straight you would gain 50% performance increase and if I make the assumption that the player turns every 3 segments on average, it’d be 20%.

I kinda wanna code a solution for this so I’ll show you what I mean maybe but its: either you create a new segment, or (EXCLUSIVE OR), if your conditions for merging segments are satisfied, instead of just updating the tick and length of the previous, you push the tick and new origin position to the previous segment’s queue (note: instead of doing origin position you could do length to maintain the level of accuracy your solution currently has, but origin position would allow updatnig slope to most recent)

And when you check for segment collision you would do what you said with just the Top of the queue of each segment (not all items so checking (which is the main bottleneck in your solution) would be the same speed)

Can you explain a little more xd?

I said combine every straight line only once so if the guy is moving straight, instead of using say 100, you’d be using 50 segments (since it’s O(N) method, it’s 50% increase in performance) and if the player takes a turn every third segment (assuming) that means a segment will be left which changes that (1/2) to (1/2)*(1/3) which is 1/5 and that is 20%.

1 Like

Ok, I must’ve missed combining only once

That’s what I meant there. Twice per segment as in combining it with the previous once.

Well I made a script for what I meant and the cylinder collision idea you said
I haven’t really benchmarked it though but if you (@Fm_Trick ) decide to use this type of method instead of the region3 please lmk

--[[
	TrailCollision
	
	Detects whether trail collides
	
	100% abstract and is O(n) but merges segments with similar directions
	accurate as sampling is
	uses cylinder-point collision (treats trail as a cylinder)
	
	does not handle rotations
	
	Idea:
	https://devforum.roblox.com/t/trail-collision-detection/148935
	
--]]

-- Constants
local mergeThreshold = math.cos(math.rad(5)) -- cos of merge threshold

-- cached vars
local tick = tick

-- main

local function addSegment(trail,start,fin,expireTime,canMerge)
	-- expireTime = tick()+lifetime
	-- canMerge = is it safe to try merging with previous segment? (if you have been calling addsegment without pause (can assume that start of this segment = end of previous segment) then set canMerge to true
	-- note that canmerge will not restrict new segments from merging with this segment
	
	local i = trail[3]
	
	local sx = start.X
	local sy = start.Y
	local sz = start.Z
	
	local dx = fin.X-sx
	local dy = fin.Y-sy
	local dz = fin.Z-sz
	
	-- normalize d
	dx = dx*dx
	dy = dy*dy
	dz = dz*dz
	
	start = (dx+dy+dz)^.5 -- reuse vars, this is actually len
	fin = 1/start -- just a temporary now
	dx = dx*fin
	dy = dy*fin
	dz = dz*fin
	-- fin norm z
	
	local prev = trail[i]
	if canMerge and i ~= 3 and prev and (prev[1]*dx + prev[2]*dy + prev[3]*dz) > mergeThreshold then -- dot product (both vecs are normalized already)
		-- add to segment
		prev[7] = prev[7]+start -- update total segment length
		
		-- get insertion point and update size
		local j = prev[9]
		j = j+2
		prev[9] = j
		
		-- insert to q
		prev[j] = start
		prev[j+1] = expireTime
	else
		-- make a new segment
		i=i+1
		trail[3] = i
		trail[i] = {
			-- direction
			dx, -- 1
			dy, -- 2
			dz,-- 3
			-- start
			sx, -- 4
			sy, -- 5
			sz, -- 6
			start, -- 7: total segment len
			10, -- 8: queue start pos
			10, -- 9: queue end pos (start with 1 elem (curr segment))
			
			start, -- 10: segment len
			expireTime, -- 11: segment expire time
		}
	end
end

local function collides(trail,pos) -- sqdist if collides, nil if not
	-- O(n)
	local flag -- whether or not to check expiration on future segments
	local time = tick()
	local sqrad = trail[1]
	for i = trail[2],trail[3] do
		local seg = trail[i]
		
		local dx
		local dy
		local dz
		
		-- point in local coordinates of start (pos-start)
		local px
		local py
		local pz
		
		local totalLen

		if not flag then -- check expiration
			local addLen = 0 -- len to add
			
			-- start and end of q
			local start = seg[8]
			local fin = seg[9]
			
			for j = start,fin,2 do
				if time>seg[j+1] then
					-- this piece of the segment is expired
					local len = seg[j]
					addLen = addLen+len
					
					seg[j] = nil
					seg[j+1] = nil
					
					start=start+2 -- increase start pos
				else
					flag = true -- no other ones after this will be expired since sequential
					break 
				end
			end
			
			if start>fin then
				-- entire segment is expired
				trail[i] = nil -- remove from trail
				trail[2] = i+1 -- increase start pos
							
				seg = nil -- use as flag to tell that it expired so dont check collision
				-- feels bad not having continue
			else
				totalLen = seg[7] - addLen -- subtract expired segments
				seg[7] = totalLen -- update total len
				seg[8] = start -- update start pos
				
				dx = seg[1]
				dy = seg[2]
				dz = seg[3]
				
				-- update new start
				px = seg[4]+dx*addLen
				py = seg[5]+dy*addLen
				pz = seg[6]+dz*addLen
				
				seg[4] = px
				seg[5] = py
				seg[6] = pz
				
				-- point in local cords
				px = pos.X-px
				py = pos.Y-py
				pz = pos.Z-pz
			end
		end

		if seg then -- its not expired
			if not dx then
				-- if flag was true then retrieve vars
				dx = seg[1]
				dy = seg[2]
				dz = seg[3]
				
				px = pos.X-seg[4]
				py = pos.Y-seg[5]
				pz = pos.Z-seg[6]
				
				totalLen = seg[7]
			end
			
			-- cylinder-point collision
			local dot = dx*px + dy*py + dz*pz
			if dot > 0 and dot < totalLen then
				local sqdist = px*px+py*py+pz*pz - dot*dot
				if sqdist < sqrad then
					return sqdist
				end
			end
		end
	end
end

local TrailCollision = {
	AddSegment = addSegment,
	Collides = collides,
}

function TrailCollision.New(rad)
	return {
		AddSegment = addSegment,
		Collides = collides,
		
		rad*rad, -- sq rad
		4, -- queue start position
		3, -- queue end position
		
	}
end

return TrailCollision

Sample place:
TrailCollision.rbxl (17.1 KB)

5 Likes

I will look through this and try implementing it when I get some time! I’ll let you know what I find in terms of efficiency differences

2 Likes