دعنا ننتقل من
السجاد إلى أشياء خطيرة. تحدثنا بالفعل عن ECS ، ما هي الأطر الموجودة في Unity ولماذا كتبوا بأنفسهم (يمكنك العثور على القائمة في نهاية المقال). الآن دعنا نتحدث عن أمثلة محددة لكيفية استخدامنا ECS في برنامج PvP الجديد للهاتف المحمول وكيفية تطبيق ميزات اللعبة. ألاحظ أننا نستخدم هذه البنية فقط لمحاكاة العالم على الخادم ونظام التنبؤ على العميل. يتم تنفيذ تصوير الكائنات وعرضها باستخدام نمط MVP - ولكن ليس حول هذا اليوم.
بنية ECS موجهة نحو البيانات ، ويتم تخزين جميع بيانات عالم اللعبة في ما يسمى بـ GameState وهي قائمة بالكيانات (الكيانات) مع بعض المكونات على كل منها. تحدد مجموعة من المكونات سلوك كائن ما. ويتركز منطق السلوك المكون في النظم.
يتكون gestestate في ECS الخاص بنا من جزأين: RuleBook و WorldState. RuleBook عبارة عن مجموعة من المكونات التي لا تتغير أثناء التطابق. يتم تخزين جميع البيانات الثابتة هناك (خصائص الأسلحة / الشخصيات ، تكوين الفرق) ويتم إرسالها إلى العميل مرة واحدة فقط - أثناء التفويض على خادم اللعبة.
النظر في مثال بسيط: تفرخ شخصية ونقلها في مساحة ثنائية الأبعاد باستخدام جويستيك. أولا ، أعلن المكونات.
هذا يحدد اللاعب وهو ضروري لتصور الشخصية:
[Component] public class Player { }
المكون التالي هو طلب لإنشاء شخصية جديدة. يحتوي على حقلين: وقت تفرخ الحرف (بالعلامات) ومعرفه:
[Component] public class PlayerSpawnRequest { public int SpawnTime; public unit PlayerId; }
مكون اتجاه الكائن في الفضاء:
[Component] public class Transform { public Vector2 Position; public float Rotation; }
المكون الذي يخزن السرعة الحالية للكائن:
[Component] public class Movement { public Vector2 Velocity; public float RotateToAngle; }
المكون الذي يخزن مدخلات اللاعب (متجه ذراع التحكم في الحركة ومتجه ذراع التحكم في دوران الأحرف):
[Component] public class Input { public Vector2 MoveVector; public Vector2 RotateVector; }
مكون ذو خصائص ثابتة للشخصية (سيتم تخزينه في RuleBook ، حيث أن هذه خاصية أساسية ولا تتغير خلال جلسة اللعبة):
[Component] public class PlayerStats { public float MoveSpeed; }
عند تحليل ميزة ما في الأنظمة ، نسترشد غالبًا بمبدأ المسؤولية الفردية: يجب أن يفي كل نظام بوظيفة واحدة ووظيفة واحدة فقط.
يمكن أن تتكون الميزات من عدة أنظمة. لنبدأ بتحديد نظام تفرخ الشخصية. يمر النظام بجميع الطلبات لإنشاء شخصية في gamestate وإذا كان الوقت العالمي الحالي يتطابق مع الوقت المطلوب ، فإنه ينشئ كيانًا جديدًا ويربط به المكونات التي تحدد المشغل:
Player ،
Transform ،
Movement .
public class SpawnPlayerSystem : ExecutableSystem { public override void Execute(GameState gs) { var deleter = gs.Pools.Deferred.GetDeleter(gs.WorldState.SpawnAvatarRequest); foreach (var avatarRequest in gs.WorldState.SpawnAvatarRequest) { if (avatarRequest.Value.SpawnTime == gs.Time) {
الآن النظر في حركة اللاعب على عصا التحكم. نحتاج إلى نظام يتعامل مع المدخلات. يمر عبر جميع مكونات المدخلات ، ويحسب سرعة اللاعب (يقف أو يتحرك) ويحول متجه ذراع التحكم في الدوران إلى زاوية الدوران:
MovementControlSystem public class MovementControlSystem : ExecutableSystem { public override void Execute(GameState gs) { var playerStats = gs.RuleBook.PlayerStats[1]; foreach (var pair in gs.Input) { var movement = gs.WorldState.Movement[pair.Key]; movement.Velocity = pair.Value.MoveVector.normalized * playerStats.MoveSpeed; movement.RotateToAngle = Math.Atan2(pair.Value.RotateVector.y, pair.Value.RotateVector.x); } } }
التالي هو نظام الحركة:
public class MovementSystem : ExecutableSystem { public override void Execute(GameState gs) { foreach (var pair in gs.WorldState.Movement) { var transform = gs.WorldState.Transform[pair.Key]; transform.Position += pair.Value.Velocity * GameState.TickDurationSec; } } }
النظام المسؤول عن دوران الكائن:
public class RotationSystem : ExecutableSystem { public override void Execute(GameState gs) { foreach (var pair in gs.WorldState.Movement) { var transform = gs.WorldState.Transform[pair.Key]; transform.Angle = pair.Value.RotateToAngle; } } }
حركة MovementSystem و
RotationSystem تعمل فقط مع مكونات
التحويل والحركة . إنهم مستقلون عن جوهر اللاعب. إذا ظهرت كيانات أخرى مع مكونات
الحركة والتحويل في لعبتنا ، فسيعمل منطق الحركة أيضًا معهم.
على سبيل المثال ، قم بإضافة مجموعة الإسعافات الأولية التي ستتحرك في خط مستقيم بطول البويضة ، وعند تحديد ، قم بتجديد صحة الشخصية. أعلن عن المكونات:
[Component] public class Health { public uint CurrentHealth; public uint MaxHealth; } [Component] public class HealthPowerUp { public uint NextChangeDirection; } [Component] public class HealthPowerUpSpawnRequest { public uint SpawnRequest; } [Component] public class HealthPowerUpStats { public float HealthRestorePercent; public float MoveSpeed; public float SecondsToChangeDirection; public float PickupRadius; public float TimeToSpawn; }
نقوم بتعديل مكون إحصائيات الشخصية بإضافة الحد الأقصى لعدد الأرواح هناك:
[Component] public class PlayerStats { public float MoveSpeed; public uint MaxHealth; }
الآن نقوم بتعديل نظام تفرخ الحروف بحيث تظهر الشخصية بأقصى قدر من الصحة:
public class SpawnPlayerSystem : ExecutableSystem { public override void Execute(GameState gs) { var deleter = gs.Pools.Deferred.GetDeleter(gs.WorldState.SpawnAvatarRequest); var playerStats = gs.RuleBook.PlayerStats[1]; foreach (var avatarRequest in gs.WorldState.SpawnAvatarRequest) { if (avatarRequest.Value.SpawnTime <= gs.Time) {
ثم نعلن نظام تفرخ من مجموعات الإسعافات الأولية لدينا:
public class SpawnHealthPowerUpSystem : ExecutableSystem { public override void Execute(GameState gs) { var deleter = gs.Pools.Deferred.GetDeleter(gs.WorldState.HealthPowerUpSpawnRequest); var healthPowerUpStats = gs.RoolBook.healthPowerUpStats[1]; foreach (var spawnRequest in gs.WorldState.HealthPowerUpSpawnRequest) {
ونظام لتغيير سرعة طقم الإسعافات الأولية. لتبسيط ، فإن مجموعة الإسعافات الأولية سوف تغير الاتجاه كل بضع ثوان:
public class HealthPowerUpMovementSystem : ExecutableSystem { public override void Execute(GameState gs) { var healthPowerUpStats = gs.RoolBook.healthPowerUpStats[1]; foreach (var pair in gs.WorldState.HealthPowerUp) { var movement = gs.WorldState.Movement[pair.Key]; if(pair.Value.NextChangeDirection <= gs.Time) { pair.Value.NextChangeDirection = (uint) (healthPowerUpStats.SecondsToChangeDirection * GameState.Hz); movement.Velocity *= -1; } } } }
نظرًا لأننا أعلنا بالفعل
MovementSystem لنقل الكائنات في اللعبة ، فنحن بحاجة فقط إلى
HealthPowerUpMovementSystem لتغيير ناقل السرعة ، كل N ثانية.
الآن ننتهي من اختيار مجموعة الإسعافات الأولية واستحقاق HP للشخصية. سنحتاج إلى مكون إضافي آخر لتخزين عدد الأرواح التي ستتلقاها الشخصية بعد اختيار مجموعة الإسعافات الأولية.
[Component] public class HealthToAdd { public int Health; public Entity Target; }
وعنصر لإزالة قوتنا:
[Component] public class DeleteHealthPowerUpRequest { }
نكتب نظامًا يعالج اختيار مجموعة الإسعافات الأولية:
public class HealthPowerUpPickUpSystem : ExecutableSystem { public override void Execute(GameState gs) { var healthPowerUpStats = gs.RoolBook.healthPowerUpStats[1]; foreach(var powerUpPair in gs.WorldState.HealthPowerUp) { var powerUpTransform = gs.WorldState.Transform[powerUpPair.Key]; foreach(var playerPair in gs.WorldState.Player) { var playerTransform = gs.WorldState.Transform[playerPair.Key]; var distance = Vector2.Distance(powerUpTransform.Position, playerTransform.Position) if(distance < healthPowerUpStats.PickupRadius) { var healthToAdd = gs.WorldState.Health[playerPair.Key].MaxHealth * healthPowerUpStats.HealthRestorePercent; var entity = gs.WorldState.CreateEntity(); entity.AddHealthToAdd(healthToAdd, gs.WorldState.Player[playerPair.Key]); var powerUpEnity = gs.WorldState[powerUpPair.Key]; powerUpEnity.AddDeleteHealthPowerUpRequest(); break; } } } } }
يمر النظام بجميع عمليات الطاقة النشطة ويحسب المسافة إلى المشغل. إذا كان أي لاعب داخل دائرة نصف قطرها التحديد ، يقوم النظام بإنشاء مكونين من عناصر الطلب:
HealthToAdd - "طلب" لإضافة حياة إلى شخصية ؛
DeleteHealthPowerUpRequest - "طلب" لإزالة مجموعة الإسعافات الأولية.
لماذا لا تضيف العدد الصحيح من الأرواح في نفس النظام؟ ننطلق من حقيقة أن المشغل يستقبل HP ليس فقط من مجموعات الإسعافات الأولية ، ولكن أيضًا من مصادر أخرى. في هذه الحالة ، يكون من المناسب فصل أنظمة اختيار مجموعة الإسعافات الأولية ونظام استحقاق الحياة للشخصية. بالإضافة إلى ذلك ، هذا أكثر اتساقًا مع مبدأ المسؤولية الفردية.
نطبق نظام تراكم الأرواح على الشخصية:
public class HealingSystem : ExecutableSystem { public override void Execute(GameState gs) { var deleter = gs.Pools.Deferred.GetDeleter(gs.WorldState.HealthToAdd); foreach(var healtToAddPair in gs.WorldState.HealthToAdd) { var healthToAdd = healtToAddPair.Value.Health; var health = healtToAddPair.Value.Target.Health; health.CurrentHealth += healthToAdd; health.CurrentHealth = Mathf.Clamp(health.CurrentHealth, 0, health.MaxHealth); deleter.Delete(healtToAddPair.Key); } } }
يمر النظام عبر جميع مكونات
HealthToAdd ، ويحسب العدد المطلوب من الأرواح في المكون
الصحي للكيان
المستهدف . هذا الكيان لا يعرف شيئًا عن المصدر والهدف وهو عالمي تمامًا. يمكن استخدام هذا النظام ليس فقط لحساب حياة الشخصية ، ولكن لأي كائنات تنطوي على وجود الأرواح وتجديدها.
لتنفيذ الميزة مع مجموعات الإسعافات الأولية ، يبقى إضافة النظام الأخير: نظام إزالة مجموعة الإسعافات الأولية بعد اختيارها.
public class DeleteHealthPowerUpSystem : ExecutableSystem { public override void Execute(GameState gs) { var deleter = gs.Pools.Deferred.GetDeleter(gs.WorldState.DeleteHealthPowerUpReques); foreach(var healthRequest in gs.WorldState.DeleteHealthPowerUpReques) { var id = healthRequest.Key; gs.WorldState.DelHealthPowerUp(id); gs.WorldState.DelTransform(id); gs.WorldState.DelMovement(id); deleter.Delete(id); } } }
ينشئ
HealthPowerUpPickUpSystem طلبًا لإزالة مجموعة الإسعافات الأولية. يمر نظام
DeleteHealthPowerUpSystem بكل هذه الطلبات ويزيل جميع المكونات التي تنتمي إلى جوهر مجموعة الإسعافات الأولية.
تم يتم تنفيذ جميع الأنظمة من الأمثلة لدينا. هناك نقطة واحدة للعمل مع ECS - يتم تنفيذ جميع الأنظمة بالتسلسل وهذا الترتيب مهم.
في مثالنا ، ترتيب الأنظمة على النحو التالي:
_systems = new List<ExecutableSystem> { new SpawnPlayerSystem(), new SpawnHealthPowerUpSystem(), new MovementControlSystem(), new HealthPowerUpMovementSystem(), new MovementSystem(), new RotationSystem(), new HealthPowerUpPickUpSystem(), new HealingSystem(), new DeleteHealthPowerUpSystem() };
في الحالة العامة ، تأتي الأنظمة المسؤولة عن إنشاء كيانات ومكونات جديدة أولاً. ثم أنظمة المعالجة ، وأخيرا أنظمة الإزالة والتنظيف.
مع التحلل السليم ، ECS لديها مرونة كبيرة. نعم ، إن تطبيقنا ليس مثاليًا ، لكنه يتيح لك تنفيذ الميزات في وقت قصير ، كما أنه يتميز بأداء جيد على الأجهزة المحمولة الحديثة. يمكنك قراءة المزيد عن ECS هنا: