| name | networking-replication |
| description | Implements networking and replication systems including RemoteEvent optimization, custom replication, lag compensation, client prediction, and bandwidth optimization. Use when building multiplayer games that need smooth networked gameplay. |
| allowed-tools | Read, Write, Edit, Glob, Grep |
Roblox Networking & Replication
When implementing networking systems, follow these Roblox-specific patterns for optimal multiplayer experience.
RemoteEvent Optimization
Batch Multiple Events
for _, enemy in ipairs(enemies) do
UpdateEnemyRemote:FireClient(player, enemy.id, enemy.position, enemy.health)
end
local updates = {}
for _, enemy in ipairs(enemies) do
table.insert(updates, {
id = enemy.id,
pos = enemy.position,
hp = enemy.health
})
end
UpdateEnemiesRemote:FireClient(player, updates)
Delta Compression
local lastSentState = {}
local function sendStateUpdate(player, entityId, newState)
local lastState = lastSentState[player.UserId] and lastSentState[player.UserId][entityId] or {}
local delta = {}
for key, value in pairs(newState) do
if lastState[key] ~= value then
delta[key] = value
end
end
if next(delta) then
StateUpdateRemote:FireClient(player, entityId, delta)
lastSentState[player.UserId] = lastSentState[player.UserId] or {}
lastSentState[player.UserId][entityId] = newState
end
end
Rate Limiting
local RateLimiter = {}
RateLimiter.calls = {}
function RateLimiter.check(player, remoteName, maxCallsPerSecond)
local key = player.UserId .. "_" .. remoteName
local now = os.clock()
RateLimiter.calls[key] = RateLimiter.calls[key] or {}
local calls = RateLimiter.calls[key]
for i = #calls, 1, -1 do
if now - calls[i] > 1 then
table.remove(calls, i)
end
end
if #calls >= maxCallsPerSecond then
return false
end
table.insert(calls, now)
return true
end
MyRemote.OnServerEvent:Connect(function(player, data)
if not RateLimiter.check(player, "MyRemote", 10) then
warn("Rate limited:", player.Name)
return
end
)
Unreliable RemoteEvents
local PositionUpdate = Instance.new("UnreliableRemoteEvent")
PositionUpdate.Name = "PositionUpdate"
PositionUpdate.Parent = ReplicatedStorage
Custom Replication System
NPC Replication with Interpolation
local NPC_UPDATE_RATE = 1/20
local function broadcastNPCPositions()
local updates = {}
for _, npc in ipairs(activeNPCs) do
table.insert(updates, {
id = npc.id,
pos = npc.PrimaryPart.Position,
rot = npc.PrimaryPart.Orientation.Y,
vel = npc.PrimaryPart.AssemblyLinearVelocity,
state = npc:GetAttribute("State"),
timestamp = workspace:GetServerTimeNow()
})
end
NPCUpdateRemote:FireAllClients(updates)
end
task.spawn(function()
while true do
broadcastNPCPositions()
task.wait(NPC_UPDATE_RATE)
end
end)
local InterpolationBuffer = {}
InterpolationBuffer.buffers = {}
InterpolationBuffer.BUFFER_TIME = 0.1
function InterpolationBuffer.addSnapshot(entityId, snapshot)
InterpolationBuffer.buffers[entityId] = InterpolationBuffer.buffers[entityId] or {}
local buffer = InterpolationBuffer.buffers[entityId]
table.insert(buffer, snapshot)
while #buffer > 10 do
table.remove(buffer, 1)
end
end
function InterpolationBuffer.getInterpolatedState(entityId)
local buffer = InterpolationBuffer.buffers[entityId]
if not buffer or #buffer < 2 then return nil end
local renderTime = workspace:GetServerTimeNow() - InterpolationBuffer.BUFFER_TIME
local prev, next
for i = #buffer, 1, -1 do
if buffer[i].timestamp <= renderTime then
prev = buffer[i]
next = buffer[i + 1]
prev
buffer[]
dt = renderTime - prev.timestamp
{
pos = prev.pos + prev.vel * dt,
rot = prev.rot,
state = prev.state
}
t = (renderTime - prev.timestamp) / (.timestamp - prev.timestamp)
{
pos = prev.pos:Lerp(.pos, t),
rot = prev.rot + (.rot - prev.rot) * t,
state = .state
}
Late Joiner Synchronization
Players.PlayerAdded:Connect(function(player)
player.CharacterAdded:Wait()
local fullState = {
enemies = {},
players = {},
worldState = {}
}
for _, enemy in ipairs(activeEnemies) do
table.insert(fullState.enemies, {
id = enemy.id,
type = enemy.Type,
pos = enemy.PrimaryPart.Position,
health = enemy.Health,
maxHealth = enemy.MaxHealth
})
end
for _, otherPlayer in ipairs(Players:GetPlayers()) do
if otherPlayer ~= player then
table.insert(fullState.players, {
userId = otherPlayer.UserId,
position = otherPlayer.Character and otherPlayer.Character.PrimaryPart.Position,
stats = getPlayerStats(otherPlayer)
})
end
end
fullState.worldState = {
timeOfDay = Lighting.ClockTime,
weather = currentWeather,
eventFlags = activeEvents
}
FullStateSyncRemote:FireClient(player, fullState)
end)
Server Authority
Authoritative Movement Validation
local MAX_SPEED = 50
local TOLERANCE = 1.5
local lastValidPositions = {}
local function validateMovement(player, claimedPosition)
local character = player.Character
if not character then return false end
local lastPos = lastValidPositions[player.UserId]
if not lastPos then
lastValidPositions[player.UserId] = {
position = claimedPosition,
time = os.clock()
}
return true
end
local deltaTime = os.clock() - lastPos.time
local distance = (claimedPosition - lastPos.position).Magnitude
local maxDistance = MAX_SPEED * deltaTime * TOLERANCE
if distance > maxDistance then
warn("Movement validation failed for", player.Name)
character:PivotTo(CFrame.new(lastPos.position))
return false
end
lastValidPositions[player.UserId] = {
position = claimedPosition,
time = .()
}
Server-Side Hit Registration
HitClaimRemote.OnServerEvent:Connect(function(player, hitData)
local attacker = player.Character
local target = getCharacterById(hitData.targetId)
if not attacker or not target then return end
local distance = (attacker.PrimaryPart.Position - target.PrimaryPart.Position).Magnitude
local maxRange = getAttackRange(hitData.attackType) * 1.2
if distance > maxRange then
warn("Hit rejected: out of range")
return
end
local lastAttack = attacker:GetAttribute("LastAttackTime") or 0
local cooldown = getAttackCooldown(hitData.attackType)
if os.clock() - lastAttack < cooldown * 0.9 then
warn("Hit rejected: attack on cooldown")
return
end
local rayParams = RaycastParams.new()
rayParams.FilterDescendantsInstances = {attacker}
local ray = workspace:Raycast(
attacker.PrimaryPart.Position,
(target.PrimaryPart.Position - attacker.PrimaryPart.Position),
rayParams
)
if ray and ray.Instance:IsDescendantOf(target)
damage = calculateDamage(hitData.attackType, attacker, target)
applyDamage(target, damage, attacker)
attacker:SetAttribute(, .())
)
Client Prediction
Input Prediction with Reconciliation
local InputHistory = {}
local MAX_HISTORY = 60
local function processInput(input)
local inputId = #InputHistory + 1
local predictedState = applyInput(localCharacter, input)
table.insert(InputHistory, {
id = inputId,
input = input,
predictedState = predictedState,
timestamp = os.clock()
})
while #InputHistory > MAX_HISTORY do
table.remove(InputHistory, 1)
end
InputRemote:FireServer(inputId, input)
end
ServerStateRemote.OnClientEvent:Connect(function(serverState)
local reconciledIndex
for i, entry in ipairs(InputHistory) do
if entry.id == serverState.lastProcessedInput then
reconciledIndex = i
break
end
end
reconciledIndex
predicted = InputHistory[reconciledIndex].predictedState
= (serverState.position - predicted.position).Magnitude
>
localCharacter:PivotTo(CFrame.new(serverState.position))
i = reconciledIndex + , #InputHistory
applyInput(localCharacter, InputHistory[i].)
i = reconciledIndex, ,
.(InputHistory, i)
)
Lag Compensation
Server-Side Lag Compensation
local PositionHistory = {}
local HISTORY_DURATION = 1
local function recordPosition(character)
local userId = Players:GetPlayerFromCharacter(character).UserId
PositionHistory[userId] = PositionHistory[userId] or {}
table.insert(PositionHistory[userId], {
position = character.PrimaryPart.Position,
timestamp = workspace:GetServerTimeNow()
})
local cutoff = workspace:GetServerTimeNow() - HISTORY_DURATION
while #PositionHistory[userId] > 0 and PositionHistory[userId][1].timestamp < cutoff do
table.remove(PositionHistory[userId], 1)
end
end
local function getPositionAtTime(userId, timestamp)
local history = PositionHistory[userId]
if not history or #history == 0 then return nil end
for i = #history, 1, -1 do
if history[i].timestamp <= timestamp then
prev = history[i]
= history[i + ]
prev.position
t = (timestamp - prev.timestamp) / (.timestamp - prev.timestamp)
prev.position:Lerp(.position, t)
history[].position
ping = shooter:GetNetworkPing()
viewTime = shooterTimestamp - ping /
targetPastPosition = getPositionAtTime(targetUserId, viewTime)
targetPastPosition
Bandwidth Optimization
Quantization
local function quantizePosition(position)
return Vector3.new(
math.floor(position.X * 10) / 10,
math.floor(position.Y * 10) / 10,
math.floor(position.Z * 10) / 10
)
end
local function quantizeRotation(rotation)
return math.floor(rotation * 100) / 100
end
local function packHealth(current, max)
return current * 65536 + max
end
local function unpackHealth(packed)
local max = packed % 65536
local current = math.(packed / )
current,
String Table for Identifiers
local StringTable = {
["FireProjectile"] = 1,
["TakeDamage"] = 2,
["UseAbility"] = 3,
}
local ReverseTable = {}
for str, id in pairs(StringTable) do
ReverseTable[id] = str
end
Entity Relevancy
local RELEVANCY_DISTANCE = 200
local function getRelevantEntities(player)
local character = player.Character
if not character then return {} end
local playerPos = character.PrimaryPart.Position
local relevant = {}
for _, entity in ipairs(allEntities) do
local distance = (entity.Position - playerPos).Magnitude
if distance <= RELEVANCY_DISTANCE then
table.insert(relevant, {entity = entity, priority = 1})
elseif distance <= RELEVANCY_DISTANCE * 2 then
table.insert(relevant, {entity = entity, priority = 0.5})
end
end
return relevant
end