How to detect when a player leaves the game from within a localscript without putting strain on the server (remote events)

I think your whole script can be simplified to something like this:

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")

local grabObjectsContainer = Workspace.GrabObjects

local MAX_HOTBAR_INDEX = 4
local DROP_OFFSET = Vector3.yAxis * 2

local function getHotbarContentsAtIndexForPlayer(player, index)
	local hotbarContainer = player:FindFirstChild(`Hotbar{index}`)
	assert(index == math.clamp(index, 1, MAX_HOTBAR_INDEX), `Invalid hotbar index {index} outside range [1, {MAX_HOTBAR_INDEX}]`)

	if not hotbarContainer then
		return {}
	end

	local hotbarContents = hotbarContainer:GetDescendants()
	return hotbarContents
end

Players.PlayerRemoving:Connect(function(player)
	print(`{player.DisplayName} is leaving the game, dropping items...`)

	local character = player.Character
	if not character then
		warn("No character found for player, aborting!")
		-- You could add some logic here to put the items in a default place instead of aborting
		return
	end

	local dropCFrame = character:GetPivot()
	for index = 1, MAX_HOTBAR_INDEX do
		local hotbarContents = getHotbarContentsAtIndexForPlayer(player, index)
		for _, item in hotbarContents do
			if item:IsA("BasePart") then
				item:PivotTo(dropCFrame + DROP_OFFSET)
				item.Parent = grabObjectsContainer
			end
		end
	end
	print(`Dropped {player.DisplayName}'s items at {dropCFrame.Position}`)
end)

Yes it can easily be simplified which I was going to do later, like I said I enjoy the process of getting it to work and then polishing it, also gives me a lot of motivation. I also do not think this will fix the issue, but let me see.

Player1 is leaving the game, dropping items… - Server - LeaveHandler:23
No character found for player, aborting! - Server - LeaveHandler:27

like I said, yes it simplifies the script but does not fix the issue.

Okay, it’s easier for me to figure out the issue with simpler code at least because it eliminates a lot of things possibly going wrong.

You must have another script conflicting with this, because when I run it in an empty baseplate, I get this:

  BusyCityGuy is leaving the game, dropping items...
  Dropped BusyCityGuy's items at 0.265884667634964, 3.9452590942382812, -1.528836727142334

What other scripts do you have that deal with character spawning or removing?

Was this test with the localserver and the seperate client test?
It shows up differently if you run it in just studio and leave.

(Ingame it will error, but in studio it will not.)

Ah good point! I can reproduce the missing character in a local server. I guess Character property can get reset before PlayerRemoving is called, that is unfortunate (and seems like a bug tbh)

So what you are saying is there is no solution? :frowning:

After thinking about it, I would probably have to store the position inside of a value inside the player, and get it that way. Which means I will also have to run a loop to keep it up to date. Very unnecessary lag >:(

You can always hardcode a solution by keeping track of the character outside of the PlayerRemoving event, much like your original solution seemed to try.

… but one thought I have, is wouldn’t you want to do this every time a character dies, not just when they leave? In which case, player.CharacterRemoving would be a more appropriate event, and it would always contain a reference to a valid character?

I have it happening when the player dies, but the problem is I am doing that by sending an event from a localscript to communicate with various scripts, which is why the original question of this post was to see how you could get the player leaving from a localscript.

I changed my mind however because I realised an event might not just be fast enough to do everything before the stuff gets deleted.

(Also the character will not be deleted in my game when someone dies, as their body will stay ragdolled in that spot as they are allowed to spectate the other players)

I might suggest implementing or using something like this wrapper, which provides a signal for the first of either when the character is removed or the humanoid died. https://create.roblox.com/store/asset/13549390287/Character-Loaded-Wrapper

Then, you can use it like this (assuming the module is a child of this script, but feel free to put it anywhere and update the path reference)

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")

local CharacterLoadedWrapper = require(script.CharacterLoadedWrapper)

local grabObjectsContainer = Workspace.GrabObjects

local MAX_HOTBAR_INDEX = 4
local DROP_OFFSET = Vector3.yAxis * 2

local function getHotbarContentsAtIndexForPlayer(player, index)
	local hotbarContainer = player:FindFirstChild(`Hotbar{index}`)
	assert(
		index == math.clamp(index, 1, MAX_HOTBAR_INDEX),
		`Invalid hotbar index {index} outside range [1, {MAX_HOTBAR_INDEX}]`
	)

	if not hotbarContainer then
		return {}
	end

	local hotbarContents = hotbarContainer:GetDescendants()
	return hotbarContents
end

local function onPlayerAdded(player)
	local characterLoadedWrapper = CharacterLoadedWrapper.new(player)
	characterLoadedWrapper.died:Connect(function(character)
		print(`{player.DisplayName} died, dropping items...`)

		local dropCFrame = character:GetPivot()
		for index = 1, MAX_HOTBAR_INDEX do
			local hotbarContents = getHotbarContentsAtIndexForPlayer(player, index)
			for _, item in hotbarContents do
				if item:IsA("BasePart") then
					item:PivotTo(dropCFrame + DROP_OFFSET)
					item.Parent = grabObjectsContainer
				end
			end
		end
		print(`Dropped {player.DisplayName}'s items at {dropCFrame.Position}`)
	end)
	
	characterLoadedWrapper.loaded:Wait()
	player.AncestryChanged:Wait()
	characterLoadedWrapper:destroy()
end

for _, player in Players:GetPlayers() do
	task.spawn(onPlayerAdded, player)
end
Players.PlayerAdded:Connect(onPlayerAdded)

Well I’m pretty sure that most games that save the characters position
(A popular example would be like, deepwoken lets say)

I think the way they do it is just attach a value to the PLAYER, not the character. The value gets updated whenever the players movement velocity is above 0, I can do the same thing but instead of putting it inside a data store I can just make a variable for it inside the player, since the player doesn’t get deleted before roblox tells scripts, only the character.

Ah perfect, so as I was saying to do it with a value I realised roblox already has an instance for specifically positions. Seems like they knew about the issue and decided a bandaid fix was good enough. I really hope they fix this soon though because it is quite a shame.

Did you try using the CharacterLoadedWrapper solution I posted above? Storing a value in the player is less efficient means you need to update it on position changed which takes resources, and that will take network overhead too because it’s replicating all those changes to every player. Better to just track a reference to the character instance and read the position at the time you need to use it (the wrapper handles the instance tracking for you)

I have zero clue how to use any API’s

Oh it’s nothing special. Just go to the link, click Get Model, then in studio go to the My Inventory tab, and insert the CharacterLoadedWrapper. In the folder that gets inserted is a module called CharacterLoadedWrapper. Drag it from that folder to the script so that it’s a child of the script. Delete the leftover folder. Now the script has a modulescript inside it that it can use by calling requre() on that modulescript, like shown in my code above. So it all just works!

Can you explain in a little more detail, Because from what I understand it’s roblox doing things in the wrong order/priority

It should be

- 1 : Player Leaves
- 2 : Roblox tells script
- 3 : Character gets deleted
- 4 : Player gets deleted,

But instead it’s

- 1 : Player Leaves
- 3 : Character Gets Deleted
- 2 : Roblox tells script
- 4 : Player gets deleted

Since roblox is doing things in the wrong order, even the API would get requested after the character is already deleted no?

You should use a remote event to send this whenever the inventory gets updated. You can set a queue/debounce if they’re updating it a lot in a short time. If it’s important for it to be saved/stored on the server, it’s a bad practice to only wait for the last moment, as various errors/crashes can occur which prevent transmitting data at the last moment. You should always send it occasionally, and preferably after they updated the inventory, keeping the server up-to-date as much as possible is ideal.

Audio has a function for starting to play when a player leaves. You can enable this setting on an audio, and have a script check when the audio starts playing. However this might already be too late to do anything.

I really don’t understand what you are talking about, that’s not an issue with my game whatsoever.

You are telling me to use a remote event, which I am already using, it seems you didn’t read the full context before you replied to this.

The game will not have save files, there are no datastores regarding the inventory. When a player dies/leaves all the items will be dropped on the ground so someone else can pick them up.

I don’t need to tell the server when the inventory is getting updated, as I already said the server is the only thing updating the inventory.

Your understanding of the issue is correct. The solution I posted above that uses CharacterLoadedWrapper does not suffer from this issue because it’s tracking the character instance from when it spawns, and uses that reference to fire its own signal when it dies. It’s abstracting some of the difficulty away; the especially useful part for us is that it fires the died signal when the humanoid dies or the character is destroyed, and ensures it only fires once. This covers both use cases in your game of the character dying (but still ragdolling) and player leaving the game.

Script.rbxm (6.1 KB)

If you’re confused with how to set it up, here’s an rbxm file you can drag into studio containing the correct setup.

I mentioned Remote events to provide a full answer, so my message alone is clear and precise and includes what’s necessary, answers here may also be used by others in the future with similar issue, and some may jump through the responses and not read everything. Sorry if this caused issues.

Sorry, I assumed you were storing something necessary on the client that the server needs to know, otherwise there would be no reason to rely on the client to inform the server through remotes events, as the server can already see all information on a server, as well the player position when they leave.