HELP Saving Custom Character's appearance

Hello,

I have a default custom character (StarterCharacter), and in the game you can customize it. Like a character customization system. You can change the bodycolors, shirts, pants, etc… But, the problem is, leaving the game/resetting the character doesnt save your character, and loads you in as the default startercharacter.

Am I supposed to replicate it to the client or something? How would I do that?

I’ve been trying to fix this for so long, pls help me

Script:
local datastore = datastores:GetDataStore("DataStore")
local players = game:GetService("Players")

local properties = {"HeadColor", "LeftArmColor", "LeftLegColor", "RightArmColor", "RightLegColor", "TorsoColor"}
local playersData = {}

local function serializeData(character)
	local data = {}
	local bodyColors = character["Body Colors"]
	for _, property in ipairs(properties) do
		data[property] = bodyColors[property].Name
	end
	
	local shirt = character:FindFirstChild("Shirt")
	if shirt then
		data["Shirt"] = shirt.ShirtTemplate
	end
	
	local pants = character:FindFirstChild("Pants")
	if pants then
		data["Pants"] = pants.PantsTemplate
	end
	
	local shirtGraphic = character:FindFirstChild("ShirtGraphic")
	if shirtGraphic then
		data["ShirtGraphic"] = shirtGraphic.Graphic
	end
	
	return data
end

local function deserializeData(character, data)
	local bodyColors = character["Body Colors"]
	for _, property in ipairs(properties) do
		bodyColors[property] = BrickColor.new(data[property])
	end
	
	if data["Shirt"] then
		local shirt = character:FindFirstChild("Shirt")
		if shirt then
			shirt.ShirtTemplate = data["Shirt"]
		else
			shirt = Instance.new("Shirt")
			shirt.Parent = character
			shirt.ShirtTemplate = data["Shirt"]
		end
	end
	
	if data["Pants"] then
		local pants = character:FindFirstChild("Pants")
		if pants then
			pants.PantsTemplate = data["Pants"]
		else
			pants = Instance.new("Pants")
			pants.Parent = character
			pants.PantsTemplate = data["Pants"]
		end
	end
	
	if data["ShirtGraphic"] then
		local shirtGraphic = character:FindFirstChild("ShirtGraphic")
		if shirtGraphic then
			shirtGraphic.Graphic = data["ShirtGraphic"]
		else
			shirtGraphic = Instance.new("ShirtGraphic")
			shirtGraphic.Parent = character
			shirtGraphic.Graphic = data["ShirtGraphic"]
		end
	end
end

local function onPlayerAdded(player)
	local function onCharacterRemoving(character)
		local function onPlayerRemoving(plr)
			if player == plr then
				local data = serializeData(character)
				local success, result = pcall(function()
					return datastore:SetAsync("BodyColors_"..player.UserId, data)
				end)

				if success then
					if result then
						print(result)
					end
				else
					warn(result)
				end
			end
			
			playersData[plr] = nil
		end

		players.PlayerRemoving:Connect(onPlayerRemoving)
	end

	local function onCharacterLoaded(character)
		local data = playersData[player]
		if data then
			deserializeData(character, data)
		end
	end

	player.CharacterRemoving:Connect(onCharacterRemoving)
	player.CharacterAppearanceLoaded:Connect(onCharacterLoaded)

	local success, result = pcall(function()
		return datastore:GetAsync("BodyColors_"..player.UserId)
	end)

	if success then
		if result then
			playersData[player] = result
		end
	else
		warn(result)
	end
end

players.PlayerAdded:Connect(onPlayerAdded)

I’m not getting any errors or warnings, but nothing is printing either

2 Likes

Have you try testing it out in the actual game and not roblox studio?

Yes, the same issue exists within the actual game. Thx for replying

Can you put

game:BindToClose(function()
	wait(1)
end)

Where would I put that? Would that even help with the issue

The character doesn’t save because it’s a custom character and there’s no datastore

You have to make a datastore to save the changes that they made to the character, then get the data and apply the changes when they join again

You can save the data locally and use the same process to apply the changes when they reset

I have a datastore, the script is under the video I posted. I feel like it should work, but nothing is happening

oops i didnt see the script

try printing the data to see if it saved correctly

I tried printing the results using this

					if result then
						print(result)
					end
				else
					warn(result)
				end
			end

but it doesnt print anything

Please put it inside your data store script

Also why is you doing bodyColors[property].Name instead of doing bodyColors[property]? Aren’t you just setting every color to the “Body Colors” which is the name?

1 Like

Also can you check if anything prints out when CharacterAdded or CharacterAppearanceLoaded event get fired?

I made a demo file, make sure to publish it and turn on the Enable Studio Access to API Services:
Skin Color Save.rbxl (81.0 KB)

2 Likes

Thank you! This does actually kinda save and load colors which is a huge step in the right direction. There is a new problem, though. When the player dies and respawns, the shirt/pants colors are removed and all the colors become the skin color. I’m pretty sure its due to this function:

plr.CharacterRemoving:Connect(function(char)
		playerSkinColorDatas[stringUserId] = char["Body Colors"].HeadColor3 
	end)

The only datatypes you can save to a datastore are strings, numbers, and booleans. If OP just put bodyColors[property], it would store a value of the BrickColor data type. @mrfIuffywaffles, here’s a modified version of your script that I created:

local datastores = game:GetService("DataStoreService")
local datastore = datastores:GetDataStore("DataStore")
local players = game:GetService("Players")

local properties = {"HeadColor", "LeftArmColor", "LeftLegColor", "RightArmColor", "RightLegColor", "TorsoColor"}
local playersData = {}
local lastsavedChar = {}

local function serializeData(plr)
	local data = playersData[plr] or {} -- check if player has data or create empty table
	local character = lastsavedChar[plr] -- we reference the last saved character
	local bodyColors = character["Body Colors"]
	for _, property in ipairs(properties) do
		data[property] = bodyColors[property].Name
	end

	local shirt = character:FindFirstChild("Shirt")
	if shirt then
		data["Shirt"] = shirt.ShirtTemplate
	end

	local pants = character:FindFirstChild("Pants")
	if pants then
		data["Pants"] = pants.PantsTemplate
	end

	local shirtGraphic = character:FindFirstChild("ShirtGraphic")
	if shirtGraphic then
		data["ShirtGraphic"] = shirtGraphic.Graphic
	end

	return data
end

local function deserializeData(player, data)
	local character = lastsavedChar[player] -- we reference the last saved character
	local bodyColors = character["Body Colors"]
	for _, property in ipairs(properties) do
		bodyColors[property] = BrickColor.new(data[property])
	end

	if data["Shirt"] then
		local shirt = character:FindFirstChild("Shirt")
		if shirt then
			shirt.ShirtTemplate = data["Shirt"]
		else
			shirt = Instance.new("Shirt")
			shirt.Parent = character
			shirt.ShirtTemplate = data["Shirt"]
		end
	end

	if data["Pants"] then
		local pants = character:FindFirstChild("Pants")
		if pants then
			pants.PantsTemplate = data["Pants"]
		else
			pants = Instance.new("Pants")
			pants.Parent = character
			pants.PantsTemplate = data["Pants"]
		end
	end

	if data["ShirtGraphic"] then
		local shirtGraphic = character:FindFirstChild("ShirtGraphic")
		if shirtGraphic then
			shirtGraphic.Graphic = data["ShirtGraphic"]
		else
			shirtGraphic = Instance.new("ShirtGraphic")
			shirtGraphic.Parent = character
			shirtGraphic.Graphic = data["ShirtGraphic"]
		end
	end

	serializeData(player)
end

local function onPlayerAdded(player)
	local function onCharacterLoaded(character)
		if (lastsavedChar[player]) then serializeData(player) end -- we check if this is the player's 2nd+ time respawning and if so, serialize the data from the old character for the new character to reference
		local data = playersData[player]
		lastsavedChar[player] = character -- set the new character
		if data then
			deserializeData(player, data)
		end
	end

	player.CharacterAppearanceLoaded:Connect(onCharacterLoaded)

	local success, result = pcall(function()
		return datastore:GetAsync("BodyColor_"..player.UserId)
	end)

	if success then
		playersData[player] = result
	else
		warn(result)
	end
end

local function saveData(plr, attempt)
	--saves the player's data and attempts 3 times if it errors
	if (attempt ~= nil and attempt > 3) then return end -- reached 3 attempts: return to avoid continuing

	local plrData = serializeData(plr)

	if (plrData ~= nil) then
		--<< Player's data exists: save the data >>--
		local success, error = pcall(function()
			return datastore:UpdateAsync("BodyColor_" .. plr.UserId, function(o_data) -- o_data is the previous version of the data
				local prev_data = o_data or {} -- in case o_data is nil, set as default
				
				print(plrData.Version, prev_data.Version)
				print(plrData.Version == prev_data.Version)

				if (plrData.Version == prev_data.Version) then -- we check if the versions of the data are the same
					-- update this data to the current
					plrData.Version = plrData.Version and plrData.Version + 1 or 1
					print(plrData)
					return plrData
				else
					-- do not update data, versions do not match
					return nil -- returning nil prevents it from sending a write request
				end
			end)
		end)

		if (success) then
			-- player's data has been saved!
			print("data key: BodyColors_" .. plr.UserId .. " has been saved!")
			playersData[plr] = nil
		else
			-- error occured, attempt to retry
			warn(error)
			saveData(plr, (attempt and attempt + 1 or 2)) -- add +1 to the current attempt or set as 2 if nil
		end
	end

end

players.PlayerAdded:Connect(onPlayerAdded)
players.PlayerRemoving:Connect(saveData)

game:BindToClose(function() -- in case of a shutdown, save the data of players before the server closes
	local to_Save = 0
	for _, plr in pairs(players:GetPlayers()) do
		if (playersData[plr]) then to_Save += 1 end -- add +1 if the player has stored data
		task.spawn(function()
			saveData(plr)
			to_Save -= 1 -- subtract 1 when data is finished
		end)
	end

	while (to_Save > 0) do task.wait() end -- yield until all data has been saved
	return -- shut the server down! all data has been saved!
end)

I added comments to (mostly) everything I changed - so feel free to look at that. Let me know if anything is wrong. :slightly_smiling_face:

2 Likes

Thank you so much for the reply, but I’m getting an error when leaving the game (line 12):
“attempt to index nil with ‘Body Colors’”

local bodyColors = character["Body Colors"] --error line

When the player leaves and the data serializes, I guess it doesn’t know what the character is. Sorry to bother you with this

From the video, I see that you’re using a custom character instead of the player’s avatar. I did this myself and found that CharacterAppearanceLoaded does not trigger. No matter how many times I respawned, it never triggered. And to be honest, I don’t really know why. I replaced it with CharacterAdded and everything worked fine:

player.CharacterAppearanceLoaded:Connect(onCharacterLoaded)

to:

player.CharacterAdded:Connect(onCharacterLoaded)

But also, I realized there were other issues other than just that. So I did some debugging and this is the final code I came up with (you can just replace everything with this):

local datastores = game:GetService("DataStoreService")
local datastore = datastores:GetDataStore("DataStore")
local players = game:GetService("Players")

local properties = {"HeadColor", "LeftArmColor", "LeftLegColor", "RightArmColor", "RightLegColor", "TorsoColor"}
local playersData = {}
local lastsavedChar = {}

local function serializeData(plr)
	local data = playersData[plr] or {} -- check if player has data or create empty table
	local character = lastsavedChar[plr] -- we reference the last saved character
	local bodyColors = character["Body Colors"]
	for _, property in ipairs(properties) do
		data[property] = bodyColors[property].Name
	end

	local shirt = character:FindFirstChild("Shirt")
	if shirt then
		data["Shirt"] = shirt.ShirtTemplate
	end

	local pants = character:FindFirstChild("Pants")
	if pants then
		data["Pants"] = pants.PantsTemplate
	end

	local shirtGraphic = character:FindFirstChild("ShirtGraphic")
	if shirtGraphic then
		data["ShirtGraphic"] = shirtGraphic.Graphic
	end
	
	playersData[plr] = data -- set player's data as modified data table


	return data
end

local function deserializeData(player, data)
	local character = lastsavedChar[player] -- we reference the last saved character
	local bodyColors = character["Body Colors"]
	for _, property in ipairs(properties) do
		bodyColors[property] = BrickColor.new(data[property])
	end

	if data["Shirt"] then
		local shirt = character:FindFirstChild("Shirt")
		if shirt then
			shirt.ShirtTemplate = data["Shirt"]
		else
			shirt = Instance.new("Shirt")
			shirt.Name = "Shirt"
			shirt.Parent = character
			shirt.ShirtTemplate = data["Shirt"]
		end
	end

	if data["Pants"] then
		local pants = character:FindFirstChild("Pants")
		if pants then
			pants.PantsTemplate = data["Pants"]
		else
			pants = Instance.new("Pants")
			pants.Name = "Pants"
			pants.Parent = character
			pants.PantsTemplate = data["Pants"]
		end
	end

	if data["ShirtGraphic"] then
		local shirtGraphic = character:FindFirstChild("ShirtGraphic")
		if shirtGraphic then
			shirtGraphic.Graphic = data["ShirtGraphic"]
		else
			shirtGraphic = Instance.new("ShirtGraphic")
			shirtGraphic.Name = "ShirtGraphic"
			shirtGraphic.Parent = character
			shirtGraphic.Graphic = data["ShirtGraphic"]
		end
	end

	serializeData(player)
end

local function onPlayerAdded(player)
	local function onCharacterLoaded(character)
		if (lastsavedChar[player]) then serializeData(player) end -- we check if this is the player's 2nd+ time respawning and if so, serialize the data from the old character for the new character to reference
		local data = playersData[player]
		
		while (data == nil and player:IsDescendantOf(players)) do
			data = playersData[player] -- we know that it will either be the player's data or an empty table
			task.wait()
		end
		
		lastsavedChar[player] = character -- set the new character
		if (next(data) ~= nil) then deserializeData(player, data) end -- we check if the dictionary is not empty and if it is not, then we deserialize that data
	end

	player.CharacterAdded:Connect(onCharacterLoaded)

	local success, result = pcall(function()
		return datastore:GetAsync("BodyColors_"..player.UserId)
	end)

	if success then
		playersData[player] = result or {}
	else
		warn(result)
	end
end

local function saveData(plr, attempt)
	--saves the player's data and attempts 3 times if it errors
	if (attempt ~= nil and attempt > 3) then return end -- reached 3 attempts: return to avoid continuing
	
	local plrData = serializeData(plr)

	if (plrData ~= nil) then
		--<< Player's data exists: save the data >>--
		local success, error = pcall(function()
			return datastore:UpdateAsync("BodyColors_" .. plr.UserId, function(o_data) -- o_data is the previous version of the data
				local prev_data = o_data or {} -- in case o_data is nil, set as default

				if (plrData.Version == prev_data.Version) then -- we check if the versions of the data are the same
					-- update this data to the current
					plrData.Version = plrData.Version and plrData.Version + 1 or 1
					return plrData
				else
					-- do not update data, versions do not match
					return nil -- returning nil prevents it from sending a write request
				end
			end)
		end)

		if (success) then
			-- player's data has been saved!
			print("data key: BodyColors_" .. plr.UserId .. " has been saved!")
			playersData[plr] = nil
		else
			-- error occured, attempt to retry
			warn(error)
			saveData(plr, (attempt and attempt + 1 or 2)) -- add +1 to the current attempt or set as 2 if nil
		end
	end

end

players.PlayerAdded:Connect(onPlayerAdded)
players.PlayerRemoving:Connect(saveData)

game:BindToClose(function() -- in case of a shutdown, save the data of players before the server closes
	local to_Save = 0
	for _, plr in pairs(players:GetPlayers()) do
		if (playersData[plr]) then to_Save += 1 end -- add +1 if the player has stored data
		task.spawn(function()
			saveData(plr)
			to_Save -= 1 -- subtract 1 when data is finished
		end)
	end

	while (to_Save > 0) do task.wait() end -- yield until all data has been saved
	return -- shut the server down! all data has been saved!
end)
1 Like

Yup, it was just a demo file, I am too lazy to save each body color xd

1 Like

well thank you anyway, you taught me a lot of stuff

Thank you so much! this works like a charm. I’ve been struggling with this issue for days now, I really appreciate it. I’ve been looking at the changes you made and I am learning how it all works. I hope you have a great day, thanks again

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