مع Unity3D ، مع إصدار الإصدار 2018 ، أصبح من الممكن استخدام نظام ECS الأصلي (من أجل الوحدة) ، المنكه بترابط متعدد في شكل نظام الوظائف. لا توجد العديد من المواد على الإنترنت (مشروعان من Unity Technologies نفسها واثنين من مقاطع الفيديو التدريبية على YouTube). حاولت أن أدرك حجم وملاءمة ECS ، فصنع مشروعًا صغيرًا ليس من المكعبات والأزرار. قبل ذلك ، لم يكن لدي أي خبرة في تصميم ECS ، لذلك استغرق الأمر يومين لدراسة المواد وإعادة بناء التفكير باستخدام OOP ، يوم للاستمتاع بالنهج ، ويوم أو يومين آخر لتطوير مشروع ، ومحاربة الوحدة ، وسحب عينات الشعر والدخان . تحتوي المقالة على القليل من النظرية ومشروع مثال صغير.
معنى ECS بسيط للغاية - كيان (
كيان ) بمكوناته (
مكون ) ، والتي تتم معالجتها بواسطة النظام (
النظام ).
الجوهر
الكيان ليس لديه منطق ولا يخزن سوى المكونات (مشابه جدًا لـ GameObject في نهج تكلفة النقرة القديم). في Unity ECS ، توجد فئة الكيان لهذا الغرض.
مكون
تقوم المكونات بتخزين البيانات فقط ، وأحيانًا لا تحتوي على أي شيء على الإطلاق وهي علامة بسيطة للمعالجة بواسطة النظام. لكن ليس لديهم أي منطق. موروث من ComponentDataWrapper. يمكن معالجتها بواسطة مؤشر ترابط آخر (ولكن هناك فارق بسيط).
النظام
الأنظمة مسؤولة عن معالجة المكونات. عند الإدخال ، يتلقون من Unity قائمة بالمكونات المعالجة للأنواع المعينة ، وفي الأساليب المحملة (نظائر التحديث ، ابدأ ، OnDestroy) يحدث سحر آليات اللعبة. موروث من ComponentSystem أو JobComponentSystem.
نظام العمل
آليات النظم التي تسمح بمعالجة المكونات بشكل متوازٍ. في نظام OnUpdate ، يتم إنشاء بنية مهمة وإضافتها إلى المعالجة. في لحظة الملل والموارد المجانية ، ستقوم الوحدة بمعالجة النتائج وتطبيقها على المكونات.
Multithreading and Unity 2018
تتم جميع أعمال نظام العمل في سلاسل رسائل أخرى ، ولا يمكن تغيير المكونات القياسية (Transform ، Rigidbody ، وما إلى ذلك) في أي مؤشر ترابط باستثناء الخيط الرئيسي. لذلك ، توجد في الحزمة القياسية مكونات "استبدال" متوافقة - مكون الموقع ، مكون التدوير ، مكون جهاز عرض مثيل الشبكة.
وينطبق الشيء نفسه على الهياكل القياسية مثل Vector3 أو Quaternion. تستخدم مكونات التوازي أبسط أنواع البيانات فقط (float3 ، float4 ، هذا كل شيء ، سيكون المبرمجون سعداء) المضافة إلى مساحة الاسم Unity ، وهناك أيضًا فئة الرياضيات لمعالجتها. لا سلاسل ، لا أنواع مرجعية ، فقط المتشددين.
"أرني الرمز"
لذا ، حان الوقت لتحريك شيء ما!
قم بإنشاء مكون يخزن قيمة السرعة ، وهو أيضًا أحد علامات النظام الذي يحرك الكائنات. تتيح لك السمة Serializable تعيين القيمة وتتبعها في المفتش.
عنصر سريع[Serializable] public struct SpeedData : IComponentData { public int Value; } public class SpeedComponent : ComponentDataWrapper<SpeedData> {}
باستخدام السمة Inject ، يحصل النظام على بنية تحتوي
فقط على مكونات تلك الكيانات التي توجد عليها جميع المكونات الثلاثة. لذلك ، إذا كان لدى بعض الكيانات مكونات PositionComponent و SpeedComponent ، ولكن ليس مكونات RotationComponent ، فلن تتم إضافة هذا الكيان إلى البنية التي تدخل النظام. وبالتالي ، من الممكن تصفية الكيانات من خلال وجود مكون.
نظام الحركة public class MovementSystem : ComponentSystem { public struct ShipsPositions { public int Length; public ComponentDataArray<Position> Positions; public ComponentDataArray<Rotation> Rotations; public ComponentDataArray<SpeedData> Speeds; } [Inject] ShipsPositions _shipsMovementData; protected override void OnUpdate() { for(int i = 0; i < _shipsMovementData.Length; i++) { _shipsMovementData.Positions[i] = new Position(_shipsMovementData.Positions[i].Value + math.forward(_shipsMovementData.Rotations[i].Value) * Time.deltaTime * _shipsMovementData.Speeds[i].Value); } } }
الآن ستتحرك جميع الكائنات التي تحتوي على هذه المكونات الثلاثة إلى الأمام بسرعة معينة.
كان الأمر سهلاً. على الرغم من أن الأمر استغرق يومًا واحدًا للتفكير في ECS.
لكن توقف. أين نظام الوظائف هنا؟
والحقيقة هي أنه لا يوجد شيء مكسور بما يكفي لاستخدام تعدد مؤشرات الترابط. حان وقت الاستراحة!
سحبت من العينات النظام الذي يؤدي إلى المباني الجاهزة. من المثير للاهتمام - إليك قطعة من التعليمات البرمجية:
مبيض EntityManager.Instantiate(prefab, entities); for (int i = 0; i < count; i++) { var position = new Position { Value = spawnPositions[i] }; EntityManager.SetComponentData(entities[i], position); EntityManager.SetComponentData(entities[i], new SpeedData { Value = Random.Range(15, 25) }); }
لذا دعونا نضع 1000 قطعة. لا يزال جيدًا جدًا لإنشاء شبكات تنسج على وحدة معالجة الرسومات. 5000 - تقريبًا أيضًا. سأري ما يحدث مع 50000 شيء.
ظهر مصحح الكيان في Unity ، مما يوضح عدد ms لكل نظام. يمكن تشغيل / إيقاف تشغيل الأنظمة في وقت التشغيل ، لمعرفة الأشياء التي تعالجها ، بشكل عام ، وهو أمر لا غنى عنه.
احصل على مثل هذه الكرة الفضائية تسجل الأداة بسرعة 15 إطارًا في الثانية ، لذا فإن النقطة بأكملها موجودة في الأرقام في قائمة الأنظمة. يحاول نظامنا ، MoveSystem ، نقل جميع الكائنات البالغ عددها 50000 في كل إطار ، ويقوم بذلك في المتوسط في 60 مللي ثانية. لذا ، الآن اللعبة مكسورة بما فيه الكفاية للتحسين.
نربط نظام العمل بنظام الحركة.
تعديل نظام الحركة public class MovementSystem : JobComponentSystem { [ComputeJobOptimization] struct MoveShipJob : IJobProcessComponentData<Position, Rotation, SpeedData> { public float dt; public void Execute(ref Position position, ref Rotation rotation, ref SpeedData speed) { position.Value += math.forward(rotation.Value) * dt * speed.Value; } } protected override JobHandle OnUpdate(JobHandle inputDeps) { var job = new MoveShipJob { dt = Time.deltaTime }; return job.Schedule(this, 1, inputDeps); } }
يرث النظام الآن من JobComponentSystem وفي كل إطار يقوم بإنشاء معالج خاص تقوم Unity بنقل نفس المكونات الثلاثة و deltaTime من النظام.
إطلاق سفينة الفضاء مرة أخرى 0.15 مللي ثانية (0.4 في القمة ، نعم) مقابل 50-70! 50 ألف قطعة! أدخلت هذه الأرقام في الآلة الحاسبة ، رداً على ذلك أظهر وجهًا سعيدًا.
الإدارة
يمكنك أن تنظر إلى الكرة الطائرة بلا نهاية ، أو يمكنك أن تطير بين السفن.
بحاجة إلى نظام لسيارات الأجرة.
مكوِّن التدوير موجود بالفعل على الجاهزة ، قم بإنشاء مكون لتخزين عناصر التحكم.
عنصر تحكم [Serializable] public struct RotationControlData : IComponentData { public float roll; public float pitch; public float yaw; } public class ControlComponent : ComponentDataWrapper<RotationControlData>{}
نحتاج أيضًا إلى مكون لاعب (على الرغم من أنه لا يمثل مشكلة في توجيه جميع السفن التي يبلغ وزنها 50 ألفًا دفعة واحدة)
PlayerComponent public struct PlayerData : IComponentData { } public class PlayerComponent : ComponentDataWrapper<PlayerData> { }
وعلى الفور ، قارئ إدخال المستخدم.
UserControlSystem public class UserControlSystem : ComponentSystem { public struct InputPlayerData { public int Length; [ReadOnly] public ComponentDataArray<PlayerData> Data; public ComponentDataArray<RotationControlData> Controls; } [Inject] InputPlayerData _playerData; protected override void OnUpdate() { for (int i = 0; i < _playerData.Length; i++) { _playerData.Controls[i] = new RotationControlData { roll = Input.GetAxis("Horizontal"), pitch = Input.GetAxis("Vertical"), yaw = Input.GetKey(KeyCode.Q) ? -1 : Input.GetKey(KeyCode.E) ? 1 : 0 }; } } }
بدلاً من الإدخال القياسي ، يمكن أن يكون هناك أي دراجة مفضلة أو AI.
وأخيرا معالجة الضوابط والدوران نفسه. لقد واجهت حقيقة أن math.euler لم يتم تنفيذها بعد ، لذلك أنقذت غارة سريعة على ويكيبيديا من التحول من زوايا أويلر إلى الرباعية.
ProcessRotationInputSystem public class ProcessRotationInputSystem : JobComponentSystem { struct LocalRotationSpeedGroup { public ComponentDataArray<Rotation> rotations; [ReadOnly] public ComponentDataArray<RotationSpeedData> rotationSpeeds; [ReadOnly] public ComponentDataArray<RotationControlData> controlData; public int Length; } [Inject] private LocalRotationSpeedGroup _rotationGroup; [ComputeJobOptimization] struct RotateJob : IJobParallelFor { public ComponentDataArray<Rotation> rotations; [ReadOnly] public ComponentDataArray<RotationSpeedData> rotationSpeeds; [ReadOnly] public ComponentDataArray<RotationControlData> controlData; public float dt; public void Execute(int i) { var speed = rotationSpeeds[i].Value; if (speed > 0.0f) { quaternion nRotation = math.normalize(rotations[i].Value); float yaw = controlData[i].yaw * speed * dt; float pitch = controlData[i].pitch * speed * dt; float roll = -controlData[i].roll * speed * dt; quaternion result = math.mul(nRotation, Euler(pitch, roll, yaw)); rotations[i] = new Rotation { Value = result }; } } quaternion Euler(float roll, float yaw, float pitch) { float cy = math.cos(yaw * 0.5f); float sy = math.sin(yaw * 0.5f); float cr = math.cos(roll * 0.5f); float sr = math.sin(roll * 0.5f); float cp = math.cos(pitch * 0.5f); float sp = math.sin(pitch * 0.5f); float qw = cy * cr * cp + sy * sr * sp; float qx = cy * sr * cp - sy * cr * sp; float qy = cy * cr * sp + sy * sr * cp; float qz = sy * cr * cp - cy * sr * sp; return new quaternion(qx, qy, qz, qw); } } protected override JobHandle OnUpdate(JobHandle inputDeps) { var job = new RotateJob { rotations = _rotationGroup.rotations, rotationSpeeds = _rotationGroup.rotationSpeeds, controlData = _rotationGroup.controlData, dt = Time.deltaTime }; return job.Schedule(_rotationGroup.Length, 64, inputDeps); } }
ربما ستسأل لماذا لا يمكنك فقط تمرير 3 مكونات دفعة واحدة إلى Job ، كما هو الحال في MoveSystem؟ لأن. لقد كافحت مع هذا لفترة طويلة ، لكني لا أعرف لماذا لا يعمل بهذه الطريقة. في العينات ، يتم تنفيذ المنعطفات من خلال ComponentDataArray ، لكننا لن نتراجع عن الشرائع.
نرمي الجاهزة على المسرح ، نعلق المكونات ، نربط الكاميرا ، نضع خلفيات مملة ، ونذهب!

الخلاصة
لقد تحرك الرجال من Unity Technologies في الاتجاه الصحيح للتعددية. لا يزال نظام العمل نفسه رطبًا (إصدار ألفا على كل حال) ، ولكنه قابل للاستخدام تمامًا ويتسارع الآن. لسوء الحظ ، فإن المكونات القياسية غير متوافقة مع نظام العمل (ولكن ليس مع ECS بشكل منفصل!) ، لذلك عليك نحت العكازات للتغلب على ذلك. على سبيل المثال ، يقوم شخص واحد من منتدى الوحدة بتنفيذ نظامه المادي لوحدة معالجة الرسومات ، مثل إحراز تقدم.
تم استخدام ECS with Unity من قبل ، وهناك العديد من نظائرها المزدهرة ، على سبيل المثال ،
مقالة مع نظرة عامة على الأكثر شهرة. كما يصف إيجابيات وسلبيات هذا النهج في الهندسة المعمارية.
من نفسي ، يمكنني إضافة ميزة مثل نقاء الشفرة. لقد بدأت بمحاولة تطبيق الحركة في نظام واحد. نما عدد مكونات التبعية بسرعة ، واضطررت إلى تقسيم الشفرة إلى أنظمة صغيرة ومريحة. ويمكن إعادة استخدامها بسهولة في مشروع آخر.
رمز المشروع هنا:
GitHub