networking-replication
π―Skillfrom taozhuo/game-dev-skills
Optimizes Roblox multiplayer networking with efficient event batching, delta compression, and rate limiting for smooth gameplay.
Installation
npx skills add https://github.com/taozhuo/game-dev-skills --skill networking-replicationSkill Details
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.
Overview
# Roblox Networking & Replication
When implementing networking systems, follow these Roblox-specific patterns for optimal multiplayer experience.
RemoteEvent Optimization
Batch Multiple Events
```lua
-- BAD: Firing many events per frame
for _, enemy in ipairs(enemies) do
UpdateEnemyRemote:FireClient(player, enemy.id, enemy.position, enemy.health)
end
-- GOOD: Batch into single event
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
```lua
-- Only send changed values
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 -- Only send if changes exist
StateUpdateRemote:FireClient(player, entityId, delta)
lastSentState[player.UserId] = lastSentState[player.UserId] or {}
lastSentState[player.UserId][entityId] = newState
end
end
```
Rate Limiting
```lua
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]
-- Remove old calls (older than 1 second)
for i = #calls, 1, -1 do
if now - calls[i] > 1 then
table.remove(calls, i)
end
end
-- Check limit
if #calls >= maxCallsPerSecond then
return false -- Rate limited
end
table.insert(calls, now)
return true
end
-- Usage in RemoteEvent handler
MyRemote.OnServerEvent:Connect(function(player, data)
if not RateLimiter.check(player, "MyRemote", 10) then
warn("Rate limited:", player.Name)
return
end
-- Process request
end)
```
Unreliable RemoteEvents
```lua
-- Use UnreliableRemoteEvent for frequent non-critical updates
local PositionUpdate = Instance.new("UnreliableRemoteEvent")
PositionUpdate.Name = "PositionUpdate"
PositionUpdate.Parent = ReplicatedStorage
-- Good for: position updates, mouse position, non-critical effects
-- Bad for: damage, purchases, important state changes
```
Custom Replication System
NPC Replication with Interpolation
```lua
-- Server: Send position snapshots
local NPC_UPDATE_RATE = 1/20 -- 20 updates per second
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)
```
```lua
-- Client: Interpolate between snapshots
local InterpolationBuffer = {}
InterpolationBuffer.buffers = {}
InterpolationBuffer.BUFFER_TIME = 0.1 -- 100ms interpolation delay
function InterpolationBuffer.addSnapshot(entityId, snapshot)
InterpolationBuffer.buffers[entityId] = InterpolationBuffer.buffers[entityId] or {}
local buffer = InterpolationBuffer.buffers[entityId]
table.insert(buffer, snapshot)
-- Keep only recent snapshots
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
-- Find surrounding snapshots
local prev, next
for i = #buffer, 1, -1 do
if buffer[i].timestamp <= renderTime then
prev = buffer[i]
next = buffer[i + 1]
break
end
end
if not prev then
return buffer[1] -- Extrapolate if too far behind
end
if not next then
-- Extrapolate forward
local dt = renderTime - prev.timestamp
return {
pos = prev.pos + prev.vel * dt,
rot = prev.rot,
state = prev.state
}
end
-- Interpolate between snapshots
local t = (renderTime - prev.timestamp) / (next.timestamp - prev.timestamp)
return {
pos = prev.pos:Lerp(next.pos, t),
rot = prev.rot + (next.rot - prev.rot) * t,
state = next.state -- Use newer state
}
end
```
Late Joiner Synchronization
```lua
-- Server: Send full state to joining players
Players.PlayerAdded:Connect(function(player)
-- Wait for character to load
player.CharacterAdded:Wait()
-- Send current game state
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
```lua
-- Server: Validate client-claimed positions
local MAX_SPEED = 50 -- studs per second
local TOLERANCE = 1.5 -- Multiplier for latency tolerance
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
-- Potential speed hack - reject and correct
warn("Movement validation failed for", player.Name)
character:PivotTo(CFrame.new(lastPos.position))
return false
end
lastValidPositions[player.UserId] = {
position = claimedPosition,
time = os.clock()
}
return true
end
```
Server-Side Hit Registration
```lua
-- Server: Validate all combat hits
HitClaimRemote.OnServerEvent:Connect(function(player, hitData)
local attacker = player.Character
local target = getCharacterById(hitData.targetId)
if not attacker or not target then return end
-- Validate distance
local distance = (attacker.PrimaryPart.Position - target.PrimaryPart.Position).Magnitude
local maxRange = getAttackRange(hitData.attackType) * 1.2 -- 20% tolerance
if distance > maxRange then
warn("Hit rejected: out of range")
return
end
-- Validate timing (attack cooldown)
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
-- Validate line of sight
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) then
-- Valid hit - apply damage
local damage = calculateDamage(hitData.attackType, attacker, target)
applyDamage(target, damage, attacker)
attacker:SetAttribute("LastAttackTime", os.clock())
end
end)
```
Client Prediction
Input Prediction with Reconciliation
```lua
-- Client: Apply inputs immediately, reconcile with server
local InputHistory = {}
local MAX_HISTORY = 60 -- 1 second at 60fps
local function processInput(input)
local inputId = #InputHistory + 1
local predictedState = applyInput(localCharacter, input)
-- Store for reconciliation
table.insert(InputHistory, {
id = inputId,
input = input,
predictedState = predictedState,
timestamp = os.clock()
})
-- Trim old history
while #InputHistory > MAX_HISTORY do
table.remove(InputHistory, 1)
end
-- Send to server
InputRemote:FireServer(inputId, input)
end
-- Client: Reconcile with server state
ServerStateRemote.OnClientEvent:Connect(function(serverState)
-- Find the input this state corresponds to
local reconciledIndex
for i, entry in ipairs(InputHistory) do
if entry.id == serverState.lastProcessedInput then
reconciledIndex = i
break
end
end
if not reconciledIndex then return end
-- Check if prediction was wrong
local predicted = InputHistory[reconciledIndex].predictedState
local error = (serverState.position - predicted.position).Magnitude
if error > 0.1 then -- Significant error
-- Snap to server position
localCharacter:PivotTo(CFrame.new(serverState.position))
-- Re-apply unacknowledged inputs
for i = reconciledIndex + 1, #InputHistory do
applyInput(localCharacter, InputHistory[i].input)
end
end
-- Remove acknowledged inputs
for i = reconciledIndex, 1, -1 do
table.remove(InputHistory, i)
end
end)
```
Lag Compensation
Server-Side Lag Compensation
```lua
-- Store position history for all players
local PositionHistory = {}
local HISTORY_DURATION = 1 -- 1 second of history
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()
})
-- Trim old entries
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
-- Find surrounding entries
for i = #history, 1, -1 do
if history[i].timestamp <= timestamp then
local prev = history[i]
local next = history[i + 1]
if not next then
return prev.position
end
-- Interpolate
local t = (timestamp - prev.timestamp) / (next.timestamp - prev.timestamp)
return prev.position:Lerp(next.position, t)
end
end
return history[1].position
end
-- Use in hit detection
local function lagCompensatedHitCheck(shooter, targetUserId, shooterTimestamp)
-- Rewind target to shooter's view time
local ping = shooter:GetNetworkPing()
local viewTime = shooterTimestamp - ping / 2
local targetPastPosition = getPositionAtTime(targetUserId, viewTime)
if not targetPastPosition then return false end
-- Check if shot would have hit at that position
-- ... perform hit detection with past position
end
```
Bandwidth Optimization
Quantization
```lua
-- Reduce precision for network transmission
local function quantizePosition(position)
return Vector3.new(
math.floor(position.X * 10) / 10, -- 0.1 stud precision
math.floor(position.Y * 10) / 10,
math.floor(position.Z * 10) / 10
)
end
local function quantizeRotation(rotation)
return math.floor(rotation * 100) / 100 -- ~0.6 degree precision
end
-- Pack multiple small values into single number
local function packHealth(current, max)
-- Assumes max health <= 65535 and current <= max
return current * 65536 + max
end
local function unpackHealth(packed)
local max = packed % 65536
local current = math.floor(packed / 65536)
return current, max
end
```
String Table for Identifiers
```lua
-- Instead of sending full strings, use numeric IDs
local StringTable = {
["FireProjectile"] = 1,
["TakeDamage"] = 2,
["UseAbility"] = 3,
-- ... etc
}
local ReverseTable = {}
for str, id in pairs(StringTable) do
ReverseTable[id] = str
end
-- Send: ActionRemote:FireServer(StringTable["FireProjectile"], data)
-- Receive: local action = ReverseTable[actionId]
```
Entity Relevancy
```lua
-- Only replicate entities relevant to each player
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
-- Close entities get full updates
table.insert(relevant, {entity = entity, priority = 1})
elseif distance <= RELEVANCY_DISTANCE * 2 then
-- Medium distance gets reduced updates
table.insert(relevant, {entity = entity, priority = 0.5})
end
-- Far entities not replicated
end
return relevant
end
```
More from this repository10
Manages audio systems with advanced sound pooling, priority management, and performance optimization for immersive game sound experiences.
animation-system skill from taozhuo/game-dev-skills
Implements robust game systems like inventory, shops, trading, and progression mechanics for Roblox RPGs with secure, stackable item management.
optimization skill from taozhuo/game-dev-skills
Generates dynamic visual effects like particle systems, camera shakes, and impact animations for enhancing game experiences in Roblox.
security-anticheat skill from taozhuo/game-dev-skills
Provides expert guidance on navigating Roblox-specific API behaviors, performance optimizations, and common coding pitfalls in Lua scripting.
Generates conceptual game art images using Gemini AI, providing precise style-specific prompts for character, environment, and UI design across various game aesthetics.
Enables comprehensive combat mechanics with advanced hitbox detection, damage calculation, and hit validation for creating dynamic player-vs-player and player-vs-NPC combat interactions.
Designs responsive, adaptive UI systems with dynamic screen sizing, safe area handling, and smooth animations for game interfaces.