يستخدم الجيل الإجرائي لزيادة تباين الألعاب. تشمل المشروعات المعروفة
Minecraft و
Enter the Gungeon و
Descenders . في هذا
المنشور ، سأشرح بعض الخوارزميات التي يمكن استخدامها عند العمل مع نظام
Tilemap ، والذي ظهر كدالة ثنائية الأبعاد في Unity 2017.2 ، ومع
RuleTile .
مع الإنشاء الإجرائي للخرائط ، ستكون كل لعبة عابرة فريدة من نوعها. يمكنك استخدام بيانات الإدخال المختلفة ، على سبيل المثال ، الوقت أو المستوى الحالي للاعب ، لتغيير المحتوى ديناميكيًا حتى بعد تجميع اللعبة.
ما هو هذا المنصب؟
سننظر في بعض الطرق الأكثر شيوعًا لإنشاء عوالم إجرائية ، بالإضافة إلى بعض الاختلافات التي قمت بإنشائها. فيما يلي مثال على ما يمكنك إنشاؤه بعد قراءة المقال. تعمل ثلاث خوارزميات معًا لإنشاء خريطة باستخدام
Tilemap و
RuleTile :
في عملية إنشاء خريطة باستخدام أي خوارزمية ، نحصل على مجموعة
int
تحتوي على جميع البيانات الجديدة. يمكنك الاستمرار في تعديل هذه البيانات أو تقديمها إلى خريطة تجانب.
قبل القراءة ، سيكون من الجيد معرفة ما يلي:
- نحن نميز ما هو البلاط وما لا يستخدم القيم الثنائية. 1 هو البلاط ، 0 هو غيابه.
- سنقوم بتخزين جميع البطاقات في صفيف صحيح ثنائي الأبعاد يتم إرجاعه إلى المستخدم في نهاية كل وظيفة (باستثناء تلك التي يتم تنفيذ العرض).
- سأستخدم دالة صفيف GetUpperBound () للحصول على ارتفاع وعرض كل خريطة ، بحيث تتلقى الدالة عددًا أقل من المتغيرات والرمز أنظف.
- غالبًا ما أستخدم Mathf.FloorToInt () ، لأن نظام إحداثيات Tilemap يبدأ في أسفل اليسار ، ويتيح لك Mathf.FloorToInt () تقريب الأرقام إلى عدد صحيح.
- تتم كتابة جميع الكود في هذا المنشور في 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) {
تحديث الخريطة
تُستخدم هذه الوظيفة فقط لتحديث الخريطة وليس لإعادة العرض. بفضل هذا ، يمكننا استخدام موارد أقل دون إعادة رسم كل مربع وبيانات التجانب الخاصة به.
public static void UpdateMap(int[,] map, Tilemap tilemap)
الضوضاء بيرلين
بيرلين الضوضاء يمكن استخدامها لأغراض مختلفة. أولاً ، يمكننا استخدامه لإنشاء الطبقة العليا من خريطتنا. للقيام بذلك ، ما عليك سوى الحصول على نقطة جديدة باستخدام الموضع الحالي x والبذر.
حل بسيط
تستخدم طريقة التوليد هذه أبسط أشكال تحقيق ضوضاء بيرلين في توليد المستويات. يمكننا استخدام وظيفة Unity فيما يتعلق بضوضاء Perlin حتى لا نكتب الكود بأنفسنا. سنستخدم أيضًا الأعداد الصحيحة لخريطة التجانب ، وذلك باستخدام
دالة Mathf.FloorToInt () .
public static int[,] PerlinNoise(int[,] map, float seed) { int newPoint;
إليك ما يبدو عليه بعد التقديم إلى خريطة تجانب:
تمهيد
يمكنك أيضا أن تأخذ هذه الوظيفة وتنعيمها. قم بتعيين فترات لتثبيت مرتفعات بيرلين ، ثم قم بإجراء تجانس بين هذه النقاط. ستصبح هذه الوظيفة أكثر تعقيدًا بعض الشيء ، لأن الفواصل الزمنية تحتاج إلى النظر في قوائم قيم الأعداد الصحيحة.
public static int[,] PerlinNoiseSmooth(int[,] map, float seed, int interval) {
في الجزء الأول من هذه الوظيفة ، نتحقق أولاً مما إذا كان الفاصل الزمني أكبر من واحد. إذا كان الأمر كذلك ، ثم توليد الضوضاء. يتم تنفيذ الجيل على فترات بحيث يمكن تطبيق التنعيم. الجزء التالي من الوظيفة هو تهدئة النقاط.
يتم تجانس على النحو التالي:
- نحصل على الموقف الحالي والأخير
- نحصل على الفرق بين نقطتين ، أهم المعلومات التي نحتاجها هي الفرق على طول المحور y
- ثم نحدد مقدار التغيير الذي يجب القيام به للوصول إلى هذه النقطة ، ويتم ذلك بقسمة الفرق في y على متغير الفاصل.
- بعد ذلك ، نبدأ في تعيين صفقات ، نذهب إلى الصفر
- عندما نصل إلى 0 على المحور y ، أضف التغيير في الارتفاع إلى الارتفاع الحالي وكرر العملية للموضع x التالي
- مع الانتهاء من كل موقف بين الموضع الأخير والحالي ، ننتقل إلى النقطة التالية
إذا كان الفاصل الزمني أقل من واحد ، فإننا ببساطة نستخدم الوظيفة السابقة ، والتي ستقوم بكل العمل لنا.
else {
دعونا نلقي نظرة على تقديم:
المشي عشوائي
المشي أعلى عشوائي
هذه الخوارزمية ينفذ الوجه عملة. يمكننا الحصول على واحد من نتيجتين. إذا كانت النتيجة هي "النسر" ، فنحن نرفع كتلة واحدة للأعلى ، وإذا كانت النتيجة هي "ذيول" ، فإننا ننقل الكتلة لأسفل. هذا يخلق ارتفاعات من خلال التحرك باستمرار لأعلى أو لأسفل. العيب الوحيد لهذه الخوارزمية هو انسداد ملحوظ للغاية. دعونا نلقي نظرة على كيفية عملها.
public static int[,] RandomWalkTop(int[,] map, float seed) {
عشوائية المشي الأعلى مع مكافحة التعرجهذا الجيل يعطينا ارتفاعات أكثر سلاسة مقارنة بتوليد ضوضاء بيرلين.
يوفر هذا الاختلاف في Random Walk نتيجة أكثر سلاسة مقارنةً بالإصدار السابق. يمكننا تنفيذه بإضافة متغيرين آخرين إلى الوظيفة:
- يتم استخدام المتغير الأول لتحديد المدة التي يستغرقها للحفاظ على الارتفاع الحالي. وهو عدد صحيح ويتم إعادة تعيين عندما يتغير الطول
- المتغير الثاني هو إدخال إلى الوظيفة ويستخدم كحد أدنى لعرض المقطع للارتفاع. سوف تصبح أكثر وضوحًا عندما ننظر إلى الوظيفة.
الآن نحن نعرف ماذا نضيف. دعنا نلقي نظرة على الوظيفة:
public static int[,] RandomWalkTopSmoothed(int[,] map, float seed, int minSectionWidth) {
كما ترون في 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;
نستخدم المعدل بدلاً من البذور لأن نتائج جيل بيرلين تبدو أفضل عند ضربها برقم من 0 إلى 0.5. كلما انخفضت القيمة ، كلما كانت النتيجة أكثر غلاء. نلقي نظرة على نتائج العينة. Gif يبدأ بقيمة معدل من 0.01 ويصل تدريجياً إلى قيمة 0.25.
من هذه GIF ، يمكن ملاحظة أن جيل Perlin مع كل زيادة يزيد ببساطة من النمط.
المشي عشوائي
في الجزء السابق ، رأينا أنه يمكنك استخدام أداة رمي العملة المعدنية لتحديد المكان الذي ستنتقل فيه المنصة لأعلى أو لأسفل. في هذا الجزء ، سوف نستخدم نفس الفكرة ، لكن
مع خيارين إضافيين لليسار واليمين التحول. هذا الاختلاف في خوارزمية المشي العشوائي يسمح لنا بإنشاء كهوف. للقيام بذلك ، نختار اتجاهًا عشوائيًا ، ثم نقل موضعنا وحذف التجانب. نواصل هذه العملية حتى نصل إلى العدد المطلوب من البلاط التي تحتاج إلى تدمير. حتى الآن ، نستخدم فقط 4 اتجاهات: أعلى ، أسفل ، يسار ، يمين.
public static int[,] RandomWalkCave(int[,] map, float seed, int requiredFloorPercent) {
تبدأ الوظيفة بما يلي:
- العثور على موقف البداية
- حساب عدد البلاط الكلمة ليتم حذفها.
- حذف البلاط في وضع البداية
- إضافة واحد إلى عدد البلاط.
ثم ننتقل إلى
while
. سوف يخلق كهفًا:
while (floorCount < reqFloorAmount) {
ماذا نفعل هنا؟
حسنًا ، أولاً ، بمساعدة عدد عشوائي ، نختار الاتجاه الذي يجب التحرك فيه. ثم نتحقق من الاتجاه الجديد من خلال
switch case
. في هذا البيان ، نتحقق مما إذا كان الموضع عبارة عن جدار. إذا لم يكن كذلك ، فاحذف العنصر مع التجانب من الصفيف. نواصل القيام بذلك حتى نصل إلى منطقة الطابق المطلوب. النتيجة مبينة أدناه:
لقد قمت أيضًا بإنشاء نسختي الخاصة من هذه الوظيفة ، والتي تتضمن أيضًا اتجاهات قطرية. رمز الوظيفة طويل جدًا ، لذا إذا كنت تريد النظر إليه ، فقم بتنزيل المشروع من الرابط في بداية هذا الجزء من المقالة.
نفق الاتجاه
يبدأ نفق الاتجاه من حافة الخريطة ويصل إلى الحافة المقابلة. يمكننا التحكم في انحناء وخشونة النفق عن طريق تمريرها إلى وظيفة الإدخال. يمكننا أيضا تعيين الحد الأدنى والحد الأقصى لطول أجزاء النفق. دعنا نلقي نظرة على التنفيذ:
public static int[,] DirectionalTunnel(int[,] map, int minPathWidth, int maxPathWidth, int maxPathChange, int roughness, int curvyness) {
ما الذي يحدث؟
أولا نضع قيمة العرض. سوف تنتقل قيمة العرض من القيمة مع الطرح إلى الموجب. بفضل هذا ، سوف نحصل على الحجم الذي نحتاجه. في هذه الحالة ، نستخدم القيمة 1 ، والتي بدورنا ستمنحنا إجمالي عرض 3 ، لأننا نستخدم القيم -1 ، 0 ، 1.
بعد ذلك ، قمنا بتعيين الموضع الأولي في x ، لذلك نأخذ منتصف عرض الخريطة. بعد ذلك ، يمكننا وضع نفق في الجزء الأول من الخريطة.
الآن دعنا ندخل في بقية الخريطة.
نقوم بإنشاء رقم عشوائي للمقارنة مع قيمة خشونة ، وإذا كان أعلى من هذه القيمة ، ثم يمكن تغيير عرض المسار. نحن أيضًا نتحقق من القيمة حتى لا يكون العرض أصغر من اللازم. في الجزء التالي من الشفرة ، نجري طريقنا عبر الخريطة. في كل مرحلة ، يحدث ما يلي:
- نحن نولد رقمًا عشوائيًا جديدًا مقارنة بقيمة الانحناء. كما هو الحال في الاختبار السابق ، إذا كانت أكبر من القيمة ، فنحن نغير النقطة المركزية للمسار. نقوم أيضًا بإجراء فحص حتى لا تتجاوز الخريطة.
- أخيرًا ، نضع نفقًا في الجزء المنشأ حديثًا.
تبدو نتائج هذا التطبيق كما يلي:
الآلي الخلوية
تستخدم
الأوتوماتة الخلوية الخلايا المجاورة لتحديد ما إذا كانت الخلية الحالية قيد التشغيل (1) أو متوقفة (0). يتم إنشاء أساس تحديد الخلايا المجاورة استنادًا إلى شبكة من الخلايا تم إنشاؤها عشوائيًا. سنقوم بإنشاء شبكة المصدر هذه باستخدام الدالة C #
Random.Next .
نظرًا لأن لدينا تطبيقين مختلفين لأتمتة الخلوية ، فقد كتبت وظيفة منفصلة لإنشاء هذه الشبكة الأساسية. الوظيفة تبدو كالتالي:
public static int[,] GenerateCellularAutomata(int width, int height, float seed, int fillPercent, bool edgesAreWalls) {
في هذه الوظيفة ، يمكنك أيضًا تعيين ما إذا كانت شبكتنا تحتاج إلى جدران أم لا. من جميع النواحي ، الأمر بسيط للغاية. نتحقق من رقم عشوائي مع ملء المئة لتحديد ما إذا تم تمكين الخلية الحالية. ألق نظرة على النتيجة:
حي مور
يستخدم حي مور لتهدئة الجيل الأولي من الأوتوماتة الخلوية. حي مور يشبه هذا:
تنطبق القواعد التالية على الحي:
- نتحقق من الجار في كل اتجاه.
- إذا كان الجار عبارة عن تجانب نشط ، فأضف واحدًا إلى عدد البلاطات المحيطة.
- إذا كان الجار بلاطة غير نشطة ، فإننا لا نفعل شيئًا.
- إذا كانت الخلية تحتوي على أكثر من 4 بلاطات محيطة ، فعليك تنشيط الخلية.
- إذا كانت الخلية تحتوي على 4 بلاطات محيطة تمامًا ، فلن نفعل شيئًا معها.
- كرر ذلك حتى نتحقق من كل بلاطة خريطة.
وظيفة فحص حي مور هي كما يلي:
static int GetMooreSurroundingTiles(int[,] map, int x, int y, bool edgesAreWalls) { 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)) {
بعد التحقق من البلاط ، نستخدم هذه المعلومات في وظيفة التنعيم. هنا ، كما في توليد الأوتوماتة الخلوية ، يمكن للمرء أن يشير إلى ما إذا كانت حواف الخريطة يجب أن تكون جدرانًا.
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))) {
من المهم أن نلاحظ هنا أن الوظيفة تحتوي على حلقة تؤدي تنعيم عدد المرات المحدد. بفضل هذا ، تم الحصول على بطاقة أكثر جمالا.
يمكننا دائمًا تعديل هذه الخوارزمية عن طريق توصيل الغرف إذا كان هناك ، على سبيل المثال ، كتلتان فقط بينهما.
حي فون نيومان
حي فون نيومان هو وسيلة أخرى شعبية لتنفيذ أوتوماتيا الخلوية. لمثل هذا الجيل ، نستخدم حيًا أبسط منه في جيل مور. الحي يشبه هذا:
تنطبق القواعد التالية على الحي:
- نتحقق من الجيران المباشرين للبلاط ، دون النظر إلى الجسور المائلة.
- إذا كانت الخلية نشطة ، فأضف واحدة إلى الكمية.
- إذا كانت الخلية غير نشطة ، فلا تفعل شيئًا.
- إذا كانت الخلية تحتوي على أكثر من جارتين ، فإننا نجعل الخلية الحالية نشطة.
- إذا كانت الخلية تحتوي على أقل من جارتين ، فإننا نجعل الخلية الحالية غير نشطة.
- إذا كان هناك جاران بالضبط ، فلا تقم بتغيير الخلية الحالية.
تستخدم النتيجة الثانية نفس مبادئ الأول ، ولكنها توسع منطقة الحي.
نتحقق من الجيران من خلال الوظيفة التالية: static int GetVNSurroundingTiles(int[,] map, int x, int y, bool edgesAreWalls) { int tileCount = 0;
بعد تلقي عدد الجيران ، يمكننا المضي قدمًا في سلاسة الصفيف. كما كان من قبل ، نحتاج إلى حلقة 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++) {
كما ترون أدناه ، فإن النتيجة النهائية هي أكثر ممتلئة بكثير من حي مور:هنا ، كما هو الحال في محيط مور ، يمكنك تشغيل برنامج نصي إضافي لتحسين الاتصالات بين أجزاء الخريطة.استنتاج
آمل أن تلهمك هذه المقالة لاستخدام نوع من الإجراءات الإجرائية في مشاريعك. إذا لم تقم بتنزيل المشروع ، فيمكنك الحصول عليه هنا .