How to save objects in your game with Serialization

I’ve seen a few topics on this question and I wanted to bring my own take on it while I guide you on my/a method of serialization.

The meaning of serialization (at least my definition) is compacting information and then uncompacting it (unserialization).

This could be an example of something like, Hello World to HelloWorld we compacted it by removing one space. The reason we need to compact information like this, especially with data is because of efficiency/usage.

Let’s talk about games such as Bloxburg where you can save your entire house up to the last light switch. To do this you’d need to save the position, orientation, the part’s name, the unique part ID, color, etc. That’s A LOT to save and that`s just for one part.

If you were save this normally it’d look something like this

local Data = {
	["Light Switch"] = {
		["Position"] = {5,20,5},
		["Orientation"] = {0,20,0},
		["Color3"] = {255,0,255},
		["UID"] = 2461361	
	}
}

byte - a small integer of data, example, the letter “a” is a byte.

Now this may not look like much because it really isn’t, but when you start needing 50 to 100 different objects, saving all this data can get burdensome, and sometimes you may not be able to save entirely. Especially when you start nesting tables, the number of bytes increases a lot more than if you were just using a single table. If you’re curious about the amount of memory specifically taken up from tables or other sources, this is a post from 2019 that clears it up Byte usage.

I want to be very clear: a byte is a very small integer of data, and normally it wouldn’t matter at all, but in some use cases, like needing to save a lot of data, it`s good to know how to save some.

Let’s very quickly compact this light switch that we created to make it use less data.

local Data = {
	["Light Switch"] = {
		["Position"] = {5,20,5},
		["Orientation"] = {0,20,0},
		["Color3"] = {255,0,255},
		["UID"] = 2461361
		
	}
}

Compacted data

local Data = {
	["LS"] = {
		p = {5,20,5},
		o = {0,20,0},
		c  = {255,0,255},
		u = 2461361
	}
}

keys - unique definer to data, [“Light Switch”] is a key, [“LS”] is a key, “p” is a key, etc.

So what we’ve done here is shorten our keys; the fewer characters they use, the fewer bytes they’ll use. Although it’s very miniscule, it’s still compact.

Unfortunately, we can’t compact this table any further without either removing or changing data or using serialization.

What’s the first form of serialization that can be used in this situation? I’ll hide the solution if you want to think about it for a minute or two.

Solution
local Data = {
	["LS"] = {
		p = "5,20,5",
		o = "0,20,0",
		c = "255,0,255",
		u= 2461361	
	}
}

Did you happen to get it? Or maybe you thought farther ahead? Or maybe you had a different idea, which could work in this situation. The thing with serialization is that there isn’t any one way of doing it.

So, yes, in the solution to serialize the data, we would have to convert the nested tables into strings. As said before, tables’ in this situation take quite a bit more bytes than if they were strings.

Why is this considered serialization? Well, that’s because we’ve compacted the bytes to a point where we’d have to uncompact them for it to be usable. We can’t throw these strings into their properties anymore.

String manipulation: the act of changing, searching, or extracting parts of a string

Now that we have a basic example of serialization, let’s also give a basic but powerful example of how to unserialize the data. The method we’re going to be using is called “string manipulation,” which sounds like a super cool power, but in this case we’re just going to separate the numbers from the strings.

So let’s say we’ve saved our serialized data and now we want to use it.

local Data = {
	["LS"] = {
		p = "5,20,5",
		o = "0,20,0",
		c = "255,0,255",
		u= 2461361	
	}
}

LightSwitch.Position = Vector3.new(0,0,0)

The best way to do this will be to split each number by their respective commas (,). To do this, we can use a Roblox function called “string.split,” which does exactly as I mentioned: you give it a string and a letter or word you want, and it’ll cut the string at those areas, like cutting a piece of bread.

local segment = string.split(Data.LS.p, ",") --  returns an array {5 20 5}
local X = segment[1] --(5)
local Y = segment[2] --(20)
local Z = segment[3] --(5)

LightSwitch.Position = Vector3.new(X,Y,Z)

We’ve successfully unserialized the “p” key, aka. the light switch position. string.split() returns an array of all the segments it splits the string up into, like if you were to cut a piece of bread into three pieces, and each of these pieces now has a number on them.

We can then get the numbers by how we’d get any other value from an array or table; in this case, we use numbers for the indexing.

So now that we have the basics of serialization, How can we use these basics to save entire plots worth of objects? Well, we just have to do more advanced serialization, specifically with strings. Hypothetically, what if we could take that entire table and make it into one string? The only thing better than that would be using numbers instead.

So let me show you an example of how it’d look to convert the whole table to a string.

"2461361:5:20:5:0:20:0:255:0:255" 

This is a fully converted table, each number is separated by a colon which will act as our separator. The deserialization of this would be done like this,

local Data = {
	key = "2461361:5:20:5:0:20:0:255:0:255" 
}

local segment = string.split(Data.Key, ":")
local UID = segment[1] --  (2461361) Unique Identifier 
local X = segment[2] -- 5 (X pos)
local Y = segment[3] -- 20 (Y pos)
local Z = segment[4] -- 5 (Z pos)
local RX = segment[5]  -- 0 (orientation's x vector)
-- etc

In some cases, you won’t need to save the color of a part or its entire orientation if it only rotates on its Y axis, but this is what it’d look like to save a part with serialization rather than unserialize that data and recreate the part using it.

So let’s put this all together to make our object-saving system.

First, you’ll need to use an ID system for all your parts that you’ll use. That’s how Minecraft uses IDs for their blocks. For example, dirt block ID = 1. The reason you need an ID for your objects is because you don’t want to save entire names; it’ll be too costly, so just give each object you’ll use an attribute called “ID” and number them from 1 to how many objects you’ll use. This way, when you need to get the object, you can just get its ID from the serialization.

The best way to serialize your code is to avoid as many tables as you can, and since you don’t want to make a table for each part, you’ll be putting all your parts in one string. The only problem with this is that we’ll have to do two segment cuts, one for the individual objects using the dash letter “|” and one for their properties using the colon (“:”)

Let’s first start off on how we’ll make these serialized strings and forewarning its going to look like a lot

Data["key"] = `{UID}:{TheObject:GetAttribute("ID")}:{math.floor(TheObject:GetPivot().Position.X * 1000) / 1000}:{math.floor(TheObject:GetPivot().Position.Y * 1000) / 1000}:{math.floor(TheObject:GetPivot().Position.Z * 1000) / 1000}:{math.floor(math.deg(y) * 1000) / 1000}|` ..Data["key"]

Here, all we’ve done is create the string for our data and add it to the current string. You’ll see that I use PivotTo to get the position, and that’s due to the fact that most likely you’ll be using models, so you can save a ton of parts into one model. We then do some fancy math to round the model vectors to the nearest thousandth, so we don’t get unnecessarily large numbers like 22.2222421512421~. We place a colon at the end of each new property, and at the end of it all, we place a “|” dash to signify the end of the object’s properties, and at the end, we’ll add on the previous string.

Now that we have all our model’s properties in the serialization, we can unserialize them when the player joins the game.

local segments = string.split(Data["key"], "|") -- Splits up the serialization into each object
		for _,object in (segments) do -- Loops through each object serialization
			local properties = string.split(object , ":") -- splits ups the object serialization into their properties

			local UID = properties [1]
			local ID = properties [2]
			local X = tonumber(properties [3])
			local Y = tonumber(properties [4])
			local Z = tonumber(properties [5])
			local Orientation = tonumber(properties [6])
			if Orientation ~= nil then
				for _,object: Model in (ObjectFolder--[[The folder the objects are contained]]) do
					if object:GetAttribute("ID") == tonumber(ID) then -- Checks if the ID of the object and the properties ID match up
						local cloneobject = object:Clone() -- Creates object
						cloneobject.Parent = workspace -- Places Object
						cloneobject:PivotTo(CFrame.new(X,Y,Z) * CFrame.Angles(0,math.rad(Orientation),0)) -- Sets position and orientation of object

                        local NumberVal = Instance.new("NumberValue")
						NumberVal.Name = "UID"
						NumberVal.Value = tonumber(UID)
						NumberVal.Parent = cloneobject
					end
				end
			end
			
		end

At this point you’re basically done, the last bit is if you want to delete specific objects, this is where our UID comes in handy

if string.find(Data["key"],tostring(Object:FindFirstChild("UID").Value)) then
		local stringFind = string.find(Data["key"], tostring(Object:FindFirstChild("UID").Value))
		local endPos = string.find(Data["key"], "|", stringFind)
		Data["key"] = string.sub(Data["key"], 1, stringFind - 1) .. string.sub(PlayerData["key"], endPos + 1)
	end

Now, I’m going to be honest, ChatGPT helped me out a little with this one since it’s pretty good at doing stuff like this. But basically, we find the string using the string.find() function, which will tell us if the UID is in the serialization; if it is, we find the “|” in the UID we found and remove everything from the UID to the “|,” which leaves us with our object removed.

Now I rushed through the last bit because there’s not a lot more to say about it other than that we use the methods we explained before, but on a mass scale.

If you’re making a procedural-generated game kind of like Minecraft and want to save the terrain and such, DON’T USE THIS METHOD save the perlin noise random seed and then reload the terrain, then you can use your serialized data to destroy or place blocks that they had done.

A serialization with over 300 different objects can look like this -

This is all one string under one key with the concatenation of all 300 objects serialization. Which I think looks pretty cool.

8 Likes