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

Otimização de netcode e multiplayer para 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 →