Player.CharacterAdded behaves inconsistently depending on whether one connects to it in a Script or in a LocalScript. The argument received on a LocalScript is consistently an empty Model, while the argument received in a Script is a Model containing character parts and a Humanoid.
Reproduction steps
- Open an empty baseplate in Studio;
- Insert a Script in
game.ServerScriptService
with the following code:
local Players = game:GetService("Players")
Players.PlayerAdded:Connect(function(new_player)
new_player.CharacterAdded:Connect(function(new_character)
print(new_player.Name, "spawned:", #new_character:GetChildren())
end)
end)
- Insert a LocalScript in
game.StarterPlayer.StarterPlayerScripts
with the following code:
local player = game:GetService("Players").LocalPlayer
player.CharacterAdded:Connect(function(new_character)
print(player.Name, "spawned:", #new_character:GetChildren())
end)
- Playtest the game (through the F5 hotkey) and reset the character a few times.
- Open the output, and note how inconsistent results are shown depending on whether it’s from the LocalScript or the Script, and how these results remain the same when you reset the character: in the client output, the received argument has no descendants.
Publishing the place and playing it through the website also renders the same results.
This is unexpected, since the developer hub guarantees that instances such as parts and the Humanoid are there when the event is fired (emphasis added to relevant portion of the quote):
Note that the
Humanoid
and its body parts (head, torso and limbs) will exist when this event fires, but clothing items likeHats
andShirts
,Pants
may take a few seconds to be added to the character (connectInstance.ChildAdded
on the added character to detect these).
(as the page does not explicitly list any differences between client and server behavior, I assume this should be expected in both cases.)
This happens in Studio and in live games, and has confirmedly existed since at least 2020-06-20, although it might have been around for much longer.
Some immediate consequences of this issue are:
- I need to yield when manipulating character Humanoids or parts on the client as they are added, with calls to
wait
orWaitForChild
or with a connection toChildAdded
, while I do not need to do this on the server. - This is not documented, so unsuspecting developers will face issues and will need to debug them, only to ultimately find this weird behavior.
Potential breaking effects fixing this issue might have:
- Existing local code relying on
ChildAdded
/DescendantAdded
on new characters to modify their parts or humanoids can break or yield indefinitely.