العودة إلى المدونة

تحسين Unreal Engine RPC: كيف تتوقف عن إغراق شبكتك في كل Tick

نُشر في 4 مايو 2026
تحسين Unreal Engine RPC: كيف تتوقف عن إغراق شبكتك في كل Tick

باختصار

يتناول هذا الدليل التقني كيفية حل مشكلة إغراق الشبكة (Network Flooding) في ألعاب Multiplayer المطورة بمحرك Unreal Engine عبر تحسين استدعاءات RPC. يشرح المقال كيفية تطبيق "Accumulator Pattern" لفصل معدل إرسال البيانات عن معدل الإطارات (FPS)، مما يقلل من استهلاك Bandwidth بنسبة تصل إلى 85%. كما يستعرض أفضل الممارسات مثل استخدام Struct Batching و Quantization لضمان أداء سلس للـ Server وتجربة لعب خالية من Lag.

يواجه كل مطور Multiplayer game في النهاية نفس اختناق الشبكة: Client يعمل بـ 144 إطارًا في الثانية (FPS) يقرر إرسال حالة الحركة المخصصة إلى Server في كل Tick. في غضون ثوانٍ، يمتلئ Network queue الخاص بـ Server بالكامل بـ Remote Procedure Calls (RPCs) زائدة عن الحاجة، مما يتسبب في Lag شديد، و Packet loss، وانفصال حتمي. في الأساس، يقوم الـ Client الخاص بك بشن هجوم Distributed Denial of Service (DDoS) على البنية التحتية للـ Server الخاص بك.

يمثل هذا السيناريو أحد أكثر الأفخاخ شيوعاً في Multiplayer game architecture. عندما يحتاج المطورون إلى إرسال مدخلات اللاعبين المخصصة، أو حالات فيزياء المركبات المعقدة، أو ميكانيكا الإطلاق السريع، يبدو وضع RPC داخل دالة Tick() هو الخيار المنطقي لاستجابة سلسة. ومع ذلك، فإن Networking layer في Unreal Engine لا تقوم بعملية Cull تلقائياً لـ RPCs المتوسطة. إذا كانت لعبتك ترسل RPC في كل Tick، فسيتم وضعها جميعاً في الـ Queue وإرسالها.

بالنسبة لتحديثات الحركة والموقع، لا تهتم أبداً بـ 143 إطاراً متوسطاً؛ فأنت تحتاج فقط إلى أحدث حالة (State) لعمل Replication لبقية الـ Clients. في هذا الدليل الشامل، سنقوم بالتعمق في unreal engine rpc optimization، لنوضح لك بالضبط كيفية عمل Throttle لنداءات الشبكة القائمة على الـ Tick، وتطبيق Smart state accumulation، وتقليل عبء Multiplayer bandwidth بشكل كبير.

خطر أحداث الشبكة المرتبطة بـ Tick

قبل تنفيذ الحل، من الضروري فهم طبيعة المشكلة. عندما تعلن عن RPC في Unreal Engine، سواء كان Server أو Client أو NetMulticast فأنت تصدر تعليمات لـ Network driver الخاص بالمحرك لعمل Serialize لبارامترات الدالة ودفعها إلى Outgoing packet queue.

مشكلة الـ Queueing

يقوم Unreal Engine بتجميع RPCs الصادرة في Packets بناءً على NetUpdateFrequency وحدود الـ Bandwidth للاتصال. إذا كان الـ Client يستدعي Server RPC في كل Tick بمعدل إطارات مرتفع، فسيحاول المحرك معالجة كل استدعاء من تلك الاستدعاءات.

إذا تم وضع علامة Reliable على الـ RPC، فإن الوضع يكون كارثياً. تضمن الـ Reliable RPCs التسليم وترتيب التنفيذ. ستمتلئ Network channel بسرعة، وإذا فاض الـ Buffer، فسيتم إغلاق الاتصال قسراً بواسطة المحرك، مما يؤدي إلى فصل اللاعب.

أما إذا كان الـ RPC بنوع Unreliable، فسيقوم المحرك بإسقاط الـ Packets عندما يمتلئ الـ Queue. بينما يمنع هذا الفصل المفاجئ، إلا أنه يؤدي إلى Rubber-banding هائل. قد يستقبل الـ Server الإطار 1، والإطار 2، ثم يسقط الإطارات 3-100، ثم يعالج الإطار 101. النتيجة هي حركة متذبذبة وغير منتظمة تفسد تجربة اللعب. هذا سبب جذري شائع عندما تحاول الفرق إصلاح مشكلة Unreal Engine RPC replication التي تكسر حالاتك.

حسابات الـ Bandwidth

دعونا ننظر إلى بعض الأرقام الملموسة. تخيل أنك ترسل Vector بسيطاً (12 bytes) و Rotator (12 bytes) عبر Server RPC. مع RPC header overhead، لنفترض وجود 32 bytes لكل استدعاء.

  • عند 30 FPS: 30 * 32 bytes = 960 bytes/second (تقريبًا 1 KB/s لكل Client).
  • عند 144 FPS: 144 * 32 bytes = 4,608 bytes/second (تقريبًا 4.6 KB/s لكل Client).
  • عند 240 FPS: 240 * 32 bytes = 7,680 bytes/second.

اضبط هذا في 64 لاعباً في Battle royale، وفجأة يعالج الـ Server الخاص بك ما يقرب من نصف ميجابايت من RPC overhead الصافي كل ثانية - فقط لتتبع الحركة الأساسية. هذا لا يقبل التوسع (Scale).

الخطوة 1: كسر الاعتماد على Tick بنمط Accumulator Pattern

الاستراتيجية الأكثر فعالية لـ unreal engine rpc optimization هي فصل معدل إرسال الشبكة عن معدل إطارات الرندر لدى الـ Client. بدلاً من دفع الـ RPC في Tick()، يجب عليك تحديث متغير محلي في كل Tick، ثم استخدام Timer لإرسال (Flush) تلك البيانات إلى الـ Server على فترات زمنية ثابتة ومتوقعة (مثلاً 10 أو 20 مرة في الثانية).

نحن نسمي هذا Accumulator Pattern. يقوم الـ Client بتجميع أحدث حالة (State) باستمرار ولكنه لا يرسلها إلا عندما تفتح بوابة الشبكة.

تحديد التردد المستهدف

لا تحتاج إلى 144 تحديثاً في الثانية لتجربة Multiplayer سلسة. معظم الـ Competitive shooters الحديثة تعمل الـ Servers الخاصة بها بتردد 30Hz أو 60Hz. لذلك، فإن إرسال تحديثات الـ Client من 15 إلى 30 مرة في الثانية عادة ما يكون أكثر من كافٍ، بشرط استخدام Client-side prediction و Server-side interpolation بشكل صحيح.

من خلال تقليل معدل الإرسال من 144Hz غير مقيد إلى 20Hz محدود، فإنك تقلل فوراً من حركة مرور الشبكة بنسبة تزيد عن 85% لهذا الإجراء المحدد.

الخطوة 2: تنفيذ Rate-Limiter في C++

لنلقِ نظرة على كيفية تنفيذ ذلك بفعالية في C++. سنقوم بإنشاء نظام حيث يتتبع الـ Client موقعه ودورانه المستهدف في كل Tick، ولكنه يرسل فقط RPC الخاص بـ Server_SendTransformUpdate بناءً على معدل إرسال شبكة محدد مسبقاً.

ملف الـ Header (.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();
};

ملف الـ Source (.cpp)

الآن، نقوم بتنفيذ المنطق. نقوم بإعداد الـ Timer في BeginPlay ونحدث متغيراتنا المعلقة في Tick ونجعل الـ Timer يتعامل مع إرسال الشبكة الفعلي.

#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.
}

لماذا تنجح هذه الـ Architecture

هذا الإعداد يحل مشكلة فيضان الشبكة بأناقة. بغض النظر عما إذا كان الـ Client يعمل بـ 30 FPS أو 300 FPS، يضمن الـ Server استقبال عدد NetworkSendRate من التحديثات في الثانية بالضبط (بافتراض عدم وجود Packet loss).

علاوة على ذلك، قمنا بتنفيذ فحص مبكر (!bHasPendingNetworkUpdate). إذا ترك اللاعب لوحة المفاتيح ليحضر قهوة، يتوقف الـ Client عن إرسال RPCs تماماً، مما يوفر Bandwidth حرج للاعبين النشطين. هذا فوز هائل للحفاظ على أداء Server ثابت.

الخطوة 3: التعامل مع State Interpolation على الـ Clients الآخرين

عندما تقلل معدل إرسال الشبكة، ستصبح الحركة على الـ Server - وبالتالي على الـ Clients المتصلين الآخرين - متقطعة. إذا أرسلت تحديثات بتردد 10Hz، فستظهر الشخصية وهي تقفز (Teleport) 10 مرات في الثانية على شاشة 60 FPS.

لإصلاح ذلك، لا يمكنك ببساطة نقل الشخصية إلى الموقع الجديد. يجب عليك استخدام Interpolation. عندما يقوم الـ Server بعمل Replicate لـ NewLocation إلى Simulated proxies (الـ Clients الآخرين الذين يراقبون اللاعب)، يجب على هؤلاء الـ Clients استخدام FMath::VInterpTo بسلاسة من موقعهم الحالي إلى الموقع المستهدف مع مرور الوقت.

يضمن ذلك أنه حتى مع وجود معدل تقييد قوي جداً (مثل 5 أو 10 تحديثات في الثانية)، يظل التمثيل المرئي سلساً للغاية. إذا كنت تعاني من مشاكل Snap غير صحيحة أثناء الـ Interpolation، فقد ترغب في مراجعة كيفية إصلاح player location desync في UEFN و Unreal Engine multiplayer.

الخطوة 4: استخدام Struct Batching لـ RPCs المعقدة

إذا كانت لعبتك تتطلب إرسال متغيرات متعددة ومختلفة، فلا ترسل RPCs منفصلة. كل RPC له Baseline header overhead (عادة حوالي 1-2 bytes كحد أدنى، ولكن عملياً أكثر عند النظر في Payload serialization).

إذا استدعيت Server_SendHealth() و Server_SendArmor() و Server_SendPosition() في نفس الـ Network flush، فأنت تدفع تكلفة الـ Header ثلاث مرات.

بدلاً من ذلك، قم بإنشاء Struct مخصص لـ Network payloads.

USTRUCT()
struct FPlayerNetworkState
{
    GENERATED_BODY()

    UPROPERTY()
    FVector Location;

    UPROPERTY()
    FRotator Rotation;

    UPROPERTY()
    uint8 CurrentWeaponIndex;

    UPROPERTY()
    bool bIsCrouching;
};

قم بتمرير هذا الـ Struct الفردي عبر RPC القائم على الـ Timer. ستقوم Reflection system في Unreal Engine بتعبئة هذه المتغيرات بكفاءة في Packet payload واحد، مما يقلل من حجم البايتات على اتصالك.

5 أفضل ممارسات لـ Unreal Engine RPC Optimization

لضمان توسع لعبتك من الاختبار المحلي إلى آلاف اللاعبين المتزامنين، اعتمد هذه القواعد الأساسية لـ Network architecture:

  1. لا ترسل RPCs في Tick أبداً بدون Gate: اعتبر هذه قاعدة صارمة. إذا كان الـ RPC داخل Tick()، فيجب أن يكون محمياً بفحص زمني (مثلاً if (TimeSinceLastRPC > 0.1f)) أو إدارته عبر Timer متكرر.
  2. أعطِ الأولوية لـ Unreliable على Reliable: للبيانات التي يتم تحديثها باستمرار (الحركة، النظر حولك، أسلحة الشعاع المستمرة)، استخدم دائماً Unreliable RPCs. إذا سقطت Packet، فإن الـ Packet التالية التي تصل بعد جزء من الثانية ستغطيها على أي حال. يجب حجز Reliable RPCs بدقة لتغييرات الحالة المطلقة (مثل إطلاق سلاح، التقاط عنصر، موت لاعب).
  3. استخدم Quantization للـ Floats والـ Vectors: عند إرسال بيانات FVector فأنت نادراً ما تحتاج إلى دقة Floating-point كاملة. يتيح لك Unreal Engine عمل Quantize للـ Vectors في RPCs (مثل FVector_NetQuantize100) والذي يقرب القيم إلى منزلتين عشريتين ويقلل الـ Bandwidth المطلوب لإرسالها.
  4. فضل الـ Standard Replication للبيانات النازلة (Downstream): بينما يجب على الـ Clients استخدام RPCs لإرسال البيانات إلى الـ Server، نادراً ما يجب على الـ Server استخدام Multicast RPCs لإرسال بيانات مستمرة إلى الأسفل. يجب على الـ Server تحديث متغير UPROPERTY(Replicated) مما يسمح لمدير الـ Replication المدمج في Unreal بالتعامل مع تحسين الـ Bandwidth وتحديد الأولويات وفرز الـ Relevancy تلقائياً.
  5. قم بعمل Profile مبكراً وبشكل متكرر: استخدم أمر net.DumpRelevantActors وأداة Network Profiler (NetworkProfiler.exe الموجودة في Engine binaries) لتصور عدد البايتات التي تستهلكها RPCs الخاصة بك في كل إطار. لا تخمن أبداً مكاسب التحسين؛ قم بقياسها تجريبياً.

التعامل مع البنية التحتية وتوسيع الـ Backend

إتقان تعقيدات Netcode في Unreal Engine هو مهمة ضخمة. أنت تقضي ساعات في ضبط Timer handles، وعمل Quantizing للـ Vectors، وتخفيف الـ Desyncs فقط للحفاظ على تشغيل الـ Dedicated servers بسلاسة دون تجاوز حدود الـ Bandwidth الخاصة بها.

بمجرد تحسين الكود داخل اللعبة، لا يزال يتعين عليك نشر وتوسيع تلك الـ Servers عالمياً. بناء هذا بنفسك يتطلب إعداد Fleet managers و Load balancers و Database sharding وإدارة SSL cert - وهي مهمة تتطلب 4-6 أسابيع من العمل المكثف على البنية التحتية والتي تبعدك عن تصميم اللعبة الفعلي.

مع horizOn، تأتي خدمات الـ Backend هذه مهيأة مسبقاً خصيصاً لمطوري الألعاب. تحصل على استضافة Dedicated server قابلة للتوسع، ومزامنة Database في الوقت الفعلي، وتحليلات قوية مباشرة، مما يتيح لك شحن لعبتك بدلاً من بنيتك التحتية.

أفكار نهائية

المفتاح لـ unreal engine rpc optimization هو إدراك أن Network bandwidth مورد محدود ومتطاير للغاية. لا يمكنك معاملة Networking layer مثل Frame buffer قياسي. من خلال الابتعاد عن التنفيذ المعتمد على الـ Tick وتبني Accumulator Pattern، ستحصل على تحكم كامل في مخرجات بيانات لعبتك. ستقلل من حمل الـ Server، وتخفف من Packet loss، وتخلق تجربة أكثر سلاسة للاعبين الذين لديهم اتصالات إنترنت متذبذبة.

تذكر أن تحسين لعبتك عملية مستمرة. توقف عن الاعتماد على سلوكيات المحرك الافتراضية لإنقاذك من فيضان الشبكة. تحكم بشكل صريح في تدفق البيانات. قم بتنفيذ حدود المعدل هذه في نموذجك الأولي الحالي، وراقب مقاييس ما قبل وما بعد باستخدام Network Profiler، وشاهد أداء الـ Server الخاص بك وهو يرتفع.

هل أنت مستعد لتوسيع الـ Multiplayer backend المحسن حديثاً؟ جرب horizOn مجاناً أو راجع API docs لترى مدى بساطة البنية التحتية الاحترافية للألعاب.


المصدر: Network: How not to send all PRC every tick?