Voltar ao Blog

Unreal Engine RPC Optimization: Como Parar de Inundar sua Rede a Cada Tick

Publicado em 4 de maio de 2026
Unreal Engine RPC Optimization: Como Parar de Inundar sua Rede a Cada Tick

Em resumo

Este guia técnico detalha estratégias essenciais de unreal engine rpc optimization para evitar o congestionamento de rede causado por chamadas excessivas no Tick. Aprenda a implementar o Accumulator Pattern em C++ para desacoplar a taxa de envio de rede do frame rate do client, reduzindo o consumo de banda em até 85%. O artigo também aborda técnicas avançadas como struct batching, quantização de vetores e o uso de interpolação para manter a fluidez do gameplay em ambientes multiplayer escaláveis.

Todo desenvolvedor de jogos multiplayer acaba enfrentando o mesmo gargalo de rede: um client rodando a 144 frames por segundo decide enviar seu estado de movimento customizado para o server a cada tick. Em segundos, a fila de rede do server fica completamente inundada com Remote Procedure Calls (RPCs) redundantes, causando lag extremo, packet loss e uma desconexão inevitável. Seu client está basicamente executando um ataque de Distributed Denial of Service (DDoS) na infraestrutura do seu próprio server.

Este cenário representa uma das armadilhas mais comuns na arquitetura de jogos multiplayer. Quando desenvolvedores precisam enviar inputs customizados de jogadores, estados complexos de física de veículos ou mecânicas de tiro rápido, colocar um RPC dentro da função Tick() parece a escolha lógica para uma responsividade suave. No entanto, a camada de networking da Unreal Engine não faz o cull automático de RPCs intermediários. Se o seu jogo envia um RPC a cada tick, todos eles são enfileirados e transmitidos.

Para atualizações de movimento e posição, você quase nunca precisa dos 143 frames intermediários; você só precisa do estado absoluto mais recente para replicar para os outros clients. Neste guia abrangente, vamos mergulhar fundo em unreal engine rpc optimization, mostrando exatamente como fazer o throttle dessas chamadas de rede baseadas em tick, implementar um acúmulo de estado inteligente e reduzir drasticamente o seu overhead de largura de banda no multiplayer.

O Perigo de Eventos de Rede Atrelados ao Tick

Antes de implementar uma solução, é crucial entender a anatomia do problema. Quando você declara um RPC na Unreal Engine, seja ele Server, Client ou NetMulticast, você está instruindo o network driver da engine a serializar os parâmetros da função e empurrá-los para a fila de pacotes de saída.

O Problema com o Enfileiramento

A Unreal Engine agrupa RPCs de saída em pacotes com base na NetUpdateFrequency da conexão e nos limites de largura de banda. Se um client está chamando um Server RPC a cada tick em um frame rate alto, a engine tentará processar cada uma dessas chamadas.

Se o RPC estiver marcado como Reliable, a situação é catastrófica. RPCs Reliable garantem a entrega e a ordem de execução. O canal de rede ficará cheio rapidamente e, se o buffer transbordar, a conexão será encerrada forçadamente pela engine, resultando em um jogador desconectado.

Se o RPC estiver marcado como Unreliable, a engine descartará pacotes quando a fila encher. Embora isso evite uma desconexão total, leva a um massive rubber-banding. O server pode receber o frame 1, frame 2, descartar os frames 3-100 e então processar o frame 101. O resultado é um movimento errático e aos saltos que arruína a experiência de gameplay. Esta é uma causa raiz comum quando as equipes estão corrigindo o problema de replicação de RPC da Unreal Engine que quebra seus estados.

A Matemática da Largura de Banda

Vamos olhar para alguns números concretos. Imagine que você está enviando um simples vector (12 bytes) e um rotator (12 bytes) via um Server RPC. Com o overhead do cabeçalho do RPC, estimamos 32 bytes por chamada.

  • A 30 FPS: 30 * 32 bytes = 960 bytes/segundo (aproximadamente 1 KB/s por client).
  • A 144 FPS: 144 * 32 bytes = 4.608 bytes/segundo (aproximadamente 4.6 KB/s por client).
  • A 240 FPS: 240 * 32 bytes = 7.680 bytes/segundo.

Multiplique isso por 64 jogadores em um battle royale, e seu server de repente estará processando quase meio megabyte de puro overhead de RPC a cada segundo — apenas para rastreamento básico de movimento. Isso não escala.

Passo 1: Quebrando a Dependência do Tick com um Accumulator Pattern

A estratégia mais eficaz para unreal engine rpc optimization é desacoplar sua taxa de envio de rede do frame rate de rendering do seu client. Em vez de disparar o RPC no Tick(), você deve atualizar uma variável local a cada tick e, em seguida, usar um timer para enviar esses dados para o server em um intervalo fixo e previsível (ex: 10 ou 20 vezes por segundo).

Chamamos isso de Accumulator Pattern. O client acumula o estado mais recente continuamente, mas só transmite quando o portão da rede se abre.

Identificando a Frequência Alvo

Você não precisa de 144 atualizações por segundo para uma experiência multiplayer suave. A maioria dos shooters competitivos modernos roda seus servers a 30Hz ou 60Hz. Portanto, enviar atualizações do client 15 a 30 vezes por segundo é geralmente mais do que suficiente, desde que você esteja usando client-side prediction e server-side interpolation adequados.

Ao reduzir a taxa de envio de um 144Hz ilimitado para um 20Hz fixo, você reduz instantaneamente seu tráfego de rede em mais de 85% para essa ação específica.

Passo 2: Implementando o Rate-Limiter em C++

Vamos ver como implementar isso de forma eficaz em C++. Criaremos um sistema onde o client rastreia sua localização e rotação desejadas a cada tick, mas só envia o RPC Server_UpdateTransform com base em uma taxa de envio de rede predefinida.

O Header File (.h)

Primeiro, definimos nossas variáveis e funções em nossa classe customizada de APawn ou ACharacter. Precisamos de um timer handle, uma taxa de atualização e as variáveis para armazenar nossos dados ainda não enviados.

UCLASS()
class MYGAME_API AMyCustomPawn : public APawn
{
    GENERATED_BODY()

public:
    AMyCustomPawn();

    virtual void Tick(float DeltaTime) override;
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

protected:
    virtual void BeginPlay() override;

    // O RPC para enviar dados ao server. Marcado como Unreliable para atualizações rápidas e contínuas.
    UFUNCTION(Server, Unreliable, WithValidation)
    void Server_SendTransformUpdate(FVector NewLocation, FRotator NewRotation);

private:
    // Timer handle para o nosso flush de rede
    FTimerHandle NetworkUpdateTimerHandle;

    // Quantas vezes por segundo queremos enviar atualizações para o server
    UPROPERTY(EditDefaultsOnly, Category = "Network")
    float NetworkSendRate;

    // Flag para rastrear se temos novos dados que ainda não foram enviados
    bool bHasPendingNetworkUpdate;

    // Os dados acumulados aguardando para serem enviados
    FVector PendingLocation;
    FRotator PendingRotation;

    // A função chamada pelo timer para fazer o flush dos dados
    void FlushNetworkUpdate();
};

O Source File (.cpp)

Agora, implementamos a lógica. Configuramos o timer no BeginPlay, atualizamos nossas variáveis pendentes no Tick e deixamos o timer lidar com a transmissão de rede real.

#include "MyCustomPawn.h"
#include "TimerManager.h"

AMyCustomPawn::AMyCustomPawn()
{
    PrimaryActorTick.bCanEverTick = true;
    
    // Padrão de 20 atualizações por segundo
    NetworkSendRate = 20.0f; 
    bHasPendingNetworkUpdate = false;
}

void AMyCustomPawn::BeginPlay()
{
    Super::BeginPlay();

    // Apenas o client local que controla o pawn deve rodar o timer de flush de rede
    if (IsLocallyControlled())
    {
        float UpdateInterval = 1.0f / NetworkSendRate; // ex: 1.0 / 20.0 = 0.05 segundos

        GetWorld()->GetTimerManager().SetTimer(
            NetworkUpdateTimerHandle,
            this,
            &AMyCustomPawn::FlushNetworkUpdate,
            UpdateInterval,
            true // Loop contínuo
        );
    }
}

void AMyCustomPawn::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    // Execute sua lógica customizada de movimento no client aqui
    // ex: FVector NewLoc = ...; FRotator NewRot = ...;
    // SetActorLocationAndRotation(NewLoc, NewRot);

    if (IsLocallyControlled())
    {
        // Em vez de chamar o RPC aqui, apenas armazenamos o estado mais recente
        PendingLocation = GetActorLocation();
        PendingRotation = GetActorRotation();
        
        // Marca que temos dados novos esperando para serem enviados
        bHasPendingNetworkUpdate = true;
    }
}

void AMyCustomPawn::FlushNetworkUpdate()
{
    // Se não houver dados novos (ex: o jogador está parado), não gaste largura de banda
    if (!bHasPendingNetworkUpdate)
    {
        return;
    }

    // Envia o estado acumulado mais recente para o server
    Server_SendTransformUpdate(PendingLocation, PendingRotation);

    // Reseta a flag até que o próximo tick modifique o estado novamente
    bHasPendingNetworkUpdate = false;
}

bool AMyCustomPawn::Server_SendTransformUpdate_Validate(FVector NewLocation, FRotator NewRotation)
{
    // Adicione validação de anti-cheat aqui. A localização é plausível?
    return true; 
}

void AMyCustomPawn::Server_SendTransformUpdate_Implementation(FVector NewLocation, FRotator NewRotation)
{
    // O server recebe os dados com rate-limit e os aplica
    SetActorLocationAndRotation(NewLocation, NewRotation);
    
    // Nota: O server então replicaria isso para outros clients, 
    // tipicamente via propriedades Replicated padrão, NÃO via Multicasting.
}

Por Que Esta Arquitetura Funciona

Esta configuração resolve elegantemente o problema de inundação de rede. Não importa se o client está rodando a 30 FPS ou 300 FPS, o server tem a garantia de receber exatamente NetworkSendRate atualizações por segundo (assumindo que não haja packet loss).

Além disso, implementamos uma verificação antecipada (!bHasPendingNetworkUpdate). Se o jogador deixar o teclado para pegar um café, o client para de enviar RPCs inteiramente, liberando largura de banda crítica para os jogadores ativos. Esta é uma vitória massiva para manter uma performance consistente do server.

Passo 3: Lidando com Interpolação de Estado em Outros Clients

Quando você reduz a taxa de envio de rede, o movimento no server — e, consequentemente, nos outros clients conectados — se tornará escalonado. Se você enviar atualizações a 10Hz, o personagem visivelmente teletransportará 10 vezes por segundo em um monitor de 60 FPS.

Para corrigir isso, você não pode simplesmente dar um snap do personagem para a nova localização. Você deve usar interpolação. Quando o server replica o NewLocation para os simulated proxies (os outros clients observando o jogador), esses clients devem suavizar o movimento usando FMath::VInterpTo da sua posição atual para a posição alvo replicada ao longo do tempo.

Isso garante que, mesmo com um rate limit agressivo (como 5 ou 10 atualizações por segundo), a representação visual permaneça fluida. Se você está tendo problemas com personagens dando snaps incorretos durante a interpolação, vale conferir como corrigir desync de localização de jogador no UEFN e no multiplayer da Unreal Engine.

Passo 4: Struct Batching para RPCs Complexos

Se o seu jogo exige o envio de múltiplas variáveis diferentes, não envie vários RPCs separados. Todo RPC tem um overhead de cabeçalho base (tipicamente cerca de 1-2 bytes no mínimo, mas na prática mais ao considerar a serialização do payload).

Se você chamar Server_SendHealth(), Server_SendArmor() e Server_SendPosition() no mesmo flush de rede, você estará pagando o custo do cabeçalho três vezes.

Em vez disso, crie uma struct dedicada para seus payloads de rede.

USTRUCT()
struct FPlayerNetworkState
{
    GENERATED_BODY()

    UPROPERTY()
    FVector Location;

    UPROPERTY()
    FRotator Rotation;

    UPROPERTY()
    uint8 CurrentWeaponIndex;

    UPROPERTY()
    bool bIsCrouching;
};

Passe esta única struct através do seu RPC baseado em timer. O sistema de reflection da Unreal Engine empacotará essas variáveis de forma eficiente em um único payload de pacote, minimizando o footprint de bytes na sua conexão.

5 Melhores Práticas para Unreal Engine RPC Optimization

Para garantir que seu jogo escale de testes locais para milhares de jogadores simultâneos, adote estas regras fundamentais para arquitetura de rede:

  1. Nunca Envie RPCs no Tick Sem um Gate: Considere isso uma regra absoluta. Se um RPC estiver dentro do Tick(), ele deve ser protegido por uma verificação de tempo (ex: if (TimeSinceLastRPC > 0.1f)) ou gerenciado via um timer em loop.
  2. Priorize Unreliable sobre Reliable: Para dados que se atualizam continuamente (movimento, rotação da câmera, armas de feixe contínuo), sempre use RPCs Unreliable. Se um pacote cair, o próximo pacote chegando uma fração de segundo depois o substituirá de qualquer maneira. RPCs Reliable devem ser estritamente reservados para mudanças de estado absolutas (ex: arma disparada, item coletado, morte do jogador).
  3. Use Quantization para Floats e Vectors: Ao enviar dados de FVector, você raramente precisa de precisão total de ponto flutuante. A Unreal Engine permite que você faça a quantização de vetores em RPCs (ex: FVector_NetQuantize100), o que arredonda os valores para duas casas decimais e corta drasticamente a largura de banda necessária para enviá-los.
  4. Prefira Replicação Padrão para Dados Downstream: Enquanto os clients devem usar RPCs para enviar dados para o server, o server raramente deve usar Multicast RPCs para enviar dados contínuos de volta. O server deve atualizar uma variável UPROPERTY(Replicated), permitindo que o replication manager nativo da Unreal cuide da otimização de banda, priorização e relevância automaticamente.
  5. Profile Cedo e Frequentemente: Use o comando net.DumpRelevantActors e a ferramenta Network Profiler (NetworkProfiler.exe localizado nos binários da Engine) para visualizar exatamente quantos bytes seus RPCs estão consumindo por frame. Nunca adivinhe seus ganhos de otimização; meça-os empiricamente.

Lidando com Infraestrutura e Escalonamento de Backend

Dominar as complexidades do netcode da Unreal Engine é uma tarefa enorme. Você gasta horas ajustando timer handles, quantizando vetores e mitigando desyncs apenas para manter seus servidores dedicados rodando sem estourar os limites de banda.

Depois que seu código in-game está finalmente otimizado, você ainda precisa fazer o deploy e escalar esses servidores globalmente. Construir isso sozinho requer configurar fleet managers, load balancers, database sharding e gerenciamento de certificados SSL — facilmente 4 a 6 semanas de trabalho intenso de infraestrutura que te afasta do design real do jogo.

Com o horizOn, esses serviços de backend já vêm pré-configurados especificamente para desenvolvedores de jogos. Você tem hospedagem de servidores dedicados escaláveis, sincronização de banco de dados em tempo real e analytics robustos prontos para uso, permitindo que você lance seu jogo em vez da sua infraestrutura.

Considerações Finais

A chave para a unreal engine rpc optimization é perceber que a largura de banda da rede é um recurso finito e altamente volátil. Você não pode tratar a camada de rede como um frame buffer padrão. Ao se afastar da execução impulsionada pelo Tick e adotar o Accumulator Pattern, você ganha controle total sobre a saída de dados do seu jogo. Você reduz a carga do server, mitiga o packet loss e cria uma experiência drasticamente mais suave para jogadores em conexões de internet instáveis.

Lembre-se de que otimizar seu jogo é um processo contínuo. Pare de confiar nos comportamentos padrão da engine para salvá-lo de inundações de rede. Assuma o controle explícito do seu fluxo de dados. Implemente esses rate limits no seu protótipo atual, monitore as métricas de antes e depois usando o Network Profiler e veja a performance do seu server disparar.

Pronto para escalar seu backend multiplayer recém-otimizado? Teste o horizOn gratuitamente ou confira a documentação da API para ver como uma infraestrutura profissional de jogos pode ser simples.


Fonte: Network: How not to send all PRC every tick?