Performance when Teleporting 150+ Players to Different Places of the Map at Once

Periodically, assuming a worst-case scenario, I want to teleport 150 players across different parts of the map simultaneously without it failing, while a few other codes would simultaneously run to reset other data.

The code currently flows like this:

  1. Server initiates the teleportation gating process (checks if they are eligible, etc.), and drops non-eligible teleportations
  2. Server jumps the player out of a seat if they are in one
  3. Server repeatedly tries to move the player to the designated location in the map, and retries every 0.5 seconds until either a) teleportation is successful or b) the timeout has reached
  4. Repeat step 1-3 for each eligible player until all players are teleported

There are a few problems I have seen when teleporting that large number of players at once:

  • We have to jump the player out of the seat if they are in once to prevent physics issues. However, simply jumping them once sometimes fails due to lag between client and server, whether that is physics, scheduler being overwhelmed, or simply just the network.

  • Attempting to teleport them on the client solely sometimes results in a desync problem between the client and server. Client would see themselves somewhere else, where the server (or another player, for that matter) may see them elsewhere. Moving teleportation (i.e. the MoveTo call) to the server solves that issue, but in turn becomes a performance bottleneck for all other scripts

  • Moving them into the task scheduler using task.spawn and task.defer does not guarantee any success when it is run only one time. The workaround I attempted was to repeatedly teleport the player until they were close enough to the destination. However, there are two issues:

      1. If one loop takes longer to complete, the other tasks (teleport or other non-related codes) cannot complete in a timely manner. Therefore creating a bottleneck.
      1. For performance reasons, I obviously can’t have teleport retry infinitely. There’s an arbitrary timeout of 5 seconds before the teleportation process gives up. However, a higher number tends to have higher teleportation success but results in some decreased performance, whereas a lower one tends to be faster but results in a higher number of failed teleportations.
  • Teleporting that many players at once and having it succeed is an integral part of the game’s mechanism. I can’t run it in parallel because MoveTo() or any CFrame-based operations are not safe in parallel.

I’m reaching out because, surely, there’s a better way to do it than the way I have described above, right?

Have you tried setting the NetworkOwnership of the HumanoidRootPart to nil?
Specifically on the server?

humanoidRootPart:SetNetworkOwner(nil)
1 Like

Perhaps you could do something like teleport 20 players, wait task.wait(), teleport another 20 players, wait task.wait(), and repeat until all 150+ players have been teleported?

Network ownership for humanoid root part is always owned by client. What issues would I solve if I even am able to set it to server for one heartbeat?

I suppose I didn’t include this caveat when I wrote the original post: each player teleportation sequence is wrapped in task.spawn, and so the scheduler always tries to schedule it this frame (or moves it to next frame if not possible).

A similar question to my response to uxianity, what issues would I be solving or otherwise managing by breaking this up further per 20-player sequence in the task scheduler?

I supposed that teleporting 20 players at a time would reduce lag and synchronisation signals sent to each player’s client, since moving 150+ players means that there are more than 900 parts being moved in one heartbeat (not including accessories or r15 avatars).

no its not owned by the client at all times.

You better just set it to nil and then make it go back to the player, thats the easiest way

I already provided a way to set the NetworkOwnership of the HumanoidRootPart to no longer be owned by the client.

A complete sequence looks like so:

-- assuming this is inside of the script BEFORE teleporting the player
local char = plr.Character -- assuming you have plr as the Player instance that is looped.
local hrp = char:FindFirstChild("HumanoidRootPart")
if not hrp then
    return
end

hrp:SetNetworkOwner(nil) -- we do this first

-- assuming you're teleporting the player with PivotTo:
char:PivotTo(destination) -- assuming destination is a cframe.

-- after teleporting:
hrp:SetNetworkOwner(plr) -- back for client to own.

And a optional one if this doesnt succeed, you could also try anchoring the humanoidrootpart BEFORE teleporting, and then once you teleported unanchor the humanoidrootpart.

-- before tp:
hrp.Anchored = true

-- after tp:
hrp.Anchored = false

And if that also doesn’t succeed, you can combine velocity manipulation and platformstand states:

-- before tp:
humanoid.PlatformStand = true
hrp.AssemblyLinearVelocity = Vector3.zero
hrp.AssemblyAngularVelocity = Vector3.zero

-- after tp:
if humanoid then
	humanoid.PlatformStand = false
end

If you want batching sequences, you could if there are a bunch of players, and if you want to let the engine "breathe", or you could simply do a normal loop within the task scheduler.

So a example of your code with all combined experiments should look something like this:


-- Teleport players to a provided destination cframe or vector3.
function teleportPlayers(players: Player | {Player}, destination: CFrame | Vector3)
	players = if typeof(players) == "Instance" and players:IsA("Player") then
		{players} elseif typeof(players) == "table" then players else {}

	destination = if typeof(destination) == "CFrame" then destination elseif
		typeof(destination) == "Vector3" then CFrame.new(destination)
		else CFrame.new()

	if #players == 0 then return end

	for i = 1, #players do -- fastest way
		local player = players[i]
		if typeof(player) == "Instance" and player:IsA("Player") then -- validates player instance
			local char = player.Character
			if not char then
				continue -- (optional but recommended)
			end

			local hrp = char:FindFirstChild("HumanoidRootPart")
			if not hrp then
				continue -- (optional but recommended)
			end 
			
			local humanoid = char:FindFirstChildOfClass("Humanoid")
			if not humanoid then
				continue -- (optional but recommended)
			end

			task.spawn(function()
				if hrp:IsDescendantOf(workspace) and hrp.Anchored == false then
					if hrp:GetNetworkOwner() ~= nil then
						hrp:SetNetworkOwner(nil) -- overwriting client ownership
					end
				end

				humanoid.PlatformStand = true -- stops movement
				hrp.AssemblyLinearVelocity = Vector3.zero -- no velocity present
				hrp.AssemblyAngularVelocity = Vector3.zero -- no velocity present
				hrp.Anchored = true -- stunned.

				task.wait() -- yield before tp.
				char:PivotTo(destination) -- model tp.
				task.wait() -- yield after tp.

				if humanoid then 
					humanoid.PlatformStand = false -- continue movement
				end

				if hrp then
					hrp.Anchored = false -- unstunned.
					-- safe check incase!
					if player and typeof(player) == "Instance" and player:IsA("Player") then
						if hrp:IsDescendantOf(workspace) and hrp.Anchored == false then
							if hrp:GetNetworkOwner() == nil then
								hrp:SetNetworkOwner(player) -- back to client ownership
							end
						end
					end
				end
			end)
		end
	end
end

This should prevent most weird desync issues.

And a tiny disclaimer.
If you’re teleporting alot of players into the same destination, it could cause flings to occur. So its preferred to put tiny offsets that distance yourself between each player character, if you prefer:


local dist = 4 -- example distance.
local offset = Vector3.new(math.random(-dist, dist), 0, math.random(-dist, dist))

char:PivotTo(destination * CFrame.new(offset))

[spoiler]If your game has seats or similar, make sure to force unsit them before teleporting. Just so you dont accidentally teleport the seat with them.[/spoiler]

Usage


As you said, you’re teleporting over 150+ players at once instantly, so this as an example:

local players = game:GetService("Players"):GetPlayers() -- {Player, ...} table
local dest = CFrame.new(0, 500, 0) -- example cframe destination.

teleportPlayers(players, dest) -- fire function!

Hope this helps!