In my opinion, Replica is a bad module. There are a lot of things which are in my opinion unnecessary and overcomplicated. So, I’ve redesigned a similar module in order to achieve some things which Replica is not able to:
- Being fully typed - By far one of the biggest improvements in Developer Experience, using Harmonic is as simple as modifying a table and the changes on that table will be replicated to all targetted clients.
- Compression is built in - using a dictionary based approach in order to compress paths to a minimal string of 2 bytes, its possible to save bandwidth without modifying developer experience and maintaining expected functionality. This is most apparent when attempting to modify deeply nested tables.
- In general, its more intuitive. Additional bloat of firing remotes and such has been discarded as it is not the aim of this module to provide that ability. This is a simple module which aims to replicate your data in a simple way.
Use cases & Examples:
-- Server
-- Define a struct that describes the structure of player data
local PlayerStats = Harmonic.struct("PlayerStats", {
Health = 100,
Coins = 0,
Inventory = {
Items = {},
Equipped = false,
},
})
-- Create a data entity for a specific player
game.Players.PlayerAdded:Connect(function(player)
local dataEntity = Harmonic.new(PlayerStats, Harmonic.ReplicationTarget.PLAYER(player))
-- Just modify the table — replication happens automatically
dataEntity._trove:Add(task.spawn(function()
while player.Parent do
dataEntity.Coins += 1
task.wait(5)
end
end))
end)
-- Client
local PlayerStats = Harmonic.forward("PlayerStats")
-- Listen for any new data entities created for this struct
Harmonic.GetDataAddedSignal(PlayerStats):Connect(function(dataEntity)
print("Received PlayerStats dataEntity!")
-- Observe live changes
dataEntity.Changed:Connect(function()
print(`Coins: {dataEntity.Coins}`)
end)
end)
Need to extend a struct? You can use extendStruct:
-- Server
local BaseEnemy = Harmonic.struct("BaseEnemy", {
Health = 100,
Damage = 10,
})
-- Create a new type of enemy derived from the base
local BossEnemy = Harmonic.extendStruct("BossEnemy", {
Health = 500,
SpecialAttack = true,
}, BaseEnemy)
-- Create an instance for replication
local dataEntity= Harmonic.new(BossEnemy)
dataEntity.Health -= 100
-- Client
local BossEnemy = Harmonic.forward("BossEnemy")
Harmonic.GetDataAddedSignal(BossEnemy):Connect(function(enemy)
print("Boss enemy replicated with", enemy.Health, "HP")
end)
Have a special Replication Target? You can add filters:
-- Only replicate to players in a specific team
local TeamAFilter = Harmonic.ReplicationTarget.CONDITION(function(player)
return player.Team and player.Team.Name == "TeamA"
end) --> CONDITIONs will recheck the replicant list every Stepped. If this behaviour is not desired and you would prefer to manually update it, use LAZYCONDITION instead.
local MatchState = Harmonic.struct("MatchState", {
Timer = 300,
WinningTeam = false,
})
local dataEntity = Harmonic.new(MatchState, TeamAFilter)
I have used the Harmonic.forward in many of these cases in order to compress down the examples, however, this comes with voiding the typed data, so the best practice with this module is to create a module list with ALL harmonics and then to use those in order to preserve the data type.
-- HarmonicStructs.lua
local BaseEnemy = Harmonic.struct("BaseEnemy", {
Health = 100,
Damage = 10,
})
local BossEnemy = Harmonic.extendStruct("BossEnemy", {
Health = 500,
SpecialAttack = true,
}, BaseEnemy)
local MatchState = Harmonic.struct("MatchState", {
Timer = 300,
WinningTeam = false,
})
local PlayerStats = Harmonic.struct("PlayerStats", {
Health = 100,
Coins = 0,
Inventory = {
Items = {},
Equipped = false,
},
})
return {
PlayerStats = PlayerStats,
BaseEnemy = BaseEnemy,
BossEnemy = BossEnemy,
MatchState = MatchState
}
and then use it like so:
game.Players.PlayerAdded:Connect(function(player)
local dataEntity = Harmonic.new(HarmonicStructs.PlayerStats, Harmonic.ReplicationTarget.PLAYER(player))
-- Just modify the table — replication happens automatically
dataEntity._trove:Add(task.spawn(function()
while player.Parent do
dataEntity.Coins += 1
task.wait(5)
end
end))
end)
-- Client
Harmonic.GetDataAddedSignal(Harmonies.PlayerStats):Connect(function(dataEntity)
print("Received PlayerStats dataEntity!")
-- Observe live changes
dataEntity.Changed:Connect(function()
print(`Coins: {dataEntity.Coins}`)
end)
end) --> properly preserves the type of dataEntitynow.
All Replication Targets:
Harmonic.ReplicationTarget.PLAYER(plr : Player) --> replicate to one person. This will also destroy all bound data entities upon the disconnection of this one plr.
Harmonic.ReplicationTarget.PLAYERS(plr : {Player}) --> replicate to people in persons (changes to the plr table is supported)
Harmonic.ReplicationTarget.LAZYPLAYERS(plr : {Player}) --> replicate to the plrs in the table, and freeze it. No changes supported
Harmonic.ReplicationTarget.CONDITION(cond : (Player)-> boolean) --> replicate to the plrs which satisfy the condition. Supported when condition state for player changes
Harmonic.ReplicationTarget.LAZYCONDITION(cond : (Player)-> boolean) --> replicate to the plrs which satisfy the condition. Does not detect changes in condition state for players.
Harmonic.ReplicationTarget.ALL() -->
Additionally, you should destroy your data entities after you are done with them:
dataEntity._trove:Add(function() print("NOOOO, SOMEONE LEFT YOUR BEAUTIFUL GAME!") end)
dataEntity:Destroy()
Things to consider:
-
Harmonic does not support Harmonies which have keys set to nil. For this, use false. If many people finds this as a downside, then it is possible to implement an inherent nil type like Harmonic.None() in order to solve this.
Harmonies.harmonic({ Key = nil })wont work. Key will not be changeable and throw an error saying that the field does not exist. Instead change Key = false -
Harmonic does not support cyclic tables.
Find the module here: https://create.roblox.com/store/asset/71924586834389/Harmonic-v001
This module is in beta, report any bugs which you have found, or any feature requests you have,