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