الخريطة
في
مقال سابق ، نظرت إلى ماهية
نظام الوظائف الجديد ، وكيف يعمل ، وكيفية إنشاء المهام ، وملئها بالبيانات ، وإجراء العمليات الحسابية متعددة الخيوط ، وشرحت بإيجاز فقط حيث يمكنك استخدام هذا النظام. في هذه المقالة ، سأحاول تحليل مثال محدد حيث يمكنك استخدام هذا النظام للحصول على مزيد من الأداء.
نظرًا لأن النظام تم تطويره في الأصل بهدف العمل مع البيانات ، فهو رائع لحل مهام البحث عن المسار.
يحتوي
Unity بالفعل على
مستكشف مسار NavMesh جيد ، لكنه لا يعمل في المشاريع ثنائية الأبعاد ، على الرغم من وجود الكثير من الحلول الجاهزة على نفس
الأصل . حسنًا ، وسنحاول إنشاء ليس فقط نظامًا يبحث عن طرق على الخريطة التي تم إنشاؤها ، ولكن سيجعل هذه الخريطة ديناميكية للغاية ، بحيث في كل مرة يتغير فيها شيء ما ، سيقوم النظام بإنشاء خريطة جديدة ، وبالطبع سنقوم بحساب كل هذا باستخدام نظام جديد للمهام ، حتى لا يتم تحميل الخيط الرئيسي.
في المثال ، تم بناء شبكة على الخريطة ، وهناك روبوت وعائق. يتم إعادة بناء الشبكة في كل مرة نقوم فيها بتغيير أي خصائص للخريطة ، سواء كان حجمها أو موقعها.
بالنسبة للطائرات ، استخدمت
SpriteRenderer بسيطًا ،
يحتوي هذا المكون على خاصية
حدود ممتازة يمكنك بسهولة من خلالها معرفة حجم الخريطة.
كل هذا في الأساس هو البداية ، لكننا لن نتوقف ونبدأ العمل على الفور.
لنبدأ مع النصوص. والأول هو نص عائق
العائق .
عقبةpublic class Obstacle : MonoBehaviour { }
داخل فئة
العقبة ، سنلتقط جميع التغييرات في العوائق على الخريطة ، على سبيل المثال ، تغيير موضع أو حجم كائن.
بعد ذلك ، يمكنك إنشاء فئة
خريطة الخريطة ، التي سيتم بناء الشبكة عليها ، وترثها من فئة
العقبة .
الخريطة public sealed class Map : Obstacle { }
سيتتبع فئة
الخريطة أيضًا جميع التغييرات على الخريطة من أجل إعادة بناء الشبكة إذا لزم الأمر.
للقيام بذلك ، املأ الفئة الأساسية
للعائق بجميع المتغيرات والأساليب اللازمة لتتبع تغييرات الكائن.
عقبة public class Obstacle : MonoBehaviour { public new SpriteRenderer renderer { get; private set;} private Vector2 tempSize; private Vector2 tempPos; protected virtual void Awake() { this.renderer = GetComponent<SpriteRenderer>(); this.tempSize = this.size; this.tempPos = this.position; } public virtual bool CheckChanges() { Vector2 newSize = this.size; float diff = (newSize - this.tempSize).sqrMagnitude; if (diff > 0.01f) { this.tempSize = newSize; return true; } Vector2 newPos = this.position; diff = (newPos - this.tempPos).sqrMagnitude; if (diff > 0.01f) { this.tempPos = newPos; return true; } return false; } public Vector2 size { get { return this.renderer.bounds.size;} } public Vector2 position { get { return this.transform.position;} } }
هنا ، سيكون لمتغير جهاز
العرض مرجع لمكون
SpriteRenderer ، وسيتم استخدام
متغيرات tempSize و
tempPos لتتبع التغييرات في حجم وموضع الكائن.
سيتم استخدام طريقة
Awake الظاهرية لتهيئة المتغيرات ،
وستتبع الطريقة الافتراضية
CheckChanges التغييرات الحالية في حجم الكائن
وموضعه وتعرض نتيجة
منطقية .
في الوقت الحالي ، دعنا نترك نص
العقبة وننتقل إلى
خريطة الخريطة نفسها ، حيث نملأها أيضًا بالمعلمات اللازمة للعمل.
الخريطة public sealed class Map : Obstacle { [Range(0.1f, 1f)] public float nodeSize = 0.5f; public Vector2 offset = new Vector2(0.5f, 0.5f); }
سيشير متغير حجم
العقدة إلى حجم الخلايا على الخريطة ، وهنا قمت
بتحديد حجمها من 0.1 إلى 1 بحيث لا تكون الخلايا على الشبكة صغيرة جدًا ، ولكنها أيضًا كبيرة جدًا. سيتم استخدام متغير
الإزاحة لوضع مسافة بادئة على الخريطة عند إنشاء الشبكة بحيث لا يتم بناء الشبكة على طول حواف الخريطة.
نظرًا لأن هناك الآن متغيرين جديدين على الخريطة ، فقد تبين أن تغييراتهما ستحتاج أيضًا إلى تتبعها. للقيام بذلك ، قم بإضافة متغيرين وقم
بتحميل طريقة
CheckChanges في فئة
الخريطة .
الخريطة public sealed class Map : Obstacle { [Range(0.1f, 1f)] public float nodeSize = 0.5f; public Vector2 offset = new Vector2(0.5f, 0.5f); private float tempNodeSize; private Vector2 tempOffset; protected override void Awake() { base.Awake(); this.tempNodeSize = this.nodeSize; this.tempOffset = this.offset; } public override bool CheckChanges() { float diff = Mathf.Abs(this.tempNodeSize - this.nodeSize); if (diff > 0.01f) { this.tempNodeSize = this.nodeSize; return true; } diff = (this.tempOffset - this.offset).sqrMagnitude; if (diff > 0.01f) { this.tempOffset = this.offset; return true; } return base.CheckChanges(); } }
تم. يمكنك الآن إنشاء نقش متحرك للخريطة على المسرح ورمي نص
خريطة عليه.

سنفعل نفس الشيء مع عقبة - إنشاء سبرايت بسيط على المسرح ورمي نص
عقبة عليه.

الآن لدينا خريطة الأشياء والعقبات على المسرح.
سيتولى البرنامج النصي
للخريطة مسؤولية تتبع جميع التغييرات على الخريطة ، حيث سنقوم في طريقة
التحديث بالتحقق من كل إطار لمعرفة التغييرات.
الخريطة public sealed class Map : Obstacle { private bool requireRebuild; private void Update() { UpdateChanges(); } private void UpdateChanges() { if (this.requireRebuild) { print(“ , !”); this.requireRebuild = false; } else { this.requireRebuild = CheckChanges(); } } }
وبالتالي ، في طريقة
UpdateChanges ، ستتعقب الخريطة تغييراتها فقط حتى الآن. يمكنك حتى بدء اللعبة الآن ومحاولة تغيير حجم الخريطة أو
تعويض الإزاحة للتأكد من تتبع جميع التغييرات.
الآن أنت بحاجة إلى تتبع التغييرات على العقبات بطريقة أو بأخرى على الخريطة. للقيام بذلك ، سنضع كل عقبة في قائمة على الخريطة ، والتي بدورها ستقوم بتحديث كل إطار في طريقة
التحديث .
في فئة
الخريطة ، قم بإنشاء قائمة بكل العوائق المحتملة على الخريطة وطريقتين ثابتتين لتسجيلها.
الخريطة public sealed class Map : Obstacle { private static Map ObjInstance; private List<Obstacle> obstacles = new List<Obstacle>(); public static bool RegisterObstacle(Obstacle obstacle) { if (obstacle == Instance) return false; else if (Instance.obstacles.Contains(obstacle) == false) { Instance.obstacles.Add(obstacle); Instance.requireRebuild = true; return true; } return false; } public static bool UnregisterObstacle(Obstacle obstacle) { if (Instance.obstacles.Remove(obstacle)) { Instance.requireRebuild = true; return true; } return false; } public static Map Instance { get { if (ObjInstance == null) ObjInstance = FindObjectOfType<Map>(); return ObjInstance; } } }
في طريقة
RegisterObstacle الثابتة ، سنقوم بتسجيل
عقبة جديدة على الخريطة وإضافتها إلى القائمة ، ولكن أولاً وقبل كل شيء من المهم أن نأخذ في الاعتبار أن الخريطة نفسها موروثة أيضًا من فئة
العقبة وبالتالي يجب أن نتحقق مما إذا كنا نحاول تسجيل البطاقة نفسها كعقبة.
على العكس من ذلك ، فإن طريقة
UnregisterObstacle الثابتة تزيل العوائق من الخريطة وتزيلها من القائمة عندما نسمح بتدميرها.
في نفس الوقت ، في كل مرة نضيف أو نزيل عقبة من الخريطة ، من الضروري إعادة إنشاء الخريطة نفسها ، لذلك بعد تنفيذ هذه الأساليب الثابتة ، قم بتعيين متغير
RequRebuild على
true .
أيضًا ، من أجل الوصول السهل إلى النص البرمجي
للخريطة من أي نص برمجي ، قمت بإنشاء خاصية
المثيل الثابتة التي ستعود لي بهذا المثيل من
الخريطة .
الآن ، دعنا نعود إلى نص
Obstacle النصي حيث سنقوم بتسجيل عقبة على الخريطة. للقيام بذلك ، أضف
طريقتين OnEnable و
OnDisable إليها.
عقبة public class Obstacle : MonoBehaviour { protected virtual void OnEnable() { Map.RegisterObstacle(this); } protected virtual void OnDisable() { Map.UnregisterObstacle(this); } }
في كل مرة ننشئ فيها عقبة جديدة أثناء اللعب على الخريطة ، سيتم تسجيلها تلقائيًا في طريقة
OnEnable ، حيث سيتم أخذها في الاعتبار عند بناء شبكة جديدة وإزالة أنفسنا من الخريطة في طريقة
OnDisable عندما يتم تدميرها أو تعطيلها.
يبقى فقط لتتبع التغييرات من العقبات نفسها في البرنامج النصي
للخريطة في طريقة
CheckChanges المحملة.
الخريطة public sealed class Map : Obstacle { public override bool CheckChanges() { float diff = Mathf.Abs(this.tempNodeSize - this.nodeSize); if (diff > 0.01f) { this.tempNodeSize = this.nodeSize; return true; } diff = (this.tempOffset - this.offset).sqrMagnitude; if (diff > 0.01f) { this.tempOffset = this.offset; return true; } foreach(Obstacle obstacle in this.obstacles) { if (obstacle.CheckChanges()) return true; } return base.CheckChanges(); } }
الآن لدينا خريطة ، عقبات - بشكل عام ، كل ما تحتاجه لبناء شبكة ، والآن يمكنك الانتقال إلى أهم شيء.
الربط
الشبكة ، في أبسط أشكالها ، عبارة عن صفيف ثنائي الأبعاد من النقاط. لبناءها تحتاج إلى معرفة حجم الخريطة وحجم النقاط عليها ، بعد بعض الحسابات نحصل على عدد النقاط أفقياً ورأسياً ، هذه هي شبكتنا.
هناك العديد من الطرق للعثور على مسار على الشبكة. ومع ذلك ، في هذه المقالة ، فإن الشيء الرئيسي هو فهم كيفية استخدام إمكانات نظام المهام بشكل صحيح ، لذلك لن أفكر هنا في خيارات مختلفة للعثور على المسار ، ومزاياها وعيوبها ، لكنني سأأخذ أبسط خيار بحث
A * .
في هذه الحالة ، يجب أن تحتوي جميع النقاط على الشبكة ، بالإضافة إلى الموقع ، على الإحداثيات وخاصية براءات الاختراع.
مع الصبر ، أعتقد أن كل شيء واضح لماذا هناك حاجة إليه ، لكن الإحداثيات ستشير إلى ترتيب النقطة على الشبكة ، ولا ترتبط هذه الإحداثيات تحديدًا بموضع النقطة في الفضاء. تُظهر الصورة أدناه شبكة بسيطة تُظهر الاختلافات في الإحداثيات من موضع ما.
لماذا الاحداثيات؟والحقيقة هي أنه في الوحدة ، للإشارة إلى موضع كائن في الفضاء ،
يتم استخدام تعويم بسيط
غير دقيق للغاية ويمكن أن يكون عددًا كسريًا أو سلبيًا ، لذلك سيكون من الصعب استخدامه لتنفيذ بحث المسار على الخريطة. يتم إجراء الإحداثيات على شكل
int واضح يكون دائمًا إيجابيًا ويسهل التعامل معه عند البحث عن نقاط مجاورة.
أولاً ، دعنا نحدد كائنًا نقطيًا ، سيكون هذا بنية
عقدة بسيطة.
العقدة public struct Node { public int id; public Vector2 position; public Vector2Int coords; }
سيحتوي هذا الهيكل على موضع
الموضع في شكل
Vector2 ، حيث مع هذا المتغير
سنرسم نقطة في الفضاء. يقوم منسق
كوورد بتنسيق
متغير في شكل
Vector2Int سيشير إلى إحداثيات نقطة على الخريطة ، ومتغير
المعرف ، ورقم حسابه الرقمي ، وباستخدامه سنقوم بمقارنة نقاط مختلفة على الشبكة والتحقق من وجود نقطة.
سيتم الإشارة إلى سالكية النقطة في شكل
خاصتها المنطقية ، ولكن نظرًا لأنه لا يمكننا استخدام
أنواع البيانات
القابلة للتحويل في نظام المهام ، فإننا سنشير إلى صلاحيتها في شكل عدد
صحيح ، لذلك استخدمت تعداد
NodeType البسيط ، حيث: 0 ليست نقطة قابلة للتمرير ، و 1 مقبول.
NodeType و Node public enum NodeType { NonWalkable = 0, Walkable = 1 } public struct Node { public int id; public Vector2 position; public Vector2Int coords; private int nodeType; public bool isWalkable { get { return this.nodeType == (int)NodeType.Walkable;} } public Node(int id, Vector2 position, Vector2Int coords, NodeType type) { this.id = id; this.position = position; this.coords = coords; this.nodeType = (int)type; } }
أيضًا ، من أجل راحة العمل مع نقطة ،
سأحمل طريقة
Equals بشكل زائد لزيادة سهولة مقارنة النقاط وأيضًا لاستكمال طريقة التحقق من وجود نقطة.
العقدة public struct Node { public override bool Equals(object obj) { if (obj is Node) { Node other = (Node)obj; return this.id == other.id; } else return base.Equals(obj); } public static implicit operator bool(Node node) { return node.id > 0; } }
نظرًا لأن رقم
معرف النقطة على الشبكة سيبدأ بوحدة واحدة ، فسوف أتحقق من وجود النقطة كشرط أن يكون
معرفها أكبر من 0.
انتقل إلى فصل
الخريطة حيث سنقوم بإعداد كل شيء لإنشاء خريطة.
لدينا بالفعل فحص لتغيير معلمات الخريطة ، والآن نحتاج إلى تحديد كيفية تنفيذ عملية بناء الشبكة. للقيام بذلك ، قم بإنشاء متغير جديد وعدة طرق.
الخريطة public sealed class Map : Obstacle { public bool rebuilding { get; private set; } public void Rebuild() {} private void OnRebuildStart() {} private void OnRebuildFinish() {} }
ستشير خاصية
إعادة البناء إلى ما إذا كانت عملية
الربط قيد التقدم. ستقوم طريقة
إعادة البناء بجمع البيانات والمهام لبناء الشبكة ، ثم
تبدأ طريقة
OnRebuildStart في عملية بناء الشبكة
وستجمع طريقة
OnRebuildFinish البيانات من المهام.
الآن دعنا نغير طريقة
UpdateChanges قليلاً حتى يتم أخذ حالة الشبكة بعين الاعتبار.
الخريطة public sealed class Map : Obstacle { public bool rebuilding { get; private set; } private void UpdateChanges() { if (this.rebuilding) { print(“ ...”); } else { if (this.requireRebuild) { print(“ , !”); Rebuild(); } else { this.requireRebuild = CheckChanges(); } } } public void Rebuild() { if (this.rebuilding) return; print(“ !”); OnRebuildStart(); } private void OnRebuildStart() { this.rebuilding = true; } private void OnRebuildFinish() { this.rebuilding = false; } }
كما ترى الآن في طريقة
UpdateChanges ، هناك شرط أنه أثناء بناء الشبكة القديمة لا يبدأ في إنشاء شبكة جديدة ، وأيضًا في طريقة
إعادة البناء ، يتحقق الإجراء الأول مما إذا كانت عملية الربط قيد التقدم بالفعل.
حل المشكلات
الآن القليل عن عملية بناء الخريطة.
نظرًا لأننا سنستخدم نظام المهام وبناء الشبكة بالتوازي لإنشاء الخريطة ، فقد استخدمت نوع مهمة
IJobParallelFor ، والتي سيتم تنفيذها لعدد معين من المرات. حتى لا يتم تحميل عملية البناء بأي مهمة منفصلة واحدة ، سنستخدم مجموعة المهام المعبأة في
JobHandle واحدة.
في أغلب الأحيان ، لبناء شبكة ، استخدم دورتين متداخلتين في بعضهما البعض لبناء ، على سبيل المثال ، أفقيًا وعموديًا. في هذا المثال ، سنقوم أيضًا ببناء الشبكة أولاً أفقيًا ثم عموديًا. للقيام بذلك ، نحسب عدد النقاط الأفقية والرأسية في طريقة
إعادة البناء ، ثم في طريقة
إعادة البناء نمر خلال الدورة على طول النقاط الرأسية ، وسنبني نقاطًا أفقية بالتوازي في المهمة. لتخيل عملية البناء بشكل أفضل ، ألق نظرة على الرسوم المتحركة أدناه.
يشير عدد النقاط الرأسية إلى عدد المهام ، وبالتالي ، ستقوم كل مهمة ببناء النقاط أفقيًا فقط ، بعد إكمال جميع المهام ، يتم تجميع النقاط في قائمة واحدة. هذا هو السبب في أنني بحاجة إلى استخدام مهمة مثل
IJobParallelFor لتمرير فهرس النقطة على الشبكة أفقيًا إلى أسلوب
التنفيذ .
وهكذا لدينا هيكل النقاط ، الآن يمكنك إنشاء هيكل مهمة المهمة وترثها من واجهة
IJobParallelFor ، كل شيء بسيط هنا.
الوظيفة public struct Job : IJobParallelFor { public void Execute(int index) {} }
نعود إلى طريقة
إعادة بناء فئة
الخريطة ، حيث سنجري الحسابات اللازمة لقياس الشبكة.
الخريطة public sealed class Map : Obstacle { public void Rebuild() { if (this.rebuilding) return; print(“ !”); Vector2 mapSize = this.size - this.offset * 2f; int horizontals = Mathf.RoundToInt(mapSize.x / this.nodeSize); int verticals = Mathf.RoundToInt(mapSize.y / this.nodeSize); if (horizontals <= 0) { OnRebuildFinish(); return; } Vector2 center = this.position; Vector2 origin = center - (mapSize / 2f); OnRebuildStart(); } }
في طريقة
إعادة البناء ، نحسب الحجم الدقيق للخريطة
sizeSize ، مع الأخذ في الاعتبار المسافة البادئة ، ثم نكتب عددًا رأسيًا في النقاط ، وفي
الأفقي عدد النقاط أفقياً. إذا كان عدد النقاط الرأسية
صفرًا ، فإننا نتوقف عن بناء الخريطة واستدعاء طريقة
OnRebuildFinish لإكمال العملية. سيشير متغير
الأصل إلى المكان الذي سنبدأ منه في بناء الشبكة - في المثال ، هذه هي النقطة اليسرى السفلية على الخريطة.
الآن يمكنك الذهاب إلى المهام نفسها وملؤها بالبيانات.
أثناء بناء الشبكة ، ستحتاج المهمة إلى مصفوفة
NativeArray حيث
سنضع النقاط ، أيضًا نظرًا
لوجود عقبات على الخريطة ، سنحتاج أيضًا إلى تمريرها إلى المهمة ، لذلك
سنستخدم صفيف
NativeArray آخر ، ثم نحتاج إلى حجم النقاط في المشكلة ، الموضع الأولي من حيث سنبني النقاط ، بالإضافة إلى الإحداثيات الأولية للسلسلة.
الوظيفة public struct Job : IJobParallelFor { [WriteOnly] public NativeArray<Node> array; [ReadOnly] public NativeArray<Rect> bounds; public float nodeSize; public Vector2 startPos; public Vector2Int startCoords; public void Execute(int index) {} }
قمت بتمييز مجموعة النقاط بالسمة
WriteOnly ، لأنه في المهمة سيكون من الضروري فقط "
كتابة " النقاط المستلمة إلى الصفيف ، على العكس من ذلك ،
يتم وضع علامة على صفيف
حدود العوائق
بسمة ReadOnly لأنه في المهمة سنقوم فقط "
بقراءة " البيانات من هذا الصفيف.
حسنًا ، الآن ، دعنا ننتقل إلى حساب النقاط نفسها لاحقًا.
نعود الآن إلى فئة
الخريطة ، حيث نشير إلى جميع المتغيرات التي تنطوي عليها المهام.
هنا ، أولاً ، نحتاج إلى
معالجة عالمية للمهام ، ومجموعة من العوائق في شكل
NativeArray ، وقائمة بالمهام التي ستحتوي على جميع النقاط المستلمة على الشبكة
والقاموس مع جميع الإحداثيات والنقاط على الخريطة ، بحيث يكون من الأسهل البحث عنها في وقت لاحق.
الخريطة public sealed class Map : Obstacle { private JobHandle handle; private NativeArray<Rect> bounds; private HashSet<NativeArray<Node>> jobs = new HashSet<NativeArray<Node>>(); private Dictionary<Vector2Int, Node> nodes = new Dictionary<Vector2Int, Node>(); }
الآن مرة أخرى ، نعود إلى طريقة
إعادة البناء ونستمر في بناء الشبكة.
أولاً ، قم بتهيئة مجموعة
حدود العقبات لتمريرها إلى المهمة.
إعادة البناء public void Rebuild() { Vector2 center = this.position; Vector2 origin = center - (mapSize / 2f); int count = this.obstacles.Count; if (count > 0) { this.bounds = new NativeArray<Rect>(count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); } OnRebuildStart(); }
نقوم هنا بإنشاء مثيل من
NativeArray من خلال مُنشئ جديد بثلاث معلمات. لقد قمت بفحص المعلمتين الأوليين في مقال سابق ، ولكن المعلمة الثالثة ستساعدنا على توفير بعض الوقت في إنشاء مصفوفة. والحقيقة هي أننا سنكتب البيانات إلى المصفوفة مباشرة بعد إنشائها ، مما يعني أننا لسنا بحاجة للتأكد من محوها. هذه المعلمة مفيدة لـ
NativeArray والتي سيتم استخدامها فقط في وضع
القراءة في المهمة.
وهكذا ، فإننا نملأ مجموعة
الحدود بالبيانات.
إعادة البناء public void Rebuild() { Vector2 center = this.position; Vector2 origin = center - (mapSize / 2f); int count = this.obstacles.Count; if (count > 0) { this.bounds = new NativeArray<Rect>(count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); for(int i = 0; i < count; i++) { Obstacle obs = this.obstacles[i]; Vector2 position = obs.position; Rect rect = new Rect(Vector2.zero, obs.size); rect.center = position; this.bounds[i] = rect; } } OnRebuildStart(); }
الآن يمكننا أن ننتقل إلى إنشاء المهام ، لذلك سنمر بدورة عبر جميع الصفوف الرأسية للشبكة.
إعادة البناء public void Rebuild() { Vector2 center = this.position; Vector2 origin = center - (mapSize / 2f); int count = this.obstacles.Count; if (count > 0) { this.bounds = new NativeArray<Rect>(count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); for(int i = 0; i < count; i++) { Obstacle obs = this.obstacles[i]; Vector2 position = obs.position; Rect rect = new Rect(Vector2.zero, obs.size); rect.center = position; this.bounds[i] = rect; } } for (int i = 0; i < verticals; i++) { float xPos = origin.x; float yPos = origin.y + (i * this.nodeSize) + this.nodeSize / 2f; } OnRebuildStart(); }
بادئ ذي بدء ، في
xPos و
yPos نحصل على الموضع الأفقي الأولي للسلسلة.
إعادة البناء public void Rebuild() { Vector2 center = this.position; Vector2 origin = center - (mapSize / 2f); int count = this.obstacles.Count; if (count > 0) { this.bounds = new NativeArray<Rect>(count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); for(int i = 0; i < count; i++) { Obstacle obs = this.obstacles[i]; Vector2 position = obs.position; Rect rect = new Rect(Vector2.zero, obs.size); rect.center = position; this.bounds[i] = rect; } } for (int i = 0; i < verticals; i++) { float xPos = origin.x; float yPos = origin.y + (i * this.nodeSize) + this.nodeSize / 2f; NativeArray<Node> array = new NativeArray<Node>(horizontals, Allocator.Persistent); Job job = new Job(); job.startCoords = new Vector2Int(i * horizontals, i); job.startPos = new Vector2(xPos, yPos); job.nodeSize = this.nodeSize; job.bounds = this.bounds; job.array = array; } OnRebuildStart(); }
بعد ذلك ، نقوم بإنشاء
NativeArray بسيط حيث سيتم وضع النقاط في المهمة ، وهنا بالنسبة
للصفيف ، تحتاج إلى تحديد عدد النقاط التي سيتم إنشاؤها أفقيًا ونوع التخصيص
المستمر ، لأن المهمة يمكن أن تستغرق وقتًا أطول من إطار واحد.
بعد ذلك ، قم بإنشاء مثيل مهمة المهمة نفسها ، ووضع الإحداثيات الأولية لسلسلة
startCoords ، والموضع الأولي لسلسلة
startPos ، وحجم نقاط حجم
العقدة ، ومجموعة
حدود العوائق ، ومجموعة النقاط نفسها في النهاية.
يبقى فقط لوضع المهمة في
متناول اليد وقائمة المهام العالمية.
إعادة البناء public void Rebuild() { Vector2 center = this.position; Vector2 origin = center - (mapSize / 2f); int count = this.obstacles.Count; if (count > 0) { this.bounds = new NativeArray<Rect>(count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); for(int i = 0; i < count; i++) { Obstacle obs = this.obstacles[i]; Vector2 position = obs.position; Rect rect = new Rect(Vector2.zero, obs.size); rect.center = position; this.bounds[i] = rect; } } for (int i = 0; i < verticals; i++) { float xPos = origin.x; float yPos = origin.y + (i * this.nodeSize) + this.nodeSize / 2f; NativeArray<Node> array = new NativeArray<Node>(horizontals, Allocator.Persistent); Job job = new Job(); job.startCoords = new Vector2Int(i * horizontals, i); job.startPos = new Vector2(xPos, yPos); job.nodeSize = this.nodeSize; job.bounds = this.bounds; job.array = array; this.handle = job.Schedule(horizontals, 3, this.handle); this.jobs.Add(array); } OnRebuildStart(); }
تم. لدينا قائمة بالمهام
ومقبضها المشترك ، الآن يمكننا تشغيل هذا
المقبض عن طريق استدعاء أسلوبها
الكامل في أسلوب
OnRebuildStart .
Onrebuildstart private void OnRebuildStart() { this.rebuilding = true; this.handle.Complete(); }
نظرًا لأن متغير
إعادة البناء سيشير إلى أن عملية
الربط جارية ، يجب أن تحدد طريقة
UpdateChanges نفسها أيضًا الحالة التي ستنتهي فيها هذه العملية باستخدام
المقبض وخاصية IsCompleted .
تحديثات private void UpdateChanges() { if (this.rebuilding) { print(“ ...”); if (this.handle.IsCompleted) OnRebuildFinish(); } else { if (this.requireRebuild) { print(“ , !”); Rebuild(); } else { this.requireRebuild = CheckChanges(); } } }
بعد الانتهاء من المهام ، سيتم استدعاء أسلوب
OnRebuildFinish حيث سنقوم بالفعل بجمع النقاط المستلمة في قائمة
قاموس عامة واحدة ، والأهم من ذلك ،
لتوضيح الموارد المحتلة.
OnRebuildFinish private void OnRebuildFinish() { this.nodes.Clear(); foreach (NativeArray<Node> array in this.jobs) { foreach (Node node in array) this.nodes.Add(node.coords, node); array.Dispose(); } this.jobs.Clear(); if (this.bounds.IsCreated) this.bounds.Dispose(); this.requireRebuild = this.rebuilding = false; }
أولاً ، نمسح قاموس العقد من النقاط السابقة ، ثم نستخدم حلقة foreach لفرز جميع النقاط التي تلقيناها من المهام ووضعها في قاموس العقد ، حيث يكون المفتاح هو الإحداثيات ( وليس الموضع !) للنقطة ، والقيمة هي النقطة نفسها. بمساعدة هذا القاموس ، سيكون من الأسهل علينا البحث عن النقاط المجاورة على الخريطة. بعد الملء ، نقوم بمسح صفيف الصفيف باستخدام طريقة التخلص وفي النهاية نقوم بمسح قائمة مهام المهام نفسها .ستحتاج أيضًا إلى مسح مجموعة حدود العوائق إذا تم إنشاؤها مسبقًا.بعد كل هذه الإجراءات ، نحصل على قائمة بجميع النقاط على الخريطة والآن يمكنك رسمها على المسرح.للقيام بذلك ، في فئة الخريطة ، قم بإنشاء طريقة OnDrawGizmos حيث سنرسم النقاط.الخريطة public sealed class Map : Obstacle { #if UNITY_EDITOR private void OnDrawGizmos() {} #endif }
الآن من خلال الحلقة نرسم كل نقطة.الخريطة public sealed class Map : Obstacle { #if UNITY_EDITOR private void OnDrawGizmos() { foreach (Node node in this.nodes.Values) { Gizmos.DrawWireSphere(node.position, this.nodeSize / 10f); } } #endif }
بعد كل هذه الإجراءات ، تبدو خريطتنا مملة نوعًا ما ، من أجل الحصول على شبكة ، تحتاج إلى توصيل النقاط ببعضها البعض.للبحث عن نقاط مجاورة ، نحتاج فقط إلى العثور على النقطة المطلوبة من خلال إحداثياتها في 8 اتجاهات ، لذلك في فئة الخريطة سنقوم بإنشاء مجموعة ثابتة بسيطة لاتجاهات الاتجاهات وطريقة البحث عن الخلية بواسطة إحداثيات GetNode .الخريطة public sealed class Map : Obstacle { public static readonly Vector2Int[] Directions = { Vector2Int.up, new Vector2Int(1, 1), Vector2Int.right, new Vector2Int(1, -1), Vector2Int.down, new Vector2Int(-1, -1), Vector2Int.left, new Vector2Int(-1, 1), }; public Node GetNode(Vector2Int coords) { Node result = default(Node); try { result = this.nodes[coords]; } catch {} return result; } #if UNITY_EDITOR private void OnDrawGizmos() {} #endif }
ستقوم طريقة GetNode بإرجاع نقطة عن طريق الإحداثيات من قائمة العقد ، ولكنك تحتاج إلى القيام بذلك بعناية ، لأنه إذا كانت إحداثيات Vector2Int غير صحيحة ، فسيحدث خطأ ، لذلك نستخدم هنا كتلة استثناء try try ، والتي ستساعد على تجاوز الاستثناء وليس " تعليق " التطبيق بأكمله مع وجود خطأ.بعد ذلك ، سنستعرض الدورة في جميع الاتجاهات ونحاول العثور على نقاط مجاورة في طريقة OnDrawGizmos ، والأهم من ذلك ، لا تنس أن تفكر في صلاحيتها.Ondrawgizmos #if UNITY_EDITOR private void OnDrawGizmos() { Color c = Gizmos.color; foreach (Node node in this.nodes.Values) { Color newColor = Color.white; if (node.isWalkable) newColor = new Color32(153, 255, 51, 255); else newColor = Color.red; Gizmos.color = newColor; Gizmos.DrawWireSphere(node.position, this.nodeSize / 10f); newColor = Color.green; Gizmos.color = newColor; if (node.isWalkable) { for (int i = 0; i < Directions.Length; i++) { Vector2Int coords = node.coords + Directions[i]; Node connection = GetNode(coords); if (connection) { if (connection.isWalkable) Gizmos.DrawLine(node.position, connection.position); } } } } Gizmos.color = c; } #endif
يمكنك الآن بدء اللعبة بأمان ومعرفة ما حدث.في هذا المثال ، قمنا ببناء الرسم البياني نفسه فقط باستخدام المهام ، ولكن هذا ما حدث بعد أن ثبت في النظام خوارزمية A * نفسها ، والتي تستخدم أيضًا نظام Job للعثور على المسار ، المصدر في نهاية المقالة .لذلك يمكنك استخدام نظام المهام الجديد لأهدافك وبناء أنظمة مثيرة للاهتمام دون بذل الكثير من الجهد.كما هو الحال في المقالة السابقة ، يتم استخدام نظام المهام بدون ECS ، ولكن إذا كنت تستخدم هذا النظام مع ECS ، يمكنك ببساطة تحقيق نتائج مذهلة في مكاسب الأداء. حظا سعيدا !مصدر مشروع الباحث عن المسار