Firecore - لعبة ممتعة على AVR



أريد مشاركة بناء آخر طويل الأمد في المساء ، مما يدل على أنه يمكنك إنشاء ألعاب حتى على الأجهزة الضعيفة.

حول ما كان عليك فعله ، وكيف تقرر ، وكيفية القيام بشيء أكثر من مجرد استنساخ بونغ آخر - مرحبًا بك في Cat.

تنبيه: مقال رائع وحركة مرور وإدراج شفرات متعددة!

باختصار عن اللعبة


أطلق النار! - الآن على AVR.

في الواقع ، هذه خارطة أخرى ، لذلك مرة أخرى يجب على الشخصية الرئيسية Shepard إنقاذ المجرة من هجوم مفاجئ من قبل أشخاص مجهولين ، يشق طريقه عبر الفضاء عبر النجوم وحقول الكويكبات في وقت واحد مسح كل نظام نجمي.
اللعبة كاملة مكتوبة بلغة C و C ++ دون استخدام مكتبة Wire من Arduino.

تحتوي اللعبة على 4 سفن للاختيار من بينها (الأخير متاح بعد المرور) ، لكل منها خصائصها الخاصة:
  • القدرة على المناورة
  • المتانة.
  • قوة السلاح.

نفذت أيضا:
  • رسومات ملونة ثنائية الأبعاد ؛
  • السلطة للأسلحة.
  • الرؤساء في نهاية المستويات ؛
  • مستويات الكويكبات (والرسوم المتحركة للدوران) ؛
  • تغيير لون الخلفية على المستويات (وليس فقط الفضاء الأسود) ؛
  • حركة النجوم في الخلفية بسرعات مختلفة (لتأثير العمق) ؛
  • التهديف والتوفير في EEPROM ؛
  • نفس الأصوات (طلقات ، انفجارات ، إلخ) ؛
  • بحر من المعارضين المتطابقين.

منصة


عودة الشبح.

سأوضح مقدمًا أن هذا النظام الأساسي يجب أن يُنظر إليه على أنه وحدة تحكم الألعاب القديمة للجيل الثالث الأول (80s ، shiru8bit ).

أيضًا ، يتم حظر تعديلات الأجهزة على الأجهزة الأصلية ، مما يضمن الإطلاق على أي لوحة مماثلة أخرى بمجرد إخراجها من العلبة.
تمت كتابة هذه اللعبة من أجل لوحة Arduino Esplora ، ولكن الانتقال إلى GBA أو أي منصة أخرى ، على ما أعتقد ، لن يكون صعبًا.
ومع ذلك ، حتى على هذا المورد ، تمت تغطية هذا المنتدى عدة مرات فقط ، ولم يكن المجالس الأخرى تستحق الذكر على الإطلاق ، على الرغم من المجتمع الكبير إلى حد ما لكل منها:
  • GameBuino META:
  • بوكيتو.
  • صانع
  • أردوبوي
  • UzeBox / FuzeBox ؛
  • وغيرها الكثير.

بادئ ذي بدء ، ما هو ليس على Esplora:
  • الكثير من الذاكرة (ROM 28 كيلو بايت ، RAM 2.5 كيلو بايت) ؛
  • الطاقة (وحدة المعالجة المركزية 8 بت عند 16 ميغا هرتز) ؛
  • DMA
  • مولد الشخصية
  • مناطق الذاكرة المخصصة أو السجلات الخاصة. الوجهة (لوحة ، بلاط ، خلفية ، إلخ) ؛
  • التحكم في سطوع الشاشة (أوه ، العديد من التأثيرات في سلة المهملات) ؛
  • موسعات مساحة العنوان (مصممي الخرائط) ؛
  • المصحح ( ولكن من يحتاجه عند وجود شاشة كاملة! ).

سأستمر في حقيقة أن هناك:
  • الأجهزة SPI (يمكن تشغيلها بسرعة F_CPU / 2) ؛
  • شاشة تستند إلى ST7735 160x128 1.44 "؛
  • قليل من المؤقتات (4 قطع فقط) ؛
  • قليل من GPIO.
  • حفنة من الأزرار (5 قطع + عصا تحكم ذات محورين) ؛
  • عدد قليل من أجهزة الاستشعار (الإضاءة ، التسارع ، ميزان الحرارة) ؛
  • باعث تهيج الجرس بيزو.

يبدو أنه لا يوجد شيء تقريبًا. ليس من المستغرب أن لا أحد يريد أن يفعل أي شيء معها باستثناء استنساخ بونغ واثنين من الألعاب الثلاث طوال هذا الوقت!
ربما تكون الحقيقة هي أن الكتابة تحت وحدة تحكم ATmega32u4 (وما شابه) تشبه البرمجة لـ Intel 8051 (التي كانت تبلغ من العمر 40 عامًا تقريبًا في وقت النشر) ، حيث تحتاج إلى مراقبة عدد كبير من الشروط واللجوء إلى الحيل والحيل المختلفة.

المعالجة المحيطية


واحد لكل شيء!

بعد النظر في الدائرة ، لوحظ بوضوح أن جميع الأجهزة الطرفية متصلة من خلال موسع GPIO (معدد الإرسال 74HC4067D مزيد من MUX) ويتم تبديلها باستخدام GPIO PF4 أو PF5 أو PF6 أو PF7 أو عاب PORTF الأقدم ، وتتم قراءة خرج MUX على GPIO - PF1.
من السهل جدًا تبديل الإدخال عن طريق تعيين القيم لمنفذ PORTF عن طريق القناع ولا ننسى العضة الصغيرة:
uint16_t getAnalogMux(uint8_t chMux) { MUX_PORTX = ((MUX_PORTX & 0x0F) | ((chMux<<4)&0xF0)); return readADC(); } 

استطلاع زر النقر:
 #define SW_BTN_MIN_LVL 800 bool readSwitchButton(uint8_t btn) { bool state = true; if(getAnalogMux(btn) > SW_BTN_MIN_LVL) { // low state == pressed state = false; } return state; } 

فيما يلي قيم المنفذ F:
 #define SW_BTN_1_MUX 0 #define SW_BTN_2_MUX 8 #define SW_BTN_3_MUX 4 #define SW_BTN_4_MUX 12 

بإضافة المزيد:
 #define BUTTON_A SW_BTN_4_MUX #define BUTTON_B SW_BTN_1_MUX #define BUTTON_X SW_BTN_2_MUX #define BUTTON_Y SW_BTN_3_MUX #define buttonIsPressed(a) readSwitchButton(a) 

يمكنك مقابلة الصليب الصحيح بأمان:
 void updateBtnStates(void) { if(buttonIsPressed(BUTTON_A)) btnStates.aBtn = true; if(buttonIsPressed(BUTTON_B)) btnStates.bBtn = true; if(buttonIsPressed(BUTTON_X)) btnStates.xBtn = true; if(buttonIsPressed(BUTTON_Y)) btnStates.yBtn = true; } 

يرجى ملاحظة أن الحالة السابقة لم يتم إعادة تعيينها ، وإلا فقد تفوتك حقيقة الضغط على المفتاح (تعمل أيضًا كحماية إضافية ضد الثرثرة).

Sfx


قليلا صاخبة.

ماذا لو لم يكن هناك DAC ، ولا رقاقة من Yamaha ، ولم يكن هناك سوى مستطيل PWM 1 بت للصوت؟
في البداية ، لا يبدو الأمر كثيرًا ، ولكن على الرغم من ذلك ، يتم استخدام PWM الماكر هنا لإعادة إنشاء تقنية "PDM audio" وبمساعدتها يمكنك القيام بذلك.

تقدم المكتبة من Gamebuino شيئًا مشابهًا وكل ما هو مطلوب هو نقل مولد فرقعة إلى GPIO آخر والموقت إلى Esplora (مخرجات timer4 و OCR4D). للتشغيل الصحيح ، يتم استخدام timer1 أيضًا لإنشاء المقاطعات وإعادة تحميل سجل OCR4D مع البيانات الجديدة.

يستخدم محرك Gamebuino أنماط الصوت (كما هو الحال في الموسيقى المتعقبة) ، مما يوفر الكثير من المساحة ، لكنك تحتاج إلى إجراء جميع العينات بنفسك ، ولا توجد مكتبات بها نماذج جاهزة.
ومن الجدير بالذكر أن هذا المحرك مرتبط بفترة تحديث تبلغ حوالي 1/50 ثانية أو 20 إطارًا / ثانية.

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

الرسومات


الخالد Pixelart.

تقوم الشاشة بتشفير الألوان في وحدتي بايت (RGB565) ، ولكن نظرًا لأن الصور بهذا التنسيق ستستهلك الكثير ، فقد تمت فهرستها جميعًا بواسطة اللوحة لتوفير المساحة ، التي وصفتها بالفعل أكثر من مرة في مقالاتي السابقة.
على عكس Famicom / NES ، لا توجد حدود لونية للصورة وهناك المزيد من الألوان المتاحة في اللوحة.

كل صورة في اللعبة عبارة عن مجموعة من البايت يتم تخزين البيانات التالية فيها:
  • العرض والارتفاع
  • بدء علامة البيانات ؛
  • القاموس (إن وجد ، ولكن المزيد عن ذلك لاحقًا) ؛
  • حمولة
  • نهاية علامة البيانات.

على سبيل المثال ، مثل هذه الصورة (مكبرة 10 مرات):


في الكود سيبدو كالتالي:
 pic_t weaponLaserPic1[] PROGMEM = { 0x0f,0x07, 0x02, 0x8f,0x32,0xa2,0x05,0x8f,0x06,0x22,0x41,0xad,0x03,0x41,0x22,0x8f,0x06,0xa2,0x05, 0x8f,0x23,0xff, }; 

أين بدون سفينة في هذا النوع؟ بعد المئات من رسومات الاختبار بفارق بكسل ، بقيت هذه السفن فقط للاعب:

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

لا تنسى طياري كل سفينة:


إن تنوع سفن العدو ليس كبيرًا جدًا ، لكن دعني أذكرك ، ليس هناك مساحة كبيرة ، لذلك هنا ثلاث سفن:


بدون مكافآت قانونية في شكل تحسين الأسلحة واستعادة الصحة ، لن يستمر اللاعب طويلًا:


بالطبع ، مع زيادة قوة البنادق ، يتغير نوع القذائف المنبعثة:


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




لإنشاء هذه الرسوم المتحركة البسيطة ، يكفي 12 صورة صغيرة:

وهي مقسمة إلى ثلاثة لكل حجم (كبير ومتوسط ​​وصغير) ولكل زاوية دوران تحتاج إلى 4 استدارة إضافية 0 و 90 و 180 و 270 درجة. في اللعبة ، يكفي استبدال المؤشر إلى الصفيف بالصورة على فاصل زمني متساوٍ مما يخلق وهم التناوب.
 void rotateAsteroid(asteroid_t &asteroid) { if(RN & 1) { asteroid.sprite.pPic = getAsteroidPic(asteroid); ++asteroid.angle; } } void moveAsteroids(void) { for(auto &asteroid : asteroids) { if(asteroid.onUse) { updateSprite(&asteroid.sprite); rotateAsteroid(asteroid); ... 

يتم ذلك فقط بسبب نقص قدرات الأجهزة ، وسيستغرق تنفيذ البرامج مثل تحويل Affine أكثر من الصور نفسها وسيكون بطيئًا جدًا.

قطعة من الساتان للراغبين.

يمكنك ملاحظة جزء من النماذج الأولية وما يظهر فقط في الاعتمادات بعد اجتياز اللعبة.

بالإضافة إلى الرسومات البسيطة ، لتوفير مساحة وإضافة تأثير رجعي ، تم التخلص من الصور الرمزية الصغيرة وجميع الحروف الرسومية التي كانت تصل إلى 30 وبعد 127 بايت من ASCII خارج الخط.
هام!
لا تنس أن const و constexpr على AVR لا يعني على الإطلاق أن البيانات ستكون في ذاكرة البرنامج ، هنا لهذا تحتاج إلى استخدام PROGMEM أيضًا.
ويرجع ذلك إلى حقيقة أن أساس AVR يعتمد على بنية Harvard ، لذلك هناك حاجة إلى رموز وصول خاصة لوحدة المعالجة المركزية للوصول إلى البيانات.

الضغط على المجرة


أسهل طريقة للتعبئة هي RLE.

بعد دراسة البيانات المعبأة ، يمكنك ملاحظة أنه لم يتم استخدام البت الأكثر أهمية في بايت الحمولة في النطاق من 0x00 إلى 0x50. يتيح لك هذا إضافة البيانات وعلامة البدء لبدء التكرار (0x80) ، والإشارة إلى عدد التكرار مع البايت التالي ، والذي يسمح لك بحزم سلسلة من 257 (+2 من حقيقة أن RLE من وحدتي بايت غبي) من وحدات بايت متطابقة في وحدتين فقط.
تنفيذ وعرض فاتح الحزم:
 void drawPico_RLE_P(uint8_t x, uint8_t y, pic_t *pPic) { uint16_t repeatColor; uint8_t tmpInd, repeatTimes; alphaReplaceColorId = getAlphaReplaceColorId(); auto tmpData = getPicSize(pPic, 0); tftSetAddrWindow(x, y, x+tmpData.u8Data1, y+tmpData.u8Data2); ++pPic; // make offset to picture data while((tmpInd = getPicByte(++pPic)) != PIC_DATA_END) { // get color index or repeat times if(tmpInd & RLE_MARK) { // is it color index? tmpInd &= DATA_MARK; // get color index to repeat repeatTimes = getPicByte(++pPic)+1; // zero RLE does not exist! } ++repeatTimes; // get color from colorTable by color index repeatColor = palette_RAM[(tmpInd == ALPHA_COLOR_ID) ? alphaReplaceColorId : tmpInd]; do { pushColorFast(repeatColor); } while(--repeatTimes); } } 

الشيء الرئيسي هو عدم عرض الصورة خارج الشاشة ، وإلا ستكون القمامة ، حيث لا يوجد فحص للحدود هنا.
يتم تفريغ صورة الاختبار في 39 مللي ثانية تقريبًا. في الوقت نفسه ، تحتل 3040 بايت ، بينما بدون الضغط سيستغرق 11200 بايت أو 22400 بايت دون فهرسة.

صورة الاختبار (مكبرة مرتين):

في الصورة أعلاه ، يمكنك رؤية التداخل ، ولكن على الشاشة يتم تخفيفه بواسطة الأجهزة ، مما يخلق تأثيرًا مشابهًا لـ CRT وفي نفس الوقت يزيد بشكل كبير من نسبة الضغط.

RLE ليس حلا سحريا


نحن نتعامل مع deja vu.

كما تعلم ، فإن RLE تسير على ما يرام مع الحزم مثل LZ. جاء WiKi إلى الإنقاذ بقائمة من طرق الضغط. كان الدافع هو الفيديو من "GameHut" حول تحليل المقدمة المستحيلة في Sonic 3D Blast.
بعد أن درست العديد من شركات التعبئة (LZ77 ، LZW ، LZSS ، LZO ، RNC ، إلخ) ، توصلت إلى استنتاج مفاده أن فاتحي الحزم:
  • تتطلب الكثير من ذاكرة الوصول العشوائي للبيانات غير المعبأة (على الأقل 64 كيلوبايت وأكثر) ؛
  • ضخم وبطيء (يحتاج البعض إلى بناء أشجار هوفمان لكل وحدة فرعية) ؛
  • لديها نسبة ضغط منخفضة مع نافذة صغيرة (متطلبات ذاكرة الوصول العشوائي الصارمة للغاية) ؛
  • لديها غموض في الترخيص.

بعد شهور من التعديلات غير المجدية ، تقرر تعديل باكر القائمة.
عن طريق القياس مع الحزم الشبيهة بـ LZ ، لتحقيق أقصى ضغط ، تم استخدام الوصول إلى القاموس ، ولكن على مستوى البايت - يتم استبدال أزواج البايت الأكثر تكرارًا بمؤشر بايت واحد في القاموس.
ولكن هناك مشكلة: كيفية التمييز بين بايت من "عدد التكرارات" من "علامة القاموس"؟
بعد جلسة طويلة بقطعة من الورق ولعبة سحرية مع الخفافيش ، ظهر هذا:
  • "علامة القاموس" هي علامة RLE (0x80) + بايت البيانات (0x50) + رقم موضع في القاموس ؛
  • قصر البايت "عدد التكرارات" على حجم علامة القاموس - 1 (0xCF) ؛
  • لا يمكن للقاموس استخدام القيمة 0xff (وهي للعلامة لنهاية الصورة).


بتطبيق كل هذا ، نحصل على حجم قاموس ثابت: لا يزيد عن 46 زوجًا بايت وتقليل RLE إلى 209 بايت. من الواضح أنه لا يمكن تغليف جميع الصور بهذه الطريقة ، لكنها لن تصبح أكثر.
في كلتا الخوارزميات ، سيكون هيكل الصورة المحزومة كما يلي:
  • 1 بايت لكل عرض وارتفاع.
  • 1 بايت لحجم القاموس ، وهو مؤشر علامة على بداية البيانات المعبأة ؛
  • من 0 إلى 92 بايت من القاموس ؛
  • 1 إلى N بايت من البيانات المعبأة.

إن أداة الحزم الناتجة على D (pickoPacker) كافية لوضعها في مجلد يحتوي على ملفات * .png مفهرسة وتشغيلها من المحطة الطرفية (أو cmd). إذا كنت بحاجة إلى مساعدة ، فقم بتشغيل الخيار "-h" أو "--help".
بعد تشغيل الأداة ، نحصل على ملفات * .h ، التي يسهل نقل محتوياتها إلى المكان الصحيح في المشروع (وبالتالي ، لا توجد حماية).

قبل التفريغ ، يتم إعداد الشاشة والقاموس والبيانات الأولية:
 void drawPico_DIC_P(uint8_t x, uint8_t y, pic_t *pPic) { auto tmpData = getPicSize(pPic, 0); tftSetAddrWindow(x, y, x+tmpData.u8Data1, y+tmpData.u8Data2); uint8_t tmpByte, unfoldPos, dictMarker; alphaReplaceColorId = getAlphaReplaceColorId(); auto pDict = &pPic[3]; // save dictionary pointer pPic += getPicByte(&pPic[2]); // make offset to picture data do { unfoldPos = dictMarker = 0; do { if((tmpByte = getPicByte(++pPic)) != PIC_DATA_END) { if(tmpByte < DICT_MARK) { buf_packed[unfoldPos] = tmpByte; } else { dictMarker = 1; setPicWData(&buf_packed[unfoldPos]) = getPicWData(pDict, tmpByte); ++unfoldPos; } ++unfoldPos; } else { break; } } while((unfoldPos < MAX_UNFOLD_SIZE) //&& (unfoldPos) && ((tmpByte > DATA_MARK) || (tmpByte > MAX_DATA_LENGTH))); if(unfoldPos) { buf_packed[unfoldPos] = PIC_DATA_END; // mark end of chunk printBuf_RLE( dictMarker ? unpackBuf_DIC(pDict) : &buf_packed[0] ); // V2V3 decoder } } while(unfoldPos); } 

يمكن تعبئة قطعة من البيانات المقروءة في قاموس ، لذلك نتحقق منها ونفككها:
 inline uint8_t findPackedMark(uint8_t *ptr) { do { if(*ptr >= DICT_MARK) { return 1; } } while(*(++ptr) != PIC_DATA_END); return 0; } inline uint8_t *unpackBuf_DIC(const uint8_t *pDict) { bool swap = false; bool dictMarker = true; auto getBufferPtr = [&](uint8_t a[], uint8_t b[]) { return swap ? &a[0] : &b[0]; }; auto ptrP = getBufferPtr(buf_unpacked, buf_packed); auto ptrU = getBufferPtr(buf_packed, buf_unpacked); while(dictMarker) { if(*ptrP >= DICT_MARK) { setPicWData(ptrU) = getPicWData(pDict, *ptrP); ++ptrU; } else { *ptrU = *ptrP; } ++ptrU; ++ptrP; if(*ptrP == PIC_DATA_END) { *ptrU = *ptrP; // mark end of chunk swap = !swap; ptrP = getBufferPtr(buf_unpacked, buf_packed); ptrU = getBufferPtr(buf_packed, buf_unpacked); dictMarker = findPackedMark(ptrP); } } return getBufferPtr(buf_unpacked, buf_packed); } 

الآن من المخزن المؤقت المستلم ، نقوم بفك حزم RLE بطريقة مألوفة ونعرضه على الشاشة:
 inline void printBuf_RLE(uint8_t *pData) { uint16_t repeatColor; uint8_t repeatTimes, tmpByte; while((tmpByte = *pData) != PIC_DATA_END) { // get color index or repeat times if(tmpByte & RLE_MARK) { // is it RLE byte? tmpByte &= DATA_MARK; // get color index to repeat repeatTimes = *(++pData)+1; // zero RLE does not exist! } ++repeatTimes; ++pData; // get color from colorTable by color index repeatColor = palette_RAM[(tmpByte == ALPHA_COLOR_ID) ? alphaReplaceColorId : tmpByte]; do { pushColorFast(repeatColor); } while(--repeatTimes); } } 

والمثير للدهشة أن استبدال الخوارزمية لم يؤثر بشكل كبير على وقت التفريغ وهو ~ 47 مللي ثانية. حوالي 8 مللي ثانية. أطول ، ولكن صورة الاختبار تستغرق 1650 بايت فقط!

حتى الإجراء الأخير


تقريبا كل شيء يمكن القيام به بشكل أسرع!

على الرغم من وجود SPI للأجهزة ، فإن قلب AVR يسبب الكثير من الصداع عند استخدامه.
من المعروف منذ فترة طويلة أن SPI على AVR ، بالإضافة إلى تشغيله بسرعة F_CPU / 2 ، لديه أيضًا سجل بيانات 1 بايت فقط (لا يمكن تحميل 2 بايت في وقت واحد).
علاوة على ذلك ، فإن جميع رموز SPI تقريبًا على AVR التي التقيت بها تعمل وفقًا لهذا المخطط:
  • تنزيل بيانات SPDR
  • استجواب بت SPIF في SPSR في حلقة.

كما ترى ، فإن التزويد المستمر للبيانات ، كما هو الحال في STM32 ، لا يشم رائحة هنا. ولكن ، حتى هنا يمكنك تسريع إخراج كلا الحزبين بمقدار 3 مللي ثانية تقريبًا!

من خلال فتح ورقة البيانات والنظر في قسم "ساعات مجموعة التعليمات" ، يمكنك حساب تكاليف وحدة المعالجة المركزية عند إرسال بايت عبر SPI:
  • دورة واحدة لتسجيل التحميل مع البيانات الجديدة ؛
  • 2 نبضة لكل بت (أو 16 نبضة لكل بايت) ؛
  • شريط واحد لكل سحر خط الساعة (بعد ذلك بقليل عن "NOP") ؛
  • ساعة واحدة للتحقق من بت الحالة في SPSR (أو ساعتين على الفرع) ؛

بشكل إجمالي ، لإرسال بكسل واحد (وحدتي بايت) ، يجب إنفاق 38 دورة ساعة أو 425600 دورة ساعة للصورة الاختبارية (11200 بايت).
مع العلم أن F_CPU == MHz 16 نحصل على 0.0000000625 62.5 نانو ثانية لكل دورة ساعة ( Process0169 ) ، وضرب القيم ، نحصل على 26 مللي ثانية تقريبًا. يطرح السؤال: "من أين كتبت في وقت سابق أن وقت التفريغ هو 39 مللي ثانية. و 47 ملي ثانية. "؟ كل شيء بسيط - منطق الحزم + معالجة المقاطعة.

فيما يلي مثال على إخراج المقاطعة:

وبدون انقطاع:

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

وقد كتب أعلاه حول "NOP سحري" معين لخط الساعة. والحقيقة هي أنه من أجل تثبيت CLK وتعيين علامة SPIF ، يلزم دورة ساعة واحدة بالضبط وبحلول وقت قراءة هذه العلامة ، يتم تعيينها بالفعل ، والتي تتجنب التفرع إلى شريطين على تعليمات BREQ.
هنا مثال بدون NOP:

ومعه:


يبدو الفرق ضئيلاً ، فقط بضع ميكروثانية ، لكن إذا أخذت مقياسًا مختلفًا:
NOP كبير:

ومعه كبير جدًا:

ثم يصبح الفرق أكثر ملحوظة ، حيث يصل إلى 4.3 مللي ثانية.

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

نطبق المعرفة وننشر الوظيفة "pushColorFast (تكرار اللون) ؛":
 #define SPDR_TX_WAIT(a) asm volatile(a); while((SPSR & (1<<SPIF)) == 0); typedef union { uint16_t val; struct { uint8_t lsb; uint8_t msb; }; } SPDR_t; ... do { #ifdef ESPLORA_OPTIMIZE SPDR_t in = {.val = repeatColor}; SPDR_TX_WAIT(""); SPDR = in.msb; SPDR_TX_WAIT("nop"); SPDR = in.lsb; #else pushColorFast(repeatColor); #endif } while(--repeatTimes); } #ifdef ESPLORA_OPTIMIZE SPDR_TX_WAIT(""); // dummy wait to stable SPI #endif } 

على الرغم من الانقطاع عن المؤقت ، فإن استخدام الخدعة أعلاه يعطي مكاسب تقارب 6 مللي ثانية.:


هذه هي الطريقة التي تسمح لك بها المعرفة البسيطة بالحديد بضغط المزيد منه وإخراج شيء مشابه:


تصادم الكولوسيوم


معركة المربعات.

بادئ ذي بدء ، المجموعة الكاملة من الأشياء (السفن ، القذائف ، الكويكبات ، المكافآت) هي هياكل (العفاريت) مع المعلمات التالية:
  • إحداثيات X و Y الحالية ؛
  • إحداثيات جديدة X ، Y ؛
  • المؤشر على الصورة.

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

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

ما سبق يبدو أسهل بكثير مما يبدو:
 bool checkSpriteCollision(sprite_t *pSprOne, sprite_t *pSprTwo) { auto tmpDataOne = getPicSize(pSprOne->pPic, 0); auto tmpDataTwo = getPicSize(pSprTwo->pPic, 0); /* ----------- Check X position ----------- */ uint8_t objOnePosEndX = (pSprOne->pos.Old.x + tmpDataOne.u8Data1); if(objOnePosEndX >= pSprTwo->pos.Old.x) { uint8_t objTwoPosEndX = (pSprTwo->pos.Old.x + tmpDataTwo.u8Data1); if(pSprOne->pos.Old.x >= objTwoPosEndX) { return false; // nope, different X positions } // ok, objects on same X lines; Go next... } else { return false; // nope, absolutelly different X positions } /* ---------------------------------------- */ /* ----------- Check Y position ----------- */ uint8_t objOnePosEndY = (pSprOne->pos.Old.y + tmpDataOne.u8Data2); if(objOnePosEndY >= pSprTwo->pos.Old.y) { uint8_t objTwoPosEndY = (pSprTwo->pos.Old.y + tmpDataTwo.u8Data2); if(pSprOne->pos.Old.y <= objTwoPosEndY) { // ok, objects on same Y lines; Go next... // yep, if we are here // then, part of one object collide wthith another object return true; } else { return false; // nope, different Y positions } } else { return false; // nope, absolutelly different Y positions } } 

يبقى إضافة هذا إلى اللعبة:
 void checkInVadersCollision(void) { decltype(aliens[0].weapon.ray) gopher; for(auto &alien : aliens) { if(alien.alive) { if(checkSpriteCollision(&ship.sprite, &alien.sprite)) { gopher.sprite.pos.Old = alien.sprite.pos.Old; rocketEpxlosion(&gopher); // now make gopher to explode \(^_^)/ removeSprite(&alien.sprite); alien.alive = false; score -= SCORE_PENALTY; if(score < 0) score = 0; } } } } 


منحنى بيزييه


سكك الفضاء.

كما هو الحال في أي لعبة أخرى من هذا النوع ، يجب أن تتحرك سفن العدو على طول المنحنيات.
تقرر تنفيذ المنحنيات التربيعية كأبسط وحدة تحكم وهذه المهمة. تكفيهم ثلاث نقاط: الأولي (P0) والنهائي (P2) والخيالي (P1). يحدد الأولين بداية ونهاية الخط ، وتصف النقطة الأخيرة نوع الانحناء.
مقال رائع عن المنحنيات.
نظرًا لأن هذا منحنى معلمة Bezier ، فإنه يحتاج أيضًا إلى معلمة أخرى - عدد النقاط الوسيطة بين نقطتي البداية والنهاية.

إجمالي نحصل هنا على مثل هذا الهيكل:
 typedef struct { // 7 bytes position_t P0; position_t P1; position_t P2; uint8_t totalSteps; } bezier_t; 
في ذلك ، الموضع_t هو هيكل من وحدتي بايت للإحداثيات X و Y.
يتم حساب البحث عن نقطة لكل إحداثية باستخدام هذه الصيغة (thx Wiki):
B = ((1.0 - t) ^ 2) P0 + 2t (1.0 - t) P1 + (t ^ 2) P2 ،
t [> = 0 && <= 1]

لفترة طويلة ، تم حل تطبيقه مباشرة بدون رياضي نقطة ثابتة:
 ... float t = ((float)pItemLine->step)/((float)pLine->totalSteps); pPos->x = (1.0 - t)*(1.0 - t)*pLine->P0.x + 2*t*(1.0 - t)*pLine->P1.x + t*t*pLine->P2.x; pPos->y = (1.0 - t)*(1.0 - t)*pLine->P0.y + 2*t*(1.0 - t)*pLine->P1.y + t*t*pLine->P2.y; ... 

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

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

المرشح الثاني من حزمة دول مجلس التعاون الخليجي لم ينجح أيضًا ، نظرًا لأن avr-gcc المستخدم لم يتم تصحيحه وبقي نوع "Accum القصير" غير متاح.

الخيار الثالث ، على الرغم من حقيقة أنه يحتوي على عدد كبير من الحصيرة. الدالات ، لها عمليات بت مشفرة على وحدات البت المحددة بتنسيق Q16.16 ، مما يجعل من المستحيل التحكم في قيم Q و I.

يمكن اعتبار هذا الأخير نسخة مبسطة من "الرياضيات الثابتة" ، ولكن الميزة الرئيسية هي القدرة على التحكم ليس فقط في حجم المتغير ، والذي يكون افتراضيًا 32 بت بتنسيق Q24.8 ، ولكن أيضًا قيم Q و I.

نتائج الاختبار في إعدادات مختلفة:
اكتبمعدل الذكاءأعلام إضافيةبايت ROMتمس. *
تعويم--423635
رياضيات ثابتة16.16-4796119
رياضيات ثابتة16.16FIXMATH_NO_OVERFLOW466489
رياضيات ثابتة16.16FIXMATH_OPTIMIZE_8BIT503692
رياضيات ثابتة16.16_NO_OVERFLOW + _8 بت491689
ثابت24.8FIXEDPT_BITS 32442064
ثابت9.7FIXEDPT_BITS 16349031
* تم إجراء الفحص على النموذج: "195.175.145.110.170.70.170" والمفتاح "-Os".

يمكن أن نرى من الجدول أن كلتا المكتبتين استهلكتا المزيد من ROM وأظهرتا أنهما أسوأ من الكود المتراكم من دول مجلس التعاون الخليجي عند استخدام تعويم.
يُلاحظ أيضًا أن المراجعة الصغيرة لتنسيق Q9.7 وانخفاض المتغير إلى 16 بت أعطت تسارعًا بمقدار 4 مللي ثانية. وتحرير ROM عند ~ 50 بايت.

كان التأثير المتوقع هو انخفاض في الدقة وزيادة في عدد الأخطاء:

وهو في هذه الحالة غير نقدي.

تخصيص الموارد


الثلاثاء والخميس يعملان لمدة ساعة فقط.

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

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

نحن ندير كل هذا ليس مرة واحدة مع نظام التشغيل ، ولكن مع آلة الحالة التي أنشأتها قبل بضع سنوات ، أو ، ببساطة ، ليس مدير المهام الصغير المزدحم.

سأكرر أسباب استخدامه بدلاً من أي من RTOS:
  • متطلبات ROM أقل (~ 250 بايت الأساسية) ؛
  • متطلبات ذاكرة وصول عشوائي أقل (~ 9 بايت لكل مهمة) ؛
  • مبدأ عمل بسيط ومفهوم ؛
  • حتمية السلوك ؛
  • إضاعة وقت أقل وحدة المعالجة المركزية ؛
  • يترك الوصول إلى الحديد ؛
  • منصة مستقلة ؛
  • مكتوب بلغة C ويسهل لفه بلغة C ++ ؛
  • بحاجة إلى دراجتي الخاصة.

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

تحتاج أولاً إلى تحديد بعض وحدات الماكرو لراحتك:
 #define T(a) a##Task #define TASK_N(a) const taskParams_t T(a) #define TASK(a,b) TASK_N(a) PROGMEM = {.pFunc=a, .timeOut=b} #define TASK_P(a) (taskParams_t*)&T(a) #define TASK_ARR_N(a) const tasksArr_t a##TasksArr[] #define TASK_ARR(a) TASK_ARR_N(a) PROGMEM #define TASK_END NULL 

يقوم إعلان المهمة بالفعل بإنشاء بنية ، وتهيئة حقولها ووضعها في ROM:
 TASK(updateBtnStates, 25); 

يشغل كل هيكل من هذا القبيل 4 بايت من ROM (اثنان لكل مؤشر واثنان لكل فاصل زمني).
المكافأة الرائعة لوحدات الماكرو هي أنه لا يعمل على إنشاء أكثر من بنية فريدة لكل وظيفة.
بعد الإعلان عن المهام اللازمة ، نضيفها إلى الصفيف ونضعها أيضًا في ROM:
 TASK_ARR( game ) = { TASK_P(updateBtnStates), TASK_P(playMusic), TASK_P(drawStars), TASK_P(moveShip), TASK_P(drawShip), TASK_P(checkFireButton), TASK_P(pauseMenu), TASK_P(drawPlayerWeapon), TASK_P(checkShipHealth), TASK_P(drawSomeGUI), TASK_P(checkInVaders), TASK_P(drawInVaders), TASK_P(moveInVaders), TASK_P(checkInVadersRespawn), TASK_P(checkInVadersRay), TASK_P(checkInVadersCollision), TASK_P(dropWeaponGift), TASK_END }; 

عند تعيين العلم USE_DYNAMIC_MEM على 0 للذاكرة الثابتة ، فإن الشيء الرئيسي الذي يجب تذكره هو تهيئة المؤشرات إلى مخزن المهام في ذاكرة الوصول العشوائي وتعيين الحد الأقصى لعددها الذي سيتم تنفيذه:
 ... tasksContainer_t tasksContainer; taskFunc_t tasksArr[MAX_GAME_TASKS]; ... initTasksArr(&tasksContainer, &tasksArr[0], MAX_GAME_TASKS); … 

تحديد المهام للتنفيذ:
 ... addTasksArray_P(gameTasksArr); … 

يتم التحكم في الحماية من التدفق الزائد بواسطة علامة USE_MEM_PANIC ، إذا كنت متأكدًا من عدد المهام ، يمكنك تعطيلها لحفظ ROM.

يبقى فقط لتشغيل المعالج:
 ... runTasks(); ... 

يوجد في الداخل حلقة لا نهائية تحتوي على المنطق الأساسي. بمجرد دخولها ، يتم استعادة المكدس أيضًا بفضل "__attribute__ ((noreturn))".
في الحلقة ، يتم فحص عناصر المصفوفة بالتناوب بحثًا عن الحاجة لاستدعاء المهمة بعد انتهاء الفاصل الزمني.
تم إجراء العد التنازلي للفترات الزمنية على أساس المؤقت 0 كنظام بكمية مقدارها 1 مللي ثانية ... على

الرغم من التوزيع الناجح للمهام في الوقت المناسب ، فقد كانت في بعض الأحيان متراكبة (غضب) ، مما تسبب في تلاشي كل شيء وكل شيء في اللعبة على المدى القصير.
بالتأكيد يجب أن يُقرر ، ولكن كيف؟ حول كيف تم تحديد كل شيء في المرة القادمة ، ولكن حاول الآن العثور على بيضة عيد الفصح في المصدر.

النهاية


لذا ، باستخدام الكثير من الحيل (والكثير منها لم أصفه) ، تحول كل شيء إلى احتواء ROM بسعة 24 كيلوبايت و 1500 بايت من ذاكرة الوصول العشوائي. إذا كان لديك أي أسئلة ، يسعدني أن أجيب عليها.
بالنسبة لأولئك الذين لم يجدوا أو لم يبحثوا عن بيضة عيد الفصح:
:
 void invadersMagicRespawn(void) { for(auto &alien : aliens) { if(!alien.alive) { alien.respawnTime = 1; } } } 

, ?
invadersMagicRespawn:
 void action() { tftSetTextSize(1); for(;;) { tftSetCP437(RN & 1); tftSetTextColorBG((((RN % 192 + 64) & 0xFC) << 3), COLOR_BLACK); tftDrawCharInt(((RN % 26) * 6), ((RN & 15) * 8), (RN % 255)); tftPrintAt_P(32, 58, (const char *)creditP0); } } a(void) { for(auto &alien : aliens) { if(!alien.alive) { alien.respawnTime = 1; } } } 

«(void)» , «action()» 10 , «disablePause();». «Matrix Falling code» . 130 ROM.


لبناء وتشغيله يكفي لوضع المجلد (أو الرابط) "esploraAPI" في "/ arduino / libraries /".

المراجع:


ملاحظة: يمكنك أن ترى وتسمع كيف يبدو كل ذلك في وقت لاحق عندما أقوم بعمل فيديو مقبول.

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


All Articles