Need something better than Touched events

Lately I have been finding Touched and TouchEnded events to be extremely unreliable. Most commonly I’ve seen Touched events fired multiple times without getting a TouchEnded in between, as well as TouchEnded and Touched events get fired while I’m standing still inside the middle of a part, as well as Touched events getting triggered when I’m not doing anything at all.

I need a simple and reusable way of guaranteeing that I get notified exactly once when I touch an object for the first time, and exactly once when I stop touching an object.

What I would like is some function like this:

-- Call Func whenever anything touches Part, and then don't call it again until
-- a TouchEnded has been received.  Func looks like function Func(TouchingPart).
-- When Func is called, game.Workspace.GetTouchingParts(Part) is guaranteed to
-- contain TouchingPart
function RegisterTouchHandler(Part, Func)

-- Call Func whenever anything stops touching Part.  Func looks like
-- function Func(TouchingPart).  When Func is called,
-- game.Workspace.GetTouchingParts(Part) is guaranteed to not contain
-- TouchingPart
function RegisterTouchEndedHandler(Part, Func)

Does anyone have a proposed implementation for such a function that is performant? I am thinking to spawn a coroutine and run a loop which calls GetTouchingParts() every 250 milliseconds or so as that seems to be more reliable, but I’m wondering if anyone has better ideas.

2 Likes

in order to prevent the Touch and TouchEnded events being fired multiple times, have you tried implementing a debounce?

Yes but that doesn’t help because the multiple events don’t always happen in rapid succession. For example, if I move my character inside of a part which I’m watching for touched events on, I get the event, and then if later (anytime at all later) call Humanoid:MoveTo() on a pet to have them move to my player, as soon as the pet touches the part, my player gets another Touched event even though he’s been standing completely still the entire time.

Then I thought “well maybe I can just keep track of whether I’ve gotten a Touched event with no TouchEnded event and ignore those Touched events” but now since I’m getting multiple touches, I’m also getting multiple TouchEndeds.

So now I have to start ref-counting the touches. It all seems incredibly hacky and I have no idea if it’s going to work in the general case, because every time I try to workaround the problems, I discover something else later.

1 Like

i’ll give my proposal, similar to what you said, about using GetTouchingParts and then the tracking parts to check when they stop touching and using the touched event when the start touching, but because their might be some inaccuracies if a part stops touching a part just for a split second you might want to use tick() to deal with potential false-positives so in a theoretical sense it would go something like this:

local ActiveThreads = {}


function TouchHandler(Part, OnTouchFunc, OnTouchEndedFunc)
     ActiveThreads[Part] = true
          coroutine.resume(coroutine.create( function()
	       local TouchingParts = {}
	       Part.Touched:Connect(function(part)
		        if not table.find(TouchingParts , part) then
		            TouchingParts[part] = tick()
		           OnTouchFunc(part)
		         end
		     end)
	      while ActiveThreads[Part] do
		   wait(.1)  --- you can change this
		    local GetTouchingParts = Part:GetTouchingParts()
		    local TouchEndedParts = {}
		         for part, Tick in pairs(TouchingParts) do
			        if math.abs(Tick - tick()) > .3 then -- and this too!
			           if not table.find(GetTouchingParts, part) then
				       table.insert(TouchEndedParts, part)
				       TouchingParts[part] = nil
				      end
				 end
			 end
			 if #TouchEndedParts == 1 then
				 OnTouchEndedFunc(TouchEndedParts[1])
			     elseif #TouchEndedParts > 1 then
				 OnTouchEndedFunc(TouchEndedParts)
			 end
		 end
	end))
end

local Part = game.Workspace.Part

local function OnTouchEnded(part)
	print(Part.Name.." Stopped Touching " ..part.Name)
end

local function OnTouched(part)
	print(Part.Name.." Touched "..part.Name)
end

TouchHandler(Part, OnTouched, OnTouchEnded )

i Also have a example of a very simple “custom” Player Touched function similar to the one in my example i provided above, Here (i really just made it for fun and out of curiosity , but it might be helpful)


Edit: Another thing that could that could be an option is to compare velocities between the touching objects to test if force is still enacting on them, in other words if they are settled or not (if that makes sense) (i haven’t tested this yet though)


On another note i do agree with you that the TouchedEnded and Touched can be quite inaccurate sometimes or i should say unreliable (possibly due to them be almost primarily physics based in roblox ). If you are ever looking for alternatives to touched events (including GetTouchingParts) you can turn to Lua-Implemented Intersection algorithms and “libraries”, like the One @IdiomicLanguage made (GJK), the only thing is that because unfortunately we aren’t given much info about certain dimensions and what makes up certain object ( i.e vertices and mesh data and such), it makes it “hard” to determine the boundaries of certain objects in the 3d world.

3 Likes

So after I wrote that I sat down to try to write the TouchDetector I had in mind. It ends up being pretty similar to yours, here’s what I came up with:

local List = require(game.ReplicatedFirst.Utility.List)

local TouchDetector = {}

--struct Registrations {
--  Callback[] TouchedRegistrations
--  Callback[] TouchEndedRegistrations
--  Part[] CurrentlyTouchingParts
--}

--struct RegistrationTable {
--	map<Part, Registrations> Registrations
--}

local RegistrationTable = {}
local IsRunning = false

local function GetRegistrationEntryForPart(Part)
	local Registrations = RegistrationTable[Part]
	if not Registrations then
		Registrations = {
			TouchedRegistrations = {},
			TouchEndedRegistrations = {},
			CurrentlyTouchingParts = {}
		}
		RegistrationTable[Part] = Registrations
	end
	return Registrations
end

function TouchDetector.RegisterTouchDetector(Part, Func)
	assert(Part and Func)
	local Registrations = GetRegistrationEntryForPart(Part)
	table.insert(Registrations.TouchedRegistrations, Func)
end

function TouchDetector.RegisterTouchEndedDetector(Part, Func)
	assert(Part and Func)
	local Registrations = GetRegistrationEntryForPart(Part)
	table.insert(Registrations.TouchEndedRegistrations, Func)
end

spawn(
	function()
		while (true) do
			wait(0.25)
			for Part, Registration in pairs(RegistrationTable) do
				local Touching = Part:GetTouchingParts()
				-- For each part that is touching that we don't currently know about,
				-- fire a NotifyTouched event
				for _, T in ipairs(Touching) do
					if not List.Contains(Registration.CurrentlyTouchingParts, T) then
						table.insert(Registration.CurrentlyTouchingParts, T)
						for _, F in ipairs(Registration.TouchedRegistrations) do
							F(T)
						end
					end
				end
				
				-- For each part that we know about that that isn't currently touching
				-- fire a NotifyTouchEnded event.
				local I = 1
				while I < #Registration.CurrentlyTouchingParts do
					local T = Registration.CurrentlyTouchingParts[I]
					if not List.Contains(Touching, T) then
						table.remove(Registration.CurrentlyTouchingParts, I)
						for _, F in ipairs(Registration.TouchEndedRegistrations) do
							F(T)
						end
					else
						I = I + 1
					end
				end
			end
		end
	end)

return TouchDetector

You can use it almost exactly like the actual Touched / TouchEnded events.

local TouchDetector = require(game.ReplicatedStorage.TouchDetector)

TouchDetector.RegisterTouchDetector(game.Workspace.Part1, 
    function(Part)
        print(string.format("Part %s touched %s", Part.Name, game.Workspace.Part1.Name))
    end)

and similarly for other parts. It’s nice in that it only uses a single co-routine no matter how many parts you try to watch, which means that you also only call GetTouchingParts() once for each unique part you add a detector to.

2 Likes