Назад к блогу

Оптимизация RPC в Unreal Engine: как перестать забивать сеть каждый тик

Опубликовано 4 мая 2026 г.
Оптимизация RPC в Unreal Engine: как перестать забивать сеть каждый тик

Коротко о главном

Данное руководство описывает методы оптимизации сетевого трафика в Unreal Engine путем внедрения Accumulator Pattern для ограничения частоты вызовов RPC. Мы рассматриваем практическую реализацию на C++, позволяющую снизить нагрузку на Bandwidth более чем на 85% без потери плавности геймплея. Статья также включает советы по использованию Unreliable RPC, квантованию данных и применению специализированной инфраструктуры horizOn для масштабирования игровых серверов.

Каждый разработчик Multiplayer-игр рано или поздно сталкивается с одним и тем же сетевым «бутылочным горлышком»: клиент, работающий на частоте 144 FPS, решает отправлять свое состояние перемещения на сервер каждый божий тик. В считанные секунды сетевая очередь сервера полностью забивается избыточными Remote Procedure Calls (RPC), что вызывает экстремальные лаги, Packet Loss и неизбежный дисконнект. По сути, ваш клиент проводит DDoS-атаку на вашу собственную серверную инфраструктуру.

Этот сценарий представляет собой одну из самых распространенных ловушек в архитектуре Multiplayer-игр. Когда разработчикам нужно передать пользовательский ввод, сложные состояния физики транспорта или механику скорострельной стрельбы, размещение RPC внутри функции Tick() кажется логичным выбором для обеспечения плавности. Однако сетевой слой Unreal Engine не отсеивает промежуточные RPC автоматически. Если ваша игра пушит RPC каждый тик, все они ставятся в очередь и передаются.

Для обновлений перемещения и позиции вам почти никогда не нужны 143 промежуточных кадра; вам нужно только самое актуальное состояние для репликации остальным клиентам. В этом подробном руководстве мы глубоко погрузимся в unreal engine rpc optimization, показав вам, как именно ограничивать эти тиковые сетевые вызовы, внедрять умную аккумуляцию состояний и радикально снижать нагрузку на Multiplayer Bandwidth.

Опасность сетевых событий, привязанных к Tick

Прежде чем внедрять решение, критически важно понять анатомию проблемы. Когда вы объявляете RPC в Unreal Engine, будь то Server, Client или NetMulticast, вы даете команду сетевому драйверу движка сериализовать параметры функции и поместить их в очередь исходящих пакетов.

Проблема с очередями

Unreal Engine объединяет исходящие RPC в пакеты на основе NetUpdateFrequency соединения и лимитов Bandwidth. Если клиент вызывает Server RPC каждый тик при высокой частоте кадров, движок попытается обработать каждый из этих вызовов.

Если RPC помечен как Reliable, ситуация становится катастрофической. Reliable RPC гарантируют доставку и порядок выполнения. Сетевой канал быстро заполнится, и если буфер переполнится, соединение будет принудительно закрыто движком, что приведет к исключению игрока.

Если RPC помечен как Unreliable, движок будет отбрасывать пакеты при переполнении очереди. Хотя это предотвращает жесткий дисконнект, это ведет к массивному эффекту Rubber-banding. Сервер может получить кадр 1, кадр 2, пропустить кадры 3–100, а затем обработать кадр 101. Результатом будет прерывистое, дерганое движение, которое портит игровой опыт. Это распространенная первопричина, когда команды пытаются исправить проблему репликации RPC в Unreal Engine, ломающую ваши состояния.

Математика Bandwidth

Давайте взглянем на конкретные цифры. Представьте, что вы отправляете простой вектор (12 байт) и ротатор (12 байт) через Server RPC. С учетом оверхеда заголовка RPC, оценим вызов в 32 байта.

  • При 30 FPS: 30 * 32 байта = 960 байт/секунду (примерно 1 КБ/с на клиента).
  • При 144 FPS: 144 * 32 байта = 4 608 байт/секунду (примерно 4.6 КБ/с на клиента).
  • При 240 FPS: 240 * 32 байта = 7 680 байт/секунду.

Умножьте это на 64 игрока в Battle Royale, и ваш сервер внезапно начнет обрабатывать почти полмегабайта чистого RPC-оверхеда каждую секунду — только для базового отслеживания перемещений. Это не масштабируется.

Шаг 1: Разрыв зависимости от Tick с помощью Accumulator Pattern

Самая эффективная стратегия для unreal engine rpc optimization — это отделение частоты отправки данных по сети от частоты рендеринга клиента. Вместо того чтобы пушить RPC в Tick(), вам следует обновлять локальную переменную каждый тик, а затем использовать таймер для сброса этих данных на сервер с фиксированным, предсказуемым интервалом (например, 10 или 20 раз в секунду).

Мы называем это Accumulator Pattern. Клиент непрерывно накапливает последнее состояние, но передает его только тогда, когда открываются «сетевые ворота».

Определение целевой частоты

Вам не нужно 144 обновления в секунду для плавного Multiplayer-опыта. Большинство современных соревновательных шутеров имеют Tickrate сервера 30Гц или 60Гц. Следовательно, отправки обновлений клиента 15–30 раз в секунду обычно более чем достаточно, при условии, что вы используете правильные Client-side prediction и Server-side interpolation.

Снизив частоту отправки с неограниченных 144 Гц до фиксированных 20 Гц, вы мгновенно сокращаете сетевой трафик более чем на 85% для этого конкретного действия.

Шаг 2: Реализация Rate-Limiter на C++

Давайте посмотрим, как эффективно реализовать это на C++. Мы создадим систему, в которой клиент отслеживает желаемое целевое местоположение и ротацию каждый тик, но отправляет RPC Server_UpdateTransform только на основе предопределенной частоты сетевой отправки.

Заголовочный файл (.h)

Сначала мы определим наши переменные и функции в кастомном классе APawn или ACharacter. Нам понадобятся Timer handle, частота обновления и переменные для хранения неотправленных данных.

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;

    // The RPC to send data to the server. Marked as Unreliable for rapid, continuous updates.
    UFUNCTION(Server, Unreliable, WithValidation)
    void Server_SendTransformUpdate(FVector NewLocation, FRotator NewRotation);

private:
    // Timer handle for our network flush
    FTimerHandle NetworkUpdateTimerHandle;

    // How many times per second we want to send updates to the server
    UPROPERTY(EditDefaultsOnly, Category = "Network")
    float NetworkSendRate;

    // Flag to track if we have new data that hasn't been sent yet
    bool bHasPendingNetworkUpdate;

    // The accumulated data waiting to be sent
    FVector PendingLocation;
    FRotator PendingRotation;

    // The function called by the timer to flush data
    void FlushNetworkUpdate();
};

Файл исходного кода (.cpp)

Теперь реализуем логику. Мы настраиваем таймер в BeginPlay, обновляем наши промежуточные переменные в Tick и позволяем таймеру управлять фактической сетевой передачей.

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

AMyCustomPawn::AMyCustomPawn()
{
    PrimaryActorTick.bCanEverTick = true;
    
    // Default to sending 20 updates per second
    NetworkSendRate = 20.0f; 
    bHasPendingNetworkUpdate = false;
}

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

    // Only the local controlling client should run the network flush timer
    if (IsLocallyControlled())
    {
        float UpdateInterval = 1.0f / NetworkSendRate; // e.g., 1.0 / 20.0 = 0.05 seconds

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

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

    // Run your custom client-side movement logic here
    // e.g., FVector NewLoc = ...; FRotator NewRot = ...;
    // SetActorLocationAndRotation(NewLoc, NewRot);

    if (IsLocallyControlled())
    {
        // Instead of calling the RPC here, we just store the latest state
        PendingLocation = GetActorLocation();
        PendingRotation = GetActorRotation();
        
        // Mark that we have fresh data waiting to be sent
        bHasPendingNetworkUpdate = true;
    }
}

void AMyCustomPawn::FlushNetworkUpdate()
{
    // If there is no new data (e.g., the player is standing still), don't waste bandwidth
    if (!bHasPendingNetworkUpdate)
    {
        return;
    }

    // Send the latest accumulated state to the server
    Server_SendTransformUpdate(PendingLocation, PendingRotation);

    // Reset the flag until the next tick modifies the state again
    bHasPendingNetworkUpdate = false;
}

bool AMyCustomPawn::Server_SendTransformUpdate_Validate(FVector NewLocation, FRotator NewRotation)
{
    // Add anti-cheat validation here. Is the location reasonable?
    return true; 
}

void AMyCustomPawn::Server_SendTransformUpdate_Implementation(FVector NewLocation, FRotator NewRotation)
{
    // The server receives the rate-limited data and applies it
    SetActorLocationAndRotation(NewLocation, NewRotation);
    
    // Note: The server would then replicate this to other clients, 
    // typically via standard Replicated properties, NOT by Multicasting.
}

Почему эта архитектура работает

Такая настройка элегантно решает проблему сетевого флуда. Независимо от того, работает ли клиент на 30 FPS или 300 FPS, сервер гарантированно будет получать ровно NetworkSendRate обновлений в секунду (при отсутствии Packet Loss).

Более того, мы внедрили раннюю проверку (!bHasPendingNetworkUpdate). Если игрок отошел от клавиатуры за кофе, клиент полностью прекращает отправку RPC, освобождая критически важный Bandwidth для активных игроков. Это огромная победа для поддержания стабильной производительности сервера.

Шаг 3: Обработка интерполяции состояний на других клиентах

Когда вы снижаете частоту сетевой отправки, движение на сервере — и, следовательно, на других подключенных клиентах — станет прерывистым. Если вы отправляете обновления с частотой 10 Гц, персонаж будет заметно телепортироваться 10 раз в секунду на мониторе с 60 FPS.

Чтобы исправить это, нельзя просто мгновенно перемещать персонажа в новую локацию. Вы должны использовать интерполяцию. Когда сервер реплицирует NewLocation вниз на Simulated proxies (другие клиенты, наблюдающие за игроком), эти клиенты должны плавно выполнять FMath::VInterpTo от своей текущей позиции к реплицированной целевой позиции с течением времени.

Это гарантирует, что даже при очень агрессивном ограничении частоты (например, 5 или 10 обновлений в секунду), визуальное представление останется идеально плавным. Если вы боретесь с некорректным поведением персонажей во время интерполяции, стоит изучить, как исправить рассинхронизацию местоположения игрока в UEFN и Unreal Engine Multiplayer.

Шаг 4: Батчинг структур для сложных RPC

Если ваша игра требует отправки нескольких различных переменных, не отправляйте несколько отдельных RPC. Каждый RPC имеет базовый оверхед заголовка (обычно минимум 1–2 байта, но на практике больше при учете сериализации полезной нагрузки).

Если вы вызываете Server_SendHealth(), Server_SendArmor() и Server_SendPosition() в одном сетевом сбросе, вы платите за заголовок трижды.

Вместо этого создайте выделенную структуру для ваших сетевых данных.

USTRUCT()
struct FPlayerNetworkState
{
    GENERATED_BODY()

    UPROPERTY()
    FVector Location;

    UPROPERTY()
    FRotator Rotation;

    UPROPERTY()
    uint8 CurrentWeaponIndex;

    UPROPERTY()
    bool bIsCrouching;
};

Передавайте эту единую структуру через ваш RPC на основе таймера. Система рефлексии Unreal Engine эффективно упакует эти переменные в единый пакет данных, минимизируя байтовый след вашего соединения.

5 лучших практик для Unreal Engine RPC Optimization

Чтобы ваша игра масштабировалась от локальных тестов до тысяч одновременных игроков, примите эти основополагающие правила сетевой архитектуры:

  1. Никогда не отправляйте RPC в Tick без фильтра: Считайте это жестким правилом. Если RPC находится внутри Tick(), он должен быть защищен проверкой времени (например, if (TimeSinceLastRPC > 0.1f)) или управляться через зацикленный таймер.
  2. Отдавайте приоритет Unreliable перед Reliable: Для данных, которые обновляются непрерывно (движение, поворот камеры, лучевое оружие), всегда используйте Unreliable RPC. Если пакет пропадет, следующий пакет, прибывший через долю секунды, все равно перезапишет его. Reliable RPC должны быть строго зарезервированы для абсолютных изменений состояния (например, выстрел, поднятие предмета, смерть игрока).
  3. Используйте Quantization для Float и Vector: При отправке данных FVector вам редко нужна полная точность с плавающей запятой. Unreal Engine позволяет использовать квантование векторов в RPC (например, FVector_NetQuantize100), что округляет значения до двух знаков после запятой и сокращает Bandwidth, необходимый для их отправки.
  4. Предпочитайте стандартную репликацию для нисходящих данных: В то время как клиенты должны использовать RPC для отправки данных на сервер, сервер редко должен использовать Multicast RPC для отправки непрерывных данных вниз. Сервер должен обновлять переменную UPROPERTY(Replicated), позволяя встроенному менеджеру репликации Unreal автоматически обрабатывать оптимизацию Bandwidth, приоритизацию и сортировку релевантности.
  5. Профилируйте рано и часто: Используйте команду net.DumpRelevantActors и инструмент Network Profiler (NetworkProfiler.exe в бинарниках движка), чтобы визуализировать, сколько именно байт потребляют ваши RPC за кадр. Никогда не гадайте о результатах оптимизации; измеряйте их эмпирически.

Управление инфраструктурой и масштабирование Backend

Освоение тонкостей Netcode в Unreal Engine — это огромная задача. Вы тратите часы на настройку таймеров, квантование векторов и устранение десинхронизаций только для того, чтобы ваши Dedicated Servers работали плавно, не превышая лимиты Bandwidth.

Как только ваш игровой код будет оптимизирован, вам все равно придется развертывать и масштабировать эти серверы по всему миру. Создание этого самостоятельно требует настройки Fleet managers, Load balancers, шардирования баз данных и управления SSL-сертификатами — это легко может занять 4–6 недель интенсивной работы над инфраструктурой, которая отвлекает вас от самого геймдизайна.

С horizOn эти Backend-сервисы поставляются уже настроенными специально для разработчиков игр. Вы получаете масштабируемый хостинг выделенных серверов, синхронизацию базы данных в реальном времени и надежную аналитику прямо из коробки, что позволяет вам выпускать игру, а не заниматься инфраструктурой.

Заключительные мысли

Ключ к unreal engine rpc optimization заключается в осознании того, что сетевой Bandwidth — это конечный и крайне волатильный ресурс. Вы не можете относиться к сетевому слою как к обычному кадровому буферу. Переходя от выполнения, управляемого Tick, к использованию Accumulator Pattern, вы получаете полный контроль над выводом данных вашей игры. Вы снижаете нагрузку на сервер, уменьшаете Packet Loss и создаете значительно более плавный опыт для игроков с нестабильным интернет-соединением.

Помните, что оптимизация вашей игры — это непрерывный процесс. Перестаньте полагаться на стандартное поведение движка, чтобы спастись от сетевых наводнений. Возьмите под явный контроль потоки ваших данных. Внедрите эти ограничения частоты в ваш текущий прототип, отследите метрики «до» и «после» с помощью Network Profiler и наблюдайте, как производительность вашего сервера взлетает до небес.

Готовы масштабировать свой оптимизированный Multiplayer-бэкенд? Попробуйте horizOn бесплатно или загляните в документацию API, чтобы увидеть, насколько простой может быть профессиональная игровая инфраструктура.


Источник: Network: How not to send all PRC every tick?