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