EDIT: I have not been updating this library much as of late, and no longer use it in my own projects. I plan on making a new library in the future to supercede Replica, once typed Luau comes out.
Get it here: Replica - Roblox
Documentation: GitHub - headjoe3/Replica: An easily controllable Server/Client data replication library
Data replication is probably one of the trickier issues with roblox game development. Once you start creating table-based data, it becomes more difficult to replicate certain portions at a time, or to listen to changes in only parts of the data. In previous games, I’ve resigned to using ValueBase and Folder instances to represent player state, as it’s usually the easiest way of getting data replicated. But even that has problems, namely that there is no good way to coordinate local changes, or bundle data changes together. Another important downside with ValueBase representation is that all data must be public. For most cases, this isn’t a problem, but it’s definitely a limiting factor.
So I recently created a new package called Replica, which contains a set of helper classes called “Replicants” with some basic Getter/Setter functions for changing and replicating data. It also has functions Collate
and Local
which control how data is replicated, or whether it will be replicated at all. Finally, it includes helper signal classes for listening to changes in certain portions of data.
Replicants
There are currently five Replicant classes that can be used (PM me suggestions if you want to see more):
- Replica.Array - A table of items with sequential integer keys
- Replica.Map - A table of items with string keys
- Replica.FactoredOr - A table with string-key factors with a “true” or “false” state; iff at least one factor is true, the FactoredOr object will resolve to “true”
- Replica.FactoredNor - Similar to FactoredOr; iff at least one factor is true, the FactoredOr object will resolve to “false”
- Replica.FactoredSum - A table with string-key factors and numeric values. The FactoredSum object will resolve to the sum of all factors.
Replicant objects are replicated tables whose values can be accessed, changed, and replicated using the :Get()
and :Set()
functions
Example:
local Replica = require(path.to.Replica)
game:GetService("Players").PlayerAdded:Connect(function(player)
-- Set up initial playerData state as a Replicant object
local playerData = Replica.Map.new({
Coins = 0,
-- Nested replicant objects
Inventory = Replica.Array.new({
"Foo",
Replica.Array.new({}),
"Bar",
})
})
-- Replicant objects can be registered and de-registered to any key.
-- Once they are registered, they will be replicated to all subscribed clients
Replica.Register(player, playerData)
end)
This will create a new Replicant map called “playerData” with nested Replicant objects.
To change a player’s coins and replicate the change, simple use:
playerData:Set("Coins", playerData:Get("Coins") + 10)
If you have a lot of changes you would like to make at once, you can use the Collate
method to freeze replication until a series of changes have been made at once
playerData:Collate(function()
playerData:Set("Coins", playerData:Get("Coins") - 10)
playerData:Set("Power", playerData:Get("Power") + 10)
end)
This will send a single replication update to all subscribed clients, so that the client will see Coins and Power change at the same time.
Client-side code can also set predictive changes to a Replicant’s state, which will be overwritten on the next server update. This is done using the :Predict()
method
local playerData = Replica.WaitForRegistered(game.Players.LocalPlayer)
playerData.OnUpdate:Connect(function(isLocal)
print("You have", playerData:Get("Coins"), "coins")
end)
path.to.humanoid.Jumping:Connect(function(isJumping)
if isJumping then
playerData:Predict("Coins", 1000)
end
end)
The above code will temporarily set the player’s coins to “1000” when the player jumps (client-side only), which will reset back to the actual value if a server update overwrites it
Finally, there are a number of helper events that you can listen to whenever an update occurs in a replicant’s data. OnUpdate
fires just after the data has changed, and WillUpdate
fires just before. There is also GetValueWillUpdateSignal
and GetValueOnUpdateSignal
which acts similarly to the GetPropertyChangedSignal
method if Instances:
playerData.WillUpdate:Connect(function(isLocal)
print("You had", playerData:Get("Coins"), "coins")
end)
playerData.OnUpdate:Connect(function(isLocal)
print("You have", playerData:Get("Coins"), "coins")
end)
Alternatively (will only fire when Coins changes, rather than the whole player data):
playerData:GetValueWillUpdateSignal("Coins"):Connect(function(isLocal)
print("You had", playerData:Get("Coins"), "coins")
end)
playerData.GetValueOnUpdateSignal:Connect("Coins"):Connect(function(isLocal)
print("You have", playerData:Get("Coins"), "coins")
end)
Here’s a demo putting it all together:
Server
local Replica = require(game.ReplicatedStorage.Replica)
game:GetService("Players").PlayerAdded:Connect(function(player)
-- Set up initial playerData state
local playerData = Replica.Map.new({
Coins = 0,
-- Replicant objects
Inventory = Replica.Array.new({
"Foo",
Replica.Array.new({}),
"Bar",
})
})
-- Replicant objects can be registered and de-registered to any key.
-- Once they are registered, they will be replicated to all subscribed clients
Replica.Register(player, playerData)
end)
game:GetService("Players").PlayerRemoving:Connect(function(player)
Replica.Unregister(player)
end)
-- The server can make changes to the replicated data using setter functions, and it
-- will automatically be replicated to subscribed clients
while wait(1) do
for _, player in pairs(game.Players:GetPlayers()) do
local playerData = Replica.GetRegistered(player)
if playerData ~= nil then
-- Collated data changes will be sent to the client in one single update
playerData:Collate(function()
-- Getters and setters automatically replicate changes to Replicant data
playerData:Set("Coins", playerData:Get("Coins") + 1)
-- The Array object allows items to be inserted and removed at any
-- position. Though I would recommend using a map instead,
-- all array changes will be buffered and replicated to the clients,
-- maintaining Replicant objects even when their position changes.
-- within the playerData tree.
local quxIndex = playerData:Get("Inventory"):IndexOf("Qux")
if quxIndex == nil then
playerData:Get("Inventory"):Insert(1, "Qux")
else
playerData:Get("Inventory"):Remove(quxIndex)
end
end)
end
end
end
Client
local Replica = require(game.ReplicatedStorage.Replica)
-- You can guarantee the existance of a registered Replicant using WaitForRegistered
local playerData = Replica.WaitForRegistered(game.Players.LocalPlayer)
print("Player data loaded")
-- The client will receive data replication updates from the server
playerData.OnUpdate:Connect(function(isLocal)
print("You have", playerData:Get("Coins"), "coins")
print("You have", playerData:Get("Inventory"):Size(), "items in your inventory")
end)
-- Wait for character to load
local char = game.Players.LocalPlayer.Character or game.Players.LocalPlayer.CharacterAdded:Wait()
local hum = char:FindFirstChildOfClass("Humanoid"); while not hum do char.ChildAdded:Wait() hum = char:FindFirstChildOfClass("Humanoid") end
-- The client can also make predictive state updates which will be overridden by
-- the server on the next update if the prediction does not match the replicated value
hum.Jumping:Connect(function(isJumping)
if isJumping then
-- The Local function allows changes to be made without any replication
-- implication. This is useful for things like UI, where you want to display
-- changes predictively, even though you have not confirmed them with the
-- server.
playerData:Predict("Coins", 1000)
end
end)
Finally, Replica lets you choose which players are able to see what portions of your data. Most Replicant objects have a “Config” argument as their second parameter, which subscribes all clients to changes to the object by default.
This can be changed using a partial configuration (see the documentation for more information Replica/Config.md at master · headjoe3/Replica · GitHub):
local playerData = Replica.Map.new({
Public = Replica.Map.new({}, {
SubscribeAll = true,
}),
Private = Replica.Map.new({}, {
SubscribeAll = false,
Whitelist = { player },
}),
})
playerData:Get("Public"):Set("Coins", 10) -- Replicated to all players
playerData:Get("Private"):Set("Secret", "I watch MLP") -- Replicated to one player only
Replica.Register(player, playerData)
In the above code, any updates within the “Public” tree will be visible to all players; however, updates within the “Private” tree will only be visible to the player that the data belongs to.
For more information on how to use the library, or the different Replicant objects such as Array
, Map
or FactoredSum
, see the documentation on github: GitHub - headjoe3/Replica: An easily controllable Server/Client data replication library
Here’s another demo using the FactoredOr, FactoredNor, and FactoredSum objects
Server
local Replica = require(game.ReplicatedStorage.Replica)
game:GetService("Players").PlayerAdded:Connect(function(player)
local playerData = Replica.Map.new({
Or = Replica.FactoredOr.new(),
Nor = Replica.FactoredNor.new(),
-- This configuration makes the Sum only visible to the player that the
-- playerData belongs to
Sum = Replica.FactoredSum.new({}, { SubscribeAll = false, Whitelist = {player} }),
})
-- These should fire on the server output only when the state has changed value
playerData:Get("Or").StateChanged:Connect(function(...)
print("Or state changed", ...)
end)
playerData:Get("Nor").StateChanged:Connect(function(...)
print("Nor state changed", ...)
end)
playerData:Get("Sum").StateChanged:Connect(function(...)
print("Sum state changed", ...)
end)
Replica.Register(player, playerData)
end)
game:GetService("Players").PlayerRemoving:Connect(function(player)
Replica.Unregister(player)
end)
local keys = {"a", "b", "c"}
while wait(2) do
for _,player in pairs(game.Players:GetPlayers()) do
local playerData = Replica.GetRegistered(player)
if playerData then
playerData:Collate(function()
local key = keys[math.random(1, #keys)]
-- The Or and Nor states should always resolve to the opposite
-- of each other, since they always toggle the same keys.
playerData:Get("Or"):Toggle(key)
playerData:Get("Nor"):Toggle(key)
-- The Sum state resolve to a random value between 0 and 3
playerData:Get("Sum"):Set(key, math.random())
end)
end
end
end
Client
local Replica = require(game.ReplicatedStorage.Replica)
-- Note that we are reading Player1's data, so if Player2 joins, they will
-- see the same data with factors in Sum omitted
local playerData = Replica.WaitForRegistered(game.Players.Player1)
print("Player data loaded")
playerData.OnUpdate:Connect(function()
print("UPDATE")
for key, factorMap in playerData:Pairs() do
print(" ", key .. ":", factorMap:ResolveState())
end
end)
If you have any questions or suggestions for improvements, let me know!
Roblox-TS typings (for those of you that use TypeScript): rbxts-replica/README.md at master · headjoe3/rbxts-replica · GitHub