تحية ، خابروفسك. كما كتبنا بالفعل ، فإن يناير غني بالإطلاقات الجديدة ، واليوم نعلن عن مجموعة لدورة جديدة من OTUS - "Game Developer for Unity" . تحسبا لبدء الدورة ، فإننا نشارككم ترجمة المواد المثيرة للاهتمام.

نحن نعيد بناء جوهر الوحدة من خلال تكتيك البيانات الموجه . مثل العديد من استوديوهات الألعاب ، نرى أيضًا مزايا رائعة في استخدام نظام مكونات الكيانات (ECS) ، ونظام مهام C # (C # Job System) و Burst Compiler. في Unite Copenhagen ، أتيحت لنا الفرصة للدردشة مع Far North Entertainment والتفكير في كيفية تنفيذ هذه الوظيفة DOTS في مشاريع Unity التقليدية.
Far North Entertainment هو استوديو سويدي مملوك من قبل خمسة أصدقاء هندسيين. منذ إصدار Down to Dungeon لـ Gear VR في أوائل عام 2018 ، كانت الشركة تعمل على لعبة تنتمي إلى النوع الكلاسيكي لألعاب الكمبيوتر الشخصي ، وهي لعبة البقاء على قيد الحياة بعد غيبوبة. ما يميز المشروع عن الآخرين هو عدد الزومبي الذين يطاردونك. لقد جذبت رؤية الفريق في هذا الصدد الآلاف من الزومبي الجياع الذين يتابعونك في جحافل ضخمة.
ومع ذلك ، واجهوا بسرعة الكثير من مشاكل الأداء بالفعل في مرحلة النماذج الأولية. بقي إنشاء هذا العدد من الأعداء والموت والتحديث وتحريك كل هذا العدد من العقبات هو
العقبة الرئيسية ، حتى بعد أن حاول الفريق حل المشكلة
بتجميع الإهمال والتهديد .
وقد أجبر ذلك المدير الفني للاستوديو أندريس إريكسون على تحويل انتباهه إلى DOTS وتغيير التفكير من وجوه موجهة إلى البيانات الموجهة. "الفكرة الرئيسية التي ساعدت على تحقيق هذا التحول هي أنه كان عليك التوقف عن التفكير في الأشياء والتسلسلات الهرمية للأشياء وبدء التفكير في البيانات ، وكيف يتم تحويلها ، وكيفية الوصول إليها" ، قال. . تعني كلماته أنه ليس من الضروري بناء بنية برمجية مع التركيز على أشياء من الحياة الحقيقية بطريقة تحل المشكلة الأكثر عمومية وتجريدية. لديه العديد من النصائح لأولئك الذين ، مثله ، يواجهون تغييراً في النظرة إلى العالم:
"اسأل نفسك ما هي المشكلة الحقيقية التي تحاول حلها ، وما هي البيانات المهمة للحصول على حل. هل ستقوم بتحويل نفس مجموعة البيانات بنفس الطريقة مرارًا وتكرارًا؟ ما مقدار البيانات المفيدة التي يمكنك وضعها في سطر واحد من ذاكرة التخزين المؤقت للمعالج؟ إذا قمت بإجراء تغييرات على التعليمات البرمجية الموجودة ، فقم بتقييم مقدار البيانات غير الهامة التي تضيفها إلى سطر ذاكرة التخزين المؤقت. هل من الممكن تقسيم الحسابات إلى عدة مؤشرات أم هل أحتاج إلى استخدام دفق أمر واحد؟ "توصل الفريق إلى أن الكيانات في نظام مكونات الوحدة هي مجرد معرفات البحث في تدفقات المكون. المكونات هي مجرد بيانات ، بينما تحتوي الأنظمة على كل المنطق وتصفية الكيانات بتوقيع محدد ، والمعروف باسم النماذج الأولية. "أعتقد أن إحدى الأفكار التي ساعدتنا على تصور أفكارنا كانت تقديم ECS كقاعدة بيانات SQL. كل نموذج أصلي هو جدول يكون كل عمود فيه مكونًا ، ولكل صف كيان فريد. في الأساس ، أنت تستخدم أنظمة لإنشاء استعلامات لجداول النماذج الأولية هذه وتنفيذ عمليات على الكيانات "، كما يقول أندرس.
تقديم DOTS
للتوصل إلى هذا الفهم ، درس الوثائق الخاصة بنظام
مكونات الكيان وأمثلة
ECS ومثال قمنا به مع Nordeus وقدمناه في Unite Austin. كانت المعلومات العامة حول بنية البيانات الموجهة مفيدة للغاية للفريق. "تقرير
مايك أكتون عن الهندسة المعمارية الموجهة للبيانات مع CppCon 2014 هو بالضبط ما فتح أعيننا على هذه الطريقة في البرمجة."
نشر فريق "أقصى الشمال" ما تعلموه في
مدونة Dev ، في سبتمبر من هذا العام ، جاءوا إلى كوبنهاغن لتبادل تجاربهم مع الانتقال إلى نهج موجه نحو البيانات في الوحدة.
تستند هذه المقالة إلى تقرير ، وتوضح بمزيد من التفصيل تفاصيل تنفيذها لـ ECS و C # Task System ومترجم Burst. تتقاسم أقصى الشمال أيضًا العديد من نماذج الكود من مشروعهم.
منظمة بيانات الزومبي
يقول أندرس: "كانت المشكلة التي واجهناها تتمثل في إقحام عمليات التشريد والتناوب لآلاف الأشياء على جانب العميل". كان الأسلوب الأولي الخاص بالكائن المنحى هو إنشاء برنامج نصي
ZombieView مجردة ورث الفئة الأصل
EntityView العامة.
EntityView هو
MonoBehaviour تعلق على
GameObject . إنه بمثابة تمثيل مرئي لنموذج اللعبة. كان كل
ZombieView مسؤولاً عن معالجة الاستيفاء الخاص بالحركة والتناوب في وظيفة
التحديث .
هذا يبدو طبيعيا ، حتى تفهم أن كل كيان يقع في الذاكرة في مكان تعسفي. هذا يعني أنه إذا كنت تقوم بالوصول إلى آلاف الكائنات ، فيجب على وحدة المعالجة المركزية إخراجها من الذاكرة واحدة تلو الأخرى ، وهذا يحدث ببطء شديد. إذا وضعت البيانات الخاصة بك في كتل مرتبة مرتبة في سلسلة ، يمكن للمعالج تخزين مجموعة كاملة من البيانات في نفس الوقت. يمكن لمعظم المعالجات الحديثة تلقي حوالي 128 أو 256 بت من ذاكرة التخزين المؤقت في دورة واحدة.
قرر الفريق تحويل الأعداء إلى DOTS على أمل حل مشكلات الأداء من جانب العميل. الأول في السطر هو وظيفة
التحديث في
ZombieView . حدد الفريق الأجزاء التي ينبغي تقسيمها إلى أنظمة مختلفة وتحديد البيانات اللازمة. كان أول وأهم شيء هو استيفاء المواقف والمنعطفات ، لأن عالم اللعبة عبارة عن شبكة ثنائية الأبعاد. هناك متغيرين عائمين هما المسئولان عن موقع ذهاب الزومبي ، والمكون الأخير هو الموضع المستهدف ، حيث يتتبع موقع الخادم بالنسبة للعدو.
[Serializable] public struct PositionData2D : IComponentData { public float2 Position; } [Serializable] public struct HeadingData2D : IComponentData { public float2 Heading; } [Serializable] public struct TargetPositionData : IComponentData { public float2 TargetPosition; }
كانت الخطوة التالية هي إنشاء نموذج أصلي للأعداء. النموذج الأصلي هو مجموعة من المكونات التي تنتمي إلى كيان معين ، وبعبارة أخرى ، هو توقيع المكون.
يستخدم المشروع المباني الجاهزة لتحديد النماذج ، حيث يحتاج الأعداء إلى مزيد من المكونات ، وبعضهم يحتاج إلى روابط إلى
GameObject . يعمل هذا حتى تتمكن من التفاف بيانات المكون الخاص بك في
ComponentDataProxy ، والتي
ستحولها إلى
MonoBehaviour ، والتي بدورها يمكن إرفاقها
بالأبنية الجاهزة. عندما تقوم بإنشاء مثيل باستخدام
EntityManager وتمرير الجاهزة ، فإنه ينشئ كيان مع جميع البيانات من المكونات التي تم إرفاقها مع الجاهزة. يتم تخزين جميع البيانات المكونة في 16 كيلو بايت قطع الذاكرة ودعا
ArchetypeChunk .
فيما يلي تصور لكيفية تنظيم تدفقات المكونات في مقطع النموذج الأصلي الخاص بنا:
"إحدى المزايا الرئيسية لقطع القطع الأصلية هي أنك لا تحتاج غالبًا إلى إعادة تخصيص مجموعة عند إنشاء كائنات جديدة ، حيث تم تخصيص الذاكرة مسبقًا مقدمًا. هذا يعني أن إنشاء كيانات يقوم بكتابة البيانات إلى نهاية تدفقات المكون داخل أجزاء النموذج الأصلي. الحالة الوحيدة عندما يكون من الضروري إجراء تخصيص الكومة مرة أخرى هي عند إنشاء كيان لا يلائم حدود المجموعة. في هذه الحالة ، سيتم بدء تخصيص جزء جديد من نموذج أولي بحجم 16 كيلوبايت ، أو إذا كان هناك جزء فارغ من نفس النموذج الأصلي ، فيمكن إعادة استخدامه. ثم سيتم تسجيل البيانات الخاصة بالكائنات الجديدة في التدفقات المكونة للمقطع الجديد . "
و multithreading من الكسالى الخاص بك
الآن بعد أن تم تعبئة البيانات بكثافة ووضعها في الذاكرة بطريقة ملائمة للتخزين المؤقت ، يمكن للفريق بسهولة استخدام نظام مهام C # لتشغيل الكود الخاص به على العديد من مراكز وحدة المعالجة المركزية بالتوازي.
كانت الخطوة التالية هي إنشاء نظام يقوم بتصفية جميع الكيانات من جميع الكتل النموذجية التي
تحتوي على مكونات PositionData2D و
HeadingData2D و
TargetPositionData .
للقيام بذلك ، أنشأ Anders وفريقه
JobComponentSystem وقاموا بإنشاء طلبهم في وظيفة
OnCreate . يبدو شيء مثل هذا:
private EntityQuery m_Group; protected override void OnCreate() { base.OnCreate(); var query = new EntityQueryDesc { All = new [] { ComponentType.ReadWrite<PositionData2D>(), ComponentType.ReadWrite<HeadingData2D>(), ComponentType.ReadOnly<TargetPositionData>() }, }; m_Group = GetEntityQuery(query); }
تعلن الشفرة عن طلب يقوم بتصفية جميع الكائنات في العالم التي لها موقع واتجاه وغرض. بعد ذلك ، أرادوا جدولة المهام لكل إطار باستخدام نظام مهام C # لتوزيع العمليات الحسابية عبر عدة مهام سير عمل.
"إن أروع شيء في نظام مهام C # هو أنه هو نفس النظام الذي تستخدمه الوحدة في الكود الخاص بها ، لذلك لم يكن لدينا ما يدعو للقلق بشأن حظر سلاسل العمليات القابلة للتنفيذ التي تحظر بعضها البعض ، والتي تتطلب نفس مراكز المعالج وتسبب في مشاكل الأداء يقول أندرس.
قرر الفريق استخدام
IJobChunk ، لأن الآلاف من الأعداء ضمنا وجود عدد كبير من القطع النموذجية التي يجب أن تطابق الطلب في وقت التشغيل. يوزع
IJobChunk الأجزاء الصحيحة عبر
مهام سير العمل المختلفة.
كل إطار ، مهمة
UpdatePositionAndHeadingJob جديدة
، هي المسؤولة عن التعامل مع الاستيفاء من المواقف وتحولات الأعداء في اللعبة.
رمز جدولة المهام كما يلي:
protected override JobHandle OnUpdate(JobHandle inputDeps) { var positionDataType = GetArchetypeChunkComponentType<PositionData2D>(); var headingDataType = GetArchetypeChunkComponentType<HeadingData2D>(); var targetPositionDataType = GetArchetypeChunkComponentType<TargetPositionData>(true); var updatePosAndHeadingJob = new UpdatePositionAndHeadingJob { PositionDataType = positionDataType, HeadingDataType = headingDataType, TargetPositionDataType = targetPositionDataType, DeltaTime = Time.deltaTime, RotationLerpSpeed = 2.0f, MovementLerpSpeed = 4.0f, }; return updatePosAndHeadingJob.Schedule(m_Group, inputDeps); }
هذا هو ما تبدو عليه المهمة:
public struct UpdatePositionAndHeadingJob : IJobChunk { public ArchetypeChunkComponentType<PositionData2D> PositionDataType; public ArchetypeChunkComponentType<HeadingData2D> HeadingDataType; [ReadOnly] public ArchetypeChunkComponentType<TargetPositionData> TargetPositionDataType; [ReadOnly] public float DeltaTime; [ReadOnly] public float RotationLerpSpeed; [ReadOnly] public float MovementLerpSpeed; }
عندما يسترجع مؤشر ترابط العامل مهمة من قائمة انتظاره ، فإنه يستدعي جوهر المهمة.
إليك ما يبدو عليه جوهر التنفيذ:
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) { var chunkPositionData = chunk.GetNativeArray(PositionDataType); var chunkHeadingData = chunk.GetNativeArray(HeadingDataType); var chunkTargetPositionData = chunk.GetNativeArray(TargetPositionDataType); for (int i = 0; i < chunk.Count; i++) { var target = chunkTargetPositionData[i]; var positionData = chunkPositionData[i]; var headingData = chunkHeadingData[i]; float2 toTarget = target.TargetPosition - positionData.Position; float distance = math.length(toTarget); headingData.Heading = math.select( headingData.Heading, math.lerp(headingData.Heading, math.normalize(toTarget), math.mul(DeltaTime, RotationLerpSpeed)), distance > 0.008 ); positionData.Position = math.select( target.TargetPosition, math.lerp( positionData.Position, target.TargetPosition, math.mul(DeltaTime, MovementLerpSpeed)), distance <= 1 ); chunkPositionData[i] = positionData; chunkHeadingData[i] = headingData; } }
"قد تلاحظ أننا نستخدم تحديد بدلاً من المتفرعة ، وهذا يسمح لنا بالتخلص من التأثير الذي يسمى تنبؤ الفرع غير الصحيح. ستقوم وظيفة التحديد بتقييم كل من التعبيرات وتحديد التعبير الذي يتطابق مع الشرط ، وإذا لم يكن من الصعب حساب تعبيراتك ، فإنني أوصي باستخدام select ، لأنه غالبًا ما يكون أرخص من انتظار استرداد وحدة المعالجة المركزية من التنبؤ غير الصحيح للفرع. " اندرس.
زيادة الإنتاجية مع انفجار
الخطوة الأخيرة في تحويل DOTS إلى موضع العدو واستيفاء الدورة التدريبية هي تمكين المحول البرمجي Burst. تبدو المهمة بسيطة جدًا بالنسبة إلى Anders: "نظرًا
لوجود البيانات في صفائف متجاورة وبما أننا نستخدم مكتبة الرياضيات الجديدة من Unity ، فكل ما كان علينا فعله هو إضافة سمة
BurstCompile إلى مهمتنا."
[BurstCompile] public struct UpdatePositionAndHeadingJob : IJobChunk { public ArchetypeChunkComponentType<PositionData2D> PositionDataType; public ArchetypeChunkComponentType<HeadingData2D> HeadingDataType; [ReadOnly] public ArchetypeChunkComponentType<TargetPositionData> TargetPositionDataType; [ReadOnly] public float DeltaTime; [ReadOnly] public float RotationLerpSpeed; [ReadOnly] public float MovementLerpSpeed; }
المترجم Burst يعطينا بيانات واحدة للتعليم المتعدد (SIMD) ؛ تعليمات الجهاز التي يمكن أن تعمل مع مجموعات متعددة من بيانات الإدخال وإنشاء مجموعات متعددة من بيانات الإخراج مع تعليمة واحدة فقط. هذا يساعدنا في ملء المزيد من الأماكن على ناقل ذاكرة التخزين المؤقت 128 بت مع البيانات الصحيحة. سمح برنامج التحويل البرمجي Burst ، مقترنًا بتكوين بيانات ونظام تخزين سهل الاستخدام ، للفريق بزيادة الإنتاجية بشكل كبير. فيما يلي الجدول الذي قاموا بتجميعه عن طريق قياس الأداء بعد كل خطوة تحويل.

هذا يعني أن "أقصى الشمال" تخلصت تمامًا من المشكلات المرتبطة باستيفاء الموقف من جانب العميل واتجاه الزومبي. يتم الآن تخزين بياناتهم في شكل مناسب للتخزين المؤقت ، ويتم تعبئة خطوط ذاكرة التخزين المؤقت فقط ببيانات مفيدة. يتم توزيع الحمل على جميع مراكز وحدة المعالجة المركزية (CPU) ، وينتج عن برنامج التحويل البرمجي Burst رمز الآلة الأمثل للغاية مع تعليمات SIMD.
أقصى الشمال الترفيه DOTS نصائح والخدع
- ابدأ في التفكير من حيث تدفقات البيانات ، لأنه في ECS ، تعد الكيانات مجرد فهارس البحث في تدفقات بيانات مكونة متوازية.
- تخيل ECS كقاعدة بيانات علائقية تكون فيها النماذج الأولية جداول ، والمكونات عبارة عن أعمدة ، والكيانات هي مؤشرات في جدول (صف).
- قم بتنظيم بياناتك في صفائف متسلسلة لاستخدام ذاكرة التخزين المؤقت للمعالج والجلب المسبق للأجهزة.
- انسَ الرغبة في إنشاء تسلسلات هرمية للكائنات ومحاولة إيجاد حل مشترك قبل فهم المشكلة الحقيقية التي تحاول حلها.
- التفكير في جمع القمامة. تجنب الإفراط في تخصيص أكوام الذاكرة في المناطق الحساسة للأداء. استخدم حاويات الوحدة المحلية الجديدة بدلاً من ذلك. ولكن كن حذرا ، عليك أن تتعامل مع التنظيف اليدوي.
- تعترف بقيمة التجريدات الخاصة بك ، حذار من النفقات العامة من استدعاء وظائف افتراضية.
- استخدام جميع النوى وحدة المعالجة المركزية مع نظام المهمة C #.
- تحليل مستوى الأجهزة. هل يقوم برنامج التحويل البرمجي Burst بإنشاء تعليمات SIMD بالفعل؟ استخدم المفتش المتفجر للتحليل.
- وقف هدر خطوط ذاكرة التخزين المؤقت في فارغة. فكر في تعبئة البيانات في خطوط ذاكرة التخزين المؤقت كتعبئة البيانات في حزم UDP.
النصيحة الرئيسية التي يريد Anders Ericsson مشاركتها هي نصيحة أكثر عمومية لأولئك الذين يجري تطوير مشروعهم بالفعل:
"حاول تحديد مناطق معينة في لعبتك حيث تواجه مشاكل في الأداء ومعرفة ما إذا كان يمكنك تطبيق DOTS على وجه التحديد في هذه المنطقة المعزولة. لا تحتاج إلى تغيير قاعدة الشفرة بأكملها! "الخطط المستقبلية
"نريد استخدام DOTS في مناطق أخرى من لعبتنا ، وقد سررنا بالإعلانات على Unite حول الرسوم المتحركة DOTS و Unity Physics و Live Link. نود أن نتعلم كيفية تحويل المزيد من كائنات اللعبة إلى كائنات ECS ، ويبدو أن الوحدة حققت تقدماً كبيراً في تنفيذ هذا ، "يخلص أندرس.
إذا كانت لديك أسئلة إضافية لفريق Far North ، نوصيك بالانضمام إلى
Discord !
راجع قائمة تشغيل
Unite Copenhagen DOTS لمعرفة كيف تستخدم استوديوهات الألعاب الحديثة الأخرى DOTS لإنشاء ألعاب رائعة عالية الأداء ، وكيف تعمل المكونات المستندة إلى DOTS مثل DOTS Physics ، و Conversion Workflow الجديد ، ومترجم Burst معًا.
لقد انتهت الترجمة ، ونحن
ندعوك لحضور ندوة عبر الإنترنت مجانًا ، والتي سنخبرك فيها بكيفية إنشاء أداة إطلاق غيبوبة في ساعة واحدة .