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