Player Life Cycle

I noticed my script that handles the players life cycle was not working 100% of the time so I looked up code on Roblox.

The new code works great. It runs every function every time (as expected).

I only have on question:

In the CharacterAppearanceLoaded, the connection is handled differently than everywhere else.

I do not know why. I also worry that it is a memory leak because it looks like it just keeps reconnecting until the player leaves the experience.

LINK TO ROBLOX: Player | Documentation - Roblox Creator Hub

MY CODE:




--==============
-- PLAYER ADDED
--==============

-- SOURCE: https://create.roblox.com/docs/reference/engine/classes/Player#LoadCharacter
-- SOURCE: https://create.roblox.com/docs/reference/engine/classes/Player#CharacterAdded
-- SOURCE: https://create.roblox.com/docs/reference/engine/classes/Player#CharacterAppearanceLoaded
-- SOURCE: https://create.roblox.com/docs/reference/engine/classes/Player#CharacterRemoving

local Players = game:GetService("Players")
local RESPAWN_DELAY = 5
Players.CharacterAutoLoads = false

local function PlayerAdded(player)
	
	print(player.Name .. "'s player has been added")
	
	
	
	--=================
	-- CHARACTER ADDED
	--=================

	local function CharacterAdded(character)
		
		print(character.Name .. "'s character has been added")

		local humanoid = character:WaitForChild("Humanoid")

		local function CharacterDied()
			
			print(character.Name .. "'s character has died")
			
			task.wait(RESPAWN_DELAY)
			player:LoadCharacter()
		end

		humanoid.Died:Connect(CharacterDied)
	end
	player.CharacterAdded:Connect(CharacterAdded)
	
	
	
	--====================
	-- CHARACTER REMOVING
	--====================

	local function CharacterRemoving(character)
		print(character.Name .. "'s character has been removed")
	end
	player.CharacterRemoving:Connect(CharacterRemoving)
	
	
	
	--===================
	-- APPEARANCE LOADED
	--===================
	
	local connection = player.CharacterAppearanceLoaded:Connect(function(character)
		
		print(character.Name .. "'s character appearance has been loaded")
		-- All accessories have loaded at this point
		--local numAccessories = #character:GetAccessories()
		--print(("Destroying %d accessories for %s"):format(numAccessories, player.Name))
		--local humanoid = character:FindFirstChildOfClass("Humanoid")
		--humanoid:RemoveAccessories()
	end)

	-- Make sure we disconnect our connection to the player after they leave
	-- to allow the player to get garbage collected
	
	player:LoadCharacter()
	player.AncestryChanged:Wait()
	connection:Disconnect()
	
end

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



--=================
-- PLAYER REMOVING
--=================

-- SOURCE: https://create.roblox.com/docs/en-us/reference/engine/classes/Players#PlayerRemoving

local function PlayerRemoving(player)
	print(player.Name .. " has left the experience")
end

Players.PlayerRemoving:Connect(PlayerRemoving)
3 Likes

Those are anonymous functions. The following two samples are effectively the same.

someEvent:Connect(function() ... end)
local function Callback() ... end
someEvent:Connect(Callback)

Both pass a function as a parameter to :Connect(). In turn, the engine calls that function each time the event occurs, and usually sends some arguments along, in your case player or player’s character. The latter function has a name and can be used in more than one :Connect() or called by you, whereas the anonymous one cannot be.

:Connect() also wraps the callback in a coroutine.


PlayerAdded runs a single time when the player joins. PlayerRemoving also runs once (except the studio play test - rarely - ends before it gets the chance to).

The LoadCharacter() call triggers CharacterRemoving, then CharacterAdded, then CharacterAppearanceLoaded.

Regarding CharacterAppearanceLoaded code sample

Line 6 throws an error; GetAccessories() is a method of humanoid, not character. Lines 14 and 15 are no longer needed because player and character instances now get destroyed automatically.


Players and characters actually indeed used to leak memory. By now, the New Player and Character Destroy Behavior should be enabled by default will soon be enabled by default, so that’s that will be taken care of on the server. Once an instance is destroyed, all connections bound to it are also deactivated.


The same script with only anonymous functions
local Players = game:GetService("Players")

local RESPAWN_DELAY = 5

Players.CharacterAutoLoads = false

local function PlayerAdded(player)
    print(player.Name .. "'s player has been added")
    
    player.CharacterAdded:Connect(function(character)
        print(character.Name .. "'s character has been added")
        
        local humanoid = character:WaitForChild("Humanoid")
        
        humanoid.Died:Once(function()
            print(character.Name .. "'s character has died")
            task.wait(RESPAWN_DELAY)
            player:LoadCharacter()
        end)
    end)
    
    player.CharacterRemoving:Connect(function(character)
        print(character.Name .. "'s character has been removed")
    end)
    
    player.CharacterAppearanceLoaded:Connect(function(character)
        print(character.Name .. "'s character appearance has been loaded")
    end)

    player:LoadCharacter()
end

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

Players.PlayerRemoving:Connect(function(player)
    print(player.Name .. " has left the experience")
end)

UPDATE @mc7oof

As it turns out, workspace.PlayerCharacterDestroyBehavior does not default to Enabled yet. According to the announcement, it will early this year.
I suggest to turn it on because it is planned to be the only behavior in the near future. If you opt to keep it disabled for the time being, it would be more efficient to utilise a maid or destroy old characters upon respawn and the player instance when the player leaves manually, rather than disconnecting individual connections.

2 Likes

Thank you or your detailed reply.

I understand your changes. But, I still don’t understand why Roblox does not treat the CharacterAppearanceLoaded the same as other connections.

Is there some reason they connected/disconnected the CharacterAppearanceLoaded in their sample:

I also noticed the humanoid/character error in their sample. Which, of course, makes me question the entire sample. :grimacing:

It doesn’t work any differently. CharacterAppearanceLoaded is just another connection that gets called shortly after CharacterAdded.

→ I’m referring to player.AncestryChanged:Wait() and connection:Disconnect().

Player becomes a descendant of PlayerService, and AncestryChanged waits for it to be parented to nil on player’s leave, before disconnecting CharacterAppearanceLoaded. None of this is necessary if workspace.PlayerCharacterDestroyBehavior is enabled.

The comments are also not entirely correct, the player instance wouldn’t be “garbage collected” regardless.

I can open a document issue report.

Edit. Done :white_check_mark:

1 Like

I mean that they are connecting/disconnecting the function when the player ancestry changes. That is the part that had me scratching my head.

1 Like

It sounds like that entire sample is outdated (and odd).

I will just use your updated script as a base for my Player Life Cycle.

Thanks for doing that!

No problem!

Feel free to use your original version, just remove those parts dedicated to disconnecting CharacterAppearanceLoaded, and perhaps pass it a named function too, for the sake of consistency. Using named or anonymous functions is more or less the matter of preference. Your callback for CharacterAdded, CharacterRemoving, and CharacterAppearanceLoaded could also be defined outside of PlayerAdded() - the only practical difference would be that they wouldn’t be able to reach out of their scope and access the player argument passed into PlayerAdded().

New sample for removing accessories
local Players = game:GetService("Players")

local function onPlayerAdded(player)
    player.CharacterAppearanceLoaded:Connect(function(character)         
        -- All accessories have loaded at this point
        local humanoid = character.Humanoid
        local accessoryList = humanoid:GetAccessories()
        print(`Destroying {#accessoryList} accessories for {player.Name}`)
        humanoid:RemoveAccessories()
    end)
end

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

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.