هذا النص مخصص لأولئك الذين يتقنون البرمجة فقط. الفكرة الرئيسية هي إظهار خطوة بخطوة كيف يمكنك إنشاء اللعبة بشكل مستقل مثل
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 بإنشاء جهاز افتراضي لك ، وإطلاق VS Code ، وفتح جهاز على الجهاز البعيد. في تاريخ أوامر المحطة الطرفية (انقر على وحدة التحكم واضغط على السهم للأعلى) ، هناك بالفعل مجموعة كاملة من الأوامر التي تسمح لك بترجمة الشفرة وتشغيلها وفتح الصورة الناتجة.
إذن ما تحتاج إلى فهمه من هذا الرمز. أولاً ، الألوان التي أقوم بتخزينها في عدد صحيح بأربعة بايت uint32_t. كل بايت هي مكون من عناصر R أو G أو B أو A.. تسمح لك الدالتان pack_color () و unpack_color () بالوصول إلى المكونات الفردية لكل لون.
الصورة الثانية ثنائية الأبعاد ، أقوم بتخزينها في الصفيف أحادي الأبعاد المعتاد. للوصول إلى البكسل مع الإحداثيات (س ، ص) أنا لا أكتب صورة [س] [ص] ، لكنني أكتب الصورة [س + ص * العرض]. إذا كانت هذه الطريقة لتعبئة المعلومات ثنائية الأبعاد في صفيف أحادي البعد جديدة بالنسبة لك ، فعندئذ يمكنك الآن الحصول على قلم والتعامل معها. بالنسبة لي شخصيا ، لا تصل هذه المرحلة إلى المخ ، بل تتم معالجتها مباشرة في النخاع الشوكي. يمكن حزم المصفوفات ثلاثية الأبعاد وأكثرها بنفس الطريقة تمامًا ، لكننا لن نرتفع فوق المكونين.
ثم أركض في صورتي في دورة مزدوجة بسيطة ، وأملأها بتدرج وحفظها على القرص بتنسيق .ppm.
المرحلة 2: ارسم خريطة المستوى
نحن بحاجة إلى خريطة لعالمنا. في هذه المرحلة ، أريد فقط تحديد بنية البيانات ورسم خريطة على الشاشة. يجب أن يبدو شيء مثل هذا:

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

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

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

التغييرات
التي يمكنك رؤيتها هنا . حول 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 يمنح المسافة إلى العقبة! ما هو لا يأتي إطلاق الشركة ليزر؟

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

المرحلة 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); }
حسنًا ، إليكم النتيجة:

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

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