If you have absolutely NO idea of how to set up a DataStore
within your script, you should take a look at this community post about how you could set up your DataStore
.
Else, how you’d go around with this would be using tables. You’d have to create different remotes for specific buttons. I’d make two RemoteEvents
for the Save button and for the Load button. I’ll also have the LocalScript
(ideally where you’re handling the ui button clicks) fire/invoke the server for the according button and the normal Script listen for those fire. And the way you’d go about sending the data, because you can’t actually store Instances
within a DataStore
, is by sending a sort of OOP (object-orientated programming) types of tables.
If you don't know what OOP tables are...
It’s basically objects that have keys with values in them, basically like the properties of an Instance, which can also be considered as a class. I know there’s a lot more than just that, but that’s how I interpreted it. (Would be appreciative if someone corrected me on this, though)
An example of an OOP type table would be something like this:
local PersonClass = {
FirstName = "John",
LastName = "Doe",
Age = 32
}
All of this is mainly created for you to learn from. So, you’d have to adjust the script to how your current script is if you plan on using it.
So, how I’d go around doing this is within the LocalScript
handling the SaveButton (or the script can handle the same button), within the Mouse1Click
function, I’d fire the corresponding remote to the server. If you already have stored some sort of information about what the player is currently wearing, use that instead.
-- I also noticed you have a place where they can name their outfit
-- ... so i'll apply that as well.
-- This all assumes that you've stored different types of clothing in a folder in ReplicatedStorage.
-- And also assumes that SaveOutfitRemote has already been declared within the script.
-- So all the data is the name of that specific item.
SaveButton.Mouse1Click:Connect(function()
SaveOutfitRemote:FireServer(outfitName)
-- You SHOULD make the OOP data on the server script,
-- since having the data here can lead to possible exploitation.
end)
And for the Load Button (where ever the script is handling that), I’d fire to the server to load the saved outfit within the Mouse1Click
function: The thing with this is, this can also be exploitable if we don’t code this correctly. So, I’ll go to the extent of coding some sanity checks on the server script.
LoadButton.Mouse1Click:Connect(function()
-- Since there are different types of outfits,
-- the player may want a specific outfit that they selected.
local Outfit = LoadOutfitRemote:FireServer("OutfitName")
end)
Scroll down if you just want the script without explanation, I wouldn’t blame you.
Now, I’d make a separate server-sided script to handle the Save/Load remotes. What I do first is create the DataStore that’ll store the outfit data and set up the script to handle the different types of remote events. Since this is loading and saving the outfits, we’d need to also have the outfit items stored in the script as well. The structure could be something like this:
local DataStoreService = game:GetService("DataStoreService")
local ReplicatedStorage = game:GetService("ReplicatedStorage") -- I believe your remotes would be somewhere in there.
local ServerStorage = game:GetService("ServerStorage") -- This is where your outfit items could potentially be stored at.
-- I'd store it here for safety purposes, but it may vary depending on how your code is structured.
local OutfitRemotes = ReplicatedStorage:WaitForChild("OutfitRemotes")
local OutfitAssets = ServerStorage:WaitForChild("OutfitAssets") -- This is where most of your outfit essentils will be located
-- Faces, Hairs, Clothing, etc.
local UserOutfits = DataStoreService:GetDataStore("UserOutfits") -- it just creates a new DataStore
OutfitRemotes.SaveOutfitRemote.OnServerEvent:Connect(function(player)
end)
OutfitRemotes.LoadOutfitRemote.OnServerEvent:Connect(function(player, outfitName)
end)
Now here’s how things get interesting. Within the save event listener (OnServerEvent
) is where we create the OOP-like table. Afterward, save it to the player’s user outfits. Since there would be different types of outfits the player would have, we’d have to account for potential overwritting. And since we can’t just add things to DataStores (from my understanding, can be wrong), we’d have to get the current player’s outfits and insert the new outfit to THAT table. Something like this would do:
OutfitRemotes.SaveOutfitRemote.OnServerEvent:Connect(function(player, outfitName)
local character = player.Character
local shirt = character:FindFirstChild("Shirt")
local pants = character:FindFirstChild("Pants")
local head = character:FindFirstChild("Head")
local face -- We'll get the face with a different method
-- to account for decals that are for face decoration.
local hairs = {} -- Expected to be different accessories.
-- We'll go the same sort of method as we did for finding the face
for _, inst in head:GetChildren() do -- We're beginning to find the face
if inst:IsA("Decal") then -- So we don't be checking for any Attachments/Motor6Ds
local foundFace = false -- If true, we should break out of the loop since there's no reason to be continuing to find the face.
for _, _face in OutfitAssets.Faces:GetChildren() do
if _face.Name == inst.Name then -- Validating that the face is within the faces folder
face = inst.Name
foundFace = true
break
end
end
if foundFace then
break
end
end
end
for _, inst in character:GetChildren() do -- Beginning to find different hairs
if inst:IsA("Accessory") then -- Finding only accessories
for _, _hair in OutfitAssets.Hair:GetChildren() do
if _hair.Name == inst.Name then -- Validating that the accessory is within the hairs folder
table.insert(hairs, inst.Name)
break
end
end
end
end
local Outfit = {
Name = outfitName, -- or "Name"
Shirt = shirt.Name, -- Or, you could use the ID to load it,
-- whatever method you choose should be coded according to that method
Pants = pants.Name, -- Same idea as above in terms of storing the data.
Hair = hairs, -- In case there'll be multiple hair being used, you'd store the name of the hair(s).
-- This is just assuming if you've put those hairs within ReplicatedStorage.
Face = face -- It can be the name or assetId of the face
}
local existingOutfits -- Expected to be a table for the outfits
local success, results = pcall(function()
existingOutfits = UserOutfits:GetAsync("Outfits." .. player.UserId)
end)
if not success and not results then
warn(player.Name, "doesn't have any outfits! Creating new data...") -- This is actually unnecessary imo..
existingOutfits = {}
else
error(results)
end
existingOutfits[Outfit.Name] = Outfit
UserOutfits:SetAsync("Outfits." .. player.UserId)
end)
Now this should be able to save the player’s current outfit with the corresponding outfitName. Now, how about loading that data? This is where we start coding the LoadOutfitRemote. What we’d have to do is get the outfits that the player has and find the one with the corresponding outfitName. (As it was passed when firing to the server) And since it’s stored in a table with all of these Outfits, we would have to loop through that big table to find the one with the EXACT name. There is a way you could have it where it finds the right outfit even where there are outfits with duplicate names, but this is a simple Load/Save system.
I also decided to create a function to make it faster to get different outfit assets. The remote event may look like this:
OutfitRemotes.LoadOutfitRemote.OnServerEvent:Connect(function(player, outfitName)
local function getAsset(assetType, name)
local library = OutfitAssets:FindFirstChild(assetType) -- assetType is just the library that it should check to find "name".
if library then -- If the library is an existing folder in OutfitAssets
for _, inst in library:GetChildren() do
if inst.Name == name then
return inst
end
end
else -- Warn the coder that they may have made a typo
warn(assetType, "doesn't exist within this folder/library!")
end
end
local character = player.Character
local Head = character:FindFirstChild("Head")
local playerShirt = character:FindFirstChild("Shirt") :: Shirt -- Would create a new Shirt if there isn't one already
local playerPants = character:FindFirstChild("Pants") :: Pants -- Same thinking from above applies here
for _, accessory in character:GetChildren() do
if accessory:IsA("Accessory") then
accessory:Destroy()
end
end
for _, item in Head:GetChildren() do
if getAsset("Face", item.Name) then
item:Destroy()
end
end
local playerOutfits
local success, results = pcall(function()
playerOutfits = UserOutfits:GetAsync("Outfits." .. player.UserId)
end)
if success then
for _outfitName, _outfitData in pairs(playerOutfits) do
-- You could actually do this two ways:
-- You can check if outfitName matches _outfitName or _outfitData.Name
-- That's ONLY if you've stored into the player's outfit table as a key (e.g. fruits["banana"], fruits["apple"], etc.)
-- Instead of a number (e.g. table[1], table[2], etc.)
if outfitName == _outfitName then
local shirt = getAsset("Shirts", _outfitData.Shirt)
local pants = getAsset("Pants", _outfitData.Pants)
local face = getAsset("Faces", _outfitData.Face)
local hair = {} -- Remember, _outfitData.Hair is a table.
for _, _hair in _outfitData.Hair do
table.insert(hair, getAsset("Hair", _hair))
end
-- Now that we have this data, we can just overwrite the current players outfit
playerShirt.ShirtTemplate = shirt.ShirtTemplate -- Apparently they decided to call these Templates instead of Ids
playerPants.PantsTemplate = pants.PantsTemplate
face:Clone().Parent = Head
for _, _hair in hair do
local _newHair = _hair:Clone()
_newHair.Parent = character
end
break -- Because we don't need to find the right outfit anymore
end
end
elseif results then
error(results)
end
end)
The final structure of the script should look something like this:
local DataStoreService = game:GetService("DataStoreService")
local ReplicatedStorage = game:GetService("ReplicatedStorage") -- I believe your remotes would be somewhere in there.
local ServerStorage = game:GetService("ServerStorage") -- This is where your outfit items could potentially be stored at.
-- I'd store it here for safety purposes, but it may vary depending on how your code is structured.
local OutfitRemotes = ReplicatedStorage:WaitForChild("OutfitRemotes")
local OutfitAssets = ServerStorage:WaitForChild("OutfitAssets") -- This is where most of your outfit essentils will be located
-- Faces, Hairs, Clothing, etc.
local UserOutfits = DataStoreService:GetDataStore("UserOutfits") -- it just creates a new DataStore
OutfitRemotes.SaveOutfitRemote.OnServerEvent:Connect(function(player, outfitName)
local character = player.Character
local shirt = character:FindFirstChild("Shirt")
local pants = character:FindFirstChild("Pants")
local head = character:FindFirstChild("Head")
local face -- We'll get the face with a different method
-- to account for decals that are for face decoration.
local hairs = {} -- Expected to be different accessories.
-- We'll go the same sort of method as we did for finding the face
for _, inst in head:GetChildren() do -- We're beginning to find the face
if inst:IsA("Decal") then -- So we don't be checking for any Attachments/Motor6Ds
local foundFace = false -- If true, we should break out of the loop since there's no reason to be continuing to find the face.
for _, _face in OutfitAssets.Faces:GetChildren() do
if _face.Name == inst.Name then -- Validating that the face is within the faces folder
face = inst.Name
foundFace = true
break
end
end
if foundFace then
break
end
end
end
for _, inst in character:GetChildren() do -- Beginning to find different hairs
if inst:IsA("Accessory") then -- Finding only accessories
for _, _hair in OutfitAssets.Hair:GetChildren() do
if _hair.Name == inst.Name then -- Validating that the accessory is within the hairs folder
table.insert(hairs, inst.Name)
break
end
end
end
end
local Outfit = {
Name = outfitName, -- or "Name"
Shirt = shirt.Name, -- Or, you could use the ID to load it,
-- whatever method you choose should be coded according to that method
Pants = pants.Name, -- Same idea as above in terms of storing the data.
Hair = hairs, -- In case there'll be multiple hair being used, you'd store the name of the hair(s).
-- This is just assuming if you've put those hairs within ReplicatedStorage.
Face = face -- It can be the name or assetId of the face
}
local existingOutfits -- Expected to be a table for the outfits
local success, results = pcall(function()
existingOutfits = UserOutfits:GetAsync("Outfits." .. player.UserId)
end)
if not success and not results then
warn(player.Name, "doesn't have any outfits! Creating new data...") -- This is actually unnecessary imo..
existingOutfits = {}
else
error(results)
end
existingOutfits[Outfit.Name] = Outfit
UserOutfits:SetAsync("Outfits." .. player.UserId)
end)
OutfitRemotes.LoadOutfitRemote.OnServerEvent:Connect(function(player, outfitName)
local function getAsset(assetType, name)
local library = OutfitAssets:FindFirstChild(assetType) -- assetType is just the library that it should check to find "name".
if library then -- If the library is an existing folder in OutfitAssets
for _, inst in library:GetChildren() do
if inst.Name == name then
return inst
end
end
else -- Warn the coder that they may have made a typo
warn(assetType, "doesn't exist within this folder/library!")
end
end
local character = player.Character
local Head = character:FindFirstChild("Head")
local playerShirt = character:FindFirstChild("Shirt") :: Shirt -- Would create a new Shirt if there isn't one already
local playerPants = character:FindFirstChild("Pants") :: Pants -- Same thinking from above applies here
for _, accessory in character:GetChildren() do
if accessory:IsA("Accessory") then
accessory:Destroy()
end
end
for _, item in Head:GetChildren() do
if getAsset("Face", item.Name) then
item:Destroy()
end
end
local playerOutfits
local success, results = pcall(function()
playerOutfits = UserOutfits:GetAsync("Outfits." .. player.UserId)
end)
if success then
for _outfitName, _outfitData in pairs(playerOutfits) do
-- You could actually do this two ways:
-- You can check if outfitName matches _outfitName or _outfitData.Name
-- That's ONLY if you've stored into the player's outfit table as a key (e.g. fruits["banana"], fruits["apple"], etc.)
-- Instead of a number (e.g. table[1], table[2], etc.)
if outfitName == _outfitName then
local shirt = getAsset("Shirts", _outfitData.Shirt)
local pants = getAsset("Pants", _outfitData.Pants)
local face = getAsset("Faces", _outfitData.Face)
local hair = {} -- Remember, _outfitData.Hair is a table.
for _, _hair in _outfitData.Hair do
table.insert(hair, getAsset("Hair", _hair))
end
-- Now that we have this data, we can just overwrite the current players outfit
playerShirt.ShirtTemplate = shirt.ShirtTemplate -- Apparently they decided to call these Templates instead of Ids
playerPants.PantsTemplate = pants.PantsTemplate
face:Clone().Parent = Head
for _, _hair in hair do
local _newHair = _hair:Clone()
_newHair.Parent = character
end
break -- Because we don't need to find the right outfit anymore
end
end
elseif results then
error(results)
end
end)
Hopefully this, LONG post helps you out in some matter. If you still would like some assistance I would be happy to help you. Other than that, happy coding!