خلق برج الدفاع في الوحدة: سيناريوهات وموجات من الأعداء

[الأجزاء الأولى والثانية والثالثة والرابعة من البرنامج التعليمي]

  • دعم أعداء الأحجام الصغيرة والمتوسطة والكبيرة.
  • إنشاء سيناريوهات اللعبة مع موجات متعددة من الأعداء.
  • فصل تكوين الأصول وحالة اللعب.
  • بدء ، وقفة ، والفوز ، والهزيمة وتسريع اللعبة.
  • إنشاء سيناريوهات تكرار لا نهاية لها.

هذا هو الجزء الخامس من سلسلة من الدروس حول إنشاء لعبة بسيطة للدفاع عن البرج . في ذلك ، سوف نتعلم كيفية إنشاء سيناريوهات اللعب التي تولد موجات من الأعداء المختلفة.

تم إنشاء البرنامج التعليمي في الوحدة 2018.4.6f1.


هو الحصول على مريح جدا.

المزيد من الأعداء


ليس من المثير للاهتمام إنشاء نفس المكعب الأزرق في كل مرة. ستكون الخطوة الأولى لدعم سيناريوهات اللعب الأكثر إثارة هي دعم عدة أنواع من الأعداء.

تكوينات العدو


هناك طرق عديدة لجعل الأعداء فريدين من نوعه ، لكننا لن نعقِّد ذلك: فنحن نصنفهم على أنهم صغار ومتوسطون وكبيرون. لوضع علامة عليها ، قم بإنشاء تعداد EnemyType .

 public enum EnemyType { Small, Medium, Large } 

قم بتغيير EnemyFactory بحيث يدعم جميع أنواع الأعداء الثلاثة بدلاً من واحد. لكل الأعداء الثلاثة ، هناك حاجة إلى حقول التكوين نفسها ، لذلك نضيف فئة متداخلة EnemyConfig تحتوي على كل منهم ، ثم نضيف ثلاثة حقول التكوين من هذا النوع إلى المصنع. نظرًا لأن هذه الفئة تستخدم فقط للتهيئة ولن نستخدمها في أي مكان آخر ، يمكنك ببساطة جعل حقولها عامة حتى يتمكن المصنع من الوصول إليها. EnemyConfig نفسها ليست مطلوبة لتكون علنية.

 public class EnemyFactory : GameObjectFactory { [System.Serializable] class EnemyConfig { public Enemy prefab = default; [FloatRangeSlider(0.5f, 2f)] public FloatRange scale = new FloatRange(1f); [FloatRangeSlider(0.2f, 5f)] public FloatRange speed = new FloatRange(1f); [FloatRangeSlider(-0.4f, 0.4f)] public FloatRange pathOffset = new FloatRange(0f); } [SerializeField] EnemyConfig small = default, medium = default, large = default; … } 

دعونا أيضًا نجعل الصحة قابلة للتخصيص لكل عدو ، لأنه من المنطقي أن يكون للأعداء الكبار أكثر من الأعداء.

  [FloatRangeSlider(10f, 1000f)] public FloatRange health = new FloatRange(100f); 

أضف معلمة كتابة إلى Get بحيث يمكنك الحصول على نوع معيّن ، وسيكون النوع الافتراضي متوسطًا. سنستخدم النوع للحصول على التكوين الصحيح ، والذي من أجله طريقة منفصلة مفيدة ، ومن ثم إنشاء العدو وتهيئته كما كان من قبل ، فقط مع وسيطة الصحة المضافة.

  EnemyConfig GetConfig (EnemyType type) { switch (type) { case EnemyType.Small: return small; case EnemyType.Medium: return medium; case EnemyType.Large: return large; } Debug.Assert(false, "Unsupported enemy type!"); return null; } public Enemy Get (EnemyType type = EnemyType.Medium) { EnemyConfig config = GetConfig(type); Enemy instance = CreateGameObjectInstance(config.prefab); instance.OriginFactory = this; instance.Initialize( config.scale.RandomValueInRange, config.speed.RandomValueInRange, config.pathOffset.RandomValueInRange, config.health.RandomValueInRange ); return instance; } 

أضف المعلمة المطلوبة للصحة إلى Enemy.Initialize لتعيين الصحة بدلاً من تحديدها حسب حجم العدو.

  public void Initialize ( float scale, float speed, float pathOffset, float health ) { … Health = health; } 

نخلق تصميم أعداء مختلفين


يمكنك اختيار تصميم الأعداء الثلاثة ، لكن في البرنامج التعليمي سأعمل جاهدين على تحقيق أقصى قدر من البساطة. لقد نسخت الجاهزة الأصلية للعدو واستخدمتها في جميع الأحجام الثلاثة ، غيرت المادة فقط: الأصفر للصغير والأزرق للمتوسط ​​والأحمر للأكبر. لم أغير مقياس المكعب الجاهز ، لكنني استخدمت تكوين مقياس المصنع لضبط الأبعاد. أيضًا ، بناءً على الحجم ، قمت بزيادة صحتهم وتقليل السرعة.


مصنع للأعداء مكعبات من ثلاثة أحجام.

أسرع طريقة هي Game.SpawnEnemy الأنواع الثلاثة في اللعبة عن طريق تغيير Game.SpawnEnemy بحيث يحصل على نوع عشوائي من العدو بدلاً من النوع الأوسط.

  void SpawnEnemy () { GameTile spawnPoint = board.GetSpawnPoint(Random.Range(0, board.SpawnPointCount)); Enemy enemy = enemyFactory.Get((EnemyType)(Random.Range(0, 3))); enemy.SpawnOn(spawnPoint); enemies.Add(enemy); } 


أعداء من أنواع مختلفة.

العديد من المصانع


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


أعداء كروية.

موجات الأعداء


الخطوة الثانية في إنشاء سيناريوهات اللعب هي رفض وضع الأعداء بتردد ثابت. يجب إنشاء الأعداء في موجات متتالية حتى ينتهي البرنامج النصي أو يخسر اللاعب.

تسلسل الخلق


تتكون موجة الأعداء من مجموعة من الأعداء التي يتم إنشاؤها واحدة تلو الأخرى حتى تكتمل الموجة. يمكن أن تحتوي الموجة على أنواع مختلفة من الأعداء ، ويمكن أن يختلف التأخير بين إنشائها. حتى لا نعقد التنفيذ ، سنبدأ بتسلسل التفريخ البسيط الذي يخلق نفس النوع من الأعداء بتردد ثابت. ثم ستكون الموجة مجرد قائمة بهذه التسلسلات.

لتكوين كل تسلسل ، قم بإنشاء فئة EnemySpawnSequence . نظرًا لأنه معقد جدًا ، ضعه في ملف منفصل. يجب أن يعرف التسلسل المصنع الذي سيتم استخدامه ونوع العدو الذي سيتم إنشاؤه وعددهم وتكرارهم. لتبسيط التكوين ، سنجعل المعلمة الأخيرة وقفة ، والتي تحدد مقدار الوقت الذي يجب أن يمر قبل إنشاء العدو التالي. لاحظ أن هذا النهج يسمح لك باستخدام العديد من مصانع العدو في الموجة.

 using UnityEngine; [System.Serializable] public class EnemySpawnSequence { [SerializeField] EnemyFactory factory = default; [SerializeField] EnemyType type = EnemyType.Medium; [SerializeField, Range(1, 100)] int amount = 1; [SerializeField, Range(0.1f, 10f)] float cooldown = 1f; } 

الأمواج


موجة هي مجموعة بسيطة من تسلسل خلق العدو. قم بإنشاء نوع EnemyWave EnemyWave له يبدأ بتسلسل قياسي واحد.

 using UnityEngine; [CreateAssetMenu] public class EnemyWave : ScriptableObject { [SerializeField] EnemySpawnSequence[] spawnSequences = { new EnemySpawnSequence() }; } 

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


موجة من مكعبات متزايدة.

هل يمكنني إضافة تأخير بين التسلسلات؟
يمكنك تنفيذه بشكل غير مباشر. على سبيل المثال ، أدخل تأخيرًا مدته أربع ثوانٍ بين مكعبات صغيرة ومتوسطة الحجم ، وقم بتقليل عدد مكعبات صغيرة بمقدار واحد ، وأدخل تسلسل مكعب صغير واحد مع توقف لمدة أربع ثوانٍ.


أربع ثوان تأخير بين مكعبات صغيرة ومتوسطة.

سيناريوهات


يتم إنشاء سيناريو اللعب من سلسلة من الأمواج. لهذا الغرض ، قم بإنشاء GameScenario أصل GameScenario واحدة من الموجات ، ثم استخدمه لإنشاء السيناريو.

 using UnityEngine; [CreateAssetMenu] public class GameScenario : ScriptableObject { [SerializeField] EnemyWave[] waves = {}; } 

على سبيل المثال ، قمت بإنشاء سيناريو به موجتان من الأعداء الصغيرة والمتوسطة (MSC) ، أولاً مع مكعبات ، ثم مع كرات.


السيناريو مع موجتين من MSC.

حركة تسلسل


تُستخدم أنواع الأصول في إنشاء برامج نصية ، ولكن نظرًا لأنها أصول ، يجب أن تحتوي على بيانات لا تتغير أثناء اللعبة. ومع ذلك ، للمضي قدمًا في السيناريو ، نحتاج بطريقة ما إلى تتبع حالتهما. إحدى الطرق هي تكرار الأصول المستخدمة في اللعبة بحيث يتعقب التكرار حالتها. لكننا لسنا بحاجة إلى تكرار الأصل بأكمله ، فالدولة فقط وروابط الأصل كافية. لذلك دعونا ننشئ فئة منفصلة State ، أولاً لـ EnemySpawnSequence . نظرًا لأنه ينطبق فقط على تسلسل ، فإننا نجعله متداخلًا. تكون صالحة فقط عندما تحتوي على إشارة إلى تسلسل ، لذلك سنمنحه طريقة مُنشئ مع معلمة تسلسل.


نوع حالة متداخلة يشير إلى تسلسله.

 public class EnemySpawnSequence { … public class State { EnemySpawnSequence sequence; public State (EnemySpawnSequence sequence) { this.sequence = sequence; } } } 

عندما نريد أن نبدأ التحرك للأمام في تسلسل ، نحتاج إلى نسخة جديدة من الحالة لهذا الغرض. أضف تسلسلات إلى طريقة Begin ، والتي تقوم ببناء وإرجاع الحالة. بفضل هذا ، سيكون كل من يتصل بـ Begin مسؤولاً عن مطابقة الحالة ، وسيظل التسلسل نفسه بلا جنسية. سيكون من الممكن التقدم بالتوازي عدة مرات في نفس التسلسل.

 public class EnemySpawnSequence { … public State Begin () => new State(this); public class State { … } } 

من أجل البقاء على قيد الحياة بعد إعادة التشغيل الساخنة ، تحتاج إلى جعله قابلاً للتسلسل.

  [System.Serializable] public class State { … } 

عيب هذا النهج هو أنه في كل مرة نقوم بتشغيل تسلسل ، نحتاج إلى إنشاء كائن حالة جديد. يمكننا تجنب تخصيص الذاكرة من خلال جعلها بنية بدلاً من الفصل. هذا أمر طبيعي طالما ظلت الحالة صغيرة. فقط ضع في اعتبارك أن الحالة هي نوع قيمة. عند نقلها ، يتم نسخها ، لذلك تتبعها في مكان واحد.

  [System.Serializable] public struct State { … } 

تتكون حالة التسلسل من جانبين فقط: عدد الأعداء المتولدة وتطور وقت الإيقاف المؤقت. نضيف طريقة Progress ، والتي تزيد من قيمة الإيقاف المؤقت بحلول دلتا الوقت ، ثم Game.Update عند الوصول إلى القيمة Game.Update ، على غرار ما يحدث مع وقت التوليد في Game.Update . سنقوم بزيادة عدد الأعداء في كل مرة يحدث هذا. بالإضافة إلى ذلك ، يجب أن تبدأ قيمة الإيقاف المؤقت بالقيمة القصوى بحيث ينشئ التسلسل أعداء دون توقف مؤقت في البداية.

  int count; float cooldown; public State (EnemySpawnSequence sequence) { this.sequence = sequence; count = 0; cooldown = sequence.cooldown; } public void Progress () { cooldown += Time.deltaTime; while (cooldown >= sequence.cooldown) { cooldown -= sequence.cooldown; count += 1; } } 


الدولة تحتوي فقط على البيانات اللازمة.

هل يمكنني الوصول إلى EnemySpawnSequence.cooldown من الدولة؟
نعم ، لأنه يتم تعيين State في نفس النطاق. لذلك ، تعرف الأنواع المتداخلة عن الأعضاء الخاصين للأنواع التي تحتوي عليها.

يجب أن يستمر التقدم حتى يتم إنشاء العدد المطلوب من الأعداء وينتهي التوقف المؤقت. في هذه المرحلة ، يجب أن يبلغ Progress عن الاكتمال ، ولكن على الأرجح سنقفز قليلاً فوق القيمة. لذلك ، في هذه اللحظة يجب أن نعيد الوقت الإضافي من أجل استخدامه مقدما في التسلسل التالي. لكي يعمل هذا ، تحتاج إلى تحويل دلتا الوقت إلى معلمة. نحتاج أيضًا إلى الإشارة إلى أننا لم ننته بعد ، ويمكن تحقيق ذلك من خلال إرجاع قيمة سالبة.

  public float Progress (float deltaTime) { cooldown += deltaTime; while (cooldown >= sequence.cooldown) { cooldown -= sequence.cooldown; if (count >= sequence.amount) { return cooldown; } count += 1; } return -1f; } 

خلق أعداء في أي مكان


من أجل تسلسل تفرخ الأعداء ، نحتاج إلى تحويل Game.SpawnEnemy إلى طريقة ثابتة عامة أخرى.

  public static void SpawnEnemy (EnemyFactory factory, EnemyType type) { GameTile spawnPoint = instance.board.GetSpawnPoint( Random.Range(0, instance.board.SpawnPointCount) ); Enemy enemy = factory.Get(type); enemy.SpawnOn(spawnPoint); instance.enemies.Add(enemy); } 

نظرًا لأن Game نفسها لم تعد تنشئ أعداء ، فيمكننا إزالة مصنع العدو وسرعة الإنشاء وعملية الترويج للإبداع ورمز إنشاء العدو من Update .

  void Update () { } 

سوف ندعو Game.SpawnEnemy في EnemySpawnSequence.State.Progress بعد زيادة عدد الأعداء.

  public float Progress (float deltaTime) { cooldown += deltaTime; while (cooldown >= sequence.cooldown) { … count += 1; Game.SpawnEnemy(sequence.factory, sequence.type); } return -1f; } 

موجة التقدم


دعونا نتبع نفس النهج في التحرك على طول تسلسل كما هو الحال عند التحرك على طول موجة كاملة. دعنا نعطي EnemyWave طريقة Begin الخاصة به ، والتي تُرجع نسخة جديدة من بنية State المتداخلة. في هذه الحالة ، تحتوي الحالة على مؤشر الموجة وحالة التسلسل النشط الذي نبدأ به مع بداية التسلسل الأول.


حالة موجة تحتوي على حالة التسلسل.

 public class EnemyWave : ScriptableObject { [SerializeField] EnemySpawnSequence[] spawnSequences = { new EnemySpawnSequence() }; public State Begin() => new State(this); [System.Serializable] public struct State { EnemyWave wave; int index; EnemySpawnSequence.State sequence; public State (EnemyWave wave) { this.wave = wave; index = 0; Debug.Assert(wave.spawnSequences.Length > 0, "Empty wave!"); sequence = wave.spawnSequences[0].Begin(); } } } 

نضيف أيضًا أسلوب Progress EnemyWave.State ، والذي يستخدم النهج نفسه كما كان من قبل ، مع تغييرات بسيطة. نبدأ بالتحرك على طول التسلسل النشط واستبدال دلتا الوقت كنتيجة لهذه الدعوة. بينما يتبقى وقت ، فإننا ننتقل إلى التسلسل التالي ، إذا تم الوصول إليه ، ونحرز تقدمًا فيه. إذا لم يتبق أي تسلسل ، فإننا نعيد الوقت المتبقي ؛ بإرجاع قيمة سالبة.

  public float Progress (float deltaTime) { deltaTime = sequence.Progress(deltaTime); while (deltaTime >= 0f) { if (++index >= wave.spawnSequences.Length) { return deltaTime; } sequence = wave.spawnSequences[index].Begin(); deltaTime = sequence.Progress(deltaTime); } return -1f; } 

تعزيز النصي


إضافة GameScenario نفس المعالجة. في هذه الحالة ، تحتوي الحالة على مؤشر الموجة وحالة الموجة النشطة.

 public class GameScenario : ScriptableObject { [SerializeField] EnemyWave[] waves = {}; public State Begin () => new State(this); [System.Serializable] public struct State { GameScenario scenario; int index; EnemyWave.State wave; public State (GameScenario scenario) { this.scenario = scenario; index = 0; Debug.Assert(scenario.waves.Length > 0, "Empty scenario!"); wave = scenario.waves[0].Begin(); } } } 

نظرًا لأننا في المستوى الأعلى ، لا تتطلب طريقة Progress معلمة ويمكنك استخدام Time.deltaTime مباشرة. لا نحتاج إلى إرجاع الوقت المتبقي ، لكننا بحاجة إلى إظهار ما إذا كان البرنامج النصي مكتملًا. سنعود false بعد نهاية الموجة الأخيرة true لإظهار أن البرنامج النصي لا يزال نشطًا.

  public bool Progress () { float deltaTime = wave.Progress(Time.deltaTime); while (deltaTime >= 0f) { if (++index >= scenario.waves.Length) { return false; } wave = scenario.waves[index].Begin(); deltaTime = wave.Progress(deltaTime); } return true; } 

تشغيل البرنامج النصي


لتشغيل برنامج نصي Game ، فأنت بحاجة إلى حقل تكوين البرنامج النصي وتتبع حالته. سنقوم فقط بتشغيل البرنامج النصي في Awake وتشغيل Update عليه حتى يتم Update حالة بقية اللعبة.

  [SerializeField] GameScenario scenario = default; GameScenario.State activeScenario; … void Awake () { board.Initialize(boardSize, tileContentFactory); board.ShowGrid = true; activeScenario = scenario.Begin(); } … void Update () { … activeScenario.Progress(); enemies.GameUpdate(); Physics.SyncTransforms(); board.GameUpdate(); nonEnemies.GameUpdate(); } 

الآن سيتم إطلاق البرنامج النصي المكوّن في بداية اللعبة. سيتم الترويج لها حتى الانتهاء ، وبعد ذلك لن يحدث شيء.


تسارع موجتان 10 مرات.

بدء ونهاية الألعاب


يمكننا إعادة إنتاج سيناريو واحد ، لكن بعد اكتماله لن يظهر أعداء جدد. لكي تستمر اللعبة ، نحتاج إلى تمكين بدء سيناريو جديد ، إما يدويًا أو لأن اللاعب فقد / فاز. يمكنك أيضًا تنفيذ اختيار من بين عدة سيناريوهات ، ولكن في هذا البرنامج التعليمي لن نفكر فيه.

بداية لعبة جديدة


من الناحية المثالية ، نحتاج إلى فرصة لبدء لعبة جديدة في أي وقت محدد. للقيام بذلك ، تحتاج إلى إعادة تعيين الحالة الحالية للعبة بأكملها ، أي أننا سنضطر إلى إعادة تعيين العديد من الكائنات. أولاً ، أضف طريقة Clear إلى GameBehaviorCollection التي تستخدم جميع تصرفاتها.

  public void Clear () { for (int i = 0; i < behaviors.Count; i++) { behaviors[i].Recycle(); } behaviors.Clear(); } 

هذا يشير إلى أنه يمكن التخلص من جميع السلوكيات ، ولكن هذا ليس هو الحال حتى الآن. GameBehavior هذا العمل ، أضف طريقة Recycle مجردة إلى GameBehavior .

  public abstract void Recycle (); 

يجب أن تتجاوز طريقة Recycle الخاصة WarEntity بشكل صريح.

  public override void Recycle () { originFactory.Reclaim(this); } 

ليس لدى Enemy بعد طريقة Recycle ، لذا أضفه. كل ما عليه فعله هو إجبار المصنع على إعادته. ثم ندعو Recycle أينما وصلنا مباشرة إلى المصنع.

  public override bool GameUpdate () { if (Health <= 0f) { Recycle(); return false; } progress += Time.deltaTime * progressFactor; while (progress >= 1f) { if (tileTo == null) { Recycle(); return false; } … } … } public override void Recycle () { OriginFactory.Reclaim(this); } 

تحتاج GameBoard أيضًا إلى إعادة GameBoard ، لذا دعنا نعطيها طريقة Clear ، التي تفرغ جميع المربعات ، GameBoard ضبط جميع نقاط الإنشاء وتحديث المحتوى ، ثم تقوم بتعيين نقاط البداية والنهاية القياسية. بعد ذلك ، بدلاً من تكرار الشفرة ، يمكننا الاتصال بـ Clear في نهاية Initialize .

  public void Initialize ( Vector2Int size, GameTileContentFactory contentFactory ) { … for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { … } } Clear(); } public void Clear () { foreach (GameTile tile in tiles) { tile.Content = contentFactory.Get(GameTileContentType.Empty); } spawnPoints.Clear(); updatingContent.Clear(); ToggleDestination(tiles[tiles.Length / 2]); ToggleSpawnPoint(tiles[0]); } 

الآن يمكننا إضافة أسلوب BeginNewGame إلى Game ، وإلقاء الأعداء ، والكائنات الأخرى والحقل ، ثم بدء تشغيل برنامج نصي جديد.

  void BeginNewGame () { enemies.Clear(); nonEnemies.Clear(); board.Clear(); activeScenario = scenario.Begin(); } 

سنتصل بهذه الطريقة في Update إذا ضغطت على B قبل الانتقال إلى البرنامج النصي.

  void Update () { … if (Input.GetKeyDown(KeyCode.B)) { BeginNewGame(); } activeScenario.Progress(); … } 

خسارة


الهدف من اللعبة هو هزيمة جميع الأعداء قبل أن يصل عدد معين منهم إلى النقطة الأخيرة. يعتمد عدد الأعداء اللازمين لبدء حالة الهزيمة على الحالة الصحية الأولية للاعب ، والتي سنضيف لها حقل تهيئة Game . بما أننا نعول الأعداء ، فسوف نستخدم عددًا صحيحًا وليس تعويمًا.

  [SerializeField, Range(0, 100)] int startingPlayerHealth = 10; 



في البداية ، لدى اللاعب 10 صحة.

في حالة Awake أو بداية لعبة جديدة ، نقوم بتعيين القيمة الأولية لصحة اللاعب الحالية.

  int playerHealth; … void Awake () { playerHealth = startingPlayerHealth; … } void BeginNewGame () { playerHealth = startingPlayerHealth; … } 

أضف طريقة EnemyReachedDestination ثابتة عامة EnemyReachedDestination يتمكن الأعداء من إخبار Game بأنهم وصلوا إلى نقطة النهاية. عندما يحدث هذا ، قلل من صحة اللاعب.

  public static void EnemyReachedDestination () { instance.playerHealth -= 1; } 

استدعاء هذه الطريقة في Enemy.GameUpdate في الوقت المناسب.

  if (tileTo == null) { Game.EnemyReachedDestination(); Recycle(); return false; } 

الآن يمكننا التحقق من حالة الهزيمة في Game.Update . إذا كانت صحة اللاعب تساوي أو تقل عن الصفر ، يتم تشغيل حالة الهزيمة. نحن ببساطة تسجيل هذه المعلومات والبدء فورا لعبة جديدة قبل المضي قدما. لكننا سنفعل ذلك فقط بصحة أولية إيجابية. هذا يسمح لنا باستخدام 0 كصحة أولية ، مما يجعل من المستحيل أن نخسره. لذلك سيكون من المناسب بالنسبة لنا لاختبار النصوص.

  if (playerHealth <= 0 && startingPlayerHealth > 0) { Debug.Log("Defeat!"); BeginNewGame(); } activeScenario.Progress(); 

فوز


البديل للهزيمة هو النصر الذي يتحقق في نهاية السيناريو ، إذا كان اللاعب لا يزال على قيد الحياة. وهذا هو ، عندما تكون نتيجة GameScenario.Progess false ، نعرض رسالة النصر في السجل ، نبدأ لعبة جديدة ، ونتحرك فورًا.

  if (playerHealth <= 0) { Debug.Log("Defeat!"); BeginNewGame(); } if (!activeScenario.Progress()) { Debug.Log("Victory!"); BeginNewGame(); activeScenario.Progress(); } 

ومع ذلك ، سيأتي النصر بعد نهاية الوقفة الأخيرة ، حتى لو كان لا يزال هناك أعداء في الميدان. نحتاج إلى تأجيل النصر حتى تختفي جميع الأعداء ، وهو ما يمكن تحقيقه عن طريق التحقق مما إذا كانت مجموعة الأعداء فارغة. نحن نفترض أن لديها خاصية IsEmpty .

  if (!activeScenario.Progress() && enemies.IsEmpty) { Debug.Log("Victory!"); BeginNewGame(); activeScenario.Progress(); } 

إضافة الخاصية المطلوبة إلى GameBehaviorCollection .

  public bool IsEmpty => behaviors.Count == 0; 

مراقبة الوقت


لنقم أيضًا بتنفيذ ميزة إدارة الوقت ، حيث سيساعد ذلك في الاختبار وغالبًا ما يكون وظيفة اللعب. للبدء ، اسمح لـ Game.Update بالتحقق من وجود مسافة ، واستخدم هذا الحدث لتمكين / تعطيل Game.Update في اللعبة. يمكن القيام بذلك عن طريق تبديل قيم Time.timeScale بين صفر وواحد. هذا لن يغير منطق اللعبة ، لكن سيجعل كل الكائنات متجمدة في مكانها. أو يمكنك استخدام قيمة صغيرة جدًا بدلاً من 0 ، على سبيل المثال 0.01 ، لإنشاء حركة بطيئة للغاية.

  const float pausedTimeScale = 0f; … void Update () { … if (Input.GetKeyDown(KeyCode.Space)) { Time.timeScale = Time.timeScale > pausedTimeScale ? pausedTimeScale : 1f; } if (Input.GetKeyDown(KeyCode.B)) { BeginNewGame(); } … } 

-, Game , .

  [SerializeField, Range(1f, 10f)] float playSpeed = 1f; 


.

, . .

  if (Input.GetKeyDown(KeyCode.Space)) { Time.timeScale = Time.timeScale > pausedTimeScale ? pausedTimeScale : playSpeed; } else if (Time.timeScale > pausedTimeScale) { Time.timeScale = playSpeed; } 


. , . , , , .


GameScenario , 1. , . , , , , .

  [SerializeField, Range(0, 10)] int cycles = 1; 


.

GameScenario.State .

  int cycle, index; EnemyWave.State wave; public State (GameScenario scenario) { this.scenario = scenario; cycle = 0; index = 0; wave = scenario.waves[0].Begin(); } 

Progress , false , . .

  public bool Progress () { float deltaTime = wave.Progress(Time.deltaTime); while (deltaTime >= 0f) { if (++index >= scenario.waves.Length) { if (++cycle >= scenario.cycles && scenario.cycles > 0) { return false; } index = 0; } wave = scenario.waves[index].Begin(); deltaTime = wave.Progress(deltaTime); } return true; } 


, . , . , . .

GameScenario . . , 0.5 ×1, ×1.5, ×2, ×2.5, .

  [SerializeField, Range(0f, 1f)] float cycleSpeedUp = 0.5f; 

GameScenario.State . 1 . Time.deltaTime .

  float timeScale; EnemyWave.State wave; public State (GameScenario scenario) { this.scenario = scenario; cycle = 0; index = 0; timeScale = 1f; wave = scenario.waves[0].Begin(); } public bool Progress () { float deltaTime = wave.Progress(timeScale * Time.deltaTime); while (deltaTime >= 0f) { if (++index >= scenario.waves.Length) { if (++cycle >= scenario.cycles && scenario.cycles > 0) { return false; } index = 0; timeScale += scenario.cycleSpeedUp; } wave = scenario.waves[index].Begin(); deltaTime = wave.Progress(deltaTime); } return true; } 


; .

? Patreon !



PDF

Source: https://habr.com/ru/post/ar466855/


All Articles