مقدمة
الهدف من هذا المشروع هو إنشاء استنساخ لمحرك DOOM باستخدام الموارد التي تم إصدارها باستخدام Ultimate DOOM (
إصدار من Steam ).
سيتم تقديمه في شكل برنامج تعليمي - لا أرغب في تحقيق أقصى أداء في الشفرة ، ولكن فقط قم بإنشاء إصدار فعال ، وبعد ذلك سأبدأ في تحسينه وتحسينه.
ليس لدي أي خبرة في إنشاء ألعاب أو محركات ألعاب ، ولدي خبرة قليلة في كتابة المقالات ، لذلك يمكنك اقتراح تغييراتك الخاصة أو حتى إعادة كتابة التعليمات البرمجية بالكامل.
فيما يلي قائمة بالموارد والروابط.
كتاب لعبة محرك الكتاب الأسود: DOOM فابيان Sanglar . واحدة من أفضل الكتب على DOOM الداخلية.
الموت يكيDOOM شفرة المصدرشفرة المصدرمتطلبات
- Visual Studio: أي IDE سيفعل ؛ سأعمل في Visual Studio 2017.
- SDL2: المكتبات.
- DOOM: نسخة من إصدار Steam من Ultimate DOOM ، نحتاج فقط إلى ملف WAD منه.
اختياري
- Slade3: أداة جيدة لاختبار عملنا.
أفكار
لا أعرف ، يمكنني إكمال هذا المشروع ، لكنني سأبذل قصارى جهدي لهذا الغرض.
سيكون Windows هو النظام الأساسي الخاص بي ، لكن بما أنني أستخدم SDL ، فستجعل المحرك يعمل تحت أي نظام أساسي آخر.
في غضون ذلك ، قم بتثبيت Visual Studio!
تمت إعادة تسمية المشروع من Handmade DOOM إلى Do It Yourself Doom باستخدام SLD (DIY Doom) بحيث لا يتم الخلط بينه وبين المشروعات الأخرى المسماة "Handmade". هناك بعض لقطات في البرنامج التعليمي حيث لا يزال يطلق عليه "DOOM اليدوية".
ملفات واد
قبل الشروع في الترميز ، دعونا نضع الأهداف ونفكر فيما نريد تحقيقه.
أولاً ، دعونا نتحقق مما إذا كان بإمكاننا قراءة ملفات موارد DOOM. جميع موارد DOOM موجودة في ملف WAD.
ما هو ملف WAD؟
"أين هي كل البيانات الخاصة بي"؟ ("أين توجد جميع بياناتي؟") إنهم في WAD! WAD هو أرشيف لجميع موارد DOOM (والألعاب المستندة إلى DOOM) الموجودة في ملف واحد.
توصل مطورو Doom إلى هذا التنسيق لتبسيط إنشاء تعديلات اللعبة.
واد ملف تشريح
يتكون ملف WAD من ثلاثة أجزاء رئيسية: رأس ، كتل ، وأدلة.
- رأس - يحتوي على معلومات أساسية حول ملف WAD وإزاحة الدليل.
- كتل - هنا هي موارد اللعبة المخزنة ، بيانات الخرائط ، العفاريت ، الموسيقى ، إلخ.
- الدلائل - الهيكل التنظيمي للبحث عن البيانات في القسم المقطوع.
<---- 32 bits ----> /------------------\ ---> 0x00 | ASCII WAD Type | 0X03 | |------------------| Header -| 0x04 | # of directories | 0x07 | |------------------| ---> 0x08 | directory offset | 0x0B -- ---> |------------------| <-- | | 0x0C | Lump Data | | | | |------------------| | | Lumps - | | . | | | | | . | | | | | . | | | ---> | . | | | ---> |------------------| <--|--- | | Lump offset | | | |------------------| | Directory -| | directory offset | --- List | |------------------| | | Lump Name | | |------------------| | | . | | | . | | | . | ---> \------------------/
تنسيق الرأس
تنسيق الدليل
أهداف
- إنشاء مشروع.
- افتح ملف WAD.
- قراءة العنوان.
- قراءة جميع الدلائل وعرضها.
هندسة معمارية
دعونا لا تعقيد أي شيء حتى الآن. قم بإنشاء فصل يفتح فقط ويقوم بتحميل WAD ، واسمه WADLoader. ثم نكتب فصلاً مسؤولاً عن قراءة البيانات وفقًا لتنسيقها ، ونطلق عليها اسم WADReader. نحتاج أيضًا إلى وظيفة
main
بسيطة تستدعي هذه الفئات.
ملاحظة: قد لا تكون هذه البنية مثالية ، وإذا لزم الأمر سنقوم بتغييرها.
الوصول إلى الرمز
لنبدأ بإنشاء مشروع C ++ فارغ. في Visual Studio ، انقر فوق ملف> جديد -> مشروع. دعنا نسميها DIYDoom.
لنقم بإضافة فئتين جديدتين: WADLoader و WADReader. لنبدأ بتنفيذ WADLoader.
class WADLoader { public: WADLoader(std::string sWADFilePath);
سيكون تنفيذ المنشئ بسيطًا: قم بتهيئة مؤشر البيانات وتخزين نسخة من المسار الذي تم نقله إلى ملف WAD.
WADLoader::WADLoader(string sWADFilePath) : m_WADData(NULL), m_sWADFilePath(sWADFilePath) { }
الآن دعنا
OpenAndLoad
إلى تنفيذ الوظيفة الإضافية لتحميل
OpenAndLoad
: نحن نحاول فقط فتح الملف على أنه ثنائي ، وفي حالة الفشل ، نعرض خطأ.
m_WADFile.open(m_sWADFilePath, ifstream::binary); if (!m_WADFile.is_open()) { cout << "Error: Failed to open WAD file" << m_sWADFilePath << endl; return false; }
إذا سارت الأمور على ما يرام ، ويمكننا العثور على الملف وفتحه ، فسنحتاج إلى معرفة حجم الملف من أجل تخصيص ذاكرة لنسخ الملف إليه.
m_WADFile.seekg(0, m_WADFile.end); size_t length = m_WADFile.tellg();
الآن نحن نعرف مقدار المساحة التي يستغرقها WAD الكامل ، وسوف نقوم بتخصيص مقدار الذاكرة الضروري.
m_WADData = new uint8_t[length];
انسخ محتويات الملف إلى هذه الذاكرة.
ربما لاحظت أنني استخدمت نوع
m_WADData
كنوع بيانات لـ
unint8_t
. هذا يعني أنني بحاجة إلى مجموعة محددة من 1 بايت (1 بايت * طول). يضمن استخدام unint8_t أن الحجم مساوٍ للبايتة (8 بتات ، والتي يمكن فهمها من اسم النوع). إذا أردنا تخصيص 2 بايت (16 بت) ، فسنستخدم unint16_t ، والذي سنتحدث عنه لاحقًا. باستخدام هذه الأنواع من التعليمات البرمجية ، يصبح الكود مستقلًا عن النظام الأساسي. سأشرح لك: إذا استخدمنا كلمة "int" ، فإن الحجم الدقيق للإدخال في الذاكرة يعتمد على النظام. إذا قمنا بترجمة "int" في تكوين 32 بت ، نحصل على حجم ذاكرة 4 بايت (32 بت) ، وعند تجميع نفس الرمز في تكوين 64 بت ، نحصل على حجم ذاكرة 8 بايت (64 بت)! والأسوأ من ذلك ، أن تجميع الكود على منصة 16 بت (قد تكون من محبي DOS) سيمنحنا 2 بايت (16 بت)!
دعونا التحقق من رمز لفترة وجيزة وتأكد من أن كل شيء يعمل. لكن أولاً نحتاج إلى تطبيق LoadWAD. أثناء استدعاء LoadWAD "OpenAndLoad"
bool WADLoader::LoadWAD() { if (!OpenAndLoad()) { return false; } return true; }
ودعونا نضيف إلى رمز الوظيفة الرئيسي الذي ينشئ مثيلًا للفئة ويحاول تحميل WAD
int main() { WADLoader wadloader("D:\\SDKs\\Assets\\Doom\\DOOM.WAD"); wadloader.LoadWAD(); return 0; }
سوف تحتاج إلى إدخال المسار الصحيح لملف WAD الخاص بك. لنقم بتشغيله!
أوتش! حصلنا على نافذة وحدة التحكم التي تفتح فقط لبضع ثوان! لا شيء مفيد بشكل خاص ... هل يعمل البرنامج؟ الفكرة! دعنا نلقي نظرة على الذاكرة ونرى ما فيها! ربما هناك سوف نجد شيئا خاصا! أولاً ، ضع نقطة توقف بالنقر المزدوج على يسار رقم السطر. يجب أن ترى شيء مثل هذا:
لقد وضعت نقطة توقف على الفور بعد قراءة جميع البيانات من الملف للنظر في صفيف الذاكرة ومعرفة ما تم تحميله فيه. الآن قم بتشغيل الكود مرة أخرى! في النافذة التلقائية ، أرى البايتات القليلة الأولى. أول 4 بايت يقول "IWAD"! عظيم ، إنه يعمل! لم اعتقد ابدا ان هذا اليوم سيأتي! حسنًا ، تحتاج إلى تهدئة ، لا يزال هناك الكثير من العمل الذي ينتظرنا!
قراءة الرأس
الحجم الكلي للرأس هو 12 بايت (من 0x00 إلى 0x0b) ، وتنقسم هذه البايتات 12 إلى 3 مجموعات. أول 4 بايت هي نوع من WAD ، عادةً "IWAD" أو "PWAD". يجب أن يكون IWAD هو WAD الرسمي الصادر عن برنامج ID ، ويجب استخدام "PWAD" في التعديلات. بمعنى آخر ، هذه مجرد طريقة لتحديد ما إذا كان ملف WAD هو إصدار رسمي ، أو تم إصداره بواسطة المودعين. لاحظ أن السلسلة ليست خالية NULL ، لذلك كن حذرا! 4 بايت التالية غير موقعة int ، والذي يحتوي على إجمالي عدد الدلائل في نهاية الملف. تشير وحدات البايت الأربع التالية إلى إزاحة الدليل الأول.
دعنا نضيف الهيكل الذي سيخزن المعلومات. سأضيف ملف رأس جديد وأطلق عليه اسم "DataTypes.h". في ذلك سنصف جميع الهياكل التي نحتاجها.
struct Header { char WADType[5];
نحتاج الآن إلى تطبيق فئة WADReader ، والتي ستقوم بقراءة البيانات من صفيف بايت WAD المحمّل. أوتش! هناك خدعة هنا - ملفات WAD بتنسيق endian الكبير ، أي أننا سنحتاج إلى تحويل البايتات لجعلها صغيرة النهاية (اليوم ، تستخدم معظم الأنظمة endian قليلاً). للقيام بذلك ، سنضيف وظيفتين ، واحدة لمعالجة 2 بايت (16 بت) ، والآخر لمعالجة 4 بايت (32 بت) ؛ إذا كنا بحاجة إلى قراءة بايت واحد فقط ، فلا يجب فعل شيء.
uint16_t WADReader::bytesToShort(const uint8_t *pWADData, int offset) { return (pWADData[offset + 1] << 8) | pWADData[offset]; } uint32_t WADReader::bytesToInteger(const uint8_t *pWADData, int offset) { return (pWADData[offset + 3] << 24) | (pWADData[offset + 2] << 16) | (pWADData[offset + 1] << 8) | pWADData[offset]; }
نحن الآن على استعداد لقراءة العنوان: عد البايتات الأربعة الأولى كـ char ، ثم أضف NULL إليها لتبسيط عملنا. في حالة عدد الدلائل والإزاحة الخاصة بهم ، يمكنك ببساطة استخدام الدوال المساعدة لتحويلها إلى التنسيق الصحيح.
void WADReader::ReadHeaderData(const uint8_t *pWADData, int offset, Header &header) {
دعنا نجمع كل ذلك معًا ، وندعو هذه الوظائف وطباعة النتائج
bool WADLoader::ReadDirectories() { WADReader reader; Header header; reader.ReadHeaderData(m_WADData, 0, header); std::cout << header.WADType << std::endl; std::cout << header.DirectoryCount << std::endl; std::cout << header.DirectoryOffset << std::endl; std::cout << std::endl << std::endl; return true; }
قم بتشغيل البرنامج ومعرفة ما إذا كان كل شيء يعمل!
! ممتاز خط IWAD مرئي بوضوح ، لكن هل الرقمان الآخران صحيحان؟ دعونا نحاول قراءة الدلائل باستخدام هذه الإزاحة ومعرفة ما إذا كان يعمل!
نحتاج إلى إضافة بنية جديدة للتعامل مع الدليل المطابق للخيارات المذكورة أعلاه.
struct Directory { uint32_t LumpOffset; uint32_t LumpSize; char LumpName[9]; };
الآن دعونا نضيف وظيفة ReadDirectories: عد الإزاحة وإخراجها!
في كل تكرار ، نقوم بضرب i * 16 للانتقال إلى زيادة الإزاحة في الدليل التالي.
Directory directory; for (unsigned int i = 0; i < header.DirectoryCount; ++i) { reader.ReadDirectoryData(m_WADData, header.DirectoryOffset + i * 16, directory); m_WADDirectories.push_back(directory); std::cout << directory.LumpOffset << std::endl; std::cout << directory.LumpSize << std::endl; std::cout << directory.LumpName << std::endl; std::cout << std::endl; }
شغّل الكود وشاهد ما يحدث. نجاح باهر! قائمة كبيرة من الدلائل.
اذا حكمنا من خلال الاسم مقطوع ، يمكننا أن نفترض أننا تمكنا من قراءة البيانات بشكل صحيح ، ولكن ربما هناك طريقة أفضل للتحقق من ذلك. سوف نلقي نظرة على إدخالات دليل WAD باستخدام Slade3.
يبدو أن اسم وحجم الكتلة يتوافق مع البيانات التي تم الحصول عليها باستخدام الرمز الخاص بنا. اليوم قمنا بعمل رائع!
ملاحظات أخرى
- في مرحلة ما ، اعتقدت أنه سيكون من الجيد استخدام المتجه لتخزين الأدلة. لماذا لا تستخدم الخريطة؟ سيكون هذا أسرع من الحصول على البيانات عن طريق البحث المتجه الخطي. هذه فكرة سيئة. عند استخدام الخريطة ، لن يتم تتبع ترتيب إدخالات الدليل ، لكننا نحتاج إلى هذه المعلومات للحصول على البيانات الصحيحة.
وفكرة خاطئة أخرى: يتم تطبيق Map في C ++ كأشجار حمراء سوداء مع وقت البحث O (log N) ، والتكرارات على الخريطة دائمًا تعطي ترتيبًا متزايدًا للمفاتيح. إذا كنت بحاجة إلى بنية بيانات تعطي متوسط الوقت O (1) وأسوأ وقت O (N) ، فعليك استخدام خريطة غير مرتبة. ليس تحميل جميع ملفات WAD في الذاكرة طريقة تنفيذ مثالية. سيكون أكثر منطقية ببساطة قراءة الدلائل في رأس الذاكرة ، ومن ثم العودة إلى ملف WAD وتحميل الموارد من القرص. نأمل في يوم من الأيام أن نتعلم المزيد عن التخزين المؤقت.
DOOMReboot : لا أوافق تمامًا. تعد 15 ميغابايت من ذاكرة الوصول العشوائي هذه تافهًا تامًا ، وستكون القراءة من الذاكرة أسرع كثيرًا من fseek الضخم ، الذي سيتعين استخدامه بعد تنزيل كل ما هو ضروري للمستوى. سيؤدي ذلك إلى زيادة وقت التنزيل بما لا يقل عن ثانية إلى ثانيتين (يستغرق الأمر أقل من 20 مللي ثانية للتنزيل طوال الوقت). fseek استخدام نظام التشغيل. ما الملف الأكثر احتمالاً في ذاكرة التخزين المؤقت RAM ، لكنه قد لا. ولكن حتى لو كان هناك ، فإنه يعد مضيعة كبيرة للموارد وهذه العمليات سوف تربك العديد من قراءات WAD من حيث ذاكرة التخزين المؤقت وحدة المعالجة المركزية. أفضل شيء هو أنه يمكنك إنشاء طرق تمهيد مختلطة وتخزين بيانات WAD لمستوى يناسب ذاكرة التخزين المؤقت L3 للمعالجات الحديثة ، حيث ستكون المدخرات مذهلة.
شفرة المصدر
شفرة المصدربيانات البطاقة الأساسية
بعد أن تعلمت قراءة ملف WAD ، دعونا نحاول استخدام بيانات القراءة. سيكون من الرائع تعلم كيفية قراءة بيانات المهمة (المستوى / العالم) وتطبيقها. يجب أن تكون "القطع" لهذه المهام (Mission Lumps) معقدة وصعبة. لذلك ، سوف نحتاج إلى التحرك وتطوير المعرفة تدريجياً. كخطوة أولى صغيرة ، دعونا ننشئ شيئًا يشبه ميزة Automap: خطة ثنائية الأبعاد لخريطة ذات منظر علوي. أولاً ، دعنا نرى ما هو داخل "مهمة البعثة".
تشريح البطاقة
لنبدأ من جديد: وصف مستويات DOOM يشبه إلى حد كبير الرسم ثنائي الأبعاد ، الذي تميزت عليه الجدران بخطوط. ومع ذلك ، للحصول على إحداثيات ثلاثية الأبعاد ، يأخذ كل جدار ارتفاع الأرض والسقف (XY هي الطائرة التي نتحرك بها أفقيًا ، و Z هو الارتفاع الذي يسمح لنا بالتحرك لأعلى ولأسفل ، على سبيل المثال ، عن طريق رفع المصعد أو القفز إلى أسفل من منصة. يتم استخدام مكونات الإحداثيات لتقديم المهمة كعالم ثلاثي الأبعاد ، ومع ذلك ، لضمان الأداء الجيد ، فإن المحرك لديه قيود معينة: لا توجد غرف تقع واحدة فوق الأخرى على المستويات ولا يمكن للمشغل البحث للأعلى وللأسفل. الصخور ، على سبيل المثال ، صواريخ ، تصعد عموديًا لتصل إلى هدف موجود على منصة أعلى.
تسببت هذه الميزات الغريبة في holivars لا نهاية لها حول ما إذا كان DOOM هو محرك ثنائي الأبعاد أو ثلاثي الأبعاد. تدريجيا ، تم التوصل إلى حل وسط دبلوماسي ، مما أنقذ العديد من الأرواح: اتفق الطرفان على تسمية "2.5D" مقبولة لكليهما.
لتبسيط المهمة والعودة إلى الموضوع ، دعونا نحاول فقط قراءة هذه البيانات ثنائية الأبعاد ومعرفة ما إذا كان يمكن استخدامها بطريقة أو بأخرى. بعد ذلك ، سنحاول تقديمها ثلاثية الأبعاد ، لكننا بحاجة الآن إلى فهم كيفية عمل الأجزاء الفردية من المحرك معًا.
بعد إجراء البحوث ، اكتشفت أن كل مهمة تتكون من مجموعة من "القطع". يتم تمثيل "كتل" هذه دائمًا في ملف WAD الخاص بلعبة DOOM بنفس الترتيب.
- الرؤوس: نقاط النهاية للجدران في 2D. شكلان VERTEX متصلان من LINEDEF. ثلاثة VERTEX متصلة تشكل جدارين / LINEDEF ، وهلم جرا. يمكن اعتبارها ببساطة نقاط اتصال لجدارين أو أكثر. (نعم ، معظم الناس يفضلون الجمع "Vertices" ، لكن John Carmack لم يعجبهم. وفقًا لـ merriam-webster ، ينطبق كلا الخيارين.
- LINEDEFS: خطوط تشكيل المفاصل بين القمم وتشكيل الجدران. لا تتصرف جميع الخطوط (الجدران) كما هي ، فهناك أعلام تحدد سلوك هذه الخطوط.
- جوانب جانبية: في الحياة الواقعية ، الجدران لها وجهان - ننظر إلى جانب ، والثاني على الجانب الآخر. يمكن أن يكون للجانبين قوام مختلف ، و SIDEDEFS هو المقطوع الذي يحتوي على معلومات النسيج للجدار (LINEDEF).
- القطاعات: القطاعات هي "غرف" حصلت عليها LINEDEF. يحتوي كل قطاع على معلومات مثل ارتفاع الأرض والسقف ، والقوام ، وقيم الإضاءة ، والإجراءات الخاصة ، مثل نقل الأرضيات / المنصات / المصاعد. تؤثر بعض هذه المعلمات أيضًا على الطريقة التي يتم بها تقديم الجدران ، على سبيل المثال ، مستوى الإضاءة وحساب إحداثيات تعيين النسيج.
- SSECTORS: (القطاعات الفرعية) تشكل مناطق محدبة داخل قطاع يتم استخدامها في التقديم جنبًا إلى جنب مع BSP الالتفافية ، وتساعد أيضًا في تحديد مكان وجود اللاعب في مستوى معين. إنها مفيدة تمامًا وغالبًا ما تستخدم لتحديد الوضع الرأسي للاعب. يتكون كل SSECTOR من أجزاء متصلة بقطاع ما ، على سبيل المثال ، من الجدران التي تشكل زاوية. يتم تخزين هذه الأجزاء من الجدران ، أو "شرائح" ، في مقطوع الخاصة بهم تسمى ...
- SEGS: أجزاء الجدار / LINEDEF. وبعبارة أخرى ، هذه هي "شرائح" الجدار / LINEDEF. يتم تقديم العالم لتجاوز شجرة BSP لتحديد الجدران التي سيتم رسمها أولاً (الأول هو الأقرب). على الرغم من أن النظام يعمل بشكل جيد للغاية ، إلا أنه يتسبب في كثير من الأحيان في انقسام الأسطر إلى قسمين أو أكثر من مجموعات التخطيط الاستراتيجي. ثم يتم استخدام هذه SEG لتقديم الجدران بدلا من LINEDEF. يتم تحديد هندسة كل SSECTOR بواسطة الأجزاء الموجودة به.
- العقد: عقدة BSP هي عقدة لبنية شجرة ثنائية تقوم بتخزين بيانات القطاع الفرعي. يتم استخدامه لتحديد بسرعة SSECTOR (و SEG) أمام اللاعب. يسمح التخلص من SEG الموجودة خلف المشغل ، وبالتالي غير المرئي ، للمحرك بالتركيز على SEGs التي يحتمل أن تكون مرئية ، مما يقلل بشكل كبير من وقت العرض.
- الأشياء: مقطوعة تسمى الأشياء عبارة عن قائمة من الجهات الفاعلة في مجال المناظر الطبيعية والمهام (الأعداء والأسلحة وما إلى ذلك). يحتوي كل عنصر من عناصر هذا المقطع على معلومات حول مثيل واحد للممثل / المجموعة ، على سبيل المثال ، نوع الكائن ونقطة الإنشاء والاتجاه وما إلى ذلك.
- رفض: يحتوي هذا المقطوع على بيانات حول القطاعات التي تكون مرئية من القطاعات الأخرى. يتم استخدامه لتحديد متى يتعلم الوحش وجود اللاعب. يتم استخدامه أيضًا لتحديد نطاق توزيع الأصوات التي أنشأها اللاعب ، على سبيل المثال ، اللقطات. عندما يكون مثل هذا الصوت قادراً على نقله إلى قطاع الوحش ، يمكنه معرفة اللاعب. يمكن أيضًا استخدام طاولة REJECT لتسريع التعرف على تصادم قذائف الأسلحة.
- BLOCKMAP: معلومات التعرف على تصادم اللاعب وحركة THING. يتكون من شبكة تغطي هندسة المهمة بأكملها. تحتوي كل خلية شبكة على قائمة LINEDEFs الموجودة داخلها أو تتقاطع معها. يتم استخدامه لتسريع التعرف على التصادمات بشكل كبير: لا يلزم إجراء اختبارات التصادم إلا لعدد قليل من LINEDEF لكل لاعب / شيء ، مما يوفر قوة الحوسبة بشكل كبير.
عند إنشاء خريطة ثنائية الأبعاد ، سنركز على VERTEXES و LINEDEFS. إذا استطعنا رسم القمم وربطها بالخطوط التي تقدمها linedef ، فعندئذ نحتاج إلى إنشاء نموذج ثنائي الأبعاد للخريطة.
تحتوي بطاقة العرض التوضيحي الموضحة أعلاه على الخصائص التالية:
- 4 قمم
- قمة الرأس 1 في (10.10)
- أعلى 2 في (10،100)
- أعلى 3 في (100 ، 10)
- الذروة 4 في (100،100)
- 4 خطوط
- خط من أعلى 1 إلى 2
- خط من أعلى 1 إلى 3
- خط من أعلى 2 إلى 4
- خط من أعلى 3 إلى 4
شكل قمة الرأس
كما قد تتوقع ، فإن بيانات قمة الرأس بسيطة للغاية - فقط x و y (نقطة) لبعض الإحداثيات.
تنسيق Linedef
يحتوي Linedef على مزيد من المعلومات ؛ فهو يصف الخط الذي يربط بين القمتين وخصائص هذا الخط (والتي ستصبح فيما بعد جدارًا).
قيم علم Linedef
لا يتم رسم جميع الخطوط (الجدران). البعض منهم لديه سلوك خاص.
أهداف
- إنشاء فئة خريطة.
- قراءة بيانات قمة الرأس.
- قراءة البيانات linedef.
هندسة معمارية
أولاً ، دعنا ننشئ صفًا ونطلق عليه الخريطة. فيه سوف نقوم بتخزين جميع البيانات المرتبطة بالبطاقة.
في الوقت الحالي ، أخطط فقط لتخزين الرؤوس والبطانات كأحد المتجهات ، بحيث يمكنني تطبيقها لاحقًا.
أيضًا ، لنكمل WADLoader و WADReader حتى نتمكن من قراءة هاتين المعلمتين الجديدتين.
الترميز
سيكون الرمز مشابهًا لرمز قراءة WAD ، وسنضيف عددًا قليلًا من الهياكل ، ثم نملأها ببيانات من WAD. لنبدأ بإضافة فئة جديدة وتمرير اسم الخريطة.
class Map { public: Map(std::string sName); ~Map(); std::string GetName();
الآن إضافة هياكل لقراءة هذه الحقول الجديدة. نظرًا لأننا قمنا بذلك بالفعل عدة مرات ، ما عليك سوى إضافتها جميعًا مرة واحدة.
struct Vertex { int16_t XPosition; int16_t YPosition; }; struct Linedef { uint16_t StartVertex; uint16_t EndVertex; uint16_t Flags; uint16_t LineType; uint16_t SectorTag; uint16_t FrontSidedef; uint16_t BackSidedef; };
بعد ذلك ، نحتاج إلى وظيفة لقراءتها من WADReader ، ستكون قريبة من ما فعلناه سابقًا. void WADReader::ReadVertexData(const uint8_t *pWADData, int offset, Vertex &vertex) { vertex.XPosition = Read2Bytes(pWADData, offset); vertex.YPosition = Read2Bytes(pWADData, offset + 2); } void WADReader::ReadLinedefData(const uint8_t *pWADData, int offset, Linedef &linedef) { linedef.StartVertex = Read2Bytes(pWADData, offset); linedef.EndVertex = Read2Bytes(pWADData, offset + 2); linedef.Flags = Read2Bytes(pWADData, offset + 4); linedef.LineType = Read2Bytes(pWADData, offset + 6); linedef.SectorTag = Read2Bytes(pWADData, offset + 8); linedef.FrontSidedef = Read2Bytes(pWADData, offset + 10); linedef.BackSidedef = Read2Bytes(pWADData, offset + 12); }
أعتقد أنه لا يوجد شيء جديد لك هنا. والآن نحن بحاجة إلى استدعاء هذه الوظائف من فئة WADLoader. اسمحوا لي أن أذكر الحقائق: تسلسل الكتل مهم هنا ، سنجد اسم الخريطة في دليل المقطوع ، متبوعًا بجميع الكتل المرتبطة بالخرائط بالترتيب المحدد. لتبسيط مهمتنا وعدم تتبع مؤشرات الكتل بشكل منفصل ، سنضيف تعدادًا يتيح لنا التخلص من الأرقام السحرية. enum EMAPLUMPSINDEX { eTHINGS = 1, eLINEDEFS, eSIDEDDEFS, eVERTEXES, eSEAGS, eSSECTORS, eNODES, eSECTORS, eREJECT, eBLOCKMAP, eCOUNT };
سأضيف أيضًا وظيفة للبحث عن خريطة باسمها في قائمة الدليل. في وقت لاحق ، من المحتمل أن نزيد من أداء هذه الخطوة باستخدام بنية بيانات الخريطة ، لأن هناك عددًا كبيرًا من السجلات هنا ، وسيتعين علينا الاطلاع عليها كثيرًا ، خاصة في بداية تحميل الموارد مثل القوام ، العفاريت ، الأصوات ، إلخ. int WADLoader::FindMapIndex(Map &map) { for (int i = 0; i < m_WADDirectories.size(); ++i) { if (m_WADDirectories[i].LumpName == map.GetName()) { return i; } } return -1; }
واو ، لقد انتهينا تقريبا! الآن ، دعونا نعول فقط VERTEXES! أكرر ، لقد فعلنا هذا بالفعل من قبل ، والآن يجب أن تفهم هذا. bool WADLoader::ReadMapVertex(Map &map) { int iMapIndex = FindMapIndex(map); if (iMapIndex == -1) { return false; } iMapIndex += EMAPLUMPSINDEX::eVERTEXES; if (strcmp(m_WADDirectories[iMapIndex].LumpName, "VERTEXES") != 0) { return false; } int iVertexSizeInBytes = sizeof(Vertex); int iVertexesCount = m_WADDirectories[iMapIndex].LumpSize / iVertexSizeInBytes; Vertex vertex; for (int i = 0; i < iVertexesCount; ++i) { m_Reader.ReadVertexData(m_WADData, m_WADDirectories[iMapIndex].LumpOffset + i * iVertexSizeInBytes, vertex); map.AddVertex(vertex); cout << vertex.XPosition << endl; cout << vertex.YPosition << endl; std::cout << std::endl; } return true; }
حسنًا ، يبدو أننا نقوم بنسخ نفس الرمز باستمرار ؛ قد تضطر إلى تحسينه في المستقبل ، ولكن في الوقت الحالي ، ستقوم بتطبيق ReadMapLinedef بنفسك (أو انظر الكود المصدري من الرابط).اللمسات الأخيرة - نحتاج إلى استدعاء هذه الوظيفة وتمرير كائن الخريطة إليها. bool WADLoader::LoadMapData(Map &map) { if (!ReadMapVertex(map)) { cout << "Error: Failed to load map vertex data MAP: " << map.GetName() << endl; return false; } if (!ReadMapLinedef(map)) { cout << "Error: Failed to load map linedef data MAP: " << map.GetName() << endl; return false; } return true; }
الآن دعونا نغير الوظيفة الرئيسية ونرى ما إذا كان كل شيء يعمل. أرغب في تحميل خريطة "E1M1" ، والتي سأقوم بنقلها إلى كائن الخريطة. Map map("E1M1"); wadloader.LoadMapData(map);
الآن دعونا ندير كل شيء. واو ، حفنة من الأرقام المثيرة للاهتمام ، لكن هل هي حقيقية؟ دعونا التحقق من ذلك!دعونا نرى ما إذا كان يمكن أن تساعدنا سليد في هذا.يمكننا العثور على الخريطة في قائمة slade وإلقاء نظرة على تفاصيل الكتل. دعنا نقارن الأرقام.! ممتاز
ماذا عن Linedef؟لقد أضفت هذا التعداد أيضًا ، والذي سنحاول استخدامه عند تقديم الخريطة. enum ELINEDEFFLAGS { eBLOCKING = 0, eBLOCKMONSTERS = 1, eTWOSIDED = 2, eDONTPEGTOP = 4, eDONTPEGBOTTOM = 8, eSECRET = 16, eSOUNDBLOCK = 32, eDONTDRAW = 64, eDRAW = 128 };
ملاحظات أخرى
في عملية كتابة الكود ، قرأت عن طريق الخطأ عددًا أكبر من البايتات أكثر من اللازم ، وتلقيت قيمًا غير صحيحة. من أجل تصحيح الأخطاء ، بدأت أبحث في إزاحة WAD في الذاكرة لمعرفة ما إذا كنت في الإزاحة الصحيحة. يمكن القيام بذلك باستخدام نافذة ذاكرة Visual Studio ، والتي تعد أداة مفيدة للغاية لتتبع البايتات أو الذاكرة (يمكنك أيضًا تعيين نقاط توقف في هذه النافذة).إذا لم تر نافذة الذاكرة ، فانتقل إلى Debug> Memory> Memory.الآن نرى القيم في الذاكرة بالسداسي عشري. يمكن مقارنة هذه القيم مع شاشة سداسي عشرية في slade بالنقر بزر الماوس الأيمن على أي كتلة وعرضها على شكل سداسي عشري.قارنهم بعنوان WAD الذي تم تحميله في الذاكرة.والشيء الأخير لهذا اليوم: لقد رأينا كل قيم قمة الرأس ، ولكن هل هناك طريقة سهلة لتصورها دون كتابة رمز؟ لا أريد تضييع الوقت في هذا الأمر ، فقط لمعرفة أننا نسير في الاتجاه الخاطئ.بالتأكيد شخص ما خلق بالفعل الراسمة. لقد قمت بـ "رسم نقاط على رسم بياني" وكانت النتيجة الأولى هي موقع Plot Points - Desmos . على ذلك ، يمكنك لصق الأرقام من الحافظة ، وسوف يرسمها. يجب أن تكون بالتنسيق "(x، y)". للحصول عليه ، فقط قم بتغيير وظيفة الإخراج إلى الشاشة. cout << "(" << vertex.XPosition << "," << vertex.YPosition << ")" << endl;
نجاح باهر! يبدو بالفعل مثل E1M1! لقد حققنا شيئا!إذا كنت كسولًا للقيام بذلك ، فإليك رابطًا إلى مخطط منقط: Plot Vertex .لكن لنأخذ خطوة أخرى: بعد قليل من العمل ، يمكننا توصيل هذه النقاط على أساس الخطوط.هنا هو الرابط: E1M1 Plot Vertexشفرة المصدر
شفرة المصدرالمواد المرجعية
عذاب ويكيZDoom ويكي