Dealing with teleporting in a fighting game?

Hey, and thanks for reading in advance.

I’m helping to code a class-based fighter in which a select few classes are able to attack and teleport simultaneously (or teleport and then attack almost immediately afterwards).

Problem is, doing sanity checks keeping possible teleports in mind is hard. The server’s idea of your position and the client’s idea of your position are fractions of a second apart, and since the legal teleports are done locally (as they should be in order to maximize responsiveness), all the server sees for a few milliseconds is a gigantic gap between where you are and where the server thinks you should be. Additionally, remote signal traffic tends to be quicker than the server’s subroutine of updating each player’s global position.

This quickly becomes an issue when attempting to verify hitboxes, as the server denies hitbox requests if the location the client sends the server for creation of a hitbox is too far away from the server’s recorded position for the player. I don’t have the option of allowing a certain number of ‘fishy’ hitboxes, either - how could the server distinguish between a legal and illegal teleportation?

Sauce:

function Core:Hitbox(player, offset, size, ignore, maxparts)
	local targets = {}
	
	local ping = (player:FindFirstChild("Ping") or {Value = 50}).Value
	local character = player.Character or player.CharacterAdded:Wait()
	
	local hume = character:WaitForChild("Humanoid", 5)
	local hrp = character:WaitForChild("HumanoidRootPart", 5)
	
	if hrp and hume then
		if ping >= 200 then
			local hitbox = RotR3.new(hrp.CFrame * offset, size)
			
			if ShowBoxes then
				local box = Instance.new("Part")
				box.BrickColor = BrickColor.new("Really blue")
				box.Material = Enum.Material.Neon; box.Transparency = .6
				box.TopSurface = "Smooth"; box.BottomSurface = "Smooth"
				box.Size = size; box.CFrame = hitbox.cframe
				box.Anchored = true; box.CanCollide = false
				
				box.Name = "Hitbox"; box.Parent = workspace
				game:GetService("Debris"):AddItem(box, 1)
			end
			
			for _,part in pairs(hitbox:cast(ignore, maxparts or 100)) do
				local Humanoid = part.Parent:FindFirstChild("Humanoid")
				if Humanoid and Humanoid.Health > 0 then
					if not FindInTable(targets, part.Parent) then
						targets[#targets + 1] = part.Parent
					end
				end
			end
			
		else spawn(function()
				local hitData = ClientRemotes.Hitbox:InvokeClient(player, offset, size, ignore, maxparts)
				local region, hits = hitData.Region, hitData.Targets
				
				local factor_v = hrp.Velocity.Magnitude * math.clamp(ping/50, 1, 1.25)
				local factor_s = hume.WalkSpeed * math.clamp(ping/50, 1, 1.25)
			
				if region.size == size 
					and (region.cframe.p - hrp.Position).Magnitude <= math.max(factor_s, factor_v) then
					
					local serverBox = RotR3.new(region.cframe, size * math.clamp(ping/20, 1, 1.5))
					local serverHits = {}
					
					if ShowBoxes then
						local box = Instance.new("Part")
						box.BrickColor = BrickColor.new("Really blue")
						box.Material = Enum.Material.Neon; box.Transparency = .6
						box.TopSurface = "Smooth"; box.BottomSurface = "Smooth"
						box.Size = serverBox.size; box.CFrame = serverBox.cframe
						box.Anchored = true; box.CanCollide = false
						
						box.Name = "Hitbox"; box.Parent = workspace
						game:GetService("Debris"):AddItem(box, 1)
					end
					
					for _,part in pairs(serverBox:cast(ignore, maxparts or 100)) do
						local Humanoid = part.Parent:FindFirstChild("Humanoid")
						if Humanoid and Humanoid.Health > 0 then
							if not FindInTable(targets, part.Parent) then
								serverHits[#serverHits + 1] = part.Parent
							end
						end
					end
					
					for _,target in pairs(hits) do
						if FindInTable(serverHits, target) then
							targets[#targets + 1] = target
						end
					end
					
				else warn("Client hitbox either mismatched size or too far away")
				end
			end)
		end
	end
	
	return targets
end

Edit - A quick breakdown of how this works:

  • Server performs hitcheck instead of the player if their ping is above 200, otherwise;
  • Server sends a signal to the client to create a hitbox with provided size and offset
  • Client returns the hitbox it created plus any targets it caught inside it
  • Server verifies this information and then creates an identical hitbox at the client’s provided location
  • Server drops any targets found in the client’s hitbox that are not also found in the server’s hitbox

Any help or advice is appreciated.

2 Likes

What you could do is when you do a legal teleport it should send an event to the server so the server knows to ignore this teleport during a sanity check.

Couldn’t exploiters then use the same event for illegal teleports?

Well you are supposed to do sanity checks on that as well.

For example:
I assume the teleport is a skill with a cooldown.
let’s say the cooldown is 3 seconds. If multiple legal teleport events are send within the 3 second cooldown period you know something is up.

2 Likes

Could you not just set the position where you want the client to teleport on the client, fire an remote and do some checks on the server-side: if the checks fail, you set the position to their previous one on the server, maybe set the HumanoidRootPart’s network owner to nil for a moment to prevent the client from replicating their invalid position?

False positives for the checks should be rare if you’ve made them properly, so you would not really need to worry about them impacting gameplay too much.

You can also easily keep track if the user teleported quite easily with this method.

Couldn’t an exploiter with a decompiler simply edit the code to not fire the remote?

Perhaps do it all server-side, by, every frame (or slower if you prefer) checking the magnitude between the player’s current position, and their previous position on the last check on the previous frame timestamp. This way, if they move more than, let’s say (just for example) 5 studs in one frame, it signals that they could be teleporting.

Something like this (Just the rough idea):

local RunSevice = game:GetService("RunService")
local Player = -- Player to be checked

local lastPos = nil
local newPos = nil

local switch = false -- To switch between lastPos and newPos

RunService.Heartbeat:Connect(function(step)
    -- Keeps a record of what the player's last position was
    if switch == false then
        lastPos = Player.Character.HumanoidRootPart.Position
        switch = true
    elseif switch == true then
        newPos = Player.Character.HumanoidRootPart.Position
        switch = false
    end

    if lastPos and newPos then
        local magnitude = (lastPos - newPos).Magnitude
        if magnitude > 5 then -- Can change 5 to whatever you want to change distance of check
            -- Do whatever you want to the player
            print("Teleportation??")
        end
    end
end)

Thoughts?

Extra: Perhaps instead of RunService.Heartbeat you’d want to use something else a bit slower, that runs, let’s say, every 1/5th of a second or so (depends on if you have fast moving powerups or something in your game). This is because it’s possible that teleportation in some exploits may take longer than 1/60th of a second (or one frame) to work. This would mean that people could teleport and if it takes 1/10th of a second to travel greater than 5 studs away, it’d get past the check.

So, you’d want to change how many times per second it runs, as well as the magnitude (or distance) to check, in order to fit your needs.

And also, I made a post on my own makeshift .magnitude function, in case you want to get an idea of how .magnitude works behind the scenes: How does magnitude and range work? - #6 by MJTFreeTime

EDIT: I just realized that you mentioned you have classes that have teleporting in your game. You’ll just need to add another if statement to the above code, like

if class ~= class1 and class ~= class2 then
    -- Code here
end

Basically just exclude the classes that have teleportation from the code entirely.

If you had an server-side anti-cheat that detects teleporting, just teleporting on the client would not work. And, as I said you can quite easily keep track if the user is teleporting to bypass the anti-cheat when legitimate teleports happen.

Yeah, I agree.

@dreadbytes Any thoughts on the code I made above for a server-side anti-teleport check?

You define step, but you never use it anywhere.
Maybe make a variable at top, say MAX_WALKSPEED, which at the check you just do
if magnitude > MAX_WALKSPEED * step then.

This should make it so that if the game is running a bit slower than usual, there wouldn’t be any false positives as it accounts for the time it took to get to the last frame and new frame when doing the check, and gets how far the player could have traveled.

You should probably add some kind of error margin to that though, as if minor lag spikes happen, it may result counter-measures being applied incorrectly.

Otherwise, it seems pretty good.

1 Like