Catch-All PlayerAdded / PlayerRemoving Functions Module

Hello developers,

If you’ve been working with Roblox Lua for even a brief time, you have no doubt used PlayerAdded and PlayerRemoving events before. These are useful for establishing server-side statistics, inventories, and abilities for every player in the game. They can also be useful on the client for creating UI’s that display information pertaining to each player.

However, if you’ve worked with these events for a long time, you may have run into an issue where the event does not run for all players. What may happen is, for example, the PlayerAdded event is connected after someone has already joined the game, so the connected function is never run on that player. Another example is a rare instance wherein the PlayerRemoving event is not fired if the server a player crashes.

I’m not sure what the name is for this issue, but there’s doubtless a fancy computer science term for it. Regardless, the solution is to connect the listener to your function, then run through all of the existing items of the same type and run the function on each of them.

function OnPlayerAdded(Player)

end

game:GetService("Players").PlayerAdded:Connect(OnPlayerAdded)
for _,Player in pairs(game:GetService("Players"):GetPlayers()) do 
	OnPlayerAdded(Player)
end

The module of discussion here

… ensures that your onPlayerAdded and onPlayerRemoving functions work on absolutely every single player, regardless of if the script ran late or if the server or a player crashed. It also works for characters, but I don’t find that particularly useful.

How to use:

local Module = 4710901436 -- Replace this with the in-game path if you want to fork the module or you just don't like using on-site code

require(Module)(
	function(JoiningPlayer)
	
	end,
	function(LeavingPlayer)
	
	end,
	function(Player, Character)
	
	end
)

You do not need to input all three of these functions. If you don’t want to use one, leave it as nil.

The following is just one example out of the many times I’ve used this. This is the server-side code for another module of mine that allows players to change their camera’s field of view with the command /fov.

If you want to nitpick about the code sample above or ask why I’m placing non UI-related client code in a ScreenGui, message me in private.

When called, the module function will return the connections should you need them. For example, this code …

local PlayerAddedConnection, PlayerRemovingConnection, CharacterConnections = require(4710901436)(
	function(JoiningPlayer)
	
	end,
	function(LeavingPlayer)
	
	end,
	function(Player, Character)
	
	end
)

… returns the PlayerAdded connection, the PlayerRemoving connection, and a table for each player’s CharacterAdded connection (index == player instance), respectively. I don’t really know why anyone would need these connections, but they are available nonetheless.

Please provide feedback if you think this could have been done better or if you have a suggestion for extra functionality.

@sleitnick did a good video on the problem at hand here as well. Thanks to @IGOTHISLOL for the video link.

9 Likes

I suggest you if you use this that you add spawn.

function OnPlayerAdded(Player)

end

game:GetService("Players").PlayerAdded:Connect(OnPlayerAdded)
for _,Player in pairs(game:GetService("Players"):GetPlayers()) do 
	OnPlayerAdded(Player)
end

Turns into:

function OnPlayerAdded(Player)

end

game:GetService("Players").PlayerAdded:Connect(OnPlayerAdded)
for _,Player in pairs(game:GetService("Players"):GetPlayers()) do 
    spawn(function()
	    OnPlayerAdded(Player)
    end)
end
1 Like

The first code sample in my post is simply a proof of concept of how to ensure every player gets the function called on them. What you saw is a good catch, but not so fast, I got there first :smiling_imp:

As you know, using spawn allows the code to continue without yielding for whatever is inside that spawned function to finish. If you read the contents of my module, though, you will see I do the same thing using coroutine.wrap and that I even made a comment explaining this very phenomenon.

local function ApplyFunctionToAll(Function)
	for _,Player in pairs(Players:GetPlayers()) do
		coroutine.wrap(Function)(Player) -- If the function has a yield, we don't want every player to have to wait in line for their function call.
	end
end

Recurring DevForum protagonist @colbert2677 rightfully notes on why not to use spawn in the post below this one.

2 Likes

Friends don’t let friends use spawn.

8 Likes

PlayerRemoving and BindToClose shouldn’t work in any way if the server crashes since all the memory is flooded, how does this module fix that issue?

1 Like

Hmmm, I was under the impression that BindToClose always runs when the server closes regardless of why. It appears that’s just the player specifically, so the only way you’d be able to save data regardless of crashes would be to have an auto-saving loop every minute or so. Though the point made in that post is in conflict with the post directly below it, which states that PlayerRemoving is in fact reliable.

I’m not sure who’s right here, and considering that it’s a pain to test with these things (can’t read print statements run in BindToClose functions because, well, the server window [including the output] closes), I’ll just keep this code as is. The way it works is that the connected function for PlayerRemoving and the loop that runs this same function on each player in BindToClose both actually check if the other event has already been run for a given player, so no function you assign for leaving the game will ever be run twice on a single player here. I should also note that the table storing each player’s boolean values (for whether the removing function has already been run on them) uses the player instance as the index, so this would not cause a failure to save should they rejoin the same server and then leave again.

Later edit: here’s what I was talking about with regards to PlayerRemoving being unreliable.

Will this also work client sided? I was making a trade system where is shows list of all players in the UI but for some reason .PlayerAdded and PlayerRemoving dont work client sided, so I had to use remote events.

PlayerAdded and PlayerRemoving work fine on the client. My guess is your UI did not take into account any of the existing players when someone joined. This module would work, as it checks if the code is running on the server before calling BindToClose.

Unfortunately however, you can’t just require the ID from a client script, nor can you have the server reparent the required on-site module to ReplicatedStorage during runtime, as testing shows the client just sees a blank script if you do this. I’ve wrriten out a detailed bug report regarding this, pending to post in the proper sbuforum as soon as I am able. For now, you will have to insert the module into your game.

1 Like

it does work client sided. I think your making a mistake that PlayerAdded would fire for the client for people who joined before him. That isn’t the case. PlayerAdded would only fire for the client when a player joins after him, which makes sense technically.

I tested it with 3 local players yet it didnt work. But this video helped;