Simplifying anti-no-clip

I’ve created an anti-no-clip, which I intend to integrate into my larger anti-cheat. Unlike most other anti-no-clip systems, mine uses spatial queries and only checks when the character moves, giving it a few unique properties:

  • Needless checks on AFK characters aren’t performed
  • Reduced false positives and negatives, especially on sharp corners
  • Checks for both characters inside of a part and characters who teleported/glitched through
  • Easily adaptable with other positional checks (e.g., anti-speed)
  • Works well down to ~10 FPS

While I’m quite proud of the system, I feel like the code itself is too convoluted. I’ve tried reducing it down to one function, but it ends up breaking it. I’ve also considered using a coroutine rather than a for loop for each player, but I’m not sure how to go about that.

Code:

local RS = game:GetService("RunService")
local PreviousPositions = {}

local function ANC(Player)
	local function Check(Player, HRP)
		if PreviousPositions[Player.Name] ~= HRP and PreviousPositions[Player.Name] ~= nil then -- check if they moved

			local Distance = (PreviousPositions[Player.Name] - HRP).Magnitude
			local CF = CFrame.lookAt(PreviousPositions[Player.Name], HRP)*CFrame.new(0, 0, -Distance/2)
			local S = Vector3.new(0.01, 0.01, Distance)
			local OP = OverlapParams.new()
			OP.FilterDescendantsInstances = {Player.Character, workspace.Ignore}
			OP.FilterType = Enum.RaycastFilterType.Blacklist
			OP.MaxParts = 1

			local Check = workspace:GetPartBoundsInBox(CF, S, OP)

			local TracePart = Instance.new("Part")
			TracePart.Anchored = true
			TracePart.CanCollide = false
			TracePart.Material = Enum.Material.Neon
			TracePart.Size = Vector3.new(0.25, 0.25, Distance)
			TracePart.CFrame = CFrame.lookAt(PreviousPositions[Player.Name], HRP)*CFrame.new(0, 0, -Distance/2) -- ty @incapaz

			if Check[1] then
				if Check[1].CanCollide == true then
						TracePart.Color = Color3.fromRGB(255, 0, 0)
				else
					TracePart.Color = Color3.fromRGB(0, 255, 0)
				end
			end

			TracePart.Parent = game.Workspace.Ignore

		end
		PreviousPositions[Player.Name] = HRP
	end

	Player.CharacterAdded:Connect(function(Character) -- reset ANC on respawn
		PreviousPositions[Player.Name] = nil
		
		while Character.Humanoid.Health > 0 do
			RS.Heartbeat:Wait()
			Check(Player, Character.HumanoidRootPart.Position)
		end

	end)
end

game.Players.PlayerAdded:Connect(ANC)

for _, Player in pairs(game.Players:GetPlayers()) do -- catch what PlayerAdded missed
	ANC(Player)
end

Video of it in action:

14 Likes

Cool script ! I’ve always wanted to make these but I had no idea for checking something like this, thanks for giving me the ideas! Also the script looks epic

1 Like

That is very nice, but what if the player lags in the client side while walking close to a wall?

I mentioned it in the bullet points, but from my limited testing, the system won’t detect false positives even on sharp turns down to ~10 FPS. You could also modify it to make use of deltaTime and not detect false positives at very low FPS.

1 Like

Would a part becoming CanCollide on while the character is inside it count as no clip?

Because the CanCollide check is performed every time the character moves, it would count as no clip as soon as the part becomes CanCollide.

Is there a reason you’re not using raycasting? If done correctly I think it’d be more efficient (performance and organization) than your :GetPartBoundsInBox call.

Also instead of having a while true loop for every character that’s in your game, why not just use one RunService.Heartbeat:ConnectParallel and loop through all valid characters. This way you’d be checking every frame and none of them would go unchecked. Also you already stated this but you can then use DeltaTime for more accuracy, not that you’d need it anyway.


I managed to write something that might clarify what I’m talking about.

RunService.Heartbeat:ConnectParallel(function(DeltaTime)
	-- ConnectParallel for performance.
	for _, Registry in pairs(PlayerRegistry) do
		task.defer(function()
			-- Just so every check runs without delay. Not sure if this is 100% necessary but when I started adding more features to my AE without task.defer, there would start to be false positives.

			-- How you get/set these variables is up to you. In my AE I store the humanoid root part, position one and position two.
			local HumanoidRootPart = Registry.HumanoidRootPart
			local PositionOne = Registry.UnverifiedPosition
			local PositionTwo = HumanoidRootPart.Position

			-- Stops the check from running at all if they aren't moving.
			if  (PositionOne - PositionTwo).Magnitude < 0.005 
				and HumanoidRootPart.AssemblyAngularVelocity.Magnitude < 0.05 
			then
				return
			end

			local RaycastParameters = RaycastParams.new()
			-- GetPartRegistry is a custom function that returns all objects that are valid. I do this with one singular Workspace.DescendantAdded and check if the object has CanCollide and other reqs to make sure it's valid.
			RaycastParameters.FilterDescendantsInstances = GetPartRegistry()
			RaycastParameters.FilterType = Enum.RaycastFilterType.Whitelist 
			RaycastParameters.IgnoreWater = true
			
			if  WorkspaceService:Raycast(
					PositionOne,
					PositionTwo - PositionOne,
					RaycastParameters
				)
			then
				--Punish the player here. Kick/kill/rubberband then stop the rest of the check function because they're already being punished for exploiting, no need to check for more exploits.
				return
			end
		end)
	end
end)

Script performance shows that our numbers aren’t really different, but I suggest not using spatial queries for noclip detection, as it doesn’t make sense.

I initially used raycasts but found that spatial queries, although slightly inefficient, return much more accurate results. Raycasts worked well but there were a few instances were it detected false negatives. In my admittedly limited testing, I am yet to find an instance of spatial queries failing. There’s also the slight bonus of being able to use the same spatial query CFrame in my trace part for debugging.

I’m not sure why I didn’t use one loop, I’ll definitely adapt my code to yours, thanks!

This is probably not from raycasting but from you forgetting to add an object to the raycast blacklist?

This can be done with a local variable.