[ الجزء الأول: البلاط وإيجاد الطريق ]- وضع نقاط خلق العدو.
- ظهور الأعداء وحركتهم عبر الميدان.
- خلق حركة سلسة بسرعة ثابتة.
- تغيير حجم وسرعة ووضع الأعداء.
هذا هو الجزء الثاني من البرنامج التعليمي على لعبة
برج الدفاع بسيطة. يفحص عملية إنشاء الأعداء وحركتهم إلى أقرب نقطة النهاية.
يتكون هذا البرنامج التعليمي في الوحدة 2018.3.0f2.
أعداء في الطريق إلى نقطة النهاية.خلق العدو (تفرخ) نقاط
قبل أن نبدأ في خلق الأعداء ، نحتاج إلى تحديد مكان وضعهم في الميدان. للقيام بذلك ، سنقوم بإنشاء نقاط تفرخ.
بلاط المحتوى
نقطة التفرخ هي نوع آخر من محتوى التجانب ، لذا أضف إدخالًا له في
GameTileContentType
.
public enum GameTileContentType { Empty, Destination, Wall, SpawnPoint }
ثم قم بإنشاء الجاهزة لتصور ذلك. نسخة مكررة من البداية الجاهزة لنقطة البداية مناسبة تمامًا لنا ، ما عليك سوى تغيير نوع المحتوى وإعطائه مادة أخرى. أنا جعلت من البرتقالي.
تكوين نقطة تفرخ.إضافة دعم نقطة تفرخ إلى مصنع المحتوى ومنحها وصلة إلى الجاهزة.
[SerializeField] GameTileContent spawnPointPrefab = default; … public GameTileContent Get (GameTileContentType type) { switch (type) { case GameTileContentType.Destination: return Get(destinationPrefab); case GameTileContentType.Empty: return Get(emptyPrefab); case GameTileContentType.Wall: return Get(wallPrefab); case GameTileContentType.SpawnPoint: return Get(spawnPointPrefab); } Debug.Assert(false, "Unsupported type: " + type); return null; }
مصنع مع دعم لنقاط تفرخ.تمكين أو تعطيل نقاط تفرخ
طريقة تبديل حالة نقطة
GameBoard
، مثل طرق التحويل الأخرى ، سنضيف إلى
GameBoard
. لكن نقاط التفريخ لا تؤثر على البحث عن المسار ، لذلك بعد التغيير لا نحتاج للبحث عن مسارات جديدة.
public void ToggleSpawnPoint (GameTile tile) { if (tile.Content.Type == GameTileContentType.SpawnPoint) { tile.Content = contentFactory.Get(GameTileContentType.Empty); } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.SpawnPoint); } }
اللعبة لا معنى لها إلا إذا كان لدينا أعداء ، وهم بحاجة إلى نقاط تفرخ. لذلك ، يجب أن يحتوي حقل اللعبة على نقطة تفرخ واحدة على الأقل. سنحتاج أيضًا إلى الوصول إلى نقاط التكاثر في المستقبل ، عندما نضيف أعداء ، لذلك دعونا نستخدم القائمة لتتبع جميع المربعات مع هذه النقاط. سنقوم بتحديث القائمة عند تبديل حالة نقطة التفرخ ومنع إزالة آخر نقطة تفرخ.
List<GameTile> spawnPoints = new List<GameTile>(); … public void ToggleSpawnPoint (GameTile tile) { if (tile.Content.Type == GameTileContentType.SpawnPoint) { if (spawnPoints.Count > 1) { spawnPoints.Remove(tile); tile.Content = contentFactory.Get(GameTileContentType.Empty); } } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.SpawnPoint); spawnPoints.Add(tile); } }
يجب أن تقوم طريقة
Initialize
الآن بتعيين نقطة التفرخ لإنشاء الحالة الصحيحة الأولية للحقل. دعونا فقط تضمين البلاط الأول ، الذي هو في الزاوية اليسرى السفلى.
public void Initialize ( Vector2Int size, GameTileContentFactory contentFactory ) { … ToggleDestination(tiles[tiles.Length / 2]); ToggleSpawnPoint(tiles[0]); }
سنجعل لمسة بديلة الآن تبديل حالة نقاط تفرخ ، ولكن عند الضغط على مفتاح Shift الأيسر (يتم فحص ضغط المفاتيح بواسطة أسلوب
Input.GetKey
) ، وحالة نقطة النهاية
void HandleAlternativeTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { if (Input.GetKey(KeyCode.LeftShift)) { board.ToggleDestination(tile); } else { board.ToggleSpawnPoint(tile); } } }
الميدان مع تفرخ النقاط.الوصول إلى نقاط تفرخ
يتعامل الحقل مع كل أشكاله ، لكن الأعداء ليسوا مسؤوليته. سنعمل على الوصول إلى نقاط
GetSpawnPoint
خلال طريقة
GetSpawnPoint
الشائعة باستخدام معلمة فهرس.
public GameTile GetSpawnPoint (int index) { return spawnPoints[index]; }
لمعرفة أي المؤشرات صحيحة ، هناك حاجة إلى معلومات حول عدد نقاط التكاثر ، لذلك سنجعلها عامة باستخدام خاصية getter العامة.
public int SpawnPointCount => spawnPoints.Count;
تفرخ العدو
التفريخ للعدو يشبه إلى حد ما إنشاء محتويات البلاط. نخلق مثيل الجاهزة من خلال المصنع ، والتي نضعها بعد ذلك في الميدان.
مصنع
سنقوم بإنشاء مصنع للأعداء يضعون كل ما يخلقه على خشبة المسرح. هذه الوظيفة شائعة مع المصنع الذي لدينا بالفعل ، لذلك دعونا نضع الكود الخاص به في الفئة الأساسية الشائعة
GameObjectFactory
. سوف نحتاج إلى أسلوب واحد فقط
CreateGameObjectInstance
مع معلمة prefab شائعة ، والتي تقوم بإنشاء مثيل
CreateGameObjectInstance
، وكذلك إدارة المشهد بأكمله. نحن نصنع الطريقة
protected
، أي أنها ستكون متاحة فقط للفئة وجميع الأنواع التي ترثها. هذا هو كل ما يفعله الفصل ، وليس المقصود استخدامه كمصنع يعمل بكامل طاقته. لذلك ، نقوم بتمييزها على أنها
abstract
، والتي لن تسمح لنا بإنشاء مثيلات من كائناتها.
using UnityEngine; using UnityEngine.SceneManagement; public abstract class GameObjectFactory : ScriptableObject { Scene scene; protected T CreateGameObjectInstance<T> (T prefab) where T : MonoBehaviour { if (!scene.isLoaded) { if (Application.isEditor) { scene = SceneManager.GetSceneByName(name); if (!scene.isLoaded) { scene = SceneManager.CreateScene(name); } } else { scene = SceneManager.CreateScene(name); } } T instance = Instantiate(prefab); SceneManager.MoveGameObjectToScene(instance.gameObject, scene); return instance; } }
تغيير
GameTileContentFactory
بحيث
GameTileContentFactory
هذا النوع من المصنع ويستخدم
CreateGameObjectInstance
في أسلوب
Get
الخاص به ، ثم قم بإزالة رمز التحكم في المشهد منه.
using UnityEngine; [CreateAssetMenu] public class GameTileContentFactory : GameObjectFactory { …
بعد ذلك ، قم بإنشاء نوع
EnemyFactory
جديد يقوم بإنشاء مثيل لـ
EnemyFactory
Enemy
واحد باستخدام الأسلوب
Get
جنبًا إلى جنب مع طريقة
Reclaim
المصاحبة.
using UnityEngine; [CreateAssetMenu] public class EnemyFactory : GameObjectFactory { [SerializeField] Enemy prefab = default; public Enemy Get () { Enemy instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; return instance; } public void Reclaim (Enemy enemy) { Debug.Assert(enemy.OriginFactory == this, "Wrong factory reclaimed!"); Destroy(enemy.gameObject); } }
نوع
Enemy
الجديد في البداية كان عليه فقط تتبع مصنعه الأصلي.
using UnityEngine; public class Enemy : MonoBehaviour { EnemyFactory originFactory; public EnemyFactory OriginFactory { get => originFactory; set { Debug.Assert(originFactory == null, "Redefined origin factory!"); originFactory = value; } } }
منزل جاهز
يحتاج الأعداء إلى التصور ، والذي يمكن أن يكون أي شيء - روبوت ، عنكبوت ، شبح ، شيء أبسط ، على سبيل المثال ، مكعب ، نستخدمه. ولكن بشكل عام ، لدى العدو نموذج ثلاثي الأبعاد لأي تعقيد. لضمان دعمها المريح ، سوف نستخدم الكائن الجذر للتسلسل الهرمي للعدو الجاهزة ، والذي يرتبط به مكون
Enemy
فقط.
الجاهزة الجذردعنا ننشئ هذا العنصر العنصر الفرعي الوحيد ، والذي سيكون جذر النموذج. يجب أن تحتوي على قيم تحويل وحدة.
جذر النموذج.تتمثل مهمة هذا الجذر النموذجي في وضع النموذج الثلاثي الأبعاد بالنسبة إلى نقطة الأصل المحلية لإحداثيات العدو ، بحيث يعتبرها نقطة مرجعية يقف عليها العدو أو يخيم عليه. في حالتنا ، سيكون النموذج عبارة عن مكعب قياسي بحجم نصف ، وسأعطيه لونًا أزرق داكن. نجعلها من جذر النموذج ونضع الموضع Y على 0.25 بحيث يقف على الأرض.
نموذج المكعبوبالتالي ، يتكون الجاهزة للعدو من ثلاثة أشياء متداخلة: الجذر الجاهز ، الجذر النموذجي ، والمكعب. قد يبدو وكأنه تمثال نصفي لمكعب بسيط ، ولكن مثل هذا النظام يسمح لك بتحريك وتحريك أي عدو دون القلق بشأن معالمه.
التسلسل الهرمي للعدو الجاهزة.دعونا إنشاء مصنع العدو وتعيين الجاهزة لذلك.
مصنع الأصول.وضع الأعداء في الميدان
لوضع الأعداء في الميدان ، يجب أن تتلقى
Game
رابطًا إلى مصنع الأعداء. نظرًا لأننا نحتاج إلى الكثير من الأعداء ، فسنضيف خيار تهيئة لضبط سرعة التفريخ ، معبراً عنها بعدد الأعداء في الثانية. النطاق المقبول هو 0.1-10 مع القيمة الافتراضية 1.
[SerializeField] EnemyFactory enemyFactory = default; [SerializeField, Range(0.1f, 10f)] float spawnSpeed = 1f;
لعبة مع مصنع العدو وسرعة التفريخ 4.سنقوم بتتبع تقدم عملية التفريخ في
Update
، ونزيده بسرعة أوقات دلتا الوقت. إذا تجاوزت قيمة prggress 1 ، فإننا
SpawnEnemy
العدو باستخدام طريقة
SpawnEnemy
الجديدة. نستمر في القيام بذلك حتى يتجاوز التقدم 1 في حال كانت السرعة عالية جدًا ووقت الإطار طويل جدًا بحيث لا يتم إنشاء العديد من الأعداء في نفس الوقت.
float spawnProgress; … void Update () { … spawnProgress += spawnSpeed * Time.deltaTime; while (spawnProgress >= 1f) { spawnProgress -= 1f; SpawnEnemy(); } }
أليس من الضروري تحديث التقدم في FixedUpdate؟نعم ، هذا ممكن ، لكن مثل هذه المواعيد الدقيقة ليست مطلوبة للعبة الدفاع عن البرج. سنقوم ببساطة بتحديث حالة اللعبة في كل إطار وجعلها تعمل بشكل جيد بما يكفي لأي دلتا الوقت.
واسمحوا
SpawnEnemy
الحصول على نقطة تفرخ عشوائي من الميدان وخلق عدو في هذا البلاط. سوف نعطي
Enemy
طريقة
SpawnOn
نفسه بشكل صحيح.
void SpawnEnemy () { GameTile spawnPoint = board.GetSpawnPoint(Random.Range(0, board.SpawnPointCount)); Enemy enemy = enemyFactory.Get(); enemy.SpawnOn(spawnPoint); }
في الوقت الحالي ، كل ما على
SpawnOn
فعله هو تعيين موقعها الخاص مساوٍ لمركز البلاط. منذ أن تم وضع نموذج الجاهزة بشكل صحيح ، سيكون المكعب العدو أعلى هذا البلاط.
public void SpawnOn (GameTile tile) { transform.localPosition = tile.transform.localPosition; }
أعداء تظهر في نقاط تفرخ.نقل الأعداء
بعد ظهور العدو ، يجب أن يبدأ التحرك على طول الطريق إلى أقرب نقطة نهاية. لتحقيق ذلك ، تحتاج إلى تحريك الأعداء. نبدأ مع انزلاق سلس بسيط من البلاط إلى البلاط ، ثم نجعل حركتهم أكثر صعوبة.
مجموعة من الأعداء
لتحديث حالة الأعداء ، سنستخدم نفس الطريقة التي تم استخدامها في سلسلة البرامج التعليمية
لإدارة الكائنات . نضيف
Enemy
طريقة
GameUpdate
العامة ، والتي تعرض معلومات حول ما إذا كان على قيد الحياة ، والذي سيكون دائمًا صحيحًا في هذه المرحلة. الآن ، فقط اجعله يتحرك للأمام وفقًا لدلتا الوقت.
public bool GameUpdate () { transform.localPosition += Vector3.forward * Time.deltaTime; return true; }
بالإضافة إلى ذلك ، نحتاج إلى الاحتفاظ بقائمة من الأعداء الأحياء وتحديثهم جميعًا ، وإزالتهم من قائمة الأعداء القتلى. يمكننا وضع كل هذا الرمز في
Game
، ولكن بدلاً من ذلك ، عزله وإنشاء نوع
EnemyCollection
. هذه فئة قابلة للتسلسل ولا ترث من أي شيء. نقدم له طريقة عامة لإضافة عدو وطريقة أخرى لتحديث المجموعة بأكملها.
using System.Collections.Generic; [System.Serializable] public class EnemyCollection { List<Enemy> enemies = new List<Enemy>(); public void Add (Enemy enemy) { enemies.Add(enemy); } public void GameUpdate () { for (int i = 0; i < enemies.Count; i++) { if (!enemies[i].GameUpdate()) { int lastIndex = enemies.Count - 1; enemies[i] = enemies[lastIndex]; enemies.RemoveAt(lastIndex); i -= 1; } } } }
ستكون
Game
الآن كافية لإنشاء مجموعة واحدة فقط ، في كل إطار يقوم بتحديثها وإضافة أعداء تم إنشاؤها إليها. سنقوم بتحديث الأعداء مباشرة بعد وضع البيض المحتمل لعدو جديد حتى يتم التحديث على الفور.
EnemyCollection enemies = new EnemyCollection(); … void Update () { … enemies.GameUpdate(); } … void SpawnEnemy () { … enemies.Add(enemy); }
أعداء يتحركون إلى الأمام.حركة على طول الطريق
الأعداء يتحركون بالفعل ، ولكن حتى الآن لا تتبع المسار. للقيام بذلك ، يحتاجون إلى معرفة إلى أين يذهبون بعد ذلك. لذلك ، دعونا نعطي
GameTile
خاصية getter شائعة للحصول على التجانب التالي على المسار.
public GameTile NextTileOnPath => nextOnPath;
عند معرفة التجانب الذي ترغب في الخروج منه ، والبلاط الذي تريد الحصول عليه ، يمكن للأعداء تحديد نقاط البداية والنهاية لتحريك بلاطة واحدة. يمكن للعدو استيفاء الموقف بين هاتين النقطتين ، وتتبع حركتهم. بعد اكتمال الخطوة ، تتكرر هذه العملية للبلاط التالي. لكن المسارات يمكن أن تتغير في أي وقت. بدلاً من تحديد أين نتحرك أكثر في عملية الحركة ، نواصل ببساطة التحرك على طول الطريق المخطط له والتحقق منه ، والوصول إلى البلاط التالي.
دع
Enemy
يتعقب كلا التجانبين بحيث لا يتأثر بتغيير في المسار. كما سيتتبع المواقف حتى لا نضطر إلى استلامها في كل إطار ، وتتبع عملية التحرك.
GameTile tileFrom, tileTo; Vector3 positionFrom, positionTo; float progress;
تهيئة هذه الحقول في
SpawnOn
. النقطة الأولى هي البلاط الذي ينتقل منه العدو ، ونقطة النهاية هي البلاط التالي على المسار. هذا يفترض وجود المربع التالي ، إلا إذا تم إنشاء العدو في نقطة النهاية ، وهو ما يجب أن يكون مستحيلًا. ثم نقوم بالتخزين المؤقت لمواقف البلاط وإعادة ضبط التقدم. لا نحتاج إلى تعيين موضع العدو هنا ، لأن طريقة
GameUpdate
تسمى في نفس الإطار.
public void SpawnOn (GameTile tile) {
سيتم تنفيذ زيادة التقدم في
GameUpdate
. دعنا نضيف دلتا وقت ثابت حتى يتحرك الأعداء بسرعة واحدة في الثانية الواحدة. عند اكتمال التقدم ، نقوم بتحويل البيانات بحيث تصبح القيمة
From
،
To
الجديدة هي المجموعة التالية على المسار. ثم نخفض التقدم. عندما تصبح البيانات ذات صلة ، فإننا نقحم موضع العدو بين
From
و
To
. نظرًا لأن المترجم قيد التقدم ، فإن قيمته هي بالضرورة في حدود 0 و 1 ، لذلك يمكننا استخدام s
Vector3.LerpUnclamped
.
public bool GameUpdate () { progress += Time.deltaTime; while (progress >= 1f) { tileFrom = tileTo; tileTo = tileTo.NextTileOnPath; positionFrom = positionTo; positionTo = tileTo.transform.localPosition; progress -= 1f; } transform.localPosition = Vector3.LerpUnclamped(positionFrom, positionTo, progress); return true; }
هذا يفرض على الأعداء اتباع المسار ، لكنه لن يتصرف عند الوصول إلى نقطة النهاية. لذلك ، قبل تغيير مواضع
From
و
To
، تحتاج إلى مقارنة التجانب التالي على المسار مع
null
. إذا كان الأمر كذلك ، فقد وصلنا إلى نقطة النهاية والعدو قد أنهى الحركة. ننفذ استعادة لذلك ونعود
false
.
while (progress >= 1f) { tileFrom = tileTo; tileTo = tileTo.NextTileOnPath; if (tileTo == null) { OriginFactory.Reclaim(this); return false; } positionFrom = positionTo; positionTo = tileTo.transform.localPosition; progress -= 1f; }
أعداء اتباع أقصر طريق.الأعداء ينتقلون الآن من وسط البلاط إلى آخر. تجدر الإشارة إلى أنهم يغيرون حالة حركتهم فقط في مراكز التجانب ، وبالتالي لا يمكنهم الاستجابة على الفور للتغيرات في هذا المجال. هذا يعني أنه في بعض الأحيان يتحرك الأعداء من خلال الجدران فقط بمجرد أن يبدأوا التحرك نحو الخلية ، لن يمنعهم شيء. لهذا السبب تحتاج الجدران أيضًا إلى مسارات حقيقية.
رد فعل الأعداء لتغيير المسارات.حركة من الحافة إلى الحافة
تبدو الحركة بين مراكز البلاط والتغيير الحاد في الاتجاه أمرًا عاديًا بالنسبة للعبة مجردة يحرك فيها الأعداء مكعبات ، ولكن عادةً ما تكون الحركة السلسة أكثر جمالا. الخطوة الأولى لتنفيذه هي الحركة ليس في المراكز ، ولكن على طول حواف البلاط.
يمكن العثور على نقطة الحافة بين البلاط المجاور عن طريق حساب متوسط مواقعها. بدلاً من حسابها في كل خطوة لكل عدو ،
GameTile.GrowPathTo
فقط عند تغيير المسار في
GameTile.GrowPathTo
. اجعله متاحًا باستخدام خاصية
ExitPoint
.
public Vector3 ExitPoint { get; private set; } … GameTile GrowPathTo (GameTile neighbor) { … neighbor.ExitPoint = (neighbor.transform.localPosition + transform.localPosition) * 0.5f; return neighbor.Content.Type != GameTileContentType.Wall ? neighbor : null; }
الحالة الخاصة الوحيدة هي الخلية النهائية ، والتي ستكون نقطة الخروج منها مركزها.
public void BecomeDestination () { distance = 0; nextOnPath = null; ExitPoint = transform.localPosition; }
تغيير
Enemy
بحيث يستخدم نقاط الخروج ، وليس مراكز التجانب.
public bool GameUpdate () { progress += Time.deltaTime; while (progress >= 1f) { … positionTo = tileFrom.ExitPoint; progress -= 1f; } transform.localPosition = Vector3.Lerp(positionFrom, positionTo, progress); return true; } public void SpawnOn (GameTile tile) { … positionTo = tileFrom.ExitPoint; progress = 0f; }
الأعداء يتحركون بين الحواف.يتمثل أحد الآثار الجانبية لهذا التغيير في أنه عندما يتحول الأعداء بسبب التغيير في المسار ، يظلون بلا حراك لمدة ثانية.
عندما تحول ، تتوقف الأعداء.توجيه
على الرغم من أن الأعداء يتحركون على طول المسارات حتى يغيروا اتجاههم. حتى يتمكنوا من النظر في اتجاه الحركة ، فهم بحاجة إلى معرفة اتجاه المسار الذي يتبعونه. سنحدد هذا أيضًا أثناء البحث عن طرق ، بحيث لا يتعين على الأعداء القيام بذلك.
لدينا أربعة اتجاهات: الشمال والشرق والجنوب والغرب. دعونا نعددهم.
public enum Direction { North, East, South, West }
ثم نعطي خاصية
GameTile
لتخزين اتجاه مسارها.
public Direction PathDirection { get; private set; }
إضافة معلمة اتجاه إلى
GrowTo
، والذي يعين الخاصية. نظرًا لأننا نزرع طريقًا من النهاية إلى البداية ، فإن الاتجاه سيكون عكس المكان الذي نزرع فيه المسار.
public GameTile GrowPathNorth () => GrowPathTo(north, Direction.South); public GameTile GrowPathEast () => GrowPathTo(east, Direction.West); public GameTile GrowPathSouth () => GrowPathTo(south, Direction.North); public GameTile GrowPathWest () => GrowPathTo(west, Direction.East); GameTile GrowPathTo (GameTile neighbor, Direction direction) { … neighbor.PathDirection = direction; return neighbor.Content.Type != GameTileContentType.Wall ? neighbor : null; }
نحن بحاجة إلى تحويل الاتجاهات إلى المنعطفات التي يتم التعبير عنها كرباعي. سيكون من المريح إذا استطعنا فقط استدعاء
GetRotation
للحصول على الاتجاه ، لذلك دعونا نفعل ذلك عن طريق إنشاء طريقة تمديد. أضف الأسلوب الثابت العام
DirectionExtensions
، أعطه صفيفًا للتخزين المؤقت
GetRotation
الضرورية ، وكذلك طريقة
GetRotation
لإرجاع قيمة الاتجاه المقابلة. في هذه الحالة ، من المنطقي وضع فئة الامتداد في نفس الملف مثل نوع التعداد.
using UnityEngine; public enum Direction { North, East, South, West } public static class DirectionExtensions { static Quaternion[] rotations = { Quaternion.identity, Quaternion.Euler(0f, 90f, 0f), Quaternion.Euler(0f, 180f, 0f), Quaternion.Euler(0f, 270f, 0f) }; public static Quaternion GetRotation (this Direction direction) { return rotations[(int)direction]; } }
ما هي طريقة التمديد؟طريقة التمديد هي طريقة ثابتة داخل فئة ثابتة تتصرف كطريقة مثيل من نوع ما. يمكن أن يكون هذا النوع فئة أو واجهة أو بنية أو قيمة أولية أو تعداد. يجب أن تحتوي الوسيطة الأولى لطريقة الامتداد على this
. إنه يحدد قيمة النوع والمثيل الذي ستعمل به الطريقة. هذا النهج يعني أن توسيع الخصائص غير ممكن.
هل هذا يسمح لك بإضافة طرق إلى أي شيء؟ نعم ، تمامًا كما يمكنك كتابة أي طريقة ثابتة لها أي نوع.
الآن يمكننا تدوير
Enemy
عند التفريخ وفي كل مرة ندخل فيها بلاطة جديدة. بعد تحديث البيانات ، يعطينا البلاط
From
الاتجاه.
public bool GameUpdate () { progress += Time.deltaTime; while (progress >= 1f) { … transform.localRotation = tileFrom.PathDirection.GetRotation(); progress -= 1f; } transform.localPosition = Vector3.LerpUnclamped(positionFrom, positionTo, progress); return true; } public void SpawnOn (GameTile tile) { … transform.localRotation = tileFrom.PathDirection.GetRotation(); progress = 0f; }
تغيير الاتجاه
بدلاً من تغيير الاتجاه على الفور ، من الأفضل أن أقحم القيم بين المنعطفات ، على غرار الطريقة التي تم بها تقريب المواقف بين المواقع. للانتقال من اتجاه إلى آخر ، نحتاج إلى معرفة التغيير في الاتجاه الذي يجب القيام به: بدون الدوران أو الدوران يمينًا أو الدوران يسارًا أو الرجوع إلى الخلف. نضيف لهذا التعداد ، والذي يمكن وضعه مرة أخرى في نفس الملف مثل
Direction
، لأنها صغيرة وذات صلة وثيقة.
public enum Direction { North, East, South, West } public enum DirectionChange { None, TurnRight, TurnLeft, TurnAround }
أضف طريقة تمديد أخرى ، وهذه المرة
GetDirectionChangeTo
، والتي ترجع تغيير الاتجاه من الاتجاه الحالي إلى التالي. إذا كانت الاتجاهات تتزامن ، فلا يوجد تحول. إذا كان القادم واحد أكثر من الحالي ، فهذا بدوره إلى اليمين. ولكن بما أن التوجيهات تتكرر ، فإن نفس الموقف سيكون عندما يكون الوضع التالي ثلاثة أقل من الوضع الحالي. مع المنعطف الأيسر ، ستكون هي نفسها ، فقط التبديل والطرح سوف يحولان الأماكن.
الحالة الوحيدة المتبقية هي العودة إلى الوراء. public static DirectionChange GetDirectionChangeTo ( this Direction current, Direction next ) { if (current == next) { return DirectionChange.None; } else if (current + 1 == next || current - 3 == next) { return DirectionChange.TurnRight; } else if (current - 1 == next || current + 3 == next) { return DirectionChange.TurnLeft; } return DirectionChange.TurnAround; }
نحن نقوم بالتناوب في بعد واحد فقط ، لذا فإن الاستيفاء الخطي للزوايا سيكون كافياً بالنسبة لنا. أضف طريقة توسيع أخرى تحصل على زاوية الاتجاه بالدرجات. public static float GetAngle (this Direction direction) { return (float)direction * 90f; }
الآن يجب عليك Enemy
تتبع الاتجاه وتغيير الاتجاه والزوايا التي تحتاج إلى إجراء الاستيفاء بينها. Direction direction; DirectionChange directionChange; float directionAngleFrom, directionAngleTo;
SpawnOn
تزداد صعوبة ، لذلك دعونا نقل رمز إعداد الحالة إلى طريقة أخرى. سنقوم بتعيين الحالة الأولية للعدو كدولة تمهيدية ، لذلك سوف نسميها PrepareIntro
. في هذه الحالة ، ينتقل العدو من المركز إلى حافة بلاطه الأولي ، لذلك لا يوجد تغيير في الاتجاه. في الزوايا From
و To
على نفسه. public void SpawnOn (GameTile tile) { Debug.Assert(tile.NextTileOnPath != null, "Nowhere to go!", this); tileFrom = tile; tileTo = tile.NextTileOnPath;
في هذه المرحلة ، نقوم بإنشاء شيء يشبه آلة الحالة الصغيرة. لإبقاء الأمور بسيطة GameUpdate
، انقل رمز الحالة إلى طريقة جديدة PrepareNextState
. ترك فقط تغيير البلاط From
و To
لنستخدمها هنا لاختبار ما إذا كان العدو قد انتهى الطريق. public bool GameUpdate () { progress += Time.deltaTime; while (progress >= 1f) { …
عند الانتقال إلى حالة جديدة ، تحتاج دائمًا إلى تغيير المواضع ، والعثور على تغيير في الاتجاه ، وتحديث الاتجاه الحالي ، وتحويل الزاوية To
إلى From
. نحن لم نعد مجموعة بدوره. void PrepareNextState () { positionFrom = positionTo; positionTo = tileFrom.ExitPoint; directionChange = direction.GetDirectionChangeTo(tileFrom.PathDirection); direction = tileFrom.PathDirection; directionAngleFrom = directionAngleTo; }
إجراءات أخرى تعتمد على تغيير الاتجاه. دعنا نضيف طريقة لكل خيار. في حالة المضي قدمًا ، To
تتزامن الزاوية مع اتجاه مسار الخلية الحالية. بالإضافة إلى ذلك ، نحتاج إلى ضبط التناوب بحيث يتطلع العدو إلى الأمام مباشرة. void PrepareForward () { transform.localRotation = direction.GetRotation(); directionAngleTo = direction.GetAngle(); }
في حالة وجود منعطف ، فإننا لا نتحول على الفور. نحتاج إلى الانتقال إلى زاوية مختلفة: 90 درجة للانعطاف إلى اليمين ، و 90 درجة للانعطاف لليسار ، و 180 درجة للرجوع إلى الخلف. لتجنب الانعطاف في الاتجاه الخاطئ بسبب التغيير في قيم الزاوية من 359 درجة إلى 0 درجة ، To
يجب الإشارة إلى الزاوية بالنسبة إلى الاتجاه الحالي. لا داعي للقلق من أن تصبح الزاوية أقل من 0 درجة أو أكثر من 360 درجة ، لأننا نستطيع Quaternion.Euler
التعامل معها. void PrepareTurnRight () { directionAngleTo = directionAngleFrom + 90f; } void PrepareTurnLeft () { directionAngleTo = directionAngleFrom - 90f; } void PrepareTurnAround () { directionAngleTo = directionAngleFrom + 180f; }
في النهاية ، PrepareNextState
يمكننا استخدام switch
لتغيير الاتجاهات لتحديد أي من الطرق الأربعة للاتصال. void PrepareNextState () { … switch (directionChange) { case DirectionChange.None: PrepareForward(); break; case DirectionChange.TurnRight: PrepareTurnRight(); break; case DirectionChange.TurnLeft: PrepareTurnLeft(); break; default: PrepareTurnAround(); break; } }
الآن في النهاية GameUpdate
نحتاج إلى التحقق مما إذا كان الاتجاه قد تغير. إذا كان الأمر كذلك ، فحرف بين الزاويتين واضبط الدوران. public bool GameUpdate () { … transform.localPosition = Vector3.LerpUnclamped(positionFrom, positionTo, progress); if (directionChange != DirectionChange.None) { float angle = Mathf.LerpUnclamped( directionAngleFrom, directionAngleTo, progress ); transform.localRotation = Quaternion.Euler(0f, angle, 0f); } return true; }
أعداء يتحولون.حركة المنحنى
يمكننا تحسين الحركة بجعل الأعداء يتحركون على طول المنحنى عند الدوران. بدلاً من المشي من الحافة إلى الحافة من البلاط ، دعهم يمشون ربع الدائرة. يقع مركز هذه الدائرة في زاوية مشتركة بين البلاط From
وعلى To
نفس الحافة التي دخل العدو من خلالها البلاط From
.ربع دائرة دوران إلى اليمين.يمكننا أن ندرك هذا من خلال تحريك العدو في قوس باستخدام علم المثلثات ، وفي نفس الوقت تحويله. ولكن يمكن تبسيط ذلك باستخدام التناوب فقط ، ونقل الأصل المحلي لإحداثيات العدو إلى وسط الدائرة. للقيام بذلك ، نحتاج إلى تغيير موضع نموذج العدو ، لذلك سنقدم Enemy
رابطًا لهذا النموذج ، يمكن الوصول إليه من خلال حقل التكوين. [SerializeField] Transform model = default;
العدو مع الإشارة إلى النموذج.استعدادًا للمضي قدمًا أو الرجوع للخلف ، يجب أن ينتقل النموذج إلى الموضع القياسي ، إلى الأصل المحلي لإحداثيات العدو. خلاف ذلك ، يجب إزاحة النموذج بمقدار نصف وحدة القياس - نصف قطر دائرة الدوران ، بعيدًا عن نقطة التحول. void PrepareForward () { transform.localRotation = direction.GetRotation(); directionAngleTo = direction.GetAngle(); model.localPosition = Vector3.zero; } void PrepareTurnRight () { directionAngleTo = directionAngleFrom + 90f; model.localPosition = new Vector3(-0.5f, 0f); } void PrepareTurnLeft () { directionAngleTo = directionAngleFrom - 90f; model.localPosition = new Vector3(0.5f, 0f); } void PrepareTurnAround () { directionAngleTo = directionAngleFrom + 180f; model.localPosition = Vector3.zero; }
الآن يجب أن ينتقل العدو نفسه إلى نقطة التحول. للقيام بذلك ، يجب أيضًا نقله نصف وحدة القياس ، لكن الإزاحة الدقيقة تعتمد على الاتجاه. دعونا نضيف Direction
طريقة تمديد مساعدة لهذا GetHalfVector
. static Vector3[] halfVectors = { Vector3.forward * 0.5f, Vector3.right * 0.5f, Vector3.back * 0.5f, Vector3.left * 0.5f }; … public static Vector3 GetHalfVector (this Direction direction) { return halfVectors[(int)direction]; }
أضف المتجه المقابل عند الدوران الأيمن أو الأيسر. void PrepareTurnRight () { directionAngleTo = directionAngleFrom + 90f; model.localPosition = new Vector3(-0.5f, 0f); transform.localPosition = positionFrom + direction.GetHalfVector(); } void PrepareTurnLeft () { directionAngleTo = directionAngleFrom - 90f; model.localPosition = new Vector3(0.5f, 0f); transform.localPosition = positionFrom + direction.GetHalfVector(); }
وعند العودة إلى الوراء ، ينبغي أن يكون الموقف هو نقطة البداية المعتادة. void PrepareTurnAround () { directionAngleTo = directionAngleFrom + 180f; model.localPosition = Vector3.zero; transform.localPosition = positionFrom; }
بالإضافة إلى ذلك ، عند حساب نقطة الخروج ، يمكننا استخدام GameTile.GrowPathTo
نصف المتجه بحيث لا نحتاج إلى الوصول إلى موقعي التجانب. neighbor.ExitPoint = neighbor.transform.localPosition + direction.GetHalfVector();
الآن ، عند تغيير الاتجاهات ، لا يتعين علينا استيفاء الموقف فيه Enemy.GameUpdate
، لأن الدوران مشارك في الحركة. public bool GameUpdate () { … if (directionChange == DirectionChange.None) { transform.localPosition = Vector3.LerpUnclamped(positionFrom, positionTo, progress); }
أعداء ينحني بسلاسة حول الزوايا.سرعة ثابتة
حتى هذه النقطة ، كانت سرعة الأعداء دائمًا مساوية لبلاطة واحدة في الثانية ، بغض النظر عن كيفية تحركها داخل التجانب. لكن المسافة التي يغطيونها تعتمد على حالتهم ، لذلك تختلف سرعتها ، معبراً عنها بوحدات في الثانية الواحدة. لكي تكون هذه السرعة ثابتة ، نحتاج إلى تغيير سرعة التقدم وفقًا للحالة. لذلك ، أضف مجال مضاعف التقدم واستخدمه لقياس الدلتا GameUpdate
. float progress, progressFactor; … public bool GameUpdate () { progress += Time.deltaTime * progressFactor; … }
ولكن إذا تغير التقدم وفقًا للحالة ، فلا يمكن استخدام قيمة التقدم المتبقية مباشرة للحالة التالية. لذلك ، قبل التحضير لدولة جديدة ، نحتاج إلى تطبيع التقدم وتطبيق المضاعف الجديد بالفعل في حالة جديدة. public bool GameUpdate () { progress += Time.deltaTime * progressFactor; while (progress >= 1f) { …
لا يتطلب التقدم إلى الأمام تغييرات ، لذلك ، يستخدم عامل 1. عند الدوران يمينًا أو يسارًا ، يمر العدو بربع دائرة نصف قطرها ½ ، وبالتالي فإن المسافة المغطاة هي ¼π. progress
يساوي واحد مقسوما على هذه القيمة. لا يجب أن تستغرق عملية العودة إلى الوراء الكثير من الوقت ، لذلك ضاعف التقدم بحيث يستغرق نصف ثانية. أخيرًا ، تغطي الحركة التمهيدية نصف البلاط فقط ، وبالتالي للحفاظ على سرعة ثابتة ، يجب أيضًا مضاعفة تقدمه. void PrepareForward () { … progressFactor = 1f; } void PrepareTurnRight () { … progressFactor = 1f / (Mathf.PI * 0.25f); } void PrepareTurnLeft () { … progressFactor = 1f / (Mathf.PI * 0.25f); } void PrepareTurnAround () { … progressFactor = 2f; } void PrepareIntro () { … progressFactor = 2f; }
لماذا المسافة تساوي 1/4 * pi؟محيط 2 أضعاف نصف القطر. لا يغطي الانتقال إلى اليمين أو اليسار ربع هذا الطول فقط ، ونصف القطر هو ½ ، وبالتالي فإن المسافة هي ½π × ½.
الحالة النهائية
نظرًا لأن لدينا حالة تمهيدية ، دعنا نضيف حالة نهائية. يختفي الأعداء حاليًا فور وصولهم إلى نقطة النهاية ، ولكن دعنا نؤجل اختفائهم إلى أن يصلوا إلى منتصف المربع النهائي. لنقم بإنشاء طريقة لذلك PrepareOutro
، قم بتعيين الحركة الأمامية ، ولكن فقط في مركز البلاط مع تقدم مضاعف للحفاظ على سرعة ثابتة. void PrepareOutro () { positionTo = tileFrom.transform.localPosition; directionChange = DirectionChange.None; directionAngleTo = direction.GetAngle(); model.localPosition = Vector3.zero; transform.localRotation = direction.GetRotation(); progressFactor = 2f; }
من أجل GameUpdate
عدم تدمير العدو في وقت مبكر جدا ، سنقوم بإزالة تحول البلاط منه. سيفعل ذلك الآن PrepareNextState
. وبالتالي ، التحقق من null
عوائد true
فقط بعد نهاية الحالة النهائية. public bool GameUpdate () { progress += Time.deltaTime * progressFactor; while (progress >= 1f) {
في PrepareNextState
سنبدأ مع التحول من البلاط. بعد ذلك ، بعد تعيين الموضع From
، ولكن قبل تعيين الموضع ، To
سوف نتحقق مما إذا كانت البلاطة تساوي To
القيمة null
. إذا كان الأمر كذلك ، فقم بإعداد الحالة النهائية وتخطي باقي الطريقة. void PrepareNextState () { tileFrom = tileTo; tileTo = tileTo.NextTileOnPath; positionFrom = positionTo; if (tileTo == null) { PrepareOutro(); return; } positionTo = tileFrom.ExitPoint; … }
أعداء بسرعة ثابتة والحالة النهائية.تقلب العدو
لدينا مجموعة من الأعداء ، وهم كلهم نفس المكعب ، يتحركون بنفس السرعة. والنتيجة أشبه ثعبان طويل من الأعداء الفردية. لنجعلهم مختلفين أكثر عن طريق تخصيص حجمهم ونزوحهم وسرعتهم بشكل عشوائي.تعويم نطاق القيمة
سنقوم بتغيير معالم الأعداء ، واختيار عشوائي خصائصها من مجموعة من القيم. ستكون البنية FloatRange
التي أنشأناها في مقالة إدارة الكائنات ، تكوين الأشكال مفيدة هنا ، لذلك فلننسخها. التغييرات الوحيدة هي إضافة مُنشئ ذو معلمة واحدة وفتح الوصول إلى الحد الأدنى والحد الأقصى باستخدام الخصائص للقراءة فقط ، بحيث كان الفاصل الزمني غير قابل للتغيير. using UnityEngine; [System.Serializable] public struct FloatRange { [SerializeField] float min, max; public float Min => min; public float Max => max; public float RandomValueInRange { get { return Random.Range(min, max); } } public FloatRange(float value) { min = max = value; } public FloatRange (float min, float max) { this.min = min; this.max = max < min ? min : max; } }
نقوم أيضًا بنسخ السمة التي تم تعيينها إليها للحد من الفاصل الزمني. using UnityEngine; public class FloatRangeSliderAttribute : PropertyAttribute { public float Min { get; private set; } public float Max { get; private set; } public FloatRangeSliderAttribute (float min, float max) { Min = min; Max = max < min ? min : max; } }
نحن بحاجة فقط لتصور شريط التمرير ، لذا انسخه FloatRangeSliderDrawer
إلى مجلد محرر . using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(FloatRangeSliderAttribute))] public class FloatRangeSliderDrawer : PropertyDrawer { public override void OnGUI ( Rect position, SerializedProperty property, GUIContent label ) { int originalIndentLevel = EditorGUI.indentLevel; EditorGUI.BeginProperty(position, label, property); position = EditorGUI.PrefixLabel( position, GUIUtility.GetControlID(FocusType.Passive), label ); EditorGUI.indentLevel = 0; SerializedProperty minProperty = property.FindPropertyRelative("min"); SerializedProperty maxProperty = property.FindPropertyRelative("max"); float minValue = minProperty.floatValue; float maxValue = maxProperty.floatValue; float fieldWidth = position.width / 4f - 4f; float sliderWidth = position.width / 2f; position.width = fieldWidth; minValue = EditorGUI.FloatField(position, minValue); position.x += fieldWidth + 4f; position.width = sliderWidth; FloatRangeSliderAttribute limit = attribute as FloatRangeSliderAttribute; EditorGUI.MinMaxSlider( position, ref minValue, ref maxValue, limit.Min, limit.Max ); position.x += sliderWidth + 4f; position.width = fieldWidth; maxValue = EditorGUI.FloatField(position, maxValue); if (minValue < limit.Min) { minValue = limit.Min; } if (maxValue < minValue) { maxValue = minValue; } else if (maxValue > limit.Max) { maxValue = limit.Max; } minProperty.floatValue = minValue; maxProperty.floatValue = maxValue; EditorGUI.EndProperty(); EditorGUI.indentLevel = originalIndentLevel; } }
مقياس النموذج
سنبدأ بتغيير حجم العدو. أضف EnemyFactory
إعدادات المقياس للخيار. يجب ألا يكون الفاصل الزمني للمقياس كبيرًا جدًا ، ولكن يكفي لإنشاء أنواع مصغرة من الأعداء. أي شيء ضمن 0.5–2 بقيمة قياسية 1. سنختار مقياسًا عشوائيًا في هذه الفترة الزمنية Get
وننقلها إلى العدو من خلال طريقة جديدة Initialize
. [SerializeField, FloatRangeSlider(0.5f, 2f)] FloatRange scale = new FloatRange(1f); public Enemy Get () { Enemy instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; instance.Initialize(scale.RandomValueInRange); return instance; }
تقوم الطريقة Enemy.Initialize
ببساطة بتعيين مقياس لنموذجها وهو نفسه عبر جميع الأبعاد. public void Initialize (float scale) { model.localScale = new Vector3(scale, scale, scale); }
نطاق المقاييس يتراوح من 0.5 إلى 1.5.إزاحة المسار
لمزيد من تدمير توحيد تدفق الأعداء ، يمكننا تغيير موقفهم النسبي داخل البلاط. إنهم يتحركون للأمام ، وبالتالي فإن التغيير في هذا الاتجاه يغير فقط توقيت حركتهم ، وهو أمر غير ملحوظ. لذلك ، سنحولهم إلى الجانب ، بعيدًا عن المسار المثالي الذي يمر عبر مراكز البلاط. إضافة EnemyFactory
إزاحة مسار إلى الفاصل وتمرير الإزاحة العشوائية إلى الأسلوب Initialize
. يمكن أن تكون الإزاحة سالبة أو موجبة ، لكن لا تزيد عن ½ ، لأن هذا من شأنه أن ينقل العدو إلى بلاط مجاور. بالإضافة إلى ذلك ، لا نريد أن يتجاوز الأعداء المربعات التي يتبعونها ، لذلك في الواقع سيكون الفاصل أقل ، على سبيل المثال ، 0.4 ، ولكن الحدود الحقيقية تعتمد على حجم العدو. [SerializeField, FloatRangeSlider(-0.4f, 0.4f)] FloatRange pathOffset = new FloatRange(0f); public Enemy Get () { Enemy instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; instance.Initialize( scale.RandomValueInRange, pathOffset.RandomValueInRange ); return instance; }
نظرًا لأن إزاحة المسار يؤثر على المسار Enemy
الذي تم نقله ، فمن الضروري تتبعه. float pathOffset; … public void Initialize (float scale, float pathOffset) { model.localScale = new Vector3(scale, scale, scale); this.pathOffset = pathOffset; }
عند التحرك مستقيمًا تمامًا (أثناء الحركة الأمامية أو النهائية أو العادية للأمام) ، فإننا ببساطة نطبق الإزاحة مباشرة على النموذج. يحدث الشيء نفسه عند العودة إلى الوراء. باستخدام المنعطف الأيمن أو الأيسر ، قمنا بالفعل باستبدال النموذج ، الذي يصبح نسبةً إلى إزاحة المسار. void PrepareForward () { transform.localRotation = direction.GetRotation(); directionAngleTo = direction.GetAngle(); model.localPosition = new Vector3(pathOffset, 0f); progressFactor = 1f; } void PrepareTurnRight () { directionAngleTo = directionAngleFrom + 90f; model.localPosition = new Vector3(pathOffset - 0.5f, 0f); transform.localPosition = positionFrom + direction.GetHalfVector(); progressFactor = 1f / (Mathf.PI * 0.25f); } void PrepareTurnLeft () { directionAngleTo = directionAngleFrom - 90f; model.localPosition = new Vector3(pathOffset + 0.5f, 0f); transform.localPosition = positionFrom + direction.GetHalfVector(); progressFactor = 1f / (Mathf.PI * 0.25f); } void PrepareTurnAround () { directionAngleTo = directionAngleFrom + 180f; model.localPosition = new Vector3(pathOffset, 0f); transform.localPosition = positionFrom; progressFactor = 2f; } void PrepareIntro () { … model.localPosition = new Vector3(pathOffset, 0f); transform.localRotation = direction.GetRotation(); progressFactor = 2f; } void PrepareOutro () { … model.localPosition = new Vector3(pathOffset, 0f); transform.localRotation = direction.GetRotation(); progressFactor = 2f; }
نظرًا لأن إزاحة المسار أثناء الدوران يغير نصف القطر ، نحتاج إلى تغيير عملية حساب مضاعف التقدم. يجب طرح إزاحة المسار من ½ للحصول على دائرة نصف قطرها إلى اليمين ، وإضافتها في حالة انعطاف إلى اليسار. void PrepareTurnRight () { … progressFactor = 1f / (Mathf.PI * 0.5f * (0.5f - pathOffset)); } void PrepareTurnLeft () { … progressFactor = 1f / (Mathf.PI * 0.5f * (0.5f + pathOffset)); }
نحصل أيضًا على نصف قطر الدوران عند الدوران 180 درجة. في هذه الحالة ، نغطي نصف الدائرة بنصف قطر يساوي إزاحة المسار ، وبالتالي فإن المسافة هي π أضعاف الإزاحة. ومع ذلك ، لا يعمل هذا عندما يكون الإزاحة صفرًا ، وفي حالات النزوح الصغيرة ، تكون المنعطفات سريعة جدًا. لتجنب المنعطفات الفورية ، يمكننا فرض الحد الأدنى لنصف القطر لحساب السرعة ، على سبيل المثال 0.2. void PrepareTurnAround () { directionAngleTo = directionAngleFrom + (pathOffset < 0f ? 180f : -180f); model.localPosition = new Vector3(pathOffset, 0f); transform.localPosition = positionFrom; progressFactor = 1f / (Mathf.PI * Mathf.Max(Mathf.Abs(pathOffset), 0.2f)); }
يقع إزاحة المسار في النطاق .250.25–0.25.لاحظ أن الأعداء الآن لا يغيرون مسار النزوح النسبي ، حتى عند الدوران لذلك ، فإن الطول الكلي للمسار لكل عدو له طريقته الخاصة.لمنع الأعداء من الوصول إلى المربعات المجاورة ، يجب على المرء أيضًا مراعاة الحد الأقصى لمقياسه الممكن. لقد حددت الحجم فقط بحد أقصى قدره 1 ، وبالتالي فإن الحد الأقصى المسموح به للإزاحة للمكعب هو 0.25. إذا كان الحجم الأقصى 1.5 ، فيجب تقليل الحد الأقصى للنزوح إلى 0.125.سرعة
آخر شيء نحن عشوائيا هو سرعة الأعداء. نضيف فاصلًا إضافيًا لذلك EnemyFactory
، وسننقل القيمة إلى النسخة التي تم إنشاؤها من العدو. دعنا نجعلها الحجة الثانية لهذه الطريقة Initialize
. يجب أن لا يكون الأعداء بطيئين أو سريعين بحيث لا تصبح اللعبة بسيطة أو صعبة المستحيل. لنقصر الفاصل الزمني على 0.2-5. يتم التعبير عن السرعة بوحدات في الثانية ، والتي تتوافق مع البلاط في الثانية فقط عند التحرك للأمام. [SerializeField, FloatRangeSlider(0.2f, 5f)] FloatRange speed = new FloatRange(1f); [SerializeField, FloatRangeSlider(-0.4f, 0.4f)] FloatRange pathOffset = new FloatRange(0f); public Enemy Get () { Enemy instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; instance.Initialize( scale.RandomValueInRange, speed.RandomValueInRange, pathOffset.RandomValueInRange ); return instance; }
الآن لا Enemy
بد لي من تتبع والسرعة. float speed; … public void Initialize (float scale, float speed, float pathOffset) { model.localScale = new Vector3(scale, scale, scale); this.speed = speed; this.pathOffset = pathOffset; }
عندما لم نقم بتعيين السرعة بشكل صريح ، استخدمنا دائمًا القيمة 1. نحتاج فقط الآن إلى إنشاء اعتماد مضاعف التقدم على السرعة. void PrepareForward () { … progressFactor = speed; } void PrepareTurnRight () { … progressFactor = speed / (Mathf.PI * 0.5f * (0.5f - pathOffset)); } void PrepareTurnLeft () { … progressFactor = speed / (Mathf.PI * 0.5f * (0.5f + pathOffset)); } void PrepareTurnAround () { … progressFactor = speed / (Mathf.PI * Mathf.Max(Mathf.Abs(pathOffset), 0.2f)); } void PrepareIntro () { … progressFactor = 2f * speed; } void PrepareOutro () { … progressFactor = 2f * speed; }
السرعة في حدود 0.75-1.25.لذلك ، حصلنا على دفق جميل من الأعداء ينتقلون إلى نقطة النهاية. في البرنامج التعليمي التالي سنتعلم كيفية التعامل معهم. تريد أن تعرف متى سيتم إصداره؟ اتبع صفحتي على Patreon !مستودعالشعبي المادة