مقدمة في البرمجة: مطلق النار ثلاثي الأبعاد البسيط من البداية خلال عطلة نهاية الأسبوع ، الجزء 1

هذا النص مخصص لأولئك الذين يتقنون البرمجة فقط. الفكرة الرئيسية هي إظهار خطوة بخطوة كيف يمكنك إنشاء اللعبة بشكل مستقل مثل Wolfenstein 3D . انتبه ، لن أتنافس مع كارماك على الإطلاق ، إنه عبقري ورمزه جميل. أنا أهدف في مكان مختلف تمامًا: أستخدم قوة الحوسبة الهائلة لأجهزة الكمبيوتر الحديثة حتى يتمكن الطلاب من إنشاء مشاريع مضحكة في غضون أيام قليلة دون الوقوع في بروز التحسين. أنا أكتب رمزًا بطيئًا على وجه التحديد ، لأنه أقصر بكثير وأسهل الفهم. يكتب Carmack 0x5f3759df ، أكتب 1 / sqrt (x). لدينا أهداف مختلفة.

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

تبين أن الورقة طويلة ، لذا قسمت النص إلى قسمين:


يبدو تنفيذ التعليمات البرمجية من مستودع التخزين الخاص بي كما يلي:


هذه ليست لعبة منتهية ، ولكنها فارغة فقط للطلاب. مثال على لعبة منتهية كتبها اثنان من الطلاب الجدد ، انظر الجزء الثاني .

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

في وقت كتابة هذا التقرير ، يحتوي المستودع على 486 سطرًا من التعليمات البرمجية:

haqreu@daffodil:~/tinyraycaster$ cat *.cpp *.h | wc -l 486 

يعتمد المشروع على SDL2 ، ولكن بشكل عام ، تظهر واجهة النافذة ومعالجة الأحداث من لوحة المفاتيح في وقت متأخر تمامًا ، في منتصف ليل السبت :) ، عندما يكون قد تم بالفعل تنفيذ جميع رموز التجسيد.

لذلك ، أقسم جميع التعليمات البرمجية إلى خطوات ، بدءًا من برنامج التحويل البرمجي C ++ العاري. كما في مقالاتي السابقة بالجدول ( tyts ، tyts ، tyts ) ، ألتزم بالقاعدة "خطوة واحدة = التزام واحد" ، حيث أن github يجعله مناسبًا جدًا لعرض محفوظات تغييرات الكود.

المرحلة 1: حفظ الصورة على القرص


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



إليك رمز C ++ الكامل الذي يرسم ما نحتاج إليه:

 #include <iostream> #include <fstream> #include <vector> #include <cstdint> #include <cassert> uint32_t pack_color(const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t a=255) { return (a<<24) + (b<<16) + (g<<8) + r; } void unpack_color(const uint32_t &color, uint8_t &r, uint8_t &g, uint8_t &b, uint8_t &a) { r = (color >> 0) & 255; g = (color >> 8) & 255; b = (color >> 16) & 255; a = (color >> 24) & 255; } void drop_ppm_image(const std::string filename, const std::vector<uint32_t> &image, const size_t w, const size_t h) { assert(image.size() == w*h); std::ofstream ofs(filename); ofs << "P6\n" << w << " " << h << "\n255\n"; for (size_t i = 0; i < h*w; ++i) { uint8_t r, g, b, a; unpack_color(image[i], r, g, b, a); ofs << static_cast<char>(r) << static_cast<char>(g) << static_cast<char>(b); } ofs.close(); } int main() { const size_t win_w = 512; // image width const size_t win_h = 512; // image height std::vector<uint32_t> framebuffer(win_w*win_h, 255); // the image itself, initialized to red for (size_t j = 0; j<win_h; j++) { // fill the screen with color gradients for (size_t i = 0; i<win_w; i++) { uint8_t r = 255*j/float(win_h); // varies between 0 and 255 as j sweeps the vertical uint8_t g = 255*i/float(win_w); // varies between 0 and 255 as i sweeps the horizontal uint8_t b = 0; framebuffer[i+j*win_w] = pack_color(r, g, b); } } drop_ppm_image("./out.ppm", framebuffer, win_w, win_h); return 0; } 

إذا لم يكن لديك مترجم في متناول اليد ، فهذا لا يهم ، إذا كان لديك حساب على github ، يمكنك رؤية هذا الرمز ، وتعديله وتشغيله (كذا!) بنقرة واحدة مباشرة من المتصفح.

فتح في gitpod

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

إذن ما تحتاج إلى فهمه من هذا الرمز. أولاً ، الألوان التي أقوم بتخزينها في عدد صحيح بأربعة بايت uint32_t. كل بايت هي مكون من عناصر R أو G أو B أو A.. تسمح لك الدالتان pack_color () و unpack_color () بالوصول إلى المكونات الفردية لكل لون.

الصورة الثانية ثنائية الأبعاد ، أقوم بتخزينها في الصفيف أحادي الأبعاد المعتاد. للوصول إلى البكسل مع الإحداثيات (س ، ص) أنا لا أكتب صورة [س] [ص] ، لكنني أكتب الصورة [س + ص * العرض]. إذا كانت هذه الطريقة لتعبئة المعلومات ثنائية الأبعاد في صفيف أحادي البعد جديدة بالنسبة لك ، فعندئذ يمكنك الآن الحصول على قلم والتعامل معها. بالنسبة لي شخصيا ، لا تصل هذه المرحلة إلى المخ ، بل تتم معالجتها مباشرة في النخاع الشوكي. يمكن حزم المصفوفات ثلاثية الأبعاد وأكثرها بنفس الطريقة تمامًا ، لكننا لن نرتفع فوق المكونين.

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



المرحلة 2: ارسم خريطة المستوى


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



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

أذكرك أن هذا الزر سوف يطلق الكود مباشرة في هذه المرحلة:

فتح في gitpod



المرحلة 3: إضافة لاعب


ماذا نحتاج أن نكون قادرين على رسم لاعب على الخريطة؟ إحداثيات GPS كافية :)



أضف متغيرين x و y ، وارسم المشغل في المكان المناسب:



التغييرات التي يمكنك رؤيتها هنا . حول gitpod لن أذكر أكثر :)

فتح في gitpod



المرحلة 4: ويعرف أيضا باسم rangefinder الظاهري أول أثر التتبع


بالإضافة إلى إحداثيات اللاعب ، سيكون من الجيد معرفة الاتجاه الذي يبحث فيه. لذلك ، نضيف لاعبًا متغيرًا آخر ، والذي يعطي اتجاه نظر اللاعب (الزاوية بين اتجاه النظرة ومحور abscissa):



والآن أريد أن أكون قادرًا على الطيران على طول أشعة البرتقال. كيف نفعل ذلك؟ بسيط للغاية. لنلقِ نظرة على مثلث أخضر صحيح. نحن نعلم أن cos (player_a) = a / c ، وأن الخطيئة (player_a) = b / c.



ماذا يحدث إذا أخذت بشكل تعسفي قيمة c (موجب) وأحسب x = player_x + c * cos (player_a) و y = player_y + c * sin (player_a)؟ سوف نجد أنفسنا في النقطة الأرجواني ؛ من خلال تغيير المعلمة c من الصفر إلى ما لا نهاية ، يمكننا أن نجعل هذه الشريحة النقطية الأرجواني على طول أشعة البرتقال الخاصة بنا ، و c هي المسافة من (x ، y) إلى (player_x ، player_y)!

قلب محرك الرسومات لدينا هو هذه الدورة:

  float c = 0; for (; c<20; c+=.05) { float x = player_x + c*cos(player_a); float y = player_y + c*sin(player_a); if (map[int(x)+int(y)*map_w]!=' ') break; } 

ننقل النقطة (س ، ص) على طول الشعاع ، إذا صادفت عقبة على الخريطة ، فإننا ننهي الدورة ، والمتغير c يمنح المسافة إلى العقبة! ما هو لا يأتي إطلاق الشركة ليزر؟



التغييرات التي يمكنك رؤيتها هنا .

فتح في gitpod



المرحلة 5: نظرة عامة على القطاع


شعاع واحد على ما يرام ، ولكن لا تزال أعيننا ترى قطاع كامل. دعنا ندعو زاوية المشاهدة (مجال الرؤية):



ودعنا نصدر 512 شعاعًا (بالمناسبة ، لماذا 512؟) ، كنس بسلاسة قطاع المشاهدة بأكمله:


التغييرات التي يمكنك رؤيتها هنا .

فتح في gitpod



المرحلة 6: 3D!


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



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



التغييرات التي يمكنك رؤيتها هنا .

فتح في gitpod



المرحلة 7: الرسوم المتحركة الأولى


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


التغييرات التي يمكنك رؤيتها هنا .

فتح في gitpod



المرحلة 8: تصحيح عين السمكة


هل لاحظت ما تأثير السمكة العظيمة الذي نحصل عليه عندما ننظر إلى الجدار عن قرب؟ يبدو مثل هذا:



لماذا؟ نعم بسيط جدا هنا ننظر إلى الجدار:



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

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



فتح في gitpod



الخطوة 9: تحميل ملف الملمس


حان الوقت للتعامل مع القوام. أنا كسول أن أكتب برنامج تنزيل الصور بنفسي ، لذلك أخذت مكتبة stb الممتازة. أعددت ملفًا به قوام للجدران ، جميع القوام مربعة ومعبأة أفقياً في الصورة:



في هذه المرحلة ، أنا فقط تحميل القوام في الذاكرة. لاختبار الشفرة المكتوبة ، أرسم ببساطة أنها نسيج مع الفهرس 5 في الزاوية اليسرى العليا من الشاشة:


التغييرات التي يمكنك رؤيتها هنا .

فتح في gitpod



المرحلة 10: الاستخدام البدائي للقوام


الآن أقوم برمي الألوان التي تم إنشاؤها عشوائيًا وتلوين جدراني من خلال أخذ البكسل الأيسر العلوي من النسيج المقابل:


التغييرات التي يمكنك رؤيتها هنا .

فتح في gitpod



المرحلة 11: تركيب الجدران الحقيقية


والآن جاءت اللحظة التي طال انتظارها عندما رأينا أخيرًا جدران الطوب:



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



بشكل عام ، الفكرة بدائية للغاية ، ولكنها تتطلب تنفيذًا دقيقًا ، نظرًا لأن لدينا أيضًا جدران "رأسية" (تلك ذات hitx قريبة من الصفر [عدد صحيح]). بالنسبة لهم ، يتم تحديد عمود النسيج بواسطة hity ، الجزء الكسري من y. التغييرات التي يمكنك رؤيتها هنا .

فتح في gitpod



المرحلة 12: حان الوقت ل refactor!


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

  for (size_t frame=0; frame<360; frame++) { std::stringstream ss; ss << std::setfill('0') << std::setw(5) << frame << ".ppm"; player.a += 2*M_PI/360; render(fb, map, player, tex_walls); drop_ppm_image(ss.str(), fb.img, fb.w, fb.h); } 

حسنًا ، إليكم النتيجة:


التغييرات التي يمكنك رؤيتها هنا .

فتح في gitpod

أن تستمر ... على الفور


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

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


All Articles