Tycoon Building Save Issues

I’m making a tycoon game where you can build and place custom objects onto your plot. I want to make Sava Data for the game, It saves the object’s positions and rotations and loads them the way I want to. Although, since this is a tycoon, there are multiple plots (4 to exact 100x100 studs each), and I want the items to load relative to the plot’s position so they spawn on the same spot for each plot while each plot is in a different position. If that makes any sense.

Heres my Data Store code so far,

local DataStoreService = game:GetService("DataStoreService")

local GameData = DataStoreService:GetDataStore("GameData0.4")

local function TableToVector3(table)
	return Vector3.new(table.X, table.Y, table.Z)
end
local function Vector3ToTable(vector)
	return {X = vector.X, Y = vector.Y, Z = vector.Z}
end

-- Convert CFrame to table
local function CFrameToTable(cframe)
	local position = Vector3ToTable(cframe.Position)
	local rotation = Vector3ToTable(cframe.Orientation)
	return {Position = position, Rotation = rotation}
end

-- Convert table to CFrame
local function TableToCFrame(table)
	local position = TableToVector3(table.Position)
	local rotation = TableToVector3(table.Rotation)
	return CFrame.new(position) * CFrame.fromEulerAnglesXYZ(rotation.X, rotation.Y, rotation.Z)
end

local dataTemplate = {
	leaderstats = {
		Cash = 200,
	},
	
	Inventory = {},
	
	Plot = {},
	
	Tools = {},
	
	Workers = {
		Lumberjack = 0,
		Miner = 0,
		Stocker = 0
	},
	
	Level = 1,
	Xp = 0,
	
	Tutorial = {
		ChopATree = false,
		PlaceAnItem = false
	}
}

local function SaveData(player)
	local inventoryData = {}
	local plotData = {}
	
	local plot = game.Workspace.Plots[player.Name]
	local plotPart = plot.PlotPart
	local structures = plot.Structures
	
	for i, item in pairs(player.Inventory:GetChildren()) do
		if item:IsA("IntValue") then
			inventoryData[item.Name] = item.Value
		end
	end
	
	for i, item:Model in pairs(structures:GetChildren()) do
		local cframe:CFrame,size = item:GetBoundingBox()
		local relativePosition = cframe.Position 
		local relativeOrientation = cframe.Rotation -- assuming the plot doesn't rotate
		
		table.insert(plotData, {
			Name = item.Name,
			Position = Vector3ToTable(relativePosition),
			Orientation = Vector3ToTable(relativeOrientation)
		})
		print("Saved " .. item.Name)
	end
	
	print(plotData)
	
	local success, err = pcall(function()
		
		GameData:SetAsync(player.UserId, {
			leaderstats = {
				Cash = player.leaderstats.Cash.Value,
			},

			Inventory = inventoryData,
			
			Plot = plotData,

			Tools = {},

			Workers = {
				Lumberjack = player.Workers.Lumberjack.Value,
				Miner = player.Workers.Miner.Value,
				Stocker = player.Workers.Stocker.Value
			},

			Level = player.Level.Value,
			Xp = player.Xp.Value,

			Tutorial = {
				ChopATree = player.Tutorial.ChopATree.Value,
				PlaceAnItem = player.Tutorial.PlaceAnItem.Value,
				Completed =  player.Tutorial.Completed.Value
			}
		})
	end)

	if not success then
		warn('Could not save data! ' .. err)
	else
		print(player.Name .. "'s Data Saved!")
		print(GameData:GetAsync(player.UserId))
	end
end

game.Players.PlayerAdded:Connect(function(player)
	wait(2)
	local data = GameData:GetAsync(player.UserId)
	local plot = game.Workspace.Plots[player.Name]
	local plotPart = plot.PlotPart
	local structures = plot.Structures
	
	print(data)
	
	if data then
	--Load Data
		player.leaderstats.Cash.Value = data.leaderstats.Cash
		player.Level.Value = data.Level 
		player.Xp.Value = data.Xp
		player.Tutorial.ChopATree.Value = data.Tutorial.ChopATree
		player.Tutorial.PlaceAnItem.Value = data.Tutorial.PlaceAnItem
		player.Workers.Lumberjack.Value = data.Workers.Lumberjack
		player.Workers.Miner.Value = data.Workers.Miner
		player.Workers.Stocker.Value = data.Workers.Stocker
		player.Tutorial.Completed.Value = data.Tutorial.Completed
		for _, itemData in pairs(data.Plot) do
			local object = game.ReplicatedStorage.ObjectFolder:FindFirstChild(itemData.Name):Clone()

			local relativePosition = TableToVector3(itemData.Position)
			local boundingCframe, boundingSize = object:GetBoundingBox()

			object:SetPrimaryPartCFrame(CFrame.new(boundingCframe, TableToVector3(itemData.Orientation)))

			for _, model in pairs(object:GetChildren()) do
				if model:IsA("Model") then
					model.Parent = workspace.Plots[player.Name].Structures
					object:Destroy()
				end
			end
			print('Loaded ' .. itemData.Name)
		end
		
		for itemName, itemQuantity in pairs(data.Inventory) do
			local item = Instance.new("IntValue")
			item.Name = itemName
			item.Value = itemQuantity
			item.Parent = player.Inventory
			for _, v in pairs(game.ReplicatedStorage.Items:GetChildren()) do
				if itemName == v.Name then
					game.ReplicatedStorage.RemoteEvents.UpdateItemUI:FireClient(player, v)
				end
			end
		end
	print(player.Name .. "'s Data Loaded!")
	else
		--Save Blank Save
		print(player.Name .. "'s Blank Save Saved!")
		GameData:SetAsync(player.UserId, dataTemplate)
	end
end)

game.Players.PlayerRemoving:Connect(function(player)
	SaveData(player)
end)

game:BindToClose(function()
	for i, v in pairs(game.Players:GetPlayers()) do
		SaveData(v)
	end
end)



1 Like

CFrame:ToObjectSpace is perfect for this. You call it on the CFrame you want to treat as the new origin, then pass the world space CFrame of the part you want relative that new origin.

local relativeCFrame = PlotPart.CFrame:ToObjectSpace(PlacedDesk.CFrame) -- Use that method to get the relative CFrame (object space) that you save in the DataStore

-- At load time, you can place the new object at the new plot by converting the relative CFrame back to world space
PlacedDesk.CFrame = PlotPart.CFrame:ToWorldSpace(relativeCFrame)

How can I get this to work with DataStores? DataStores can’t save CFrames, could you possibly make a variation of the Vector3ToTable function I made but for CFrames?

Use CFrame:GetComponents() like this post DataStore and CFrame storing - #3 by NuclearTheNoob

The term you’re looking for is serialization.

In short, it’s a method of converting something which can’t be saved. In this case, a CFrame can’t be saved, so we serialize:

-- functions
local function SerializeVector(vector: Vector3)
	return { x = vector.X, y = vector.Y, z = vector.Z }
end

return function(cframe: CFrame)
	return {
		p = SerializeVector(cframe.Position), x = SerializeVector(cframe.RightVector), y = SerializeVector(cframe.UpVector), z = SerializeVector(-cframe.LookVector)
	}
end

It’s pretty much the equivalent to CFrame:GetComponents(). The only change is that we’re pulling the rotation matrices (LookVector, RightVector, etc…) instead of individual components (r01, r02, r10 etc…)

The code I provided should be used within a ModuleScript and you can call it like this:

local serializeCFrame = require(ROOT_PATH_FOR_MODULE)

-- this returns our serialized data:
local serialized = serializeCFrame(cframe)

Here’s also a function for deserialization:

-- functions
local function SerialToVector(serializedVector: any)
	return Vector3.new(serializedVector.x, serializedVector.y, serializedVector.z)
end

return function(serializedCFrame: any)
	return CFrame.fromMatrix(
		SerialToVector(serializedCFrame.p),
		SerialToVector(serializedCFrame.x),
		SerialToVector(serializedCFrame.y),
		SerialToVector(serializedCFrame.z)
	)
end

This seemed to work, thanks so much!

It works, although I’ve run into an issue. Every time I join the game the items are 1 stud higher. My theory is that the 1 stud Y size of the plot part is being accounted for. Here is my current code. I’m not super familiar with CFrames so I’m not sure where to start when it comes to solving this.

--Save items
for i, item:Model in pairs(structures:GetChildren()) do
	local cframe:CFrame,size = item:GetBoundingBox()
	local relativeCFrame:CFrame = plotPart.CFrame:ToObjectSpace(cframe)
		
	table.insert(plotData, {
		Name = item.Name,
		Position = CFrameToTable(relativeCFrame),
	})
	print("Saved " .. item.Name)
end```

--Load Items
for _, itemData in pairs(data.Plot) do
			local object = game.ReplicatedStorage.ObjectFolder:FindFirstChild(itemData.Name):Clone()

			-- Calculate the global position based on the relative position within the plot and plot size
			local relativePosition:CFrame = plotPart.CFrame:ToWorldSpace(tableToCframe(itemData.Position))

			object:SetPrimaryPartCFrame(relativePosition)

			for _, model in pairs(object:GetChildren()) do
				if model:IsA("Model") then
					model.Parent = workspace.Plots[player.Name].Structures
					object:Destroy()
				end
			end
			print('Loaded ' .. itemData.Name)
		end

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