This article outlines some internal functionality of Touched and TouchEnded event replication in the distributed physics engine, with the goal of helping you understand how to approach touch event scenarios in your Roblox experiences.
To get the most out of this material, you should be an advanced developer who’s familiar with network replication delay.
Example Scenarios
Server/Client Async Scenario
In one example scenario, a server script has Touched and TouchEnded event listeners for a PartB, and the server owns that part. When PartB touches/un-touches another part PartA on the server (both with CanTouch set to true), an event is triggered and replicates to other clients. Note that if PartA and PartB have touched/un-touched on one client, it might not be the case on the server or another client.
The following diagram illustrates this scenario across time (t):
PartB moves closer to PartA and touches it, since the movement of PartA on Client1 has not yet broadcast to Server. A Touched event is triggered here.
t = 2
Client1
Receives the Touched event broadcast from Server at t=1, as well as the CFrame update of PartB. However, the received event appears inaccurate since PartA previously moved on this client at t=1 and does not overlap PartB.
Server
CFrame of PartA is received from Client1, meaning PartA and PartB move apart and trigger a TouchEnded event.
Client2
Receives the Touched event broadcast from Server at t=1, as well as the CFrame update of PartB. On this client, the event appears accurate since Server has not yet replicated the new non-touching CFrame of PartA.
t = 3
Client1
Receives the TouchEnded event broadcast from Server at t=2, although this event appears inaccurate since PartA and PartB never touched on this client across t=0 to t=3.
Client2
Receives the TouchEnded event broadcast from Server at t=2. On this client, the event appears accurate since Server has replicated the new non-touching position of PartA (note that PartA and PartB were touching at t=2 on this client).
Client Event Dispatching
If PartA is owned by one client and PartB is owned by another client, you might get the same event twice everywhere, because each client triggers its event and broadcasts it to the server which then rebroadcasts it to the other client, as long as both parts exist on the receiving client. In our engine, we try our best to merge this “same” event, however, it’s difficult to identify and merge all of the same events.
The following diagram illustrates this scenario across time (t):
Client2 PartB moves closer to PartA and touches it, triggering a Touched event which is broadcast to Server. The CFrame change of PartB is also replicated to Server.
t = 2
Server The Touched event and the CFrame update for PartB from Client2 at t=1 is received and then rebroadcast to Client1.
t = 3
Client1 Receives the Touched event and CFrame update for PartB broadcast from Server at t=2. A second Touched event is triggered on PartA which is broadcast to Server.
t = 4
Server The Touched event for PartA from Client1 at t=3 is received and then rebroadcast to Client2.
t = 5
Client2 Receives the Touched event for PartA broadcast from Server at t=4.
Guidance
If the callback function needs to be executed immediately, make sure the local client owns at least one of the two parts (see Network Ownership) so the event triggers locally.
You can always expect to receive a Touched or TouchEnded event if it was ever triggered on remote server/clients, but anticipate a delay. This guarantees eventual consistency.
For security purposes, use the server to validate if a Touched or TouchEnded event is valid or not if the event was triggered on a client. For example, check the distance between the two parts to make sure they are close enough to trigger the touch event.
Thanks for the heads up. Also, if you are wondering how exploiters call into Touched, they often use functions such as firetouchinterest, and they should be able to forcefully call Touched callbacks on the client.
This is very useful! Knowing how those events work at a deeper level is very helpful
There is one (or two) things that are confusing me though
I don’t understand how Client1 can replicate the .Touched event to the server, if it doesn’t have network ownership of the part?
And, the client will still trigger .Touched events for parts that aren’t owned by it, instead of just relying on the event being replicated to it, is that right?
iirc even in a LocalScript, it’ll listen to the network owner. If the client owns the part, the client will trigger Touched instantly. If the server or another client owns the part, there’s a delay since it waits for the event to be reported.
Nice, altrough personally i think i’d just have Touched events that deal damage to a player that is managed by a NPC be owned by a client, even if a exploiter fires they will damage themselves.
Volume-based triggers can just use Overlap Params API on the server.
The second figure should probably also be updated to show the second Touched even on PartA instead of PartB
Is this the reason that .Touched fires twice? So like PartA collides with PartB from Client1’s perspective, and triggers the .Touched for PartA and PartB (and not only PartA), but then the .Touched coming from Client2 replicates and triggers .Touched for PartB again, is that right?
I’m running into issues where the Server has a .Touched and .TouchEnded on an Anchored Part (let’s call this PartA) however ,Touched and .TouchEnded would sometimes fire even when the Player.Character is still inside of the PartA and this has caused a lot of issues so I have implemented a new method to deal with this
Code
local touchUtil = {}
local function isPointInPart(point: Vector3, part: BasePart): boolean
local relPos: Vector3 = part.CFrame:PointToObjectSpace(point)
return math.abs(relPos.X) < part.Size.X * 0.5
and math.abs(relPos.Y) < part.Size.Y * 0.5
and math.abs(relPos.Z) < part.Size.Z * 0.5
end
export type TouchHandler = {
Parts: { [BasePart]: boolean },
}
function touchUtil.create(part: BasePart): TouchHandler
local touchHandler: TouchHandler = {
Parts = {},
}
part.Touched:Connect(function(otherPart: BasePart)
print(`{part} Touched {otherPart}`)
print(part:GetTouchingParts())
if touchHandler.Parts[otherPart] then
return warn(`{otherPart} Has touched {part}`)
end
touchHandler.Parts[otherPart] = true
return print(`Touch Started {otherPart}`)
end)
part.TouchEnded:Connect(function(otherPart: BasePart)
print(`{part} TouchEnded {otherPart}`)
print(part:GetTouchingParts())
if not touchHandler.Parts[otherPart] then
return warn(`{otherPart} Has not touched {part}`)
end
if isPointInPart(otherPart.Position, part) then
return warn(`{otherPart} Is in {part}`)
end
touchHandler.Parts[otherPart] = nil
return print("Touch Ended")
end)
return touchHandler
end
return touchUtil
The problem is TouchEnded fires before Player.Character has fully exited PartA so isPointInPart would return true and thus breaking my code, please observe the output especially from :GetTouchingParts()
The same code however works perfectly when ran locally as per this Thread here
I could run this code on the client but then it’d have security issues, when I run this code on the server it has inaccuracies
so from your perspective what would be the best solution to the problem? @APandaPoet