TouchedListener Event for Parts

Touched Listener Event

Why?

Roblox’s touched listener for BaseParts is vulnerable to exploiters due to the fact that they can remove touch interests, or be forced to not be fired. This means that you could have a kill brick using a touched listener, and exploiters can cause that event to not be fired.

Source 1

Solution

The solution I came up with was to create my own touched event listener so I could get more customization and combat exploiters.

V1.0.0 Touched Listener
--[[
	Author: buutterfIIes
	Version: v1.0.0
	Description: TouchedListener runs every function thats connected to it each time the part is touched
	Uses RunService to detect parts
	
	https://devforum.roblox.com/t/all-about-object-oriented-programming/8585 for information on Object Oriented Programming in Luau
	
	-- EXAMPLE --
	
	local TouchedEvent = require(script.TouchedListener)

	local Event = TouchedEvent.new(workspace.Part)
	Event:Connect(function(Part)
		if Part.Parent:FindFirstChild("Humanoid") then
			Part.Parent.Humanoid:TakeDamage(math.huge)
		end
	end)

	Event:Once(function(Part)
		print("Works once!")
	end)

	Event:Wait()

	print("Waits for touch")
	
	-- EXAMPLE --
]]

local RunService = game:GetService("RunService")

type T_TouchedListener = {
	Connect: (self: T_TouchedListener, Callback: (OtherPart: BasePart | MeshPart) -> any) -> nil,
	Disconnect: (self: T_TouchedListener) -> nil,
	Wait: (self: T_TouchedListener) -> thread,
	Once: (self: T_TouchedListener, Callback: (OtherPart: BasePart | MeshPart) -> any) -> nil,
	Connections: {T_Connection},
	TouchedPartsCache: {BasePart | MeshPart},
	MainConnection: RBXScriptConnection,
	Params: OverlapParams
}

local TouchedListener = {}
TouchedListener.__index = TouchedListener

function TouchedListener.new(Part: BasePart | MeshPart, Params: OverlapParams): T_TouchedListener
	-- Define Params variable if it isn't provided
	if Params == nil then
		Params = OverlapParams.new()
		Params.FilterType = Enum.RaycastFilterType.Exclude
		Params.FilterDescendantsInstances = {Part, workspace.Baseplate}
	end

	local NewEvent = {}

	setmetatable(NewEvent, TouchedListener)

	NewEvent.Connections = {}
	NewEvent.TouchedPartsCache = {}
	NewEvent.Params = Params

	local function PartsTouchedHandler(PartsTouching)
		local PartsTouchingMap = {}

		for _, TouchingPart in PartsTouching do
			-- Add part to cache
			PartsTouchingMap[TouchingPart] = true

			if NewEvent.TouchedPartsCache[TouchingPart] == nil then
				NewEvent.TouchedPartsCache[TouchingPart] = true
				
				-- Loop through connections
				for Index, Connection: T_Connection in ipairs(NewEvent.Connections) do
					-- Call the callback function
					Connection.Callback(TouchingPart)
					
					-- Remove the callback function if event is supposed to callback once
					if Connection.Once then
						table.remove(NewEvent.Connections, Index)
					end
				end
			end
		end

		-- Remove part from cache if its no longer touching
		for CachedPart, _ in NewEvent.TouchedPartsCache do
			if not PartsTouchingMap[CachedPart] then
				NewEvent.TouchedPartsCache[CachedPart] = nil
			end
		end
	end

	NewEvent.MainConnection = RunService.Heartbeat:Connect(function()
		if Part:IsA("BasePart") then			
			-- Get parts touching
			local PartsTouching = workspace:GetPartBoundsInBox(Part.CFrame, Part.Size, NewEvent.Params)

			PartsTouchedHandler(PartsTouching)
		elseif Part:IsA("MeshPart") then
			-- Get parts touching
			local PartsTouching = workspace:GetPartsInPart(Part, Params)

			PartsTouchedHandler(PartsTouching)
		end
	end)

	return NewEvent
end

-- Method to add a new callback function to Connections table of event object 
function TouchedListener:Connect(Callback)
	if typeof(Callback) ~= "function" then
		error("Callback of TouchedListener:Connect must be a function. Got: " .. typeof(Callback))
	end

	table.insert(self.Connections, {Callback = Callback, Once = false})
end

-- Method to disconnect TouchedListener when no longer in use
function TouchedListener:Disconnect()
	self.MainConnection:Disconnect()
	table.clear(self.Connections)
end

-- Method to wait for a signal fire
function TouchedListener:Wait()
	local Thread = coroutine.running()

	self:Once(function()
		task.spawn(Thread)
	end)

	return coroutine.yield()
end

-- Method to call functions once when part is touched
function TouchedListener:Once(Callback)
	if typeof(Callback) ~= "function" then
		error("Callback of TouchedListener:Once must be a function. Got: " .. typeof(Callback))
	end

	table.insert(self.Connections, {Callback = Callback, Once = true})
end

return TouchedListener

Basic Documentation

  • :Connect(Callback) Connects a callback function to the event listener

  • :Wait() Waits for the event to be fired

  • :Once() Calls callback once when part is touched

  • :Disconnect() Disconnects event listener and callbacks connected to it

Example

Basic usage example

Example
local TouchedEvent = require(script.TouchedListener)

local Event = TouchedEvent.new(workspace.Part)
Event:Connect(function(Part)
	if Part.Parent:FindFirstChild("Humanoid") then
		Part.Parent.Humanoid:TakeDamage(math.huge)
	end
end)

Event:Once(function(Part)
	print("Works once!")
end)

Event:Wait()

print("Waits for touch")

Client Sided

According to a user named mnxwg2y (on discord) exploiters can intercept roblox events client-sided. It is recommended to use this module on the server.

(Unsure about other events ex: RunService.RenderedStepped though)

V1.0.0 Touched Listener With Warning
--[[
	Author: buutterfIIes
	Version: v1.0.0
	Description: TouchedListener runs every function thats connected to it each time the part is touched
	Uses RunService to detect parts
	
	https://devforum.roblox.com/t/all-about-object-oriented-programming/8585 for information on Object Oriented Programming in Luau
	
	-- EXAMPLE --
	
	local TouchedEvent = require(script.TouchedListener)

	local Event = TouchedEvent.new(workspace.Part)
	Event:Connect(function(Part)
		if Part.Parent:FindFirstChild("Humanoid") then
			Part.Parent.Humanoid:TakeDamage(math.huge)
		end
	end)

	Event:Once(function(Part)
		print("Works once!")
	end)

	Event:Wait()

	print("Waits for touch")
	
	-- EXAMPLE --
]]

local RunService = game:GetService("RunService")


type T_TouchedListener = {
	Connect: (self: T_TouchedListener, Callback: (OtherPart: BasePart | MeshPart) -> any) -> nil,
	Disconnect: (self: T_TouchedListener) -> nil,
	Wait: (self: T_TouchedListener) -> thread,
	Once: (self: T_TouchedListener, Callback: (OtherPart: BasePart | MeshPart) -> any) -> nil,
	Connections: {T_Connection},
	TouchedPartsCache: {BasePart | MeshPart},
	MainConnection: RBXScriptConnection,
	Params: OverlapParams
}

local TouchedListener = {}
TouchedListener.__index = TouchedListener

function TouchedListener.new(Part: BasePart | MeshPart, Params: OverlapParams): T_TouchedListener
	-- Define Params variable if it isn't provided
	if Params == nil then
		Params = OverlapParams.new()
		Params.FilterType = Enum.RaycastFilterType.Exclude
		Params.FilterDescendantsInstances = {Part, workspace.Baseplate}
	end

	local NewEvent = {}

	setmetatable(NewEvent, TouchedListener)

	NewEvent.Connections = {}
	NewEvent.TouchedPartsCache = {}
	NewEvent.Params = Params

	local function PartsTouchedHandler(PartsTouching)
		local PartsTouchingMap = {}

		for _, TouchingPart in PartsTouching do
			-- Add part to cache
			PartsTouchingMap[TouchingPart] = true

			if NewEvent.TouchedPartsCache[TouchingPart] == nil then
				NewEvent.TouchedPartsCache[TouchingPart] = true

				-- Loop through connections
				for Index, Connection: T_Connection in ipairs(NewEvent.Connections) do
					-- Call the callback function
					Connection.Callback(TouchingPart)

					-- Remove the callback function if event is supposed to callback once
					if Connection.Once then
						table.remove(NewEvent.Connections, Index)
					end
				end
			end
		end

		-- Remove part from cache if its no longer touching
		for CachedPart, _ in NewEvent.TouchedPartsCache do
			if not PartsTouchingMap[CachedPart] then
				NewEvent.TouchedPartsCache[CachedPart] = nil
			end
		end
	end

	NewEvent.MainConnection = RunService.Heartbeat:Connect(function()
		if Part:IsA("BasePart") then			
			-- Get parts touching
			local PartsTouching = workspace:GetPartBoundsInBox(Part.CFrame, Part.Size, NewEvent.Params)

			PartsTouchedHandler(PartsTouching)
		elseif Part:IsA("MeshPart") then
			-- Get parts touching
			local PartsTouching = workspace:GetPartsInPart(Part, Params)

			PartsTouchedHandler(PartsTouching)
		end
	end)
	
	if RunService:IsClient() then
		warn("Events are vulnerable to exploiters client sided. Recommended to check touching parts on the server.")
	end

	return NewEvent
end

-- Method to add a new callback function to Connections table of event object 
function TouchedListener:Connect(Callback)
	if typeof(Callback) ~= "function" then
		error("Callback of TouchedListener:Connect must be a function. Got: " .. typeof(Callback))
	end

	table.insert(self.Connections, {Callback = Callback, Once = false})
end

-- Method to disconnect TouchedListener when no longer in use
function TouchedListener:Disconnect()
	self.MainConnection:Disconnect()
	table.clear(self.Connections)
end

-- Method to wait for a signal fire
function TouchedListener:Wait()
	local Thread = coroutine.running()

	self:Once(function()
		task.spawn(Thread)
	end)

	return coroutine.yield()
end

-- Method to call functions once when part is touched
function TouchedListener:Once(Callback)
	if typeof(Callback) ~= "function" then
		error("Callback of TouchedListener:Once must be a function. Got: " .. typeof(Callback))
	end

	table.insert(self.Connections, {Callback = Callback, Once = true})
end

return TouchedListener

Version 1.0.1

Change Log:

  • Added a :ListenToPlayer listener (Gets the player from the character if a character touched the part)
Module
--[[
	Author: buutterfIIes
	Version: v1.0.1
	Description: TouchedListener runs every function thats connected to it each time the part is touched
	Uses RunService to detect parts
	
	https://devforum.roblox.com/t/all-about-object-oriented-programming/8585 for information on Object Oriented Programming in Luau
	
	-- EXAMPLE --
	
	local TouchedEvent = require(script.TouchedListener)

	local Event = TouchedEvent.new(workspace.Part)
	Event:Connect(function(Part)
		if Part.Parent:FindFirstChild("Humanoid") then
			Part.Parent.Humanoid:TakeDamage(math.huge)
		end
	end)

	Event:Once(function(Part)
		print("Works once!")
	end)

	Event:Wait()

	print("Waits for touch")
	
	-- EXAMPLE --
]]

local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

type T_TouchedListener = {
	Connect: (self: T_TouchedListener, Callback: (OtherPart: BasePart | MeshPart) -> any) -> nil,
	Disconnect: (self: T_TouchedListener) -> nil,
	Wait: (self: T_TouchedListener) -> thread,
	Once: (self: T_TouchedListener, Callback: (OtherPart: BasePart | MeshPart) -> any) -> nil,
	ListenForPlayer: (self: T_TouchedListener, Callback: (Player: Player) -> any) -> nil,
	Connections: {T_Connection},
	TouchedPartsCache: {BasePart | MeshPart},
	MainConnection: RBXScriptConnection,
	Params: OverlapParams
}

local TouchedListener = {}
TouchedListener.__index = TouchedListener

function TouchedListener.new(Part: BasePart | MeshPart, Params: OverlapParams): T_TouchedListener
	-- Define Params variable if it isn't provided
	if Params == nil then
		Params = OverlapParams.new()
		Params.FilterType = Enum.RaycastFilterType.Exclude
		Params.FilterDescendantsInstances = {Part, workspace.Baseplate}
	end

	local NewEvent = {}

	setmetatable(NewEvent, TouchedListener)

	NewEvent.Connections = {}
	NewEvent.TouchedPartsCache = {}
	NewEvent.Params = Params

	local function PartsTouchedHandler(PartsTouching)
		local PartsTouchingMap = {}

		for _, TouchingPart in PartsTouching do
			-- Add part to cache
			PartsTouchingMap[TouchingPart] = true

			if NewEvent.TouchedPartsCache[TouchingPart] == nil then
				NewEvent.TouchedPartsCache[TouchingPart] = true

				-- Loop through connections
				for Index, Connection: T_Connection in ipairs(NewEvent.Connections) do
					-- Call the callback function
					Connection.Callback(TouchingPart)

					-- Remove the callback function if event is supposed to callback once
					if Connection.Once then
						table.remove(NewEvent.Connections, Index)
					end
				end
			end
		end

		-- Remove part from cache if its no longer touching
		for CachedPart, _ in NewEvent.TouchedPartsCache do
			if not PartsTouchingMap[CachedPart] then
				NewEvent.TouchedPartsCache[CachedPart] = nil
			end
		end
	end

	NewEvent.MainConnection = RunService.Heartbeat:Connect(function()
		if Part:IsA("BasePart") then			
			-- Get parts touching
			local PartsTouching = workspace:GetPartBoundsInBox(Part.CFrame, Part.Size, NewEvent.Params)

			PartsTouchedHandler(PartsTouching)
		elseif Part:IsA("MeshPart") then
			-- Get parts touching
			local PartsTouching = workspace:GetPartsInPart(Part, Params)

			PartsTouchedHandler(PartsTouching)
		end
	end)

	if RunService:IsClient() then
		warn("Events are vulnerable to exploiters client sided. Recommended to check touching parts on the server.")
	end

	return NewEvent
end

-- Method to add a new callback function to Connections table of event object 
function TouchedListener:Connect(Callback)
	if typeof(Callback) ~= "function" then
		error("Callback of TouchedListener:Connect must be a function. Got: " .. typeof(Callback))
	end

	table.insert(self.Connections, {Callback = Callback, Once = false})
end

-- Method to disconnect TouchedListener when no longer in use
function TouchedListener:Disconnect()
	self.MainConnection:Disconnect()
	table.clear(self.Connections)
end

-- Method to wait for a signal fire
function TouchedListener:Wait()
	local Thread = coroutine.running()

	self:Once(function()
		task.spawn(Thread)
	end)

	return coroutine.yield()
end

-- Method to call functions once when part is touched
function TouchedListener:Once(Callback)
	if typeof(Callback) ~= "function" then
		error("Callback of TouchedListener:Once must be a function. Got: " .. typeof(Callback))
	end

	table.insert(self.Connections, {Callback = Callback, Once = true})
end

-- Custom Connections
function TouchedListener:ListenForPlayer(Callback)
	if typeof(Callback) ~= "function" then
		error("Callback of TouchedListener:ListenForPlayer must be a function. Got: " .. typeof(Callback))
	end
	
	self:Connect(function(OtherPart)
		local Humanoid = OtherPart.Parent:FindFirstChildOfClass("Humanoid")
		
		if Humanoid then
			local Player = Players:GetPlayerFromCharacter(Humanoid.Parent)
			
			Callback(Player)
		end
	end)
end

return TouchedListener

Version 1.0.2

Change Log:

  • Made listener use :GetPartsInPart instead of :GetPartBoundsInBox for BaseParts
  • Fixed MeshParts using passed in params instead of the created object’s params
Module
--[[
	Author: buutterfIIes
	Version: v1.0.2
	Description: TouchedListener runs every function thats connected to it each time the part is touched
	Uses RunService to detect parts
	
	https://devforum.roblox.com/t/all-about-object-oriented-programming/8585 for information on Object Oriented Programming in Luau
	
	-- EXAMPLE --
	
	local TouchedEvent = require(script.TouchedListener)

	local Event = TouchedEvent.new(workspace.Part)
	Event:Connect(function(Part)
		if Part.Parent:FindFirstChild("Humanoid") then
			Part.Parent.Humanoid:TakeDamage(math.huge)
		end
	end)

	Event:Once(function(Part)
		print("Works once!")
	end)

	Event:Wait()

	print("Waits for touch")
	
	-- EXAMPLE --
]]

local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

type T_TouchedListener = {
	Connect: (self: T_TouchedListener, Callback: (OtherPart: BasePart | MeshPart) -> any) -> nil,
	Disconnect: (self: T_TouchedListener) -> nil,
	Wait: (self: T_TouchedListener) -> thread,
	Once: (self: T_TouchedListener, Callback: (OtherPart: BasePart | MeshPart) -> any) -> nil,
	ListenForPlayer: (self: T_TouchedListener, Callback: (Player: Player) -> any) -> nil,
	Connections: {T_Connection},
	TouchedPartsCache: {BasePart | MeshPart},
	MainConnection: RBXScriptConnection,
	Params: OverlapParams
}

local TouchedListener = {}
TouchedListener.__index = TouchedListener

function TouchedListener.new(Part: BasePart | MeshPart, Params: OverlapParams): T_TouchedListener
	-- Define Params variable if it isn't provided
	if Params == nil then
		Params = OverlapParams.new()
		Params.FilterType = Enum.RaycastFilterType.Exclude
		Params.FilterDescendantsInstances = {Part, workspace.Baseplate}
	end

	local NewEvent = {}

	setmetatable(NewEvent, TouchedListener)

	NewEvent.Connections = {}
	NewEvent.TouchedPartsCache = {}
	NewEvent.Params = Params

	local function PartsTouchedHandler(PartsTouching)
		local PartsTouchingMap = {}

		for _, TouchingPart in PartsTouching do
			-- Add part to cache
			PartsTouchingMap[TouchingPart] = true

			if NewEvent.TouchedPartsCache[TouchingPart] == nil then
				NewEvent.TouchedPartsCache[TouchingPart] = true

				-- Loop through connections
				for Index, Connection: T_Connection in ipairs(NewEvent.Connections) do
					-- Call the callback function
					Connection.Callback(TouchingPart)

					-- Remove the callback function if event is supposed to callback once
					if Connection.Once then
						table.remove(NewEvent.Connections, Index)
					end
				end
			end
		end

		-- Remove part from cache if its no longer touching
		for CachedPart, _ in NewEvent.TouchedPartsCache do
			if not PartsTouchingMap[CachedPart] then
				NewEvent.TouchedPartsCache[CachedPart] = nil
			end
		end
	end

	NewEvent.MainConnection = RunService.Heartbeat:Connect(function()
		if Part:IsA("BasePart") then			
			-- Get parts touching
			local PartsTouching = workspace:GetPartsInPart(Part.CFrame, Part.Size, NewEvent.Params)

			PartsTouchedHandler(PartsTouching)
		elseif Part:IsA("MeshPart") then
			-- Get parts touching
			local PartsTouching = workspace:GetPartsInPart(Part, NewEvent.Params)

			PartsTouchedHandler(PartsTouching)
		end
	end)

	if RunService:IsClient() then
		warn("Events are vulnerable to exploiters client sided. Recommended to check touching parts on the server.")
	end

	return NewEvent
end

-- Method to add a new callback function to Connections table of event object 
function TouchedListener:Connect(Callback)
	if typeof(Callback) ~= "function" then
		error("Callback of TouchedListener:Connect must be a function. Got: " .. typeof(Callback))
	end

	table.insert(self.Connections, {Callback = Callback, Once = false})
end

-- Method to disconnect TouchedListener when no longer in use
function TouchedListener:Disconnect()
	self.MainConnection:Disconnect()
	table.clear(self.Connections)
end

-- Method to wait for a signal fire
function TouchedListener:Wait()
	local Thread = coroutine.running()

	self:Once(function()
		task.spawn(Thread)
	end)

	return coroutine.yield()
end

-- Method to call functions once when part is touched
function TouchedListener:Once(Callback)
	if typeof(Callback) ~= "function" then
		error("Callback of TouchedListener:Once must be a function. Got: " .. typeof(Callback))
	end

	table.insert(self.Connections, {Callback = Callback, Once = true})
end

-- Custom Connections
function TouchedListener:ListenForPlayer(Callback)
	if typeof(Callback) ~= "function" then
		error("Callback of TouchedListener:ListenForPlayer must be a function. Got: " .. typeof(Callback))
	end
	
	self:Connect(function(OtherPart)
		local Humanoid = OtherPart.Parent:FindFirstChildOfClass("Humanoid")
		
		if Humanoid then
			local Player = Players:GetPlayerFromCharacter(Humanoid.Parent)
			
			Callback(Player)
		end
	end)
end

return TouchedListener
5 Likes