Feedback for PlayerManager Module

What PlayerManager does is simplify Player join / leave and Character spawn / despawn events and provide more control over what happens when with additional bells and whistles such as ragdoll, corpse, unified character mass, sanitizing accessories, etc

-- TODO:

local CONFIGURATION = script:GetAttributes()

local Players = game:GetService("Players")
Players.CharacterAutoLoads = CONFIGURATION.CharacterAutoLoads
Players.RespawnTime = CONFIGURATION.RespawnTime

local StarterPlayer = game:GetService("StarterPlayer")
StarterPlayer.LoadCharacterAppearance = CONFIGURATION.LoadCharacterAppearance

local HttpService = game:GetService("HttpService")

local BLACKLIST: { [string]: boolean } = HttpService:JSONDecode(CONFIGURATION.BLACKLIST)

local CHARACTERS_FOLDER = Instance.new("Folder")
CHARACTERS_FOLDER.Name = "Characters"
CHARACTERS_FOLDER.Parent = workspace

local ragdollUtil = require(script.ragdollUtil) :: (Humanoid) -> ()

local Debris = game:GetService("Debris")

-- Execution order
-- PlayerAdded > PlayerJoined

-- PlayerAdded is when you initialize the player, for example getting sessiondata
-- PlayerJoined is after initialization

-- Execution order
-- PlayerRemoving > PlayerDestroyed

-- PlayerRemoving is when you deinitialize the player, for example saving sessiondata
-- PlayerDestroyed is after deinitialization

local PlayerManager = { PlayerAdded = {}, PlayerJoined = {}, PlayerRemoving = {}, PlayerDestroyed = {} }

-- Execution order
-- CharacterLoaded > CharacterAdded > CharacterSpawned

-- CharacterLoaded is when the Character is created
-- CharacterAdded is when the Player.Character is set to Character
-- CharacterSpawned is when the Character.Parent is set to workspace

-- CharacterRemoving > CharacterDestroyed

-- Died

local CharacterManager = PlayerManager
CharacterManager.Characters = {}

function CharacterManager.handleOnCharacterLoaded(player: Player, callback: (character: Model) -> ())
	local CharacterLoaded = CharacterManager.Characters[player].CharacterLoaded
	table.insert(CharacterLoaded, callback)
end

function CharacterManager.handleOnCharacterAdded(player: Player, callback: (character: Model) -> ())
	local CharacterAdded = CharacterManager.Characters[player].CharacterAdded
	table.insert(CharacterAdded, callback)
end

function CharacterManager.handleOnCharacterSpawned(player: Player, callback: (character: Model) -> ())
	local CharacterSpawned = CharacterManager.Characters[player].CharacterSpawned
	table.insert(CharacterSpawned, callback)

	if player.Character then
		task.spawn(callback, player.Character)
	end
end

function CharacterManager.handleOnCharacterDied(player: Player, callback: (character: Model) -> ())
	local Died = CharacterManager.Characters[player].Died
	table.insert(Died, callback)
end

function CharacterManager.handleOnCharacterRemoving(player: Player, callback: (character: Model) -> ())
	local CharacterRemoving = CharacterManager.Characters[player].CharacterRemoving
	table.insert(CharacterRemoving, callback)
end

function CharacterManager.handleOnCharacterDestroyed(player: Player, callback: (character: Model) -> ())
	local CharacterDestroyed = CharacterManager.Characters[player].CharacterDestroyed
	table.insert(CharacterDestroyed, callback)
end

local function setDensity(Size: Vector3): number
	local volume: number = Size.X * Size.Y * Size.Z
	return 5 / volume
end

function CharacterManager._loadCharacter(player: Player)
	local userid: number = Players:GetUserIdFromNameAsync(player.Name)

	-- 215718515 // https://www.roblox.com/catalog/215718515/Fiery-Horns-of-the-Netherworld
	-- 89171071 // https://www.roblox.com/catalog/89171071/Boss-White-Hat
	-- 305888394 // https://www.roblox.com/catalog/305888394/Brighteyes-Witches-Brew-Hat

	-- local humanoidDescription: HumanoidDescription = Players:GetHumanoidDescriptionFromUserId(userid)
	-- humanoidDescription.HatAccessory ..= ",215718515 ,89171071, 305888394"
	-- local character = Players:CreateHumanoidModelFromDescription(humanoidDescription, Enum.HumanoidRigType.R15)

	local character = Players:CreateHumanoidModelFromUserId(userid)
	character.Name = player.Name

	character.ModelStreamingMode = Enum.ModelStreamingMode.PersistentPerPlayer
	character:AddPersistentPlayer(player)

	do
		local humanoid: Humanoid = character.Humanoid

		humanoid.BreakJointsOnDeath = CONFIGURATION.BreakJointsOnDeath
		humanoid.RequiresNeck = CONFIGURATION.RequiresNeck
		humanoid.AutomaticScalingEnabled = CONFIGURATION.AutomaticScalingEnabled

		humanoid:SetStateEnabled(Enum.HumanoidStateType.Dead, CONFIGURATION.DeathEnabled)

		local rootpart = humanoid.RootPart
		rootpart.RootPriority = 127

		if CONFIGURATION.SetDensity then
			rootpart.CustomPhysicalProperties = PhysicalProperties.new(setDensity(rootpart.Size), 0.3, 0.5)
		end

		for _, descendant: Instance in character:GetDescendants() do
			if descendant:IsA("BasePart") then
				local basepart: BasePart = descendant
				basepart.Material = Enum.Material.SmoothPlastic
				basepart.CastShadow = false
				basepart.Massless = true
				basepart.CanTouch = false
				basepart.CanQuery = false
			end
		end

		if CONFIGURATION.SanitizeAccessories then
			for _, accessory: Accessory in humanoid:GetAccessories() do
				for _, descendant: Instance in accessory:GetDescendants() do
					if BLACKLIST[descendant.ClassName] then
						descendant:Destroy()
					end
				end
			end
		end

		if CONFIGURATION.RagdollEnabled then
			ragdollUtil(humanoid)
		end
	end

	local CharacterLoaded = CharacterManager.Characters[player].CharacterLoaded
	for _, callback: (character: Model) -> () in ipairs(CharacterLoaded) do
		callback(character)
	end

	player.Character = character
end

function CharacterManager._spawnCharacter(player: Player)
	local character = player.Character

	character.Parent = CHARACTERS_FOLDER

	local humanoid = character.Humanoid :: Humanoid
	local rootpart = humanoid.RootPart

	rootpart:SetNetworkOwner(player)

	if not CONFIGURATION.LayeredClothingEnabled then
		local humanoidDescription: HumanoidDescription = humanoid:GetAppliedDescription()
		humanoidDescription:SetAccessories({}, false)
		humanoid:ApplyDescriptionReset(humanoidDescription)
	end

	local CharacterSpawned = CharacterManager.Characters[player].CharacterSpawned

	for _, callback: (character: Model) -> () in ipairs(CharacterSpawned) do
		task.spawn(callback, character)
	end
end

Players.PlayerAdded:Connect(function(addedPlayer: Player)
	for _, callback in ipairs(PlayerManager.PlayerAdded) do
		callback(addedPlayer)
	end

	for _, callback in ipairs(PlayerManager.PlayerJoined) do
		task.spawn(callback, addedPlayer)
	end
end)

function PlayerManager.handleOnPlayerAdded(callback: (Player) -> ())
	table.insert(PlayerManager.PlayerAdded, callback)

	for _, player: Player in ipairs(Players:GetPlayers()) do
		task.spawn(callback, player)
	end
end

PlayerManager.handleOnPlayerAdded(function(addedPlayer: Player)
	CharacterManager.Characters[addedPlayer] = {
		CharacterLoaded = {},
		CharacterAdded = {},
		CharacterSpawned = {},

		Died = {},

		CharacterRemoving = {},
		CharacterDestroyed = {},
	}
end)

function PlayerManager.handleOnPlayerJoined(callback: (Player) -> ())
	table.insert(PlayerManager.PlayerJoined, callback)

	for _, player: Player in ipairs(Players:GetPlayers()) do
		task.spawn(callback, player)
	end
end

PlayerManager.handleOnPlayerJoined(function(joiningPlayer: Player)
	local player = joiningPlayer

	do
		player.CharacterAdded:Connect(function(addedCharacter: Model)
			local character = addedCharacter

			if CharacterManager.Characters[player] then
				local CharacterAdded = CharacterManager.Characters[player].CharacterAdded
				for _, callback in ipairs(CharacterAdded) do
					task.spawn(callback, character)
				end
			end

			character.Destroying:Once(function()
				local CharacterDestroyed = CharacterManager.Characters[player].CharacterDestroyed
				for _, callback in ipairs(CharacterDestroyed) do
					task.defer(callback, character)
				end
			end)

			CharacterManager._spawnCharacter(player)
		end)
	end

	CharacterManager.handleOnCharacterSpawned(player, function(spawnedCharacter)
		local humanoid = spawnedCharacter.Humanoid :: Humanoid

		humanoid.Died:Once(function()
			local Died = CharacterManager.Characters[player].Died
			for _, callback in ipairs(Died) do
				task.defer(callback, humanoid)
			end
		end)
	end)

	CharacterManager.handleOnCharacterDied(player, function()
		if CONFIGURATION.CorpseEnabled then
			local corpse = player.Character:Clone()
			player.Character = nil
			corpse.Parent = workspace

			local humanoid: Humanoid = corpse.Humanoid

			humanoid:ChangeState(Enum.HumanoidStateType.Dead)

			Debris:AddItem(corpse, CONFIGURATION.CorpseTime)
		end

		task.delay(CONFIGURATION.RespawnTime, function()
			player.Character = nil
			CharacterManager._loadCharacter(player)
		end)
	end)

	do
		player.CharacterRemoving:Connect(function(removingCharacter)
			local character = removingCharacter
			local humanoid = character.Humanoid :: Humanoid

			if CONFIGURATION.KillOnDespawned then
				humanoid:SetStateEnabled(Enum.HumanoidStateType.Dead, true)
				humanoid:ChangeState(Enum.HumanoidStateType.Dead)
			end

			local CharacterRemoving = CharacterManager.Characters[player].CharacterRemoving
			for _, callback in ipairs(CharacterRemoving) do
				task.defer(callback, character)
			end
		end)
	end

	do
		player.Destroying:Once(function()
			for _, callback in ipairs(PlayerManager.PlayerDestroyed) do
				task.defer(callback, player)
			end
			CharacterManager.Characters[player] = nil
		end)
	end

	CharacterManager._loadCharacter(player)
end)

Players.PlayerRemoving:Connect(function(removingPlayer: Player)
	for _, callback in ipairs(PlayerManager.PlayerRemoving) do
		task.spawn(callback, removingPlayer)
	end
end)

function PlayerManager.handleOnPlayerRemoving(callback: (Player) -> ())
	table.insert(PlayerManager.PlayerRemoving, callback)
end

function PlayerManager.handleOnPlayerDestroyed(callback: (Player) -> ())
	table.insert(PlayerManager.PlayerDestroyed, callback)
end

if CONFIGURATION.TestEnabled then
	PlayerManager.handleOnPlayerAdded(function()
		print("handleOnPlayerAdded", tick())
	end)

	PlayerManager.handleOnPlayerJoined(function(player: Player)
		print("handleOnPlayerJoined", tick())

		CharacterManager.handleOnCharacterLoaded(player, function()
			print("handleOnCharacterLoaded", tick())
		end)

		CharacterManager.handleOnCharacterAdded(player, function()
			print("handleCharacterAdded", tick())
		end)

		CharacterManager.handleOnCharacterSpawned(player, function()
			print("handleOnCharacterSpawned", tick())
		end)

		CharacterManager.handleOnCharacterRemoving(player, function()
			print("handleOnCharacterRemoving", tick())
		end)

		CharacterManager.handleOnCharacterDestroyed(player, function()
			print("handleOnCharacterDestroyed", tick())
		end)
	end)

	PlayerManager.handleOnPlayerRemoving(function()
		print("handleOnPlayerRemoving", tick())
	end)

	PlayerManager.handleOnPlayerDestroyed(function()
		print("handleOnPlayerDestroyed", tick())
	end)
end

return PlayerManager
3 Likes

This is all preference, but two things that I’d consider changing are:
Add strict as this script’s inference mode (the code below should be put on line 1 btw). It’ll make typechecking so much easier to debug and more useful. By default in luau (without strict), the script will automatically convert your value into the ‘correct type’ if this isn’t implemented, which is good for avoiding errors but also defeats half the purpose of type checking.

–!strict

Secondly, I personally avoid using do statements, as they’re not necessary in luau if your already using proper naming for your respective scope (which you are!)

It’s already —!strict, I’m using VSCode, LuauLSP and Rojo and I already have that enabled automatically

I can include it for people using Studio Script Editor

In regards to do statements, it’s mostly just for organizing and visual separation and it also let’s me collapse code to hide it from view

thank you for your feedback!

1 Like

UPDATE 1

  • Added option for LayeredClothing through “LayeredClothingEnabled” configuration
  • Added option to Sanitize Accessories (This removes any unwanted descendants from the Accessory for example, Sound, Script, Fire, Sparkles, etc)
  • Added a list of unwanted ClassNames through “BLACKLIST” configuration for Sanitizing Accessories
  • Added option to set Humanoid scaling “AutomaticScalingEnabled” configuration
1 Like