I spent a few hours working on a custom attribute module for my game Corautmors. I call it ‘metadata’
I will never open source it to the public
But I did find it really cool and lightweight (~400 LoC) so I wanted to post about it
But how it works is:
First you require the module
local metadataServer = require('server@metadataServer')
Then you create a part to assign the metadata to. This is the part you’re adding ‘attributes’ to, like with roblox’s attribute system
local part = Instance.new('Part', workspace.Terrain)
Next, you create something called a metareference. In this example I create 100 of them, named i
, with a bitsize of 4 (max is 0b1111) and a default value of 15 (0b1111). I insert them into a table stuff
which is the ‘metareferences’ you pass into a partition, which ill get to in a sec
local stuff = {}
for i = 1, 100 do
table.insert(stuff, {name=`{i}`, bitSize=4, default=15})
end
Here I’m making the metadata and then partitioning it.
local pt = metadataServer.New(part, stuff):Partition()
A partition is how the metareferences is divided. The metarefs table ‘stuff’ contains all the metarefs, and with a partition you can select which metarefs you wish to replicate as the first argument: {string}
, containing the names of the metarefs for that partition. In this example I’m passing no arguments, because I want to use all the metarefs in that partition. However this is how you’d generally use partitions:
local md = metadataServer.New(part, {
{name=`apples`, bitSize=4, default=15}
{name=`bananas`, bitSize=4, default=15}
{name=`broccoli`, bitSize=4, default=15}
{name=`asparagus`, bitSize=4, default=15}
})
local fruits = md:Partition({'apples', 'bananas'}) -- this partition can only replicate apples and bananas
local vegetables = md:Partition({'broccoli', 'asparagus'}) -- this partition can only replicate broccoli and asparagus
-- as a replication example
fruits:PushReplicator(somePlayer, somePlayer2) -- let us replicate the fruits to some player. note: this doesnt mean replicate
fruits:Set('apples', 5) -- set apples to 5
fruit:Replicate() -- replicate it to someplayer and someplayer2
Here I’m configuring a bunch of settings
pt:SetDefaultBufferRate(1)
pt:SetThrottleOnDefault(true)
pt:PushReplicator(game.Players:WaitForChild('Ilucere'))
SetDefaultBufferRate basically throttles the amount of replications per second. If it’s 1, then only 1 replication per second can be done with that partition. If its 1/16, then 16 replications can be done in 1 second.
And finally, we replicate. In the for _, v loop I’m just setting each value to something random (within its 4 bit constraint). After that, I’m replicating all the changes to the client.
for _ = 1, 1000 do
for _, v in next, stuff do
local x = math.random(0, 15)
print(v.name, x)
pt:Set(v.name, x)
end
pt:Replicate()
task.wait(1)
end
That was a short runthrough of a server-side example. As you expect, the client recieves the data and updates it for a specific object. However, we’re still pretty surface level. So I’m gonna go into more depth
The module itself fundamentally works by combining binary numbers into one bitstream, with a max size of 31 bits. With the fruits and veggies example, they’re all conveniently combined into a 16 bit bitstream. However, what if we had 2 more fruits and 2 more vegetables, each with a bitSize of 4. Then that would be 32 bits, and we cant have that. So instead I create a new bitstream and put the extra fruit/veggie that’s overflowing the bitstream, in the new bitstream. And this process is repeated up to 15 times, as I only want a maximum number of 15 bitstreams in the system, which is reasonable for a pretty big game.
When a new partition is made, its metareferences are replicated to any new replicant, along with each metarefs bitstream (called groups) and its shift. When the client receives data, it uses the shift, group, and the bitSize of the number, and conveniently puts it into a dictionary with the key being the metarefs name, and the data extracted from the bitstream (using the group, shift, and bitSize together) being the value.
pushMetadataChange = function(instance, ...)
local metadata = totalPartitions[instance]
local bitStreams = {...}
local partitionStartingGroupMix = bitStreams[1]
local partitionNumber, initialGroup = bit32.extract(partitionStartingGroupMix, 0, 4), bit32.extract(partitionStartingGroupMix, 4, 4)
local targetPartition;
for _, v in ipairs(metadata.partitions) do
if (v.number == partitionNumber) then
targetPartition = v
break
end
end
local maxGroup = initialGroup + #bitStreams
for _, metaRef in ipairs(targetPartition.metadataReferences) do
if (metaRef.group < initialGroup or metaRef.group > maxGroup) then
continue
end
local value = bit32.extract(bitStreams[metaRef.group], metaRef.shift, metaRef.bitSize)
if (type(metaRef.default) == 'boolean') then
value = (value == 1) and (true) or (false)
else
if (metaRef.minMax) then
if (metaRef.minMax.Min < 0) then
value += metaRef.minMax.Min
end
end
if (metaRef.precision) then
value /= 10^metaRef.precision
end
end
targetPartition.localData[metaRef.name] = value
print(metaRef.name, value)
end
end,
That’s the clientside code for receiving data.
You may also notice this line:
local partitionNumber, initialGroup = bit32.extract(partitionStartingGroupMix, 0, 4), bit32.extract(partitionStartingGroupMix, 4, 4)
In the system, when determining the group and shift of each metaref, I start at shift 8. This is to accomodate for the partition number and the initial group (ill get to both in a sec) being the first 8 bits (0b11111111). The partition number is just an ID representing which partition is being modified or whatever in the remote. A metaref table is unique to each partition. The initial group is which group (bitstream) we’re starting on when we receive the bitstreams. This is the minimum modified bitstream. Say our bitstreams looks like this on the server: 18 632 0 0. We trunucate the extra 0’s to save some data, and are left with 18 632. The first 8 bits of that 632 represents the initial group and partition number keep in mind.
I don’t know what else to talk about or go more in depth about, so AMA if you’re curious.