عشوائية مولد كهف ثنائي الأبعاد

مقدمة


إذا كنت كسولًا جدًا بحيث لا تعتني بوقتك ، وتحقق مستوى للعبتك ، فقد وصلت إلى المكان الصحيح.

ستخبرك هذه المقالة بالتفصيل كيف يمكنك استخدام إحدى طرق الجيل الأخرى باستخدام مثال المرتفعات والكهوف. سننظر في خوارزمية Aldous-Broder وكيفية جعل الكهف الناتج أكثر جمالا.

في نهاية قراءة المقال ، يجب أن تحصل على شيء مثل هذا:

الملخص


النظرية


الجبل


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

الكهف


ولإنشاء الأبراج المحصنة ، اخترت - كما بدا لي - خوارزمية ممتازة. بعبارات بسيطة ، يمكن تفسيره على النحو التالي: فلنمتلك متغيرين (ربما عشرة) متغيرين X و Y ، وصفيف ثنائي الأبعاد 50 × 50 ، نعطي هذه المتغيرات قيمًا عشوائية داخل الصفيف ، على سبيل المثال ، X = 26 ، و Y = 28 . بعد ذلك ، نقوم بنفس الإجراءات عدة مرات: نحصل على رقم عشوائي من صفر إلى

عددالمتغيرات2

، في حالتنا ، ما يصل إلى أربعة ؛ وبعد ذلك ، اعتمادًا على العدد الذي تم إسقاطه ، نتغير
متغيراتنا:

switch (Random.Range(0, 4)) { case 0: X += 1; break; case 1: X -= 1; break; case 2: Y += 1; break; case 3: Y -= 1; break; } 

ثم ، بالطبع ، نتحقق لنرى ما إذا كان أي متغير قد سقط خارج حدود مجالنا:

  X = X < 0 ? 0 : (X >= 50 ? 49 : X); Y = Y < 0 ? 0 : (Y >= 50 ? 49 : Y); 

بعد كل هذه الاختبارات ، نقوم بشيء في قيم X و Y الجديدة لصفيفنا (على سبيل المثال: أضف واحدًا إلى العنصر) .

 array[X, Y] += 1; 

تحضير


من أجل بساطة التنفيذ والتصور لأساليبنا ، هل سنرسم الأشياء الناتجة؟ أنا سعيد لأنك لا تمانع! سنفعل ذلك مع Texture2D .

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

 public static class ground_libray 

والثاني طبيعي ، فقط لن نحتاج إلى طريقة التحديث .

أيضًا ، لنقم بإنشاء كائن لعبة على المسرح ، باستخدام مكون SpriteRenderer

جزء عملي


مما تتكون؟


للعمل مع البيانات ، سنستخدم صفيفًا ثنائي الأبعاد. يمكنك أن تأخذ مصفوفة من أنواع مختلفة ، من البايت أو int ، إلى Color ، ولكن أعتقد أن ذلك سيكون أفضل ما يمكن القيام به:

نوع جديد
نكتب هذا الشيء في ground_libray .

 [System.Serializable] public class block { public float[] color = new float[3]; public block(Color col) { color = new float[3] { col.r, col.g, col.b }; } } 


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

ماسيف


دعونا ، قبل أن نبدأ بتوليد الجبل ، نعين المكان الذي سنخزنه فيه .

في نص الأرض الأرضي ، كتبت ما يلي:

  public int ground_size = 128; ground_libray.block[,] ground; Texture2D myT; 

ground_size - حجم مجالنا (أي أن المصفوفة ستتكون من 16384 عنصر).
ground_libray.block [،] ground - هذا هو مجالنا للجيل.
Texture2D myT هو ما سنعتمد عليه.

كيف ستعمل؟
سيكون مبدأ العمل معنا على النحو التالي - سوف نطلق على بعض أساليب ground_libray من مولد الأرض ، مع إعطاء الحقل الأرضي الأول.

لنقم بإنشاء الطريقة الأولى في البرنامج النصي ground_libray:

صنع الجبل
  public static float mount_noise = 0.02f; public static void generate_mount(ref block[,] b) { int h_now = b.GetLength(1) / 2; for (int x = 0; x < b.GetLength(0); x++) for (int y = 0; y < h_now; y++) { b[x, y] = new block(new Color(0.7f, 0.4f, 0)); h_now += Random.value > (1.0f - mount_noise) ? (Random.value > 0.5 ? 1 : -1) : 0; } } 

وسنحاول على الفور فهم ما يحدث هنا: كما قلت ، نراجع فقط أعمدة المصفوفة b الخاصة بنا ، وفي نفس الوقت نغير متغير الارتفاع h_now ، الذي كان في الأصل يساوي نصف 128 (64) . ولكن لا يزال هناك شيء جديد - mount_noise . هذا المتغير مسؤول عن فرصة تغيير h_now ، لأنه إذا قمت بتغيير الارتفاع في كثير من الأحيان ، سيبدو الجبل مثل المشط .

اللون
قمت على الفور بتعيين لون بني قليلاً ، دعه يكون على الأقل بعضًا - في المستقبل لن نحتاج إليه.

الآن دعنا نذهب إلى ground_generator ونكتب هذا في طريقة البدء :

  ground = new ground_libray.block [ground_size, ground_size]; ground_libray.generate_mount(ref ground); 

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

لماذا لا أستطيع رؤية الجبل الخاص بي؟


لنرسم الآن ما حصلنا عليه!

للرسم ، سنكتب الطريقة التالية في ground_libray :

رسم
  public static void paint(block[,] b, ref Texture2D t) { t = new Texture2D(b.GetLength(0), b.GetLength(1)); t.filterMode = FilterMode.Point; for (int x = 0; x < b.GetLength(0); x++) for (int y = 0; y < b.GetLength(1); y++) { if (b[x, y] == null) { t.SetPixel(x, y, new Color(0, 0, 0, 0)); continue; } t.SetPixel(x, y, new Color( b[x, y].color[0], b[x, y].color[1], b[x, y].color[2] ) ); } t.Apply(); } 

هنا لن نعطي شخصًا ما مجالنا ، سنعطي نسخة منه فقط (على الرغم من ذلك ، بسبب فئة الكلمة ، قدمنا ​​أكثر من مجرد نسخة) . سنقدم أيضًا Texture2D الخاص بنا لهذه الطريقة.

أول سطرين: نقوم بإنشاء نسيجنا بحجم الحقل وإزالة التصفية .

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

وبالطبع ، عند الانتهاء ، ننتقل إلى ground_generator ونضيف هذا:

  ground = new ground_libray.block [ground_size, ground_size]; ground_libray.generate_mount(ref ground); //   ground_libray.paint(ground, ref myT); GetComponent<SpriteRenderer>().sprite = Sprite.Create(myT, new Rect(0, 0, ground_size, ground_size), Vector3.zero ); 

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

لا يقبل SpriteRenderer Texture2D في أي مكان ، ولكن لا شيء يمنعنا من إنشاء نقش متحرك من هذا النسيج - Sprite.Create ( نسيج ، مستطيل بإحداثيات الركن الأيسر السفلي وأعلى اليمين ، إحداثيات المحور ).

سيتم تسمية هذه الخطوط بالأحدث ، وسنضيف الباقي فوق طريقة الطلاء !

لي


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

 ref block[,] b -     . int thick -    int size -         Color outLine -   

الكهف
  public static void make_cave(ref block[,] b, int thick, int size, Color outLine) { int xNow = Random.Range(0, b.GetLength(0)); int yNow = Random.Range(0, b.GetLength(1) / 2); for (int i = 0; i < size; i++) { b[xNow, yNow] = null; make_thick(ref b, thick, new int[2] { xNow, yNow }, outLine); switch (Random.Range(0, 4)) { case 0: xNow += 1; break; case 1: xNow -= 1; break; case 2: yNow += 1; break; case 3: yNow -= 1; break; } xNow = xNow < 0 ? 0 : (xNow >= b.GetLength(0) ? b.GetLength(0) - 1 : xNow); yNow = yNow < 0 ? 0 : (yNow >= b.GetLength(1) ? b.GetLength(1) - 1 : yNow); } } 

بادئ ذي بدء ، أعلنا عن المتغيرين X و Y ، ولكننا أطلقنا عليهما اسم xNow و yNow على التوالي.

الأول ، وهو xNow ، يحصل على قيمة عشوائية من صفر إلى حجم الحقل في البعد الأول.

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

بعد ذلك ، تنتقل الحلقة على الفور ، ويعتمد عدد علاماتها على معلمة الحجم . في كل مرة نقوم بتحديث الحقل في موقعي xNow و yNow ، وعندها فقط نقوم بتحديثها بأنفسنا (يمكن وضع التحديثات الميدانية في النهاية - لن تشعر بالفرق)

هناك أيضًا طريقة make_thick ، في المعلمات التي نمر فيها مجالنا ، وعرض شوط الكهف ، وموضع التحديث الحالي للكهف ولون السكتة الدماغية :

السكتة الدماغية
  static void make_thick (ref block[,] b, int t, int[] start, Color o) { for (int x = (start[0] - t); x < (start[0] + t); x++) { if (x < 0 || x >= b.GetLength(0)) continue; for (int y = (start[1] - t); y < (start[1] + t); y++) { if (y < 0 || y >= b.GetLength(1)) continue; if (b[x, y] == null) continue; b[x, y] = new block(o); } } } 

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


الآن دعنا نضيف هذا الخط إلى مولد الأرض :

 ground_libray.make_cave(ref ground, 2, 10000, new Color(0.3f, 0.3f, 0.3f)); 

يمكنك تثبيت البرنامج النصي ground_generator كمكون على كائن لدينا والتحقق من كيفية عمله!



المزيد عن الكهوف ...
  • لإنشاء المزيد من الكهوف ، يمكنك استدعاء طريقة make_cave عدة مرات (استخدم حلقة)
  • لا يؤدي تغيير معلمة الحجم دائمًا إلى زيادة حجم الكهف ، ولكنه غالبًا ما يصبح أكبر
  • بتغيير المعلمة السميكة ، يمكنك زيادة عدد العمليات بشكل ملحوظ:
    إذا كانت المعلمة 3 ، فسيكون عدد المربعات في دائرة نصف قطرها 36 ، لذا مع حجم المعلمة = 40،000 ، سيكون عدد العمليات 36 * 40،000 = 1440000


تصحيح الكهف




هل لاحظت أن الكهف في هذا المنظر لا يبدو أفضل؟ الكثير من التفاصيل الإضافية (ربما تفكر بشكل مختلف) .

للتخلص من شوائب بعض # 4d4d4d سنكتب هذه الطريقة في ground_libray :

منظف
  public static void clear_caves(ref block[,] b) { for (int x = 0; x < b.GetLength(0); x++) for (int y = 0; y < b.GetLength(1); y++) { if (b[x, y] == null) continue; if (solo(b, 2, 13, new int[2] { x, y })) b[x, y] = null; } } 

ولكن سيكون من الصعب فهم ما يحدث هنا إذا كنت لا تعرف وظيفة الوظيفة الفردية :

  static bool solo (block[,] b, int rad, int min, int[] start) { int cnt = 0; for (int x = (start[0] - rad); x <= (start[0] + rad); x++) { if (x < 0 || x >= b.GetLength(0)) continue; for (int y = (start[1] - rad); y <= (start[1] + rad); y++) { if (y < 0 || y >= b.GetLength(1)) continue; if (b[x, y] == null) cnt += 1; else continue; if (cnt >= min) return true; } } return false; } 

في معلمات هذه الوظيفة ، يجب أن يكون مجالنا ، نصف قطر التحقق من النقطة ، "عتبة التدمير" وإحداثيات النقطة المطلوب فحصها .
فيما يلي شرح مفصل لما تقوم به هذه الوظيفة:
int cnt هو عداد "العتبة" الحالية
فيما يلي دورتان تتحققان من جميع النقاط حول النقطة التي تم تمرير إحداثياتها للبدء . إذا كانت هناك نقطة فارغة ، فإننا نضيف واحدة إلى cnt ، عند الوصول إلى "عتبة الدمار" ، نعيد الحقيقة - النقطة غير ضرورية . وإلا فإننا لا نلمسها.

قمت بتعيين حد التدمير إلى 13 نقطة فارغة ، ونصف قطر التحقق هو 2 (أي أنه سيتحقق من 24 نقطة ، لا يشمل النقطة المركزية)
مثال
سيبقى هذا واحد دون أذى ، حيث لا يوجد سوى 9 نقاط فارغة.



لكن هذه لم تكن محظوظة - حوالي 14 نقطة فارغة



وصف موجز للخوارزمية: نمر عبر الحقل بأكمله ونتحقق من جميع النقاط لمعرفة ما إذا كانت هناك حاجة إليها.

بعد ذلك ، نضيف ببساطة السطر التالي إلى مولد الأرض :

 ground_libray.clear_caves(ref ground); 

الملخص


كما نرى ، فإن معظم الجسيمات غير الضرورية اختفت ببساطة.

أضف بعض الألوان


يبدو جبلنا رتيباً للغاية ، أجده مملاً.

دعنا نضيف بعض اللون. أضف طريقة level_paint إلى ground_libray :

رسم الجبال
  public static void level_paint(ref block[,] b, Color[] all_c) { for (int x = 0; x < b.GetLength(0); x++) { int lvl_div = -1; int counter = 0; int lvl_now = 0; for (int y = b.GetLength(1) - 1; y > 0; y--) { if (b[x, y] != null && lvl_div == -1) lvl_div = y / all_c.Length; else if (b[x, y] == null) continue; b[x, y] = new block(all_c[lvl_now]); lvl_now += counter >= lvl_div ? 1 : 0; lvl_now = (lvl_now >= all_c.Length) ? (all_c.Length - 1) : lvl_now; counter = counter >= lvl_div ? 0 : (counter += 1); } } } </ <cut />source>           .    ,       ,   .       ,      .          <b>Y </b>  ,      . </spoiler>     <b>ground_generator </b> : <source lang="cs"> ground_libray.level_paint(ref ground, new Color[3] { new Color(0.2f, 0.8f, 0), new Color(0.6f, 0.2f, 0.05f), new Color(0.2f, 0.2f, 0.2f), }); 

اخترت 3 ألوان فقط: الأخضر والأحمر الداكن والرمادي الداكن .
بالطبع ، يمكنك تغيير كل من عدد الألوان وقيم كل منها. اتضح مثل هذا:

الملخص


ولكن لا يزال يبدو صارمًا جدًا لإضافة القليل من العشوائية إلى الألوان ، سنكتب هذه الخاصية في ground_libray :

ألوان عشوائية
  public static float color_randomize = 0.1f; static float crnd { get { return Random.Range(1.0f - color_randomize, 1.0f + color_randomize); } } 

والآن في طرق level_paint و make_thick ، في السطور التي نعين فيها الألوان ، على سبيل المثال في make_thick :

 b[x, y] = new block(o); 

سنكتب هذا:

 b[x, y] = new block(o * crnd); 

وفي level_paint

 b[x, y] = new block(all_c[lvl_now] * crnd); 


في النهاية ، يجب أن يبدو كل شيء على النحو التالي:

الملخص



المساوئ


لنفترض أن لدينا حقلًا بحجم 1024 × 1024 ، نحتاج إلى توليد 24 كهفًا ، سمك حوافها سيكون 4 ، والحجم 80،000.

1024 * 1024 + 24 * 64 * 80،000 = 5،368،832،000،000 عملية.

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

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


All Articles