How do I set an attribute that saves to a player?

So I’m trying to make a character system that saves, and recently I’ve received a tip saying it would work if i “set an attribute to the player and store it in datastore”. Obviously, being the weak developer that I am, I have no idea how to implement this. Now, I know what a datastore is but I’ve never set an attribute to a player in my life. If anyone is willing to help, it would be greatly appreciated. Thank you!

2 Likes

To set a attrbrute to a player you first need get the player the most slimplest option for a tutoral is game.Players.playeradded:Connect(function(player) to set a atttibute via script is player:SetAttrbrute("money", 0") then you just refence the attrbyte value when saving by do player:GetAttbrute("money") you now have a attrbute on the player.

I will make a more on depth script in 7 hours just not on studio rn

1 Like

Idk if it’s really best way, it’s the simpliest, attributes are string → any map, which isn’t great if you want a lot of data cuz each character is 1 byte!

To save attributes you need only to retrieve this string and it’s value, and save it inside table to a datastore

To load attributes you only need to take that string and value, and set new attribute to a player

Note: There is limit of iirc 100 attributes per instance, so below is more complex solution

Solution number 2 is more complex, as you save player’s data inside a module script and use remote events to replicate it to client, this way you can update the value when some action is taken (for instance, when player collects a coin) or even only if client asks, this way you remove unnecesary replication of instances and reduce lag in your game

You actually did it. Omgosh.

To set and get attributes:

Instance:SetAttribute(Name, Value)
Instance:GetAttribute(Name)

Instance:SetAttribute

That’s the easy part. Just one function.
Now you have to load this when the player joins.

First, include DataStoreService in a serverscript:

local DataStoreService = game:GetService("DataStoreService")

Now we have to get your data store using GetDataStore.

local store = DataStoreService:GetDataStore("this is the name of the store, can be anything")

This is just a place where your game can store information.

You can get the a value from a datastore using GetAsync.

You can once again just pick a name for the key you want to read, but I suggest you add some form of player identifier to it to not mix up different players’ data.

--something like character + userid
store:GetAsync("Character"..plr.UserId)
--.. adds two strings together.

But watch out! This is a network call, it may fail, so wrap it in pcall (protected call) or xpcall (if you want a custom fn to run when an error occurs). pcall returns two objects, a boolean, indicating whether the call succeeded, and either the return value from the fn wrapped in pcall if it succeeded, or the error message if it failed.

pcall(function()
     return store:GetAsync("Character"..plr.UserId)
end

Finally, you assign the value retrieved from your key to the player’s attribute, or a default value if it didn’t succeed:

plr:SetAttribute("Character", success and result or "Default value")

Or you might just choose to kick the player. It’s up to you on how you will handle failure. Combining the snippets, we can assemble a PlayerAdded handler:

local players = game:GetService("Players")
local DataStoreService = game:GetService("DataStoreService")

local store = DataStoreService:GetDataStore("your name")

players.PlayerAdded:Connect(function(plr)
      local success, result = pcall(function()
           return store:GetAsync("Character"..plr.UserId)
      end

      if not success then
         warn(result)--output error msg
       --kick the player here if you will
       -- or you could retry here
      end
  
      plr:SetAttribute("Character", success and result or "Default value")
end)

Now, once we have an attribute, we can connect a changed event handler to it so whenever you set the attribute it will change the player’s character to the corresponding character. Don’t worry attributes are only replicated from server to clients so an exploiter can’t set the attribute and suddenly become any character.

plr:GetAttributeChangedSignal("Character"):Connect(function()
     local targetName = plr:GetAttribute("Character")

    local char = --use the name to retrieve the model
    if not char then return end

    local cframe = plr.Character and plr.Character.PrimaryPart.CFrame or CFrame.new()
--switch characters, then move the new character to the old chars position if it exists.
   plr.Character = char
   char:PivotTo(cframe)
end)

Now whenever you set this attribute, the character will change.

Finally, we need a way to save our character.
We can use SetAsync for this. We will not use UpdateAsync, since there’s no reason to read the old value(unless you want to).

store:SetAsync(Name, Value)
--so you just write to the store:
store:SetAsync("Character"..plr.UserId, plr:GetAttribute("Character"))

Set async can fail, so handle this error gracefully, wrap it in pcall, or xpcall.

When the player leaves we want to save their data, when the game closes we also want to save the data.

So we use game:BindToClose and players.PlayerRemoving:

local function save(plr)
     local success, result = pcall(function()
          store:SetAsync("Character"..plr.UserId, plr:GetAttribute("Character"))
     end)

     if not success then
        warn(result)--print error msg
      --maybe add retrying here?
     end
end

players.PlayerRemoving:Connect(save)
game:BindToClose(function()
     for _, plr in ipairs(players:GetPlayers()) do
         save(plr)
     end
end)

Now if you don’t kick the player when their data failed to load you cannot save the data now because it is a default value, so you should add a boolean in the player.PlayerAdded handler to indicate whether to save data or not:

players.PlayerAdded:Connect(function(plr)
--other code
--maybe like an attribute?
plr:SetAttribute("CanSaveData", success)
end)

And then in the save function add a check to see if the data isn’t the default value:

local function save(plr)
if not plr:GetAttribute("CanSaveData") then return end
--other code
end

This ensures the player’s data isn’t overriden with the default value.

Combining the snippets we get:

local players = game:GetService("Players")
local DataStoreService = game:GetService("DataStoreService")

local store = DataStoreService:GetDataStore("your name")

local function save(plr)
   if not plr:GetAttribute("CanSaveData") then return end
  local success, result = pcall(function()
      store:SetAsync("Character"..plr.UserId, plr:GetAttribute("Character"))
  end)

  if not success then
      warn(result)
     --handle error
  end
end

players.PlayerAdded:Connect(function(plr)
     local success, result = pcall(function()
          return store:GetAsync("Character"..plr.UserId)
     end)

    if not success then
       warn(result)
     --handle error
    end

    plr:SetAttribute("CanSaveData", success)
    plr:SetAttribute("Character", success and result or "default value")

    plr:GetAttributeChangedSignal("Character"):Connect(function()
     local targetName = plr:GetAttribute("Character")
     local char = -- get char from name

     if not char then return end

     local cframe = plr.Character and plr.Character.PrimaryPart.CFrame or CFrame.new()
     plr.Character = char
     char:PivotTo(cframe)
end)
end)

players.PlayerRemoving:Connect(save)
game:BindToClose(function()
    for _, plr in ipairs(players:GetPlayers()) do
         save(plr)
    end
end)

Now, you’re basically done. You have a very simple system. Now what if you want to store more data than only the character?

Well we can use tables to store the data instead!
We just need to change the code a bit:

local cache = {}

First we can add a cache, so when we need the data again we don’t need to perform another network call.

Then change stuff to match tables:

local function load(plr)
   if cache[plr] then return cache[plr] end
   local success, result = pcall(function()
       return store:GetAsync(tostring(plr.UserId))
   end)

   if success then 
   cache[plr] = result
   plr:SetAttribute("CanSaveData", true)
return result 
end
  --simple retry mechanism
      for i = 1, 5 do
          if success then 
           cache[plr] = result
           plr:SetAttribute("CanSaveData", true)
return result 
end
          success, result = pcall(function()
            return store:GetAsync(tostring(plr.UserId))
          end
      end


      warn(result)
-maybe kick plr
      plr:SetAttribute("CanSaveData", false)
      return {["Character"] = "default"}--default value
end)
plr.PlayerAdded:Connect(function(plr)
   local result = load(plr)
   plr:SetAttribute("Character", result["Character"])
end)
local function save(plr, data)
   if not plr:GetAttribute("CanSaveData") then return end
   cache[plr] = data

   local success, result = pcall(function()
       store:SetAsync(tostring(plr.UserId), data)
   end)

   if success then return end
   for i=1, 5 do
      if success then return end
      success, result = pcall(function()
         store:SetAsync(tostring(plr.UserId), data)
      end)
   end

   warn(result)
end)

So that’s basically how you save and load data

And my post is becoming too long so I’ll end it here, hope this helps!

Edit: I typed this on mobile and now my hand hurts cus of the typing :sob:

1 Like

I’ve changed the script to match what I think its supposed to match like. If I changed anything that I wasn’t supposed to, please let me know and correct me.

local players = game:GetService("Players")
local DataStoreService = game:GetService("DataStoreService")

local store = DataStoreService:GetDataStore("Characters")

local function save(plr)
	if not plr:GetAttribute("CanSaveData") then return end
	local success, result = pcall(function()
		store:SetAsync("Character"..plr.UserId, plr:GetAttribute("Character"))
	end)

	if not success then
		warn(result)
	end
end

players.PlayerAdded:Connect(function(plr)
	local success, result = pcall(function()
		return store:GetAsync("Character"..plr.UserId)
	end)

	if not success then
		warn(result)
		--handle error
	end

	plr:SetAttribute("CanSaveData", success)
	plr:SetAttribute("Character", success and result or "default value")

	plr:GetAttributeChangedSignal("Character"):Connect(function()
		local targetName = plr:GetAttribute("Character")
		local char = "PlaceholderCharacter"

			if not char then return end

		local cframe = plr.Character and plr.Character.PrimaryPart.CFrame or CFrame.new()
		plr.Character = char
		char:PivotTo(cframe)
	end)
end)

players.PlayerRemoving:Connect(save)
game:BindToClose(function()
	for _, plr in ipairs(players:GetPlayers()) do
		save(plr)
	end
end)

also can you please tell me how I would change change the attribute? Thanks :smiley:!

1 Like

Uhh char is suppposed to be a model. If you set the Character you’d want to switch to the corresponding model right? So the string is I’ m assuming the name of the model, so you’d maybe look in replicatedstorage or wherever you store characters, for the character with the given name.

so

local char = someservice:FindFirstChild(targetName)

Also you forgot to change the default value to like a name of a character that you have

And you can set the attribute again using SetAttribute again.