Exploit proof Touch detection!

In this tutorial I will explain why .Touched is easily exploitable and what simple step you can take to make your game extra secure!

by MineJulRBX_FS

Author Avatar

Contents

Disclaimer

This is not a complete fix, exploiters could still teleport to the parts in which .Touched is checked. The server can see if a player is teleporting around, so you could create an anti exploit for this already. But the caveat here, the server is blind to the .Touched event itself and that is what I will be diving into in this tutorial!


What is .Touched?

.Touched is an event used on BaseParts. Once a BasePart touches another Part, the .Touched event will be fired for that given otherPart. It is important to remember that at least one of the parts cannot be Anchored upon collision, and that manipulating the CFrame will be ignored as well. Touch detection is physic based.

How is .Touched easily exploitable?

The server does not check where a Player or any part has touched something. It is the client (the players device) which informs the server when something has been touched. This means an exploiter could move the object or otherwise activate the .Touched event on their client, and the server would have no idea that they cheated. This does not have to be just the player character actually, they can even cause server parts to activate .Touched of other server parts even if they aren't touching on the server! That is how much trust is given to the client when it comes to touch.

Why it works this way is explained in a bonus section further down, titled Latency?

How do we fix this?

With .Magnitude! There are more ways to check if the player is in the right place. It varies depending on your needs, but I will keep this tutorial to .Magnitude as it is really simple to use.

local Distance = (BasePart1.Position - BasePart2.Position).Magnitude
print(Distance) -- Returns the distance between the positions in studs

Here is an example of how .Magnitude works. You enclose 2 positions in a set of parentheses/brackets (). And the .Magnitude function calculates the distance between them returning you a number in measurement of studs. It is measured from the center of the parts so the size of the parts is not accounted for.


Let's get to it!

Start with creating our variables and reference the part we want to be touched.

local BasePart = script.Parent -- This expects the script to be put inside the BasePart

local Distance = 4 -- How close they need to be

Alternatively, you could do BasePart.Size.X or whichever of X,Y,Z that is the longest instead of manually inputting the distance you desire. This could cause a problem if you care about precision, I explain why in the section below, otherwise feel free to skip that and continue to the code underneath.

These 2 chapters are bonus information:

Precision?

Since .Magnitude is measured from the center of BasePart, using the longest side of the BasePart as distance would count twice the length of the BasePart. This still gives a limited area a player has to be for the touch to count, but it is not precise. If you want it to detect from the distance of actually touching the part, you would have to divide the longest distance by 2. That is however still not perfect enough. ㅤ If you have a perfect square box, the diagonal distance to the corner is longer than to the sides. This could mean that if you use BasePart.Size.X / 2, at the corner the player would already be inside the part and touching, but distance wise it does not yet consider the player to actually be close enough. This issue is more significant the larger the part is. ㅤ Example, if the box size is 2x2x2, the distance from the center to the sides would be 1, since the center is halfway. But to the corner, that would actually be near 1.4 distance. If the player is the size of 10 as an example, the 0.4 difference would probably be insignficant. But if the box size is 50x50x50, to the corner the distance would be 35, but to the sides it is 25 distance. That means the entire box could be covering the player of size 10 before they reach the necessary distance. ㅤ The solution, Lenght√2 gives the diagonal distance, and then from the center we just divide it by 2 to get halfway. In Luau you would do it like this: BasePart.Size.X * math.sqrt(2) / 2. This still gives more headroom if the player touches from the sides, but it is better than to forget and not count the corners. Magnitude is not able to be absolutely precise when the distance around the BasePart is not the same. If you require such precision, Magnitude is not the solution.

Latency?

Internet connection is not instant, you have to wait for the server, and the server have to wait for you to know when anything has changed. This creates an issue in that the server, or other players, they do not get your real time position in the game of your player character. Your movement will be slightly slacking behind from what the server can see, to where you actually are in the game on your own screen. ㅤ This means, when we check if the player is within the needed distance. The player may see that they are actually touching the part, or within that distance. But the server is still waiting for an update that the players location is actually there! ㅤ This is actually why the server is blind to where the player is upon .Touched, and the client is all that checks if the player touched something. Because then we can act upon the touch much faster than when we see the player actually touching the thing on the server. This creates a smoother and more responsive experience for the player. ㅤ Most of the time, the difference in latency is less than a second, but the game will feel much smoother the faster it responds upon the .Touched event. The way to fix this is to allow a little more headroom for how close the player has to be! If you use the precise distance just add + 2, adjust as you please. We already check if they are really close, so we do not need it to be more precise. ㅤ However, if you would like for the player to be inside the BasePart to consider it properly touched, you may not need to give any headroom.

Tutorial continued

BasePart.Touched:Connect(function(hit) -- This runs when the BasePart is touched
	
	if (BasePart.Position - hit.Position).Magnitude < Distance then
		print(hit.Name .. ' is confirmed to be within reach of '.. BasePart.Name)
	end
	
end)

Now we have confirmed that whatever hit/touched the BasePart is within appropiate distance to be reaching said BasePart. This could be either the player themselves, or other BaseParts manipulated on the client.

Let's check for the player specifically.

local Players = game:GetService('Players')

local BasePart = script.Parent -- This expects the script to be put inside the BasePart

local Distance = 4 -- How close they need to be

Same part as before, but with the addition of the player service.

BasePart.Touched:Connect(function(hit) -- This runs when the BasePart is touched
	
	if (BasePart.Position - hit.Position).Magnitude < Distance then
		
		local Player = Players:GetPlayerFromCharacter(hit.Parent) -- Checks if the touching part is a player
		
		if Player then
			print(Player.Name .. ' is confirmed to be within reach of ' .. BasePart.Name)
		end
	end
	
end)

Now we have confirmed that whatever touched the BasePart is both within appropiate distance to reach, as well being a player and not some other object.

if Player and (BasePart.Position - hit.Position).Magnitude < Distance then
	
	print(Player.Name .. ' is confirmed to be within reach of ' .. BasePart.Name)
	
elseif Player and (BasePart.Position - hit.Position).Magnitude < Distance + 15 then
	
	print(Player.Name .. ' is not within reach of ' .. BasePart.Name)
end

Technically you could use this as an anti-exploit, however you would have to be careful. The server will be slow at updating a player location for some players with terrible internet, so you would have to allow a lot of headroom to not incorrectly moderate players. You can always log it as further evidence to back up other instances or reports of an exploiter.

Some games that have teleporters, teleport menues or fast travel options. They add a timeout on the anti-exploit whenever the player gets teleported to allow the client and server to understand the players new location before checking if they're actually exploiting.

Roundup

Thanks a lot for reading this tutorial, I hope it has given a bit of insight on how .Touched works underneath the surface and ways to work with it. If you are not concerned or just don't care about exploiters activating .Touched events at incorrect times or distances, that's completely fine. In many cases it wouldn't really matter, like in a tycoon where only the owner of the tycoon can buy things, we are not concerned about it affecting other players. But lootboxes or money spawning in the map, they could easily be stolen by an exploiter if they activate by .Touched.

If you are interested in doing this for your game and would like to make life a lot easier on you, or get a tad more advanced, keep reading!


Going advanced

Finding out you can do this to make your game just a bit more exploit proof, you may find yourself wanting to do this for all of your .Touched events! And that's great! But having to type out the whole if (BasePart.Position - hit.Position).Magnitude < Distance then every single time gets very repetitive and tiring... so what is the solution?

Modules!

Yep, we're going here! This module will be kept relatively simple, but you could make it much more advanced with metatables which allows for more customization, features and simplification in use.

First I'll show you how we're going to be using this module in our primary scripts, and how it makes life easier for us.

-- require your module, you can call it anything you want.
local betterTouch = require(game.ServerStorage.betterTouchModule)

local BasePart = script.Parent

betterTouch(BasePart):Connect(function(hit)
	print(hit.Name .. ' is confirmed to be within reach of '.. BasePart.Name)
end)

--Here's the regular touched for comparison:
BasePart.Touched:Connect(function(hit)
	print(hit.Name .. ' we have no idea if this .Touched is exploited or not')
end)

Let's start by creating our module.

local module = {}

return module

So this is the default text we find in a ModuleScript, which is a great start.

return function(object : Instance)
	local functions = {} -- a table which contains the functions we would like to return
	
	return functions
end

But I'm gonna completely change that around by returning a function directly. This will allow us to do a simple function call function(BasePart) and then connect our custom touch event.

return function(object : Instance)
	local functions = {} -- a table which contains the functions we would like to return
	
	
	function functions:Connect(func : (otherPart : BasePart) -> (), Distance : number)
		
		return
	end
	
	return functions
end

Here is our custom connect function, now we just have to check for the touch.

return function(object : Instance)
	local functions = {} -- a table which contains the functions we would like to return
	
	function functions:Connect(func : (otherPart : BasePart) -> (), Distance : number)
		
		object.Touched:Connect(function(hit) -- same function as previously
			
			if (hit.Position - object.Position).Magnitude < (Distance or FindDistance()) then
				print(Player.Name .. ' is confirmed to be within reach of ' .. BasePart.Name)
			end
		end)
		
		return
	end
	
	return functions
end

Now we've added the touch function and check for the distance, same as before. But you may have seen I've added a new thing. (Distance or FindDistance()), in case we want to set our own distance requirement, there is the option, just as before. But to make it easier on ourselves, we don't require to input any distance, we let it find the distance itself if we don't give it any.

local functions = {} -- just to show where we put FindDistance in the script

local function FindDistance() -- This function returns which side of the object is the longest
	return (object.Size.X > object.Size.Y and object.Size.X > object.Size.Z) and object.Size.X or (object.Size.Y > object.Size.Z and object.Size.Y or object.Size.Z)
end

This will return the number of whichever side is the longest. If you think that's a mess, here is the if statement instead:

local function FindDistance() -- This function returns which side of the object is the longest
	if object.Size.X > object.Size.Y and object.Size.X > object.Size.Z then
		return object.Size.X
	elseif object.Size.Y > object.Size.Z then
		return object.Size.Y
	else
		return object.Size.Z
	end
end

Alright, back to our module.

return function(object : Instance)
	local functions = {} -- a table which contains the functions we would like to return
	
	local bind = Instance.new('BindableEvent') -- Create a bind that works as our custom event
	
	local function FindDistance() -- This function returns which side of the object is the longest
		return (object.Size.X > object.Size.Y and object.Size.X > object.Size.Z) and object.Size.X or (object.Size.Y > object.Size.Z and object.Size.Y or object.Size.Z)
	end
	
	function functions:Connect(func : (otherPart : BasePart) -> (), Distance : number)
		
		object.Touched:Connect(function(hit) -- same function as previously
			
			if (hit.Position - object.Position).Magnitude < (Distance or FindDistance()) then
				
				bind:Fire(hit) -- Fires our bind and sends the hit with it
				
			end
		end)
		
		return bind.Event:Connect(func) -- Returns the connection from our bind
	end
	
	return functions
end

And this completes our module! Feel free to include what you learned from Latency? and Precision? section in the module yourself.

Going to show again how this is used.

local Players = game:GetService('Players')
-- require your module, you can call it anything you want.
local betterTouch = require(game.ServerStorage.betterTouchModule)
	
local BasePart = script.Parent
	
betterTouch(BasePart):Connect(function(hit)
	local Player = Players:GetPlayerFromCharacter(hit.Parent)
	if Player then
		print(Player.Name .. ' is confirmed to be within reach of ' .. BasePart.Name)
	end
end) -- You can add your own distance by putting it after the end like so `end,Distance)`
-- You can also :Disconnect() the connection like any other connection.

Roundup, again...

This is my first tutorial/article I've ever made, so please inform me of any errors or confusions. I would like for my tutorial to be very easy to understand and follow, so I'm likely going to come back and make some adjustments to better the learning ability in the readers!

I educate scripting to people in real life, so I'm excited to gain some experience in creating articles like this.

View in-game to comment, award, and more!