The CharacterAdded and PlayerAdded scripting events are a beginner’s trap!
You are almost guaranteed to run into issues the first time using them. I’ve seen this issue pop up for new programmers in my experience. This tutorial will help you understand the problem.
TL;DR please don’t do this:
-- naive method
game.Players.PlayerAdded:Connect(function(player)
player.CharacterAdded:Connect(function(character)
print("character loaded!", character)
-- YOUR CODE HERE
end
end
Do this instead:
-- proper method
local function newCharacter(character)
print("character loaded!", character)
-- YOUR CODE HERE
end
local function newPlayer(player)
if player.Character then
task.defer(newCharacter, player.Character)
end
player.CharacterAdded:Connect(newCharacter)
end
for _,player in game.Players:GetPlayers() do
task.defer(newPlayer, player)
end
game.Players.PlayerAdded:Connect(newPlayer)
“Why shouldn’t I do the naive method?”
Issue: binding .added events without checking for existing children
By doing the naive method, you will eventually* get code that doesn’t run in Studio when you connect CharacterAdded/PlayerAdded, and it will happens randomly.
Race conditions (in this context) are an issue in Roblox where certain code blocks fire out of order or in a random order.
You want to set your character’s WalkSpeed to 32 when they first spawn in. You create a server script in ServerScriptService.
game.Players.PlayerAdded:Connect(function(player)
player.CharacterAdded:Connect(function(character)
character.Humanoid.WalkSpeed = 32
end
end
In Roblox studio, we can’t guarantee that this script loads before the player spawns in*. Sometimes the game spawns you in first, THEN the script runs. Other times, the script runs first, THEN you spawn in. In the example, you will only get WalkSpeed = 32 IF the script runs before you join the game.
If the player joined the game before we do :Connect, PlayerAdded will not trigger. Same with CharacterAdded, if the player spawned BEFORE we call :Connect, it won’t trigger.
We MUST call PlayerAdded:Connect() before the first players join the game.
We MUST call CharacterAdded:Connect() before the first character respawns.
This is normally easy to do for a published game, since servers often need to boot up before letting players in. You will rarely see this issue in-game (although it can still happen).
Studio Testing does not have this luxury.
We have no control over which gets to go first. The Roblox engine decides it.
Therefore, the best we can do is account for the already existing players/characters that spawned in before the script runs.
Hence:
if player.Character then -- account for an already spawned character
handleCharacter()
end
player.CharacterAdded:Connect(handleCharacter)
OR
for _,player in game.Players:GetPlayers() do -- loop through all existing players
handlePlayer(player)
end
game.Players.PlayerAdded:Connect(handlePlayer)
This guarantees that the function will ALWAYS bind to the player/character regardless of ordering, join time, or yielding.
* Correction pointed out by @Judgy_Oreo:
Correction + Author's Notes
Correction: You will not face this issue if your script has no yields prior to the event binds. For example, the naive code above works perfectly by itself.
However, in real game projects with varying dependencies on open sourced code, roblox async APIs, streaming enabled, :WaitForChild(), InvokeServer/Client, varying programmer styles, and other yielding causes are bound to show up in the code prior to character binding. Worse yet, the script becomes a ticking time bomb for future problems when eventually a yielding setup is used. Without proper planning and heavy enforcement of a specific code style, we cannot unintentionally avoid this issue. Therefore, I think it is safer to implement this setup than to not do it.
Apply first, connect last
Why can’t I do:
player.CharacterAdded:Connect(handleCharacter) -- inversion, connect first, loop after
if player.Character then
handleCharacter()
end
I have not seen this cause issue in practice. I believe this is bad habit though. There may or may not be a scenario where CharacterAdded might trigger simultaneously along with player.Character, leading to a double trigger, which could be bad. In my years of experience I’ve only been using connect-last and can’t vouch if connect-first is valid and unlikely to cause bugs. TL;DR you can do it but at your own risk.
if player.Character then -- my preferred ordering
handleCharacter()
end
player.CharacterAdded:Connect(handleCharacter) -- connect-last
“Why do you use task.defer()?”
Consider this scenario:
local function newPlayer(player) -- this function yields!
task.wait(5)
end
for _,v in game.Players:GetPlayers() do
newPlayer(v)
end
game.Players.PlayerAdded:Connect(newPlayer)
If your binded function has a wait(), it will yield the loop, stalling the PlayerAdded event. If anyone joins the game during that for loop, they won’t be “caught” by the function.
We MUST perform the for loop quickly and bind PlayerAdded:Connect(), otherwise, we have a blind spot of at least 5 seconds in this example!
Error handling/disconnecting events
This tutorial’s scope does not in include disconnecting events. Rarely do I ever disconnect Character/Player binds. For me, these binds are usually part of scripts that run once in runtime. You will have to figure out a disconnecting system.
Errors will show the correct stack trace so they don’t need additional work.
“Ok, I won’t do the thing! Do you have a module to do this automatically?”
Yes, I wrote this for PlayerAdded and CharacterAdded since they are the most common use cases.
--[[
SAMPLE CODE:
local CharacterBind = require(...)
CharacterBind:BindPlayer(function(player: Player)
-- this code is guaranteed to run once for all players, current or future
print("procesing player", player)
task.wait(1) -- simulate yielding
print("procesing done", player)
end)
CharacterBind:BindCharacter(function(character: Model, player: Player)
-- this code is guaranteed to run once for every character, current or future
-- respawning will also trigger this code on the new character
print("procesing character", character, "belonging to player", player)
task.wait(1) -- simulate yielding
print("procesing done", character, "belonging to player", player)
end)
--]]
local module = {}
function module:BindPlayer(playerFunction)
-- runs playerFunction on all players, including ones that join in the future
for _,player in game:GetService("Players"):GetPlayers() do
task.defer(playerFunction, player)
end
game:GetService("Players").PlayerAdded:Connect(playerFunction)
end
function module:BindCharacter(characterFunction)
-- runs playerFunction on all characters, including ones that join/respawn in the future
module:BindPlayer(function(player)
if player.Character then
task.defer(characterFunction, player.Character, player)
end
player.CharacterAdded:Connect(function(character)
characterFunction(character, player)
end)
end)
end
return module