لعبة مثل ماين كرافت او تراريا، تجد فيها أن العالم دائما عشوائي، مع ذلك يحتوى الكثير من العناصر الاساسية مثل الاشجار او البحار، وتلك العناصر ليست عشوائية بحق، بل لها قوانينها، فلن تجد شجرة تطوير في الهواء مثلا.
العشوائية في حد ذاتها نوعان، عشوائية تامة، وعشوائية مُنظمة. العشوائية التامة لا يمكن انشاء عالم بها، لأنه كما ذكرت، قد تحدث الكثير من الاخطاء، مثل وجود شجرة في الهواء أو وجود الكثير من بقع الماء غير المتصلة!
أما ذلك البناء العشوائي المُنظم ،فتسمى التوليد الإجرائي – Procedural generation، التوليد الإجرائي لن يُحدث تلك المشكلة، بل ستجد أن بُقع الماء مع بعضها وبٌقع الأرض مع بعضها. ويمكنك التحكم في مقدار كل شيء.
و للوصول إلى هذا النظام فإن لكل مُحرك العاب له طريقته الخاصة، وفي هذا المقال أوضح كيف يمكن إنشاء نظام الخرائط العشوائي ذلك بالإصدار الرابع من المحرك (Godot 4).
عناصر المقال
مبدأ التوليد الإجرائي (Noise Texture)
تخيل معي خط عليه 100 نقطة، اختر 10 نقاط عشوائية منه، ثم ضع لها ارتفاع عشوائي من 1 إلى -1، وبالنسبة للبقية، فاجعلها فقط “تتناسق” مع النقاط الـ10 الاساسية، وهذا ما ستحصل عليه :
كما ترى، فان هذا الخط به نقاط مرتفعة ونقاط منخفضة، والأهم، أن النقاط التي بين الارتفاع والهبوط متناسقة معها، فليست عشوائية بالكامل، وذلك بالظبط المبدأ الذي يعمل عليه التوليد الإجرائي.
الفارق هو استخدام ما يسمى بالـ Noise Texture، أو نسيج التشويش، وهي صورة بها ايضا الكثير من النقاط، بل الكثير الكثير من النقاط، ولكن من المنظور العلوي بدل الجانبي.
فكما تلاحظ في الصورة التي بالجانب، فإن ذلك يكون العديد من الـ”بقع” بعضها باللون الأبيض والبعض باللون الاسود (بدلا من -1 و1). ولكن تظل يشار إلى تلك النقاط بحسب درجتها من السواد والبياض بالارقام من -1 وحتى 1 (1, 0.9, 0.8 . . . . -0.9, -1)
باستخدام هذا النسيج، تستطيع بناء العالم خاصتك، فمثلا تجعل البقع السوداء (ما قل عن 0) هي الماء، و البقع البيضاء (ما زاد عن 0) هي الارض.
ويمكنك اضافة الكثير من الانسجة في العالم الواحد، فتستعمل واحد لإنشاء التلال والهضاب والجبال والغابات، كل ذلك بحسب الشرط/الرقم الذي تريد
مشروع توليد إجرائي في Godot
يكفي كلاما نظريا، اقفز الآن لمحرك الالعاب غودو، وقم بعمل مشروع جديد، وفيه ضع عقدة خارطة القطع – TileMap والتي ستكون الأساس الذي منه نبني العالم. وبالطبع جهزها بالصور التي تريد. يمكنك رؤية هذه المقال لمعرفة كيف.
بالنسبة لي، فلم اكتفي بوضع صورة واحدة فقط، بل ايضا استعملت نظام التضاريس Terrain، وذلك لجعل البناء التلقائي مُتكامل. فيضع القطعة المناسبة على حسب ما تجاوره، وإنشاء هذا الأمر يمكنك الاطلاع على هذه المقالة.
اخيرا، قم بإنشاء شيفرة (script) على خارطة القطع تلك TileMap، لأن تلك العقدة به اهم الاوامر البرمجية التي نحتاجها، وهي :
clear() # تمسح جميع القطع الموضوعة
local_to_map(Vector2) # استخراج موقع نقطة ما على شبكة العالم
set_cell() # لوضع القطعة المطلوبة في المكان المطلوب
set_cells_terrain_connect() # لوضع القطعة المطلوبة في المكان المطلوب مع تشغيل تضاريسها
وعلى هذا الاساس (بالاضافة للمبدأ في المقدمة) يمكن القول أن المطلوب فعله هو :
- إنشاء نسيج تشويش عشوائي
- تحديد المنطقة المراد العمل عليها في العالم
- المرور على كل نقطة فيها ومعرفة مُقابلها في شبكة العالم -عن طريق `local_to_map`
- باستخدام نفس النقطة نرى المقابل لها في النسيج
- إنشاء شروط التحقق الخاصة بالتشويش (إن كان أكثر من 0 ستكون ارض، واقل من ذلك هو الماء)
- في حال تحقق الشرط نضع القطع المطلوبة – باستخدام set_cell
إنشاء نسيج التشويش، فإن المحرك لديه بالفعل مورد – Resource خاص بذلك ويسمى FastNoiseLite. لذا يمكنك فقط إنشاء متغير جديد، واعطائه ذلك التشويش كقيمة، ثم اعطه بذرة-seed عشوائية كالتالي :
var floor = FastNoiseLite.new() # إنشاء مورد نسيج تشوش جديد
func _ready():
floor.seed = randi() # إعطائه بذرة عشوائية
pass
ثم المهمة الثانية والثالثة معا وهي تحديد المنطقة المراد العمل عليها. والمرور على كل نقطة فيها (x, y). والخيار الاسهل هنا هو استعمال ابعاد نافذة اللعبة، والتي في العادة تكون بعرض 1152 وطول 648.
ولكن وجب التنبيه، انه لا حاجة للمرور بالفعل على كل نقطة منها، بل يعتمد الامر على حجم القطع Tiles خاصتك، فمثلا انا استخدم قطع بحجم 16*16 بكسل، بالتالي سامر فقط على كل 16 نقطة، – أي (0-16-32-48. . )
func generate_chunk(): # إنشاء دالة متخصصة في التوليد الإجرائي واستدعائها وقت الحاجة
var height = 648 # الارتفاع
var width = 1152 #العرض
for y in range(0, height, 16): # المرور على كل 16 بكسل طوليا
for x in range(0, width, 16): # المرور على كل 16 بكسل عرضيا
# بقية الشروط والمهام
فكما تلاحظ، استعملت الامر range، وفيه حددت نقطة البداية، ونقطة النهاية، وما مقدار الانتقال والذي هو حجم القطع خاصتك، كما انشئت متغيري العرض والطول فقط لتوضيح التعليمات البرمجية هذه، ولكن يمكنك الاستغناء عنها.
وللحصول على هذه النقطة (X,Y) يمكن معرفة المقابل لها في نسيج التشويش وايضا في شبكة العالم هكذا :
var tile_pos = local_to_map(Vector2(x,y)) # تحديد المقابل لنقطة في شبكة العالم
var floor_point = floor.get_noise_2d(x, y) # تحديد المقابل للنقطة في نسيج التشويش
تذكر أن الشيفرة (script) موجودة بالفعل على خريطة القطع (TileMap)، بالتالي يمكن استخدام الأمر local_to_map بلا الحاجة لاستدعاء هذه العقدة.
المتغير floor_point في الوقت الحالي لا يعطيك نقطة، بل يعطيك قيمة تلك النقطة، فكما وضحت في المبدء، قد تكون تلك النقطة بيضاء او سوداء او اي درجة من بينهما، ويُشار إليهم بالارقام من -1 إلى 1.
لذا شرطي الخاص هو إذا كانت قيمة النقطة في التشويش أقل من 0.3، سأضع أرض، غير ذلك ساضع ماء، بالنسبة للماء فواقع اني فقط وضعت خلفية تبدو كالماء، لذلك عدم وضع قطعة، يعني إظهار الخلفية المائية.
فيكون الشرط مع استخدام القطع كالتالي (مع شرح وافي لكل سطر) :
if floor_point < .3: # شرطي الخاص ومتى تُضع قطعة الأرض
set_cell( # وضع قطعة أرض فقط دون تناسق مع القطع المجاورة
0, # الطبقة
tile_pos, # الموضع المطلوب وضع القطعة فيه
0, #رقم الأطلس المُستخدم
Vector2i(2, 4)# موضوع القطعة المطلوبة على الاطلس
)
set_cells_terrain_connect( # أمر وضع القطعة مع تناسقها مع مجاورتها
0, # الطبقة
[tile_pos], # مصفوفة بالأماكن المطلوبة (حاليا فقط مكان واحد)
0, # رقم مجموعة التضاريس
0 # التضاريس المطلوب استخدامها من المجموعة
)
الحق انه يجب عليك استخدام اما set_cell او set_cells_terrain_connect، انا وضعت الاثنان لشرح، ولكن فقط استعمل الأمر المناسب لك.
بهذا تكون انتهت خوارزمية التوليد الاجرائي، وفي نهاية الدالة اضع الأمر await مع مؤقت بمدة زمنية صغيرة جدا، وذلك فقط لأنظر إليها وهي توضع، فيكون شكل الدالية النهائية والنتيجة كالتالي :
extends TileMap
var floor = FastNoiseLite.new()
func _ready():
floor.seed = randi()
generate_chunk()
func generate_chunk():
var height = 648 # الارتفاع
var width = 1152 #العرض
for y in range(0, height, 16): # المرور على كل 16 بكسل طوليا
for x in range(0, width, 16): # المرور على كل 16 بكسل عرضيا
var tile_pos = local_to_map(Vector2(x,y))
var floor_point = floor.get_noise_2d(x, y)
if floor_point < .3: # شرطي الخاص ومتى تُضع قطعة الأرض
set_cells_terrain_connect(0,[tile_pos],0,0)
await get_tree().create_timer(0.001).timeout # مؤقت صغير بين وضع كل قطعة والثانية
أفكار واقتراحات
يمكنك الان انشاء عدة أنسجة، وكل نسيج يتخصص في أمر ما، ربما تستعمل واحد لإنشاء المناطق (biomes)، واخر تستعمله لاضافة العناصر البيئية كالاشجار والصخور والجبال والانهار، أو حتى تحديد حرارة الجو !
فمثلا تجعل الأرض هي اي رقم اعلى من 0.2، والجبال لاي رقم اعلى من 0.8، وتجعل الانهار فقط ما بين 0 و 0.2، وباستعمال عدة أنسجة، تستخدم واحد لمعرفة الطقس، وواحد للأشجار، فإن كان الجو حارا والمكان فوق الأرض، وتحقق شرط نسيج الاشجار، وتضع صبارا بدلا من شجرة عادية !
وايضا تستطيع استعماله في توزيع موارد العالم أو إنشاء الغاز عشوائية، او وجود مدن إذا تحققت مجموعة كاملة من الشروط حتى، فقط الأمر متروك لخيالك. كما يمكن الحصول على هذا المشروع واكثر لداعمي المنصة.