CFrame data not saving to DataStore

  1. What do you want to achieve?
    I want to save the player’s position values into IntValues on DataStore.

  2. What is the issue?
    player.Character.Position.X/Y/Z does not save into DataStore.

  3. What solutions have you tried so far?
    Saving the position data into variables and saving the variables to DataStore.

I am trying to save the player’s position information into DataStore so that I can use it to have the player start off in the world where he left off. However, my script is not saving the player.Character.Position.X to DataStore.

I’ve tried saving hardcoded numbers, and it works fine. I’ve printed out player.Character.Position.X’s value and type and it’s a number. For some reason, DataStore does not want to save this into the database.

I would appreciate it if someone can tell me what I’m doing wrong.

local DS = game:GetService("DataStoreService")
local SaveCFrame = DS:GetDataStore("SaveCFrame")

game.Players.PlayerAdded:Connect(function(plr)
	local Location_Storage = Instance.new("Folder", plr)
	Location_Storage.Name = "Location_Storage"
	local CFrameX = Instance.new("IntValue", Location_Storage)
	CFrameX.Name = "CFrameX"
	local CFrameY = Instance.new("IntValue", Location_Storage)
	CFrameY.Name = "CFrameY"
	local CFrameZ = Instance.new("IntValue", Location_Storage)
	CFrameZ.Name = "CFrameZ"

	local Location_StorageData
	local success, errormessage = pcall(function()
		Location_StorageData = SaveCFrame:GetAsync(plr.UserId.."-Location_StorageData")
	end)

	if success then
		if Location_StorageData then
			CFrameX.Value = Location_StorageData[1] --CFrameX
			CFrameY.Value = Location_StorageData[2] --CFrameY
			CFrameZ.Value = Location_StorageData[3] --CFrameZ
		else
			CFrameX.Value = 0
			CFrameY.Value = 0
			CFrameZ.Value = 0
		end
	end
end)

game.Players.PlayerRemoving:Connect(function(plr)
	local xpos = plr.Character.LowerTorso.Position.X
	local ypos = plr.Character.LowerTorso.Position.Y
	local zpos = plr.Character.LowerTorso.Position.Z

	local success, errormessage = pcall(function()
		SaveCFrame:SetAsync(plr.UserId.."-Location_StorageData", {
			123,
			123,
			-123
			
			--[[
			plr.Character.LowerTorso.Position.X,
			plr.Character.LowerTorso.Position.Y,
			plr.Character.LowerTorso.Position.Z
			]]
		})
	end)

	if success then
		print("Successfully saved data!")
	else
		warn("ERROR: "..errormessage)
	end
end)

Please do not ask people to write entire scripts or design entire systems for you. If you can’t answer the three questions above, you should probably pick a different category.

3 Likes

you could use tables to save data other than numbers
I did it once and it worked

1 Like

I think you can save a CFrame to the DataStore using tables
Example:

local data = {
datacframe = CFrame.new(Vector3.new(0, 0, 0))
}

I think you understand
Hope this helps

1 Like

The reason this is failing is because Roblox removes the character before the PlayerRemoving event is fired. You are trying to access plr.Character.LowerTorso while plr.Character is a nil reference.

It seems like you’re making this a lot more complicated than it needs to be as well.
I’ll write up a better example for you shortly.


Nope, this is incorrect. Roblox’s built-in types do not serialize to JSON so they are not supported by DataStores.

5 Likes

You could use a CFrameValue to simplify it. CFrameValue | Documentation - Roblox Creator Hub

1 Like

Here’s a full, (mostly) best-practices implementation of what you’re trying to do here.

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

local saveCFrame = DataStoreService:GetDataStore("SaveCFrame")
local saveSemaphore = 0

local function onGameClosing()
	-- Don't shut down until we've finished every save request.
	while saveSemaphore > 0 do
		wait(1)
	end
end

local function onCharacterAdded(character)
	local player = Players:GetPlayerFromCharacter(character)
	
	if not player then
		return
	end
	
	local rootPart = character.PrimaryPart
	local lastLoc = player:FindFirstChild("LastLocation")
	local humanoid = character:FindFirstChildOfClass("Humanoid")
	
	if not (humanoid and rootPart and lastLoc) then
		return
	end
	
	-- Wait for a brief moment so the CFrame change we apply isn't
	-- overwritten by the player's own automatic spawn CFrame.
	-- We only do this once on the player's first spawn.
	
	if not CollectionService:HasTag(player, "HasRestored") then
		local cf = lastLoc.Value
		
		delay(.1, function ()
			if game:IsAncestorOf(character) then
				character:SetPrimaryPartCFrame(cf)
				CollectionService:AddTag(player, "HasRestored")
			end
		end)
	end
	
	spawn(function ()
		while game:IsAncestorOf(character) do
			if humanoid.FloorMaterial ~= Enum.Material.Air then
				lastLoc.Value = rootPart.CFrame
			end
			
			wait(1)
		end
	end)
end

local function onPlayerAdded(player)
	local lastLoc = Instance.new("CFrameValue")
	lastLoc.Name = "LastLocation"
	lastLoc.Parent = player
	
	local success, errorMsg = pcall(function ()
		local components = saveCFrame:GetAsync(player.UserId .. "-Location_StorageData")
		
		if components then
			local cf = CFrame.new(unpack(components))
			lastLoc.Value = cf
		end
	end)
	
	if not success then
		warn("ERROR LOADING LOCATION FOR " .. player.Name .. ":", errorMsg)
	end
	
	if player.Character then
		onCharacterAdded(player.Character)
	end
	
	saveSemaphore += 1
	player.CharacterAdded:Connect(onCharacterAdded)
end

local function onPlayerRemoving(player)
	local lastLoc = player:FindFirstChild("LastLocation")
	
	if lastLoc and lastLoc:IsA("CFrameValue") then
		-- Wrap the CFrame's components into an array.
		local saveCF = { lastLoc.Value:GetComponents() }
		
		local success, errorMsg = pcall(function ()
			saveCFrame:UpdateAsync(player.UserId .. "-Location_StorageData", function (oldCF)
				-- Just overwrite what is there.
				return saveCF
			end)
		end)
		
		if not success then
			warn("ERROR SAVING LOCATION FOR" .. player.Name .. ":", errorMsg)
		end
	end
	
	saveSemaphore -= 1
end

game:BindToClose(onGameClosing)
Players.PlayerAdded:Connect(onPlayerAdded)
Players.PlayerRemoving:Connect(onPlayerRemoving)
6 Likes

You should save it as seperate IntValues like this

x = 10
y = 10
z = 10

then you could load it like this

player.HumanoidRootPart.CFrame = CFrame.new(player.whereyousavestuff.x.Value,player.whereyousavestuff.y.Value,player.whereyousavestuff.z.Value

Please read all of the replies before responding.

2 Likes

I thought that’s what I’m trying to do. But it’s not saving the numbers onto DataStore.

Can you tell me why?

Couldn’t have asked for a more clear example of how to best code the system, and I am marking this as the answer.

However, I still want to know why the numbers aren’t saving in my original example.

1 Like

There’s a race condition between when PlayerRemoving is fired and when the game is removing the player’s character.

Player.Character is a referent property. If the property isn’t set, reading it will return nil. So it’s kind of like a pointer.

By the time your function callback to Players.PlayerRemoving was invoked, the game may have already cleaned up the player’s character from the game and null’d out its reference in the Character property.

Thus, in trying to evaluate:
plr.Character.LowerTorso.Position.X

The action is interpreted as:
nil.LowerTorso.Position.X

Which is invalid because you cannot index a nil value.


This race condition can be reproduced 100% of the time if you deliberately insert a delay between when PlayerRemoving fired and when you were trying to read the LowerTorso’s position.

game:BindToClose(function ()
	-- Hold the DataModel open for an extra second.
	wait(1)
end)

game.Players.PlayerRemoving:Connect(function (player)
	wait()
	print(player.Character.LowerTorso.Position.X)
end)

In general, its a bad practice to directly index objects if you aren’t 100% confident they are still there and haven’t been deleted or changed by something else.

Heck, the Character property might not be nil, but there’s a non-zero chance the LowerTorso may have been deleted if the player fell out of the world and left the game.

Trying to index an object’s child will throw an error if that child doesn’t exist.
That’s why we have functions like FindFirstChild and WaitForChild :slight_smile:!

2 Likes

Thank you for helping me understand the situation. I would never have gotten that without this post.

1 Like

Exactly. However, on the flip side, you can manually take apart the coordinate frame components and arrange them into a table, and convert that table to JSON. Some developers will take that extra step to save a CFrame.

However, the only applicable case for this, or at least that I can think of, is something that would require an absolute exact CFrame to work, such as a custom animator. If you want to save a position, save by a checkpoint name or by a decompiled vector.