الأنماط الإجرائية التي يمكن استخدامها مع بطاقات التجانب

يستخدم الجيل الإجرائي لزيادة تباين الألعاب. تشمل المشروعات المعروفة Minecraft و Enter the Gungeon و Descenders . في هذا المنشور ، سأشرح بعض الخوارزميات التي يمكن استخدامها عند العمل مع نظام Tilemap ، والذي ظهر كدالة ثنائية الأبعاد في Unity 2017.2 ، ومع RuleTile .

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

ما هو هذا المنصب؟


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


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

قبل القراءة ، سيكون من الجيد معرفة ما يلي:

  1. نحن نميز ما هو البلاط وما لا يستخدم القيم الثنائية. 1 هو البلاط ، 0 هو غيابه.
  2. سنقوم بتخزين جميع البطاقات في صفيف صحيح ثنائي الأبعاد يتم إرجاعه إلى المستخدم في نهاية كل وظيفة (باستثناء تلك التي يتم تنفيذ العرض).
  3. سأستخدم دالة صفيف GetUpperBound () للحصول على ارتفاع وعرض كل خريطة ، بحيث تتلقى الدالة عددًا أقل من المتغيرات والرمز أنظف.
  4. غالبًا ما أستخدم Mathf.FloorToInt () ، لأن نظام إحداثيات Tilemap يبدأ في أسفل اليسار ، ويتيح لك Mathf.FloorToInt () تقريب الأرقام إلى عدد صحيح.
  5. تتم كتابة جميع الكود في هذا المنشور في C #.

جيل الصفيف


ينشئ GenerateArray صفيف int جديد من الحجم المحدد. يمكننا أيضًا الإشارة إلى ما إذا كان يجب ملء المصفوفة أم أنها فارغة (1 أو 0). هنا هو الكود:

 public static int[,] GenerateArray(int width, int height, bool empty) { int[,] map = new int[width, height]; for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { if (empty) { map[x, y] = 0; } else { map[x, y] = 1; } } } return map; } 

تقديم خريطة


تستخدم هذه الوظيفة لتقديم خريطة على خريطة تجانب. ندور حول عرض الخريطة وارتفاعها ، ونضع مربعات فقط عندما يكون للصفيف في النقطة المختبرة قيمة 1.

 public static void RenderMap(int[,] map, Tilemap tilemap, TileBase tile) { //Clear the map (ensures we dont overlap) tilemap.ClearAllTiles(); //Loop through the width of the map for (int x = 0; x < map.GetUpperBound(0) ; x++) { //Loop through the height of the map for (int y = 0; y < map.GetUpperBound(1); y++) { // 1 = tile, 0 = no tile if (map[x, y] == 1) { tilemap.SetTile(new Vector3Int(x, y, 0), tile); } } } } 

تحديث الخريطة


تُستخدم هذه الوظيفة فقط لتحديث الخريطة وليس لإعادة العرض. بفضل هذا ، يمكننا استخدام موارد أقل دون إعادة رسم كل مربع وبيانات التجانب الخاصة به.

 public static void UpdateMap(int[,] map, Tilemap tilemap) //Takes in our map and tilemap, setting null tiles where needed { for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { //We are only going to update the map, rather than rendering again //This is because it uses less resources to update tiles to null //As opposed to re-drawing every single tile (and collision data) if (map[x, y] == 0) { tilemap.SetTile(new Vector3Int(x, y, 0), null); } } } } 

الضوضاء بيرلين


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

حل بسيط


تستخدم طريقة التوليد هذه أبسط أشكال تحقيق ضوضاء بيرلين في توليد المستويات. يمكننا استخدام وظيفة Unity فيما يتعلق بضوضاء Perlin حتى لا نكتب الكود بأنفسنا. سنستخدم أيضًا الأعداد الصحيحة لخريطة التجانب ، وذلك باستخدام دالة Mathf.FloorToInt () .

 public static int[,] PerlinNoise(int[,] map, float seed) { int newPoint; //Used to reduced the position of the Perlin point float reduction = 0.5f; //Create the Perlin for (int x = 0; x < map.GetUpperBound(0); x++) { newPoint = Mathf.FloorToInt((Mathf.PerlinNoise(x, seed) - reduction) * map.GetUpperBound(1)); //Make sure the noise starts near the halfway point of the height newPoint += (map.GetUpperBound(1) / 2); for (int y = newPoint; y >= 0; y--) { map[x, y] = 1; } } return map; } 

إليك ما يبدو عليه بعد التقديم إلى خريطة تجانب:


تمهيد


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

 public static int[,] PerlinNoiseSmooth(int[,] map, float seed, int interval) { //Smooth the noise and store it in the int array if (interval > 1) { int newPoint, points; //Used to reduced the position of the Perlin point float reduction = 0.5f; //Used in the smoothing process Vector2Int currentPos, lastPos; //The corresponding points of the smoothing. One list for x and one for y List<int> noiseX = new List<int>(); List<int> noiseY = new List<int>(); //Generate the noise for (int x = 0; x < map.GetUpperBound(0); x += interval) { newPoint = Mathf.FloorToInt((Mathf.PerlinNoise(x, (seed * reduction))) * map.GetUpperBound(1)); noiseY.Add(newPoint); noiseX.Add(x); } points = noiseY.Count; 

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

 //Start at 1 so we have a previous position already for (int i = 1; i < points; i++) { //Get the current position currentPos = new Vector2Int(noiseX[i], noiseY[i]); //Also get the last position lastPos = new Vector2Int(noiseX[i - 1], noiseY[i - 1]); //Find the difference between the two Vector2 diff = currentPos - lastPos; //Set up what the height change value will be float heightChange = diff.y / interval; //Determine the current height float currHeight = lastPos.y; //Work our way through from the last x to the current x for (int x = lastPos.x; x < currentPos.x; x++) { for (int y = Mathf.FloorToInt(currHeight); y > 0; y--) { map[x, y] = 1; } currHeight += heightChange; } } } 

يتم تجانس على النحو التالي:

  1. نحصل على الموقف الحالي والأخير
  2. نحصل على الفرق بين نقطتين ، أهم المعلومات التي نحتاجها هي الفرق على طول المحور y
  3. ثم نحدد مقدار التغيير الذي يجب القيام به للوصول إلى هذه النقطة ، ويتم ذلك بقسمة الفرق في y على متغير الفاصل.
  4. بعد ذلك ، نبدأ في تعيين صفقات ، نذهب إلى الصفر
  5. عندما نصل إلى 0 على المحور y ، أضف التغيير في الارتفاع إلى الارتفاع الحالي وكرر العملية للموضع x التالي
  6. مع الانتهاء من كل موقف بين الموضع الأخير والحالي ، ننتقل إلى النقطة التالية

إذا كان الفاصل الزمني أقل من واحد ، فإننا ببساطة نستخدم الوظيفة السابقة ، والتي ستقوم بكل العمل لنا.

  else { //Defaults to a normal Perlin gen map = PerlinNoise(map, seed); } return map; 

دعونا نلقي نظرة على تقديم:


المشي عشوائي


المشي أعلى عشوائي


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

 public static int[,] RandomWalkTop(int[,] map, float seed) { //Seed our random System.Random rand = new System.Random(seed.GetHashCode()); //Set our starting height int lastHeight = Random.Range(0, map.GetUpperBound(1)); //Cycle through our width for (int x = 0; x < map.GetUpperBound(0); x++) { //Flip a coin int nextMove = rand.Next(2); //If heads, and we aren't near the bottom, minus some height if (nextMove == 0 && lastHeight > 2) { lastHeight--; } //If tails, and we aren't near the top, add some height else if (nextMove == 1 && lastHeight < map.GetUpperBound(1) - 2) { lastHeight++; } //Circle through from the lastheight to the bottom for (int y = lastHeight; y >= 0; y--) { map[x, y] = 1; } } //Return the map return map; } 


عشوائية المشي الأعلى مع مكافحة التعرج

هذا الجيل يعطينا ارتفاعات أكثر سلاسة مقارنة بتوليد ضوضاء بيرلين.

يوفر هذا الاختلاف في Random Walk نتيجة أكثر سلاسة مقارنةً بالإصدار السابق. يمكننا تنفيذه بإضافة متغيرين آخرين إلى الوظيفة:

  • يتم استخدام المتغير الأول لتحديد المدة التي يستغرقها للحفاظ على الارتفاع الحالي. وهو عدد صحيح ويتم إعادة تعيين عندما يتغير الطول
  • المتغير الثاني هو إدخال إلى الوظيفة ويستخدم كحد أدنى لعرض المقطع للارتفاع. سوف تصبح أكثر وضوحًا عندما ننظر إلى الوظيفة.

الآن نحن نعرف ماذا نضيف. دعنا نلقي نظرة على الوظيفة:

 public static int[,] RandomWalkTopSmoothed(int[,] map, float seed, int minSectionWidth) { //Seed our random System.Random rand = new System.Random(seed.GetHashCode()); //Determine the start position int lastHeight = Random.Range(0, map.GetUpperBound(1)); //Used to determine which direction to go int nextMove = 0; //Used to keep track of the current sections width int sectionWidth = 0; //Work through the array width for (int x = 0; x <= map.GetUpperBound(0); x++) { //Determine the next move nextMove = rand.Next(2); //Only change the height if we have used the current height more than the minimum required section width if (nextMove == 0 && lastHeight > 0 && sectionWidth > minSectionWidth) { lastHeight--; sectionWidth = 0; } else if (nextMove == 1 && lastHeight < map.GetUpperBound(1) && sectionWidth > minSectionWidth) { lastHeight++; sectionWidth = 0; } //Increment the section width sectionWidth++; //Work our way from the height down to 0 for (int y = lastHeight; y >= 0; y--) { map[x, y] = 1; } } //Return the modified map return map; } 

كما ترون في gif الموضح أدناه ، فإن تجانس خوارزمية المشي العشوائية يتيح لك الحصول على شرائح مسطحة جميلة على المستوى.


استنتاج


آمل أن تلهمك هذه المقالة لاستخدام الجيل الإجرائي في مشاريعك. إذا كنت ترغب في معرفة المزيد حول الخرائط التي تم إنشاؤها من الناحية الإجرائية ، فاستكشف المصادر الممتازة لـ Procedural Generation Wiki أو Roguebasin.com .

في الجزء الثاني من المقال ، سوف نستخدم الجيل الإجرائي لإنشاء أنظمة الكهوف.

الجزء 2


كل ما سنناقشه في هذا الجزء يمكن العثور عليه في هذا المشروع . يمكنك تنزيل الأصول وتجربة الخوارزميات الإجرائية الخاصة بك.


الضوضاء بيرلين


في الجزء السابق ، نظرنا في طرق لتطبيق ضوضاء Perlin لإنشاء طبقات عليا. لحسن الحظ ، يمكن أيضًا استخدام ضوضاء بيرلين لإنشاء كهف. يتم تحقيق ذلك من خلال حساب قيمة ضوضاء Perlin الجديدة ، والتي تستقبل معلمات الموضع الحالي مضروبة في المعدل. المُعدّل قيمة من 0 إلى 1. كلما زادت قيمة المُعدّل ، زاد توليد Perlin الأكثر فوضوية. بعد ذلك ، نقوم بتقريب هذه القيمة إلى عدد صحيح (0 أو 1) ، والذي نقوم بتخزينه في صفيف الخريطة. انظر كيف يتم تنفيذ هذا:

 public static int[,] PerlinNoiseCave(int[,] map, float modifier, bool edgesAreWalls) { int newPoint; for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { if (edgesAreWalls && (x == 0 || y == 0 || x == map.GetUpperBound(0) - 1 || y == map.GetUpperBound(1) - 1)) { map[x, y] = 1; //Keep the edges as walls } else { //Generate a new point using Perlin noise, then round it to a value of either 0 or 1 newPoint = Mathf.RoundToInt(Mathf.PerlinNoise(x * modifier, y * modifier)); map[x, y] = newPoint; } } } return map; } 

نستخدم المعدل بدلاً من البذور لأن نتائج جيل بيرلين تبدو أفضل عند ضربها برقم من 0 إلى 0.5. كلما انخفضت القيمة ، كلما كانت النتيجة أكثر غلاء. نلقي نظرة على نتائج العينة. Gif يبدأ بقيمة معدل من 0.01 ويصل تدريجياً إلى قيمة 0.25.


من هذه GIF ، يمكن ملاحظة أن جيل Perlin مع كل زيادة يزيد ببساطة من النمط.

المشي عشوائي


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

 public static int[,] RandomWalkCave(int[,] map, float seed, int requiredFloorPercent) { //Seed our random System.Random rand = new System.Random(seed.GetHashCode()); //Define our start x position int floorX = rand.Next(1, map.GetUpperBound(0) - 1); //Define our start y position int floorY = rand.Next(1, map.GetUpperBound(1) - 1); //Determine our required floorAmount int reqFloorAmount = ((map.GetUpperBound(1) * map.GetUpperBound(0)) * requiredFloorPercent) / 100; //Used for our while loop, when this reaches our reqFloorAmount we will stop tunneling int floorCount = 0; //Set our start position to not be a tile (0 = no tile, 1 = tile) map[floorX, floorY] = 0; //Increase our floor count floorCount++; 

تبدأ الوظيفة بما يلي:

  1. العثور على موقف البداية
  2. حساب عدد البلاط الكلمة ليتم حذفها.
  3. حذف البلاط في وضع البداية
  4. إضافة واحد إلى عدد البلاط.

ثم ننتقل إلى while . سوف يخلق كهفًا:

 while (floorCount < reqFloorAmount) { //Determine our next direction int randDir = rand.Next(4); switch (randDir) { //Up case 0: //Ensure that the edges are still tiles if ((floorY + 1) < map.GetUpperBound(1) - 1) { //Move the y up one floorY++; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase floor count floorCount++; } } break; //Down case 1: //Ensure that the edges are still tiles if ((floorY - 1) > 1) { //Move the y down one floorY--; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase the floor count floorCount++; } } break; //Right case 2: //Ensure that the edges are still tiles if ((floorX + 1) < map.GetUpperBound(0) - 1) { //Move the x to the right floorX++; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase the floor count floorCount++; } } break; //Left case 3: //Ensure that the edges are still tiles if ((floorX - 1) > 1) { //Move the x to the left floorX--; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase the floor count floorCount++; } } break; } } //Return the updated map return map; } 

ماذا نفعل هنا؟


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


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

نفق الاتجاه


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

 public static int[,] DirectionalTunnel(int[,] map, int minPathWidth, int maxPathWidth, int maxPathChange, int roughness, int curvyness) { //This value goes from its minus counterpart to its positive value, in this case with a width value of 1, the width of the tunnel is 3 int tunnelWidth = 1; //Set the start X position to the center of the tunnel int x = map.GetUpperBound(0) / 2; //Set up our random with the seed System.Random rand = new System.Random(Time.time.GetHashCode()); //Create the first part of the tunnel for (int i = -tunnelWidth; i <= tunnelWidth; i++) { map[x + i, 0] = 0; } 

ما الذي يحدث؟


أولا نضع قيمة العرض. سوف تنتقل قيمة العرض من القيمة مع الطرح إلى الموجب. بفضل هذا ، سوف نحصل على الحجم الذي نحتاجه. في هذه الحالة ، نستخدم القيمة 1 ، والتي بدورنا ستمنحنا إجمالي عرض 3 ، لأننا نستخدم القيم -1 ، 0 ، 1.

بعد ذلك ، قمنا بتعيين الموضع الأولي في x ، لذلك نأخذ منتصف عرض الخريطة. بعد ذلك ، يمكننا وضع نفق في الجزء الأول من الخريطة.


الآن دعنا ندخل في بقية الخريطة.

  //Cycle through the array for (int y = 1; y < map.GetUpperBound(1); y++) { //Check if we can change the roughness if (rand.Next(0, 100) > roughness) { //Get the amount we will change for the width int widthChange = Random.Range(-maxPathWidth, maxPathWidth); //Add it to our tunnel width value tunnelWidth += widthChange; //Check to see we arent making the path too small if (tunnelWidth < minPathWidth) { tunnelWidth = minPathWidth; } //Check that the path width isnt over our maximum if (tunnelWidth > maxPathWidth) { tunnelWidth = maxPathWidth; } } //Check if we can change the curve if (rand.Next(0, 100) > curvyness) { //Get the amount we will change for the x position int xChange = Random.Range(-maxPathChange, maxPathChange); //Add it to our x value x += xChange; //Check we arent too close to the left side of the map if (x < maxPathWidth) { x = maxPathWidth; } //Check we arent too close to the right side of the map if (x > (map.GetUpperBound(0) - maxPathWidth)) { x = map.GetUpperBound(0) - maxPathWidth; } } //Work through the width of the tunnel for (int i = -tunnelWidth; i <= tunnelWidth; i++) { map[x + i, y] = 0; } } return map; } 

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

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

تبدو نتائج هذا التطبيق كما يلي:


الآلي الخلوية


تستخدم الأوتوماتة الخلوية الخلايا المجاورة لتحديد ما إذا كانت الخلية الحالية قيد التشغيل (1) أو متوقفة (0). يتم إنشاء أساس تحديد الخلايا المجاورة استنادًا إلى شبكة من الخلايا تم إنشاؤها عشوائيًا. سنقوم بإنشاء شبكة المصدر هذه باستخدام الدالة C # Random.Next .

نظرًا لأن لدينا تطبيقين مختلفين لأتمتة الخلوية ، فقد كتبت وظيفة منفصلة لإنشاء هذه الشبكة الأساسية. الوظيفة تبدو كالتالي:

 public static int[,] GenerateCellularAutomata(int width, int height, float seed, int fillPercent, bool edgesAreWalls) { //Seed our random number generator System.Random rand = new System.Random(seed.GetHashCode()); //Initialise the map int[,] map = new int[width, height]; for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { //If we have the edges set to be walls, ensure the cell is set to on (1) if (edgesAreWalls && (x == 0 || x == map.GetUpperBound(0) - 1 || y == 0 || y == map.GetUpperBound(1) - 1)) { map[x, y] = 1; } else { //Randomly generate the grid map[x, y] = (rand.Next(0, 100) < fillPercent) ? 1 : 0; } } } return map; } 

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


حي مور


يستخدم حي مور لتهدئة الجيل الأولي من الأوتوماتة الخلوية. حي مور يشبه هذا:


تنطبق القواعد التالية على الحي:

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

وظيفة فحص حي مور هي كما يلي:

 static int GetMooreSurroundingTiles(int[,] map, int x, int y, bool edgesAreWalls) { /* Moore Neighbourhood looks like this ('T' is our tile, 'N' is our neighbours) * * NNN * NTN * NNN * */ int tileCount = 0; for(int neighbourX = x - 1; neighbourX <= x + 1; neighbourX++) { for(int neighbourY = y - 1; neighbourY <= y + 1; neighbourY++) { if (neighbourX >= 0 && neighbourX < map.GetUpperBound(0) && neighbourY >= 0 && neighbourY < map.GetUpperBound(1)) { //We don't want to count the tile we are checking the surroundings of if(neighbourX != x || neighbourY != y) { tileCount += map[neighbourX, neighbourY]; } } } } return tileCount; } 

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

 public static int[,] SmoothMooreCellularAutomata(int[,] map, bool edgesAreWalls, int smoothCount) { for (int i = 0; i < smoothCount; i++) { for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { int surroundingTiles = GetMooreSurroundingTiles(map, x, y, edgesAreWalls); if (edgesAreWalls && (x == 0 || x == (map.GetUpperBound(0) - 1) || y == 0 || y == (map.GetUpperBound(1) - 1))) { //Set the edge to be a wall if we have edgesAreWalls to be true map[x, y] = 1; } //The default moore rule requires more than 4 neighbours else if (surroundingTiles > 4) { map[x, y] = 1; } else if (surroundingTiles < 4) { map[x, y] = 0; } } } } //Return the modified map return map; } 

من المهم أن نلاحظ هنا أن الوظيفة تحتوي على حلقة تؤدي تنعيم عدد المرات المحدد. بفضل هذا ، تم الحصول على بطاقة أكثر جمالا.


يمكننا دائمًا تعديل هذه الخوارزمية عن طريق توصيل الغرف إذا كان هناك ، على سبيل المثال ، كتلتان فقط بينهما.

حي فون نيومان


حي فون نيومان هو وسيلة أخرى شعبية لتنفيذ أوتوماتيا الخلوية. لمثل هذا الجيل ، نستخدم حيًا أبسط منه في جيل مور. الحي يشبه هذا:


تنطبق القواعد التالية على الحي:

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

تستخدم النتيجة الثانية نفس مبادئ الأول ، ولكنها توسع منطقة الحي.

نتحقق من الجيران من خلال الوظيفة التالية:

 static int GetVNSurroundingTiles(int[,] map, int x, int y, bool edgesAreWalls) { /* von Neumann Neighbourhood looks like this ('T' is our Tile, 'N' is our Neighbour) * * N * NTN * N * */ int tileCount = 0; //Keep the edges as walls if(edgesAreWalls && (x - 1 == 0 || x + 1 == map.GetUpperBound(0) || y - 1 == 0 || y + 1 == map.GetUpperBound(1))) { tileCount++; } //Ensure we aren't touching the left side of the map if(x - 1 > 0) { tileCount += map[x - 1, y]; } //Ensure we aren't touching the bottom of the map if(y - 1 > 0) { tileCount += map[x, y - 1]; } //Ensure we aren't touching the right side of the map if(x + 1 < map.GetUpperBound(0)) { tileCount += map[x + 1, y]; } //Ensure we aren't touching the top of the map if(y + 1 < map.GetUpperBound(1)) { tileCount += map[x, y + 1]; } return tileCount; } 

بعد تلقي عدد الجيران ، يمكننا المضي قدمًا في سلاسة الصفيف. كما كان من قبل ، نحتاج إلى حلقة forلإكمال عدد مرات التكرار المنقولة إلى المدخلات.

 public static int[,] SmoothVNCellularAutomata(int[,] map, bool edgesAreWalls, int smoothCount) { for (int i = 0; i < smoothCount; i++) { for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { //Get the surrounding tiles int surroundingTiles = GetVNSurroundingTiles(map, x, y, edgesAreWalls); if (edgesAreWalls && (x == 0 || x == map.GetUpperBound(0) - 1 || y == 0 || y == map.GetUpperBound(1))) { //Keep our edges as walls map[x, y] = 1; } //von Neuemann Neighbourhood requires only 3 or more surrounding tiles to be changed to a tile else if (surroundingTiles > 2) { map[x, y] = 1; } else if (surroundingTiles < 2) { map[x, y] = 0; } } } } //Return the modified map return map; } 

كما ترون أدناه ، فإن النتيجة النهائية هي أكثر ممتلئة بكثير من حي مور:


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

استنتاج


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

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


All Articles