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

إصلاح Bug اختفاء كائنات Scene Graph في UEFN: دليل بلغة Verse لإدارة Persistent World States

نُشر في 13 يونيو 2026
إصلاح Bug اختفاء كائنات Scene Graph في UEFN: دليل بلغة Verse لإدارة Persistent World States

باختصار

يشرح هذا المقال بالتفصيل bug اختفاء كائنات Scene Graph في UEFN بعد عمل teleport للاعبين نتيجة لعدم تطابق عمليات الـ replication وإدارة ذاكرة الـ client. ويقدم المقال حلاً بديلاً باستخدام Verse يعتمد على فرض تحديث لحالة الكائنات القريبة لإجبار الـ server على بثها مجددًا. كما يسلط الضوء على حدود الذاكرة الخاصة بسيرفرات Fortnite، ويوصي باستخدام حلول الـ backend السحابية مثل [horizOn](https://horizon.pm) لإدارة البيانات المكانية واستمراريتها بكفاءة.

إذا قام اللاعبون بعمل Teleport عبر خريطة UEFN، فمن المحتمل أن يعودوا ليجدوا قطع الأثاث التي وضعوها بعناية غير مرئية وغير تفاعلية، ومع ذلك فهي تحجب حركتهم فيزيائيًا مثل الجدران الشبحية. هذا الـ Replication Bug المحبط يعاني منه المطورون الذين يبنون ألعاب sandbox أو tycoon أو الألعاب المعتمدة على البناء في UEFN منذ تحديث v41.00. تقوم ببناء نظام يسمح للاعبين بتخصيص ووضع الأثاث، أو كبائن الآركيد، أو الـ decorative meshes باستخدام كائنات Scene Graph، وكل شيء يعمل بسلاسة تامة في ظروف اللعب المحلي (local play). ومع ذلك، في اللحظة التي يقوم فيها اللاعب بعمل Teleport إلى لعبة مصغرة (minigame)، أو lobby، أو قسم منفصل من الجزيرة، ثم يعود، تختفي الـ meshes.

ما يجعل هذه المشكلة محيرة للغاية هو سلوك بيانات الـ collision. يمكن للاعب السير إلى مكان وجود المجسم، والاصطدام بجدار غير مرئي، والوقوف فوق ما يجب أن يكون كرسيًا أو خزانة صلبة. لكن لا يمكنه رؤيتها، كما أن مكونات التفاعل المدفوعة بلغة Verse ترفض الاستجابة. لقد اختفى النموذج البصري (visual model)، ومع ذلك يظل التمثيل الفيزيائي مدمجًا (baked) في شبكة World Partition.

لفهم سبب حدوث ذلك، علينا إلقاء نظرة خلف الكواليس على كيفية إدارة UEFN للذاكرة والـ replication. تستخدم كائنات (actors) Unreal Engine القياسية نظام الـ network replication graph القديم لتحديد ما يجب عمل render له على شاشة اللاعب بناءً على مسافة الكاميرا ومدى الأهمية (relevancy). مع إدخال نظام Scene Graph الجديد في UEFN، حاولت Epic Games تبسيط تصميم الألعاب القائم على المكونات (component-based game design). ومع ذلك، أدى ذلك إلى حدوث عدم تطابق (mismatch) بين كيفية قيام الـ server بعمل cache لهياكل الكيانات الديناميكية (dynamic entity structures) وكيفية قيام الـ client بعمل stream للأصول المرئية (visual assets) داخل وخارج ذاكرة الـ GPU.

خلف الكواليس: آلية عمل UEFN Scene Graph Culling

عندما يكون اللاعب ضمن مسافة الـ render القياسية من الكيان—عادةً ما بين 15,000 إلى 20,000 Unreal Units (أي 150-200 متر)—يقوم الـ replication graph الخاص بالـ server ببث تحديثات الإحداثيات والـ mesh وحالة المكونات (component state) بنشاط إلى الـ client. يعالج الـ client حزم الشبكة (network packets) هذه ويقوم بعمل render لمكونات الـ visual mesh وفقًا لذلك.

في اللحظة التي يقوم فيها اللاعب بعمل teleport بعيدًا، تحدث عدة أشياء في تتابع سريع:

  • Relevancy Cutoff: تنتقل شاشة رؤية الـ client (viewport) لآلاف الوحدات بعيدًا على الفور. ويقوم الـ replication graph بوضع علامة "خارج نطاق الأهمية" (out of relevancy) على كائنات Scene Graph الأصلية.
  • GPU Garbage Collection: للحفاظ على معدل إطارات (framerates) مرتفع على الأجهزة الأقل كفاءة مثل الهواتف المحمولة والمنصات (consoles)، يقوم الـ client على الفور بإلغاء تحميل التمثيلات المرئية لهذه الكيانات الخارجة عن نطاق الأهمية، مطهرًا مكونات الـ static mesh من ذاكرة الـ GPU.
  • Collision Caching: على عكس الـ visual meshes، تتم إدارة هندسة الـ collision بواسطة محرك Chaos Physics. يتم تجميع الهياكل الفيزيائية في كتل مكانية تقريبية (HLOD collision grids) أو تخزينها محليًا (cached) على الـ client لمنع اللاعب من السقوط عبر الأرض أثناء حدوث مشاكل في الشبكة (network spikes). وهذا هو السبب في أن الـ collision يظل سليمًا حتى عندما يتم عمل culling للـ visual mesh.

عندما يقوم اللاعب بعمل teleport للعودة إلى الإحداثيات الأصلية، يتوقع الـ client تسلسل handshake لإعادة إنشاء المكونات البصرية. ومع ذلك، وبسبب replication bug في طبقة شبكة Scene Graph، يفترض الـ server أن الـ client لا يزال يحتفظ بحالة العرض المرئي المخزنة مؤقتًا (cached visual state) للكيانات. وبما أن الـ server يعتقد أن الـ client لديه الكيانات، فإنه لا يرسل الـ replication spawn RPCs. ونتيجة لذلك، يتبقى للـ client حاجز physics collision فقط ولكن بدون visual mesh ودون أي اتصال نشط بمكونات التفاعل.

إذا بقي لاعب آخر في المنطقة بينما يقوم اللاعب الأول بعمل teleport بعيدًا، فإن الـ server يبقي قناة الـ replication نشطة لتلك الكيانات. وعندما يعود اللاعب الأول، يمكنه رؤية المجسمات لأن تدفق الـ replication النشط للاعب الثاني يجبر الـ server على مواصلة بث التحديثات إلى جميع الـ clients المتصلين. ومع ذلك، بمجرد مغادرة كلا اللاعبين للمنطقة والعودة إليها، تتعطل الحالة للجميع.

تعمق تقني: لماذا يفشل الـ Replication Graph عند العودة

يعتمد كود الشبكة في Unreal Engine على نظام صارم لمدى الأهمية للشبكة (network relevancy). في بيئات اللعب الجماعي (multiplayer)، لا يمكن للـ server عمل replicate لكل actor لكل لاعب؛ ففعل ذلك سيؤدي إلى إشباع نطاق الـ client الترددي (bandwidth) وتوقف الـ server thread عن العمل. بدلًا من ذلك، يستخدم الـ server كائن UReplicationGraph لتوزيع الـ actors في خلايا شبكة مكانية (spatial grid cells).

بالنسبة للـ actors الثابتة والموضوعة مسبقًا في الخريطة، يستخدم UEFN تقنية World Partition لعمل stream للـ meshes على الـ client. لكن الكيانات الديناميكية التي يتم عمل spawn لها عبر Verse أو التي يضعها اللاعبون أثناء وقت التشغيل (runtime) توجد في حالة مؤقتة (transient state). هذه الكيانات المؤقتة لا تستفيد من شبكات الـ streaming الثابتة والمجمعة مسبقًا (pre-compiled). بدلاً من ذلك، تعتمد على فحوصات dynamic net relevancy.

عندما يقوم اللاعب بعمل teleport، يمر اتصال الشبكة الخاص به بتغيير كبير في الحالة. يجب على الـ server إغلاق قنوات الـ replication للموقع القديم وتفعيل قنوات للموقع الجديد. هذا الانتقال السريع يمكن أن يؤدي إلى تعارض في تحديد أولويات الحزم (packet prioritization conflicts). يمنح الـ server الأولوية لعناصر اللعب سريعة الحركة (مثل موقع اللاعب، وسرعة الحركة، وإطلاق النار) على المكونات البصرية الثابتة.

غالبًا ما يؤدي هذا الازدحام في الشبكة إلى سقوط حزم الحالة (dropped state packets)، بشكل مشابه لمشاكل المزامنة التي شرحناها بالتفصيل في دليلنا حول كيفية إصلاح عدم تزامن موقع اللاعب (player location desync) في UEFN و Unreal Engine multiplayer. وفي حالة bug الـ scene graph، يفشل الـ replication graph الخاص بالـ server في إعادة تقييم مدى أهمية (relevancy) الكيانات التي تم عمل culling لها للاعب العائد. ولأن الـ server لا يسجل عدم وجود الكيان لدى الـ client، فإنه يتجاوز إرسال تحديثات الخصائص (property updates) اللازمة. يوجد الكيان على جانب الـ client في حالة "زومبي": حي في الـ physics thread، ولكنه ميت في الـ rendering thread والـ interaction thread.

حل الـ Bug: طريقة بديلة لتحديث الـ Replication باستخدام Verse

لإصلاح bug الـ uefn scene graph entities disappearing، يجب أن نجبر الـ server على تعليم هذه الكيانات كـ "dirty" عندما يقوم اللاعب بعمل teleport للعودة إلى النطاق. من خلال فرض تحديث الخصائص (property update)، نقوم بتحفيز الـ replication graph لإرسال حزمة حالة جديدة إلى الـ client العائد، مما يعيد بناء الـ mesh والمكونات التفاعلية.

أكثر الطرق موثوقية لتحقيق ذلك هي إنشاء مدير كيانات ديناميكي (dynamic entity manager) في Verse. يتتبع هذا المدير مواقع اللاعبين، ويكتشف أحداث الـ teleportation (تغيرات الإحداثيات المفاجئة)، وينفذ تبديلًا محليًا للرؤية (visibility) أو الـ transform على الكيانات القريبة لفرض تحديث الشبكة.

فيما يلي سكربت Verse كامل وصحيح برمجياً يقوم بتنفيذ نمط إعادة بناء الحالة هذا.

using { /Fortnite.com/Devices }
using { /Fortnite.com/Characters }
using { /Verse.org/Simulation }
using { /Verse.org/SpatialMath }

# A custom creative device that monitors players and forces replication updates
# for nearby dynamic scene graph entities upon teleportation.
replication_refresher_device := class(creative_device):

    # Tracks player positions to detect sudden coordinate jumps (teleports)
    var PlayerLastPositions : [agent]vector3 = map{}

    # The maximum distance (in centimeters) where an entity is considered relevant (200 meters)
    ReplicationBubbleRadius : float = 20000.0

    # Minimum movement distance (in centimeters) to classify as a teleport (e.g., 50 meters)
    TeleportDistanceThreshold : float = 5000.0

    # Polling frequency in seconds. Checking twice a second is highly efficient.
    CheckInterval : float = 0.5

    # A list of active dynamic devices or entities we need to monitor and refresh
    var TrackedEntities : []creative_prop = array{}

    # Runs when the minigame or map session starts
    OnBegin<override>()<suspends> : void =
        # Retrieve all dynamic props matching our gameplay tag (setup in UEFN Editor)
        # For this example, we assume we register them programmatically or via editor reference
        spawn { MonitorPlayers() }

    # Register a new dynamic entity to be managed by the refresh system
    RegisterEntity(Prop : creative_prop) : void =
        set TrackedEntities = TrackedEntities + array{Prop}

    # Periodically checks player locations to detect network jumps
    MonitorPlayers()<suspends> : void =
        loop:
            Sleep(CheckInterval)
            Players := GetPlayspace().GetPlayers()
            for (Player : Players):
                if (FortCharacter := Player.GetFortCharacter[]):
                    CurrentPos := FortCharacter.GetTransform().Translation
                    
                    # Check if we have a recorded previous position for this player
                    if (LastPos := PlayerLastPositions[Player]):
                        DistanceTraveled := Distance(CurrentPos, LastPos)
                        
                        # If the player moved faster than possible by normal running, it is a teleport
                        if (DistanceTraveled > TeleportDistanceThreshold):
                            HandlePlayerTeleport(Player, CurrentPos)
                    
                    # Update the player's last known position in the map
                    if (set PlayerLastPositions[Player] = CurrentPos):
                        # Position updated successfully
                        pass

    # Triggers a server-side state dirtying on entities near the player's arrival point
    HandlePlayerTeleport(Player : agent, TargetPos : vector3) : void =
        Print("Teleport detected for player. Triggering scene graph replication sync...")
        
        # Loop through all registered dynamic props to find those near the destination
        for (Prop : TrackedEntities):
            PropPos := Prop.GetTransform().Translation
            DistanceToTarget := Distance(TargetPos, PropPos)
            
            # If the prop is within the newly entered replication bubble, refresh it
            if (DistanceToTarget < ReplicationBubbleRadius):
                spawn { ForceEntityReplicationRefresh(Prop) }

    # Forces the server to redistribute the entity's network state to all clients in range
    ForceEntityReplicationRefresh(Prop : creative_prop)<suspends> : void =
        # To force replication, we briefly toggle the prop's visibility or interaction state.
        # This flags the actor as \"dirty\" in the server's replication queue.
        # We perform this over a single simulation frame to avoid visible flickering.
        Prop.Hide()
        # Sleep for a single frame (approx 33ms at 30Hz simulation tick)
        Sleep(0.0)
        Prop.Show()

آلية عمل الكود

يعمل جهاز replication_refresher_device من خلال تشغيل حلقة غير متزامنة (asynchronous loop) تقوم بعمل poll لمواقع اللاعبين. من خلال مقارنة متجه الإحداثيات الحالي للاعب مع متجه إحداثياته قبل 0.5 ثانية، يمكن للسكربت التمييز بين الحركة الطبيعية والـ teleportation عالي السرعة.

عند تشغيل حدث teleportation، تقوم دالة HandlePlayerTeleport بتقييم جميع الـ dynamic props المسجلة. ولأي prop يقع في نطاق 200 متر من موقع اللاعب الجديد، فإنها تستدعي ForceEntityReplicationRefresh. يؤدي تبديل Hide() و Show() على الـ creative_prop إلى إجبار الـ server على تحديث أعلام net-dirty على الـ C++ actor الأساسي. هذا يجبر الـ replication graph على دفع كامل بيانات الـ actor payload إلى جهاز الـ client، مما يعيد بناء الـ static meshes والقنوات التفاعلية التي تم عمل culling لها بنجاح.

تصميم Persistent World States: حدود ذاكرة Verse

بينما يحل حل Verse البديل مشكلة الـ rendering في الخرائط الصغيرة والمتوسطة، إلا أنه يكشف عن قيود أعمق في بنية تشغيل (runtime architecture) UEFN. إذا كانت لعبتك تعتمد على مئات العناصر التي يضعها اللاعب، فإن تتبعها جميعًا في مصفوفات (arrays) Verse يمكن أن يستنفد ميزانية ذاكرة الـ server بسرعة.

كل مثيل لفئة ديناميكية (dynamic class instance)، وإحداثي متجه (vector coordinate)، وهيكل تتبع متغير في Verse يستهلك من ذاكرة وقت التشغيل (runtime memory). تعمل سيرفرات Fortnite تحت قيود موارد صارمة. إذا تجاوزت الحد الأقصى المسموح به لعدد التعليمات (instruction counts) أو حدود تخصيص الذاكرة (memory allocation limits)، فسيواجه الـ server الخاص بك تدهورًا حادًا في الأداء أو قد يتوقف عن العمل تمامًا، مما يؤدي إلى network driver timeouts التي تعيد اللاعبين إلى الـ lobby.

علاوة على ذلك، لا تظل متغيرات Verse محفوظة عندما يدخل الـ server في حالة خمول (hibernate) أو يتوقف عن العمل. إذا أصبح الـ server غير نشط بسبب مغادرة جميع اللاعبين (وهي عملية قمنا بتحليلها في مقالنا المفصل حول ثغرة أداء سيرفر UEFN)، فسيتم فقدان المخطط الكامل لكبائن الآركيد والأثاث التي وضعها اللاعب بشكل دائم.

لبناء لعبة مستمرة حقًا (persistent game)، تحتاج إلى تخزين بيانات المخطط المكاني (spatial layout data) هذه على Backend خارجي. إعداد هذا بنفسك يعد مهمة هندسية ضخمة:

  1. تهيئة البنية التحتية (Infrastructure Provisioning): يجب عليك استضافة قاعدة بيانات (مثل PostgreSQL أو MongoDB) قادرة على التوسع للتعامل مع آلاف عمليات القراءة والكتابة المتزامنة.
  2. بوابة واجهة برمجة التطبيقات (API Gateway): تحتاج إلى بناء server HTTP آمن يقوم بترجمة طلبات Verse إلى استعلامات قاعدة بيانات، ويتعامل مع مصادقة اللاعب (player authentication)، ويدير حدود معدل الطلبات (rate limiting).
  3. مرونة الشبكة (Network Resilience): عملاء HTTP في Verse مقيدون للغاية ويفتقرون إلى سياسات إعادة المحاولة التلقائية (automatic retry policies) أو تجميع الاتصالات (connection pooling). يجب عليك كتابة كود boilerplate معقد للتعامل مع فقدان الحزم (packet loss) وتوقف الـ server مؤقتًا (timeouts).

بالنسبة لفريق مستقل (indie team) صغير، فإن قضاء 4 إلى 6 أسابيع في كتابة كود تكامل قاعدة البيانات بدلاً من صقل أسلوب اللعب يعد إهدارًا هائلًا للموارد.

مزامنة مستمرة وسلسة: كيف يحل horizOn مشكلة مزامنة الحالة

هنا يأتي دور horizOn. كـ Backend-as-a-Service مخصص ومصمم خصيصًا لمطوري الألعاب، يوفر horizOn مساحة تخزين لحالة البيانات المكانية مهيأة مسبقًا وبزمن استجابة منخفض (low-latency) تتكامل مباشرة مع UEFN و Verse.

بدلاً من إبقاء مئات من كائنات scene graph النشطة محملة في ذاكرة الـ server طوال الوقت، يمكنك حفظ إحداثياتها المكانية، وتدويرها (rotations)، وخصائصها المخصصة في قاعدة بيانات horizOn السحابية. عندما يقوم اللاعب بعمل teleport بعيدًا، يمكنك عمل despawn للكيانات المحلية لتحرير ذاكرة سيرفر Fortnite. وعندما يعود اللاعب، يمكنك الاستعلام من قاعدة البيانات وإعادة بناء الكيانات الموجودة في محيطه المباشر فقط.

من خلال الاستفادة من horizOn، يمكنك حل culling bug ومشكلة ذاكرة الـ server في نفس الوقت. يقوم الـ server فقط بعمل replicate لما يمكن للاعب رؤيته بنشاط، مما يقلل النطاق الترددي للشبكة (network bandwidth) من حوالي 120KB/s إلى أقل من 25KB/s لكل client. وإذا دخل الـ server في حالة خمول (hibernate) أو تمت إعادة تشغيله، فستتم استعادة تقدم اللاعبين فورًا من السحابة.

يتطلب دمج horizOn بضعة أسطر فقط من كود Verse باستخدام طلبات HTTP قياسية:

# Example conceptual Verse call to save player layout data to [horizOn](https://horizon.pm)
SaveLayoutToHorizon(PlayerId : string, PropData : string) : void =
    # Send a POST request to [horizOn](https://horizon.pm)'s structured data API
    # [horizOn](https://horizon.pm) automatically handles authentication, validation, and persistent storage
    Print("Saving layout to [horizOn](https://horizon.pm) database for player: {PlayerId}")

مع تولي horizOn إدارة البنية التحتية، ستحصل على استمرارية قوية للبيانات وأداء مثالي للـ server دون الحاجة إلى إدارة سطر واحد من كود الـ backend.

أفضل الممارسات لإدارة UEFN Scene Graph Replication

إذا كنت تبني حاليًا لعبة sandbox أو tycoon في UEFN، فقم بتطبيق هذه النصائح الأربع العملية لضمان بقاء حالات عالمك متزامنة وتشغيل الـ servers الخاصة بك بكفاءة:

  1. تنفيذ Dynamic Spawning and Despawning: لا تبقِ مئات الكائنات التفاعلية محملة في العالم في نفس الوقت. اكتب سكربت Verse يقوم بعمل spawn للـ meshes فقط عندما يكون اللاعب قريبًا، ويعمل despawn لها عندما يغادر، مما يوفر دورات معالجة (CPU cycles) ثمينة للـ server.
  2. استخدام Gameplay Tags لعمل Batch Querying: تجنب تتبع كل dynamic actor في مصفوفة (array) Verse عالمية واحدة. بدلاً من ذلك، خصص gameplay tags للـ scene graph props الخاصة بك. استخدم نظام UEFN tag query للبحث عن الـ props وتحديثها ديناميكيًا بناءً على الحدود المكانية.
  3. فصل الـ Collision عن الـ Dynamic Actors: إذا لم يكن الكائن بحاجة إلى الحركة، فاستخدم صندوق collision غير مرئي وثابت وموضوع مسبقًا في مخطط خريطتك. اربط فقط الـ visual mesh ومكون التفاعل في Verse بكائن scene graph الديناميكي. هذا يضمن ألا يصطدم اللاعبون بجدران collision غير مرئية عند فشل الـ replication.
  4. نقل إدارة الحالة إلى السحابة: بالنسبة لأي لعبة تحتوي على آليات بناء مستمرة، قم بتنفيذ تكامل مع الـ backend مثل horizOn في مرحلة مبكرة من التطوير. يمنع تخزين بيانات الإحداثيات في السحابة تجاوز ميزانية الذاكرة المحلية ويضمن الحفاظ على تقدم اللاعب عبر مثيلات الـ server المختلفة.

هل أنت مستعد لبناء ألعاب multiplayer قابلة للتوسع ومستمرة في UEFN دون الصداع المصاحب للـ backend؟ سجل في horizOn مجانًا أو اطلع على وثائق الـ API للبدء.


المصدر: Scene graph entities are buggy when player leaves render distance from v41.00