Netcode e Multiplayer: Otimização de Latência e Sincronização em Jogos Online

Guia completo de netcode para jogos multiplayer: arquiteturas, sincronização, compensação de lag e técnicas avançadas de otimização
Netcode e Multiplayer: Otimização de Latência e Sincronização em Jogos Online
Introdução: O Desafio do Multiplayer
Criar uma experiência multiplayer fluida e responsiva é um dos maiores desafios técnicos no desenvolvimento de jogos. Com jogadores ao redor do mundo, latências variáveis e condições de rede imprevisíveis, o netcode precisa ser robusto e otimizado. Este guia explorará arquiteturas, técnicas de sincronização e estratégias avançadas para criar jogos online de alta qualidade.
A Importância do Bom Netcode
A diferença entre um jogo multiplayer de sucesso e um fracasso frequentemente está no netcode. Lag, desync e "teleports" podem arruinar completamente a experiência. Dominar estas técnicas é essencial para criar jogos competitivos e cooperativos que os jogadores adoram.
Arquiteturas de Rede
Cliente-Servidor vs P2P
# Comparação de arquiteturas de rede
class NetworkArchitecture:
def __init__(self):
self.architectures = {
"client_server": {
"pros": [
"Autoritativo - previne cheating",
"Estado consistente",
"Fácil de escalar",
"Controle centralizado"
],
"cons": [
"Custo de servidores",
"Latência adicional",
"Ponto único de falha"
],
"best_for": ["Jogos competitivos", "MMOs", "Battle Royale"],
"latency": "RTT completo"
},
"p2p": {
"pros": [
"Sem custo de servidor",
"Menor latência entre peers",
"Descentralizado"
],
"cons": [
"Vulnerável a cheating",
"Difícil sincronização",
"NAT traversal complexo",
"Host migration"
],
"best_for": ["Jogos cooperativos", "RTS pequenos", "Fighting games"],
"latency": "RTT/2 entre peers"
},
"hybrid": {
"pros": [
"Balanceia custos e performance",
"Flexibilidade",
"Servidor para matchmaking"
],
"cons": [
"Complexidade adicional",
"Dois sistemas para manter"
],
"best_for": ["Jogos com lobbies", "Sessões pequenas"],
"latency": "Variável"
}
}
def calculate_latency(self, architecture, player_count, avg_ping):
"""Calcular latência esperada por arquitetura"""
if architecture == "client_server":
# Todos passam pelo servidor
return avg_ping
elif architecture == "p2p":
# Latência média entre todos os peers
# Cresce com O(n²) conexões
connections = player_count * (player_count - 1) / 2
return avg_ping * (1 + connections / 100)
elif architecture == "hybrid":
# Depende do que está sendo sincronizado
return avg_ping * 0.7 # Estimativa
return avg_ping
Implementação Cliente-Servidor
using Unity.Netcode;
using System.Collections.Generic;
using UnityEngine;
public class ClientServerNetworking : NetworkBehaviour
{
// Servidor Autoritativo
public class AuthoritativeServer : NetworkBehaviour
{
private Dictionary<ulong, PlayerState> playerStates = new Dictionary<ulong, PlayerState>();
private float tickRate = 64f; // 64 ticks por segundo
private float tickTimer = 0f;
void Update()
{
if (!IsServer) return;
tickTimer += Time.deltaTime;
if (tickTimer >= 1f / tickRate)
{
ServerTick();
tickTimer = 0f;
}
}
void ServerTick()
{
// Processar inputs dos clientes
ProcessClientInputs();
// Simular física e game state
SimulateGameState();
// Enviar state atualizado aos clientes
BroadcastStateToClients();
}
void ProcessClientInputs()
{
foreach (var player in playerStates)
{
var inputs = GetPendingInputs(player.Key);
foreach (var input in inputs)
{
// Validar input
if (ValidateInput(input, player.Value))
{
ApplyInput(input, player.Value);
}
}
}
}
[ClientRpc]
void SendStateUpdateClientRpc(GameState state, ClientRpcParams clientRpcParams = default)
{
// Enviar estado atualizado para clientes específicos
if (IsClient)
{
ApplyServerState(state);
}
}
}
// Cliente com Predição
public class PredictiveClient : NetworkBehaviour
{
private Queue<InputCommand> unacknowledgedInputs = new Queue<InputCommand>();
private PlayerState localState;
private PlayerState serverState;
private int inputSequence = 0;
void FixedUpdate()
{
if (!IsLocalPlayer) return;
// Capturar input
InputCommand input = CaptureInput();
input.sequence = inputSequence++;
input.timestamp = Time.time;
// Aplicar localmente (predição)
ApplyInputLocally(input);
// Enviar ao servidor
SendInputToServerRpc(input);
// Guardar para reconciliação
unacknowledgedInputs.Enqueue(input);
}
[ServerRpc]
void SendInputToServerRpc(InputCommand input)
{
// Servidor processa o input
ProcessInputOnServer(input);
}
public void ReceiveServerState(PlayerState state, int lastProcessedInput)
{
serverState = state;
// Reconciliação
ReconcileWithServer(lastProcessedInput);
}
void ReconcileWithServer(int lastProcessedInput)
{
// Remover inputs já processados
while (unacknowledgedInputs.Count > 0 &&
unacknowledgedInputs.Peek().sequence <= lastProcessedInput)
{
unacknowledgedInputs.Dequeue();
}
// Corrigir posição se necessário
if (Vector3.Distance(localState.position, serverState.position) > 0.1f)
{
// Aplicar correção suave
localState.position = Vector3.Lerp(
localState.position,
serverState.position,
Time.deltaTime * 10f
);
}
// Re-aplicar inputs não confirmados
foreach (var input in unacknowledgedInputs)
{
ApplyInputLocally(input);
}
}
}
}
Sincronização de Estado
Interpolação e Extrapolação
// Sistema de interpolação e extrapolação para movimento suave
class NetworkInterpolation {
constructor() {
this.stateBuffer = [];
this.bufferTime = 100; // ms de buffer
this.maxExtrapolationTime = 200; // ms máximo de extrapolação
}
// Interpolação entre estados
interpolatePosition(currentTime) {
// Tempo de renderização (passado)
const renderTime = currentTime - this.bufferTime;
// Encontrar estados para interpolar
let state1 = null;
let state2 = null;
for (let i = 0; i < this.stateBuffer.length - 1; i++) {
if (this.stateBuffer[i].timestamp <= renderTime &&
this.stateBuffer[i + 1].timestamp >= renderTime) {
state1 = this.stateBuffer[i];
state2 = this.stateBuffer[i + 1];
break;
}
}
if (state1 && state2) {
// Interpolar entre estados
const t = (renderTime - state1.timestamp) /
(state2.timestamp - state1.timestamp);
return {
x: this.lerp(state1.position.x, state2.position.x, t),
y: this.lerp(state1.position.y, state2.position.y, t),
z: this.lerp(state1.position.z, state2.position.z, t),
rotation: this.slerpRotation(state1.rotation, state2.rotation, t)
};
} else if (this.stateBuffer.length > 0) {
// Extrapolar se necessário
return this.extrapolatePosition(currentTime);
}
return null;
}
// Extrapolação para compensar lag
extrapolatePosition(currentTime) {
if (this.stateBuffer.length < 2) {
return this.stateBuffer[0]?.position || null;
}
const latest = this.stateBuffer[this.stateBuffer.length - 1];
const previous = this.stateBuffer[this.stateBuffer.length - 2];
// Calcular velocidade
const dt = latest.timestamp - previous.timestamp;
const velocity = {
x: (latest.position.x - previous.position.x) / dt,
y: (latest.position.y - previous.position.y) / dt,
z: (latest.position.z - previous.position.z) / dt
};
// Tempo de extrapolação
const extrapolationTime = Math.min(
currentTime - latest.timestamp,
this.maxExtrapolationTime
);
// Aplicar extrapolação com damping
const damping = Math.max(0, 1 - extrapolationTime / this.maxExtrapolationTime);
return {
x: latest.position.x + velocity.x * extrapolationTime * damping,
y: latest.position.y + velocity.y * extrapolationTime * damping,
z: latest.position.z + velocity.z * extrapolationTime * damping,
rotation: latest.rotation
};
}
lerp(a, b, t) {
return a + (b - a) * Math.max(0, Math.min(1, t));
}
slerpRotation(q1, q2, t) {
// Simplified quaternion slerp
let dot = q1.x * q2.x + q1.y * q2.y + q1.z * q2.z + q1.w * q2.w;
if (dot < 0) {
q2 = { x: -q2.x, y: -q2.y, z: -q2.z, w: -q2.w };
dot = -dot;
}
if (dot > 0.95) {
// Linear interpolation for small angles
return {
x: this.lerp(q1.x, q2.x, t),
y: this.lerp(q1.y, q2.y, t),
z: this.lerp(q1.z, q2.z, t),
w: this.lerp(q1.w, q2.w, t)
};
}
const angle = Math.acos(dot);
const sinAngle = Math.sin(angle);
const t1 = Math.sin((1 - t) * angle) / sinAngle;
const t2 = Math.sin(t * angle) / sinAngle;
return {
x: q1.x * t1 + q2.x * t2,
y: q1.y * t1 + q2.y * t2,
z: q1.z * t1 + q2.z * t2,
w: q1.w * t1 + q2.w * t2
};
}
}
Delta Compression
public class DeltaCompression
{
// Compressão de estado usando deltas
public class StateCompressor
{
private Dictionary<ulong, GameState> lastSentStates = new Dictionary<ulong, GameState>();
public byte[] CompressState(GameState currentState, ulong clientId)
{
if (!lastSentStates.ContainsKey(clientId))
{
// Primeiro estado - enviar completo
lastSentStates[clientId] = currentState;
return SerializeFullState(currentState);
}
// Calcular delta
GameState lastState = lastSentStates[clientId];
DeltaState delta = CalculateDelta(lastState, currentState);
// Atualizar último estado enviado
lastSentStates[clientId] = currentState;
// Comprimir e retornar delta
return CompressDelta(delta);
}
private DeltaState CalculateDelta(GameState oldState, GameState newState)
{
DeltaState delta = new DeltaState();
delta.frameNumber = newState.frameNumber;
// Apenas incluir campos que mudaram
if (oldState.position != newState.position)
{
delta.hasPosition = true;
delta.position = newState.position;
}
if (oldState.rotation != newState.rotation)
{
delta.hasRotation = true;
// Comprimir rotação para 2 bytes
delta.compressedRotation = CompressRotation(newState.rotation);
}
if (oldState.health != newState.health)
{
delta.hasHealth = true;
delta.health = newState.health;
}
// Comprimir movimento em bits
delta.movementFlags = 0;
if (newState.isMoving) delta.movementFlags |= 1;
if (newState.isJumping) delta.movementFlags |= 2;
if (newState.isCrouching) delta.movementFlags |= 4;
if (newState.isSprinting) delta.movementFlags |= 8;
return delta;
}
private ushort CompressRotation(Quaternion rotation)
{
// Comprimir rotação Y (yaw) em 16 bits
float yaw = rotation.eulerAngles.y;
return (ushort)(yaw / 360f * 65535);
}
private byte[] CompressDelta(DeltaState delta)
{
using (var stream = new MemoryStream())
using (var writer = new BinaryWriter(stream))
{
// Header com flags indicando que campos estão presentes
byte header = 0;
if (delta.hasPosition) header |= 1;
if (delta.hasRotation) header |= 2;
if (delta.hasHealth) header |= 4;
writer.Write(header);
writer.Write(delta.frameNumber);
if (delta.hasPosition)
{
// Quantizar posição para economizar bytes
writer.Write((short)(delta.position.x * 100));
writer.Write((short)(delta.position.y * 100));
writer.Write((short)(delta.position.z * 100));
}
if (delta.hasRotation)
{
writer.Write(delta.compressedRotation);
}
if (delta.hasHealth)
{
writer.Write((byte)delta.health);
}
writer.Write(delta.movementFlags);
return stream.ToArray();
}
}
}
}
Compensação de Lag
Lag Compensation para Hit Detection
# Sistema de lag compensation para tiros
class LagCompensation:
def __init__(self, history_duration=1.0):
self.history_duration = history_duration
self.position_history = {} # player_id -> [(timestamp, position)]
self.hitbox_history = {}
def record_player_state(self, player_id, position, hitbox, timestamp):
"""Registrar estado do jogador para histórico"""
if player_id not in self.position_history:
self.position_history[player_id] = []
self.hitbox_history[player_id] = []
# Adicionar novo estado
self.position_history[player_id].append((timestamp, position))
self.hitbox_history[player_id].append((timestamp, hitbox))
# Limpar estados antigos
cutoff_time = timestamp - self.history_duration
self.position_history[player_id] = [
(t, p) for t, p in self.position_history[player_id]
if t > cutoff_time
]
def rewind_and_check_hit(self, shooter_id, shot_timestamp, shot_origin, shot_direction):
"""Rebobinar tempo e verificar se acertou"""
# Calcular latência do atirador
shooter_latency = self.get_player_latency(shooter_id)
rewind_time = shot_timestamp - shooter_latency
# Rebobinar todos os jogadores para o momento do tiro
rewound_players = {}
for player_id in self.position_history:
if player_id == shooter_id:
continue # Não rebobinar o próprio atirador
position = self.get_position_at_time(player_id, rewind_time)
hitbox = self.get_hitbox_at_time(player_id, rewind_time)
rewound_players[player_id] = (position, hitbox)
# Fazer raycast com posições rebobinadas
hit_result = self.raycast_check(
shot_origin,
shot_direction,
rewound_players
)
return hit_result
def get_position_at_time(self, player_id, target_time):
"""Interpolar posição em momento específico"""
if player_id not in self.position_history:
return None
history = self.position_history[player_id]
# Encontrar dois estados para interpolar
for i in range(len(history) - 1):
t1, pos1 = history[i]
t2, pos2 = history[i + 1]
if t1 <= target_time <= t2:
# Interpolar entre os dois estados
t = (target_time - t1) / (t2 - t1)
return self.lerp_position(pos1, pos2, t)
# Se não encontrar, retornar mais próximo
return history[-1][1] if history else None
def raycast_check(self, origin, direction, player_positions):
"""Verificar colisão de raycast com jogadores"""
hits = []
for player_id, (position, hitbox) in player_positions.items():
if position is None:
continue
# Verificar interseção ray-box
if self.ray_box_intersection(origin, direction, position, hitbox):
distance = self.calculate_distance(origin, position)
hits.append({
"player_id": player_id,
"distance": distance,
"position": position
})
# Retornar hit mais próximo
if hits:
return min(hits, key=lambda x: x["distance"])
return None
Rollback Netcode
public class RollbackNetcode : MonoBehaviour
{
// Sistema de rollback para jogos de luta e ação
public class RollbackSystem
{
private const int MAX_ROLLBACK_FRAMES = 8;
private GameState[] stateHistory = new GameState[MAX_ROLLBACK_FRAMES];
private InputFrame[] inputHistory = new InputFrame[MAX_ROLLBACK_FRAMES];
private int currentFrame = 0;
private int confirmedFrame = 0;
public void ReceiveRemoteInput(int frame, PlayerInput input, int playerId)
{
// Input chegou do passado - precisa fazer rollback
if (frame < currentFrame)
{
// Salvar estado atual
GameState currentState = GetCurrentGameState();
// Voltar ao frame do input
RollbackToFrame(frame);
// Aplicar o input recebido
ApplyInput(input, playerId);
// Re-simular até o presente
ResimulateToPresent(frame, currentFrame);
// Verificar dessincronização
if (!StatesMatch(currentState, GetCurrentGameState()))
{
HandleDesync();
}
}
else
{
// Input para frame futuro - guardar
StoreInputForFuture(frame, input, playerId);
}
}
void RollbackToFrame(int targetFrame)
{
int rollbackAmount = currentFrame - targetFrame;
if (rollbackAmount > MAX_ROLLBACK_FRAMES)
{
Debug.LogError($"Rollback muito grande: {rollbackAmount} frames");
return;
}
// Restaurar estado salvo
int historyIndex = targetFrame % MAX_ROLLBACK_FRAMES;
RestoreGameState(stateHistory[historyIndex]);
}
void ResimulateToPresent(int fromFrame, int toFrame)
{
for (int frame = fromFrame; frame <= toFrame; frame++)
{
// Pegar inputs do frame (local e remoto)
var inputs = GetInputsForFrame(frame);
// Simular frame
SimulateGameFrame(inputs);
// Salvar estado
int historyIndex = frame % MAX_ROLLBACK_FRAMES;
stateHistory[historyIndex] = CaptureGameState();
}
}
public void PredictInput(int playerId, int frame)
{
// Se não recebeu input remoto, prever baseado no último
if (!HasInputForFrame(playerId, frame))
{
PlayerInput prediction;
if (frame > 0 && HasInputForFrame(playerId, frame - 1))
{
// Repetir último input conhecido
prediction = GetInputForFrame(playerId, frame - 1);
}
else
{
// Input neutro
prediction = new PlayerInput();
}
// Marcar como predição para possível rollback
prediction.isPredicted = true;
StoreInput(playerId, frame, prediction);
}
}
}
// GGPO-style implementation
public class GGPOImplementation
{
private int localFrame = 0;
private int remoteFrame = 0;
private int frameAdvantage = 0;
private const int MAX_FRAME_ADVANTAGE = 3;
public void UpdateFrameAdvantage()
{
frameAdvantage = localFrame - remoteFrame;
// Limitar vantagem de frames
if (frameAdvantage > MAX_FRAME_ADVANTAGE)
{
// Pausar execução local até remote alcançar
PauseLocalExecution();
}
else if (frameAdvantage < -MAX_FRAME_ADVANTAGE)
{
// Acelerar execução local
RunExtraFrames(Math.Abs(frameAdvantage) - MAX_FRAME_ADVANTAGE);
}
}
}
}
Otimização de Bandwidth
Compressão de Dados
// Sistema de compressão de dados para rede
class NetworkCompression {
constructor() {
this.compressionTechniques = {
quantization: this.quantizeFloat,
bitPacking: this.packBits,
deltaEncoding: this.encodeDelta,
runLength: this.runLengthEncode
};
}
// Quantização de floats para reduzir precisão
quantizeFloat(value, min, max, bits) {
const range = max - min;
const maxInt = (1 << bits) - 1;
// Normalizar para 0-1
let normalized = (value - min) / range;
normalized = Math.max(0, Math.min(1, normalized));
// Quantizar
const quantized = Math.round(normalized * maxInt);
return quantized;
}
// Desquantizar
dequantizeFloat(quantized, min, max, bits) {
const range = max - min;
const maxInt = (1 << bits) - 1;
const normalized = quantized / maxInt;
return min + normalized * range;
}
// Empacotar múltiplos valores em bits
packBits(values) {
let packed = 0;
let bitOffset = 0;
for (const config of values) {
const { value, bits } = config;
const mask = (1 << bits) - 1;
packed |= (value & mask) << bitOffset;
bitOffset += bits;
}
return packed;
}
// Desempacotar bits
unpackBits(packed, configs) {
const values = [];
let bitOffset = 0;
for (const config of configs) {
const { bits } = config;
const mask = (1 << bits) - 1;
const value = (packed >> bitOffset) & mask;
values.push(value);
bitOffset += bits;
}
return values;
}
// Comprimir posição 3D
compressPosition(position, worldBounds) {
const compressed = {
x: this.quantizeFloat(position.x, worldBounds.minX, worldBounds.maxX, 16),
y: this.quantizeFloat(position.y, worldBounds.minY, worldBounds.maxY, 16),
z: this.quantizeFloat(position.z, worldBounds.minZ, worldBounds.maxZ, 16)
};
// Empacotar em 6 bytes ao invés de 12
const buffer = new ArrayBuffer(6);
const view = new DataView(buffer);
view.setUint16(0, compressed.x, true);
view.setUint16(2, compressed.y, true);
view.setUint16(4, compressed.z, true);
return buffer;
}
// Comprimir rotação usando menor componente
compressRotation(quaternion) {
// Quaternion tem 4 componentes, mas podemos deduzir uma
const components = [quaternion.x, quaternion.y, quaternion.z, quaternion.w];
// Encontrar maior componente
let maxIndex = 0;
let maxValue = Math.abs(components[0]);
for (let i = 1; i < 4; i++) {
if (Math.abs(components[i]) > maxValue) {
maxIndex = i;
maxValue = Math.abs(components[i]);
}
}
// Garantir que maior componente é positivo
if (components[maxIndex] < 0) {
components[0] *= -1;
components[1] *= -1;
components[2] *= -1;
components[3] *= -1;
}
// Comprimir 3 componentes (10 bits cada) + 2 bits para índice
const compressed = [];
let bitIndex = 0;
for (let i = 0; i < 4; i++) {
if (i !== maxIndex) {
// Normalizar para -0.707 a 0.707 e quantizar
const normalized = components[i] / 0.707;
const quantized = this.quantizeFloat(normalized, -1, 1, 10);
compressed.push(quantized);
}
}
// Total: 32 bits (4 bytes) ao invés de 16 bytes
return {
omittedIndex: maxIndex,
components: compressed
};
}
}
Priority System
public class NetworkPrioritySystem
{
// Sistema de prioridade para otimizar bandwidth
public class EntityPriority
{
public float CalculatePriority(GameObject entity, GameObject viewer)
{
float priority = 0f;
// Distância (mais importante)
float distance = Vector3.Distance(entity.transform.position, viewer.transform.position);
float distancePriority = 1f / (1f + distance * 0.01f);
priority += distancePriority * 10f;
// Velocidade (objetos em movimento são mais importantes)
Rigidbody rb = entity.GetComponent<Rigidbody>();
if (rb != null)
{
float speed = rb.velocity.magnitude;
priority += speed * 0.5f;
}
// Visibilidade
if (IsVisible(entity, viewer))
{
priority += 5f;
}
// Relevância gameplay
if (entity.CompareTag("Player"))
{
priority += 20f;
}
else if (entity.CompareTag("Projectile"))
{
priority += 15f;
}
else if (entity.CompareTag("Pickup"))
{
priority += 3f;
}
// Tempo desde última atualização
float timeSinceUpdate = Time.time - GetLastUpdateTime(entity);
priority += timeSinceUpdate * 2f;
return priority;
}
bool IsVisible(GameObject entity, GameObject viewer)
{
// Frustum culling simplificado
Camera viewerCamera = viewer.GetComponentInChildren<Camera>();
if (viewerCamera != null)
{
Plane[] planes = GeometryUtility.CalculateFrustumPlanes(viewerCamera);
Bounds bounds = entity.GetComponent<Collider>().bounds;
return GeometryUtility.TestPlanesAABB(planes, bounds);
}
return false;
}
}
// Agendador de updates baseado em prioridade
public class UpdateScheduler
{
private Dictionary<ulong, float> updateRates = new Dictionary<ulong, float>();
private Dictionary<ulong, float> lastUpdateTime = new Dictionary<ulong, float>();
public bool ShouldSendUpdate(ulong entityId, float priority)
{
// Calcular rate baseado em prioridade
float updateRate = CalculateUpdateRate(priority);
if (!lastUpdateTime.ContainsKey(entityId))
{
lastUpdateTime[entityId] = 0f;
}
float timeSinceLastUpdate = Time.time - lastUpdateTime[entityId];
if (timeSinceLastUpdate >= 1f / updateRate)
{
lastUpdateTime[entityId] = Time.time;
return true;
}
return false;
}
float CalculateUpdateRate(float priority)
{
// Taxa de update baseada em prioridade
// Alta prioridade = 60Hz, Baixa = 1Hz
if (priority > 30) return 60f;
if (priority > 20) return 30f;
if (priority > 10) return 15f;
if (priority > 5) return 5f;
return 1f;
}
}
}
Técnicas Avançadas
Deterministic Lockstep
# Simulação determinística para RTS
class DeterministicLockstep:
def __init__(self, player_count):
self.player_count = player_count
self.current_turn = 0
self.turn_length_ms = 100 # 100ms por turno
self.input_buffer = {} # turn -> player_id -> commands
self.confirmed_turn = -1
def submit_commands(self, player_id, turn, commands):
"""Submeter comandos para um turno"""
if turn not in self.input_buffer:
self.input_buffer[turn] = {}
self.input_buffer[turn][player_id] = commands
# Verificar se todos enviaram comandos
if len(self.input_buffer[turn]) == self.player_count:
self.execute_turn(turn)
def execute_turn(self, turn):
"""Executar turno quando todos os inputs chegaram"""
if turn != self.confirmed_turn + 1:
print(f"Erro: turno fora de ordem {turn}")
return
# Pegar todos os comandos
all_commands = self.input_buffer[turn]
# Executar em ordem determinística
for player_id in sorted(all_commands.keys()):
commands = all_commands[player_id]
for command in commands:
self.execute_command(command, player_id)
# Atualizar simulação física deterministicamente
self.physics_step(fixed_delta=0.1)
self.confirmed_turn = turn
# Limpar buffer
if turn - 10 in self.input_buffer:
del self.input_buffer[turn - 10]
def execute_command(self, command, player_id):
"""Executar comando de forma determinística"""
# Usar math determinístico
import decimal
decimal.getcontext().prec = 10
# Garantir ordem de execução
units = sorted(command.units, key=lambda u: u.id)
for unit in units:
# Cálculos usando ponto fixo
new_x = decimal.Decimal(str(unit.x)) + decimal.Decimal(str(command.dx))
new_y = decimal.Decimal(str(unit.y)) + decimal.Decimal(str(command.dy))
unit.x = float(new_x)
unit.y = float(new_y)
def handle_desync(self, checksum_local, checksum_remote):
"""Lidar com dessincronização"""
if checksum_local != checksum_remote:
print("DESYNC DETECTADO!")
# Estratégias:
# 1. Resync do zero
# 2. Rollback para último estado conhecido bom
# 3. Kick do jogador dessincronizado
self.request_full_resync()
Métricas e Monitoramento
Network Analytics
// Sistema de analytics para netcode
class NetworkAnalytics {
constructor() {
this.metrics = {
latency: [],
packetLoss: [],
bandwidth: [],
jitter: [],
desyncEvents: 0,
rollbackFrames: []
};
}
measureLatency(sendTime, receiveTime) {
const rtt = receiveTime - sendTime;
this.metrics.latency.push(rtt);
// Calcular jitter
if (this.metrics.latency.length > 1) {
const previousRtt = this.metrics.latency[this.metrics.latency.length - 2];
const jitter = Math.abs(rtt - previousRtt);
this.metrics.jitter.push(jitter);
}
return rtt;
}
calculatePacketLoss(sent, received) {
const loss = ((sent - received) / sent) * 100;
this.metrics.packetLoss.push(loss);
return loss;
}
getNetworkQuality() {
const avgLatency = this.average(this.metrics.latency);
const avgPacketLoss = this.average(this.metrics.packetLoss);
const avgJitter = this.average(this.metrics.jitter);
let quality = "Good";
if (avgLatency > 150 || avgPacketLoss > 5 || avgJitter > 50) {
quality = "Poor";
} else if (avgLatency > 100 || avgPacketLoss > 2 || avgJitter > 30) {
quality = "Fair";
}
return {
quality,
avgLatency,
avgPacketLoss,
avgJitter,
recommendation: this.getRecommendation(quality)
};
}
getRecommendation(quality) {
switch(quality) {
case "Poor":
return "Reduzir tick rate, aumentar interpolação";
case "Fair":
return "Ativar compressão agressiva";
default:
return "Configurações ótimas";
}
}
average(arr) {
if (arr.length === 0) return 0;
return arr.reduce((a, b) => a + b, 0) / arr.length;
}
}
Recursos e Ferramentas
Frameworks e Bibliotecas
- Mirror: Networking para Unity
- Photon: Multiplayer as a Service
- Netcode for GameObjects: Solução oficial Unity
- PlayFab: Backend multiplayer
- Nakama: Server open-source
Ferramentas de Debug
- Wireshark: Análise de pacotes
- Network Simulator: Simular condições ruins
- Unity Multiplayer Tools: Profiling de rede
- Clumsy: Simular latência e packet loss
Conclusão
Criar netcode robusto é uma arte que combina técnicas de compressão, predição, interpolação e sincronização. O segredo está em entender as limitações da rede e projetar sistemas que funcionem bem mesmo em condições adversas. Teste exaustivamente em condições reais e sempre priorize a experiência do jogador.
🌐 Domine Multiplayer Games! Aprenda netcode avançado e crie jogos online incríveis. Teste vocacional gratuito →
Próximos Passos
Comece com arquitetura cliente-servidor autoritativa. Implemente predição e interpolação básicas. Teste com latência e packet loss simulados. Itere baseado em feedback real de jogadores. Lembre-se: netcode perfeito não existe, mas netcode bom o suficiente sim.
🚀 Curso de Programação Multiplayer! Torne-se especialista em jogos online. Inscreva-se agora →

