
Saya ingin berbagi konstruksi jangka panjang malam yang lain, yang menunjukkan bahwa Anda dapat membuat game bahkan pada perangkat keras yang lemah.
Tentang apa yang harus Anda lakukan, bagaimana diputuskan, dan bagaimana melakukan sesuatu lebih dari sekadar klon Pong lainnya - selamat datang di Cat.
Perhatian: artikel bagus, traffic, dan banyak sisipan kode!
Secara singkat tentang permainan
Tembak`em! - sekarang di AVR.
Sebenarnya, ini adalah shmap lain, jadi sekali lagi karakter utama
Shepard harus menyelamatkan galaksi dari serangan mendadak oleh orang tak dikenal, membuat jalan melalui ruang melalui bintang-bintang dan bidang asteroid secara bersamaan membersihkan setiap sistem bintang.
Seluruh permainan ditulis dalam C dan C ++ tanpa menggunakan perpustakaan Wire dari Arduino.
Permainan memiliki 4 kapal untuk dipilih (yang terakhir tersedia setelah lewat), masing-masing dengan karakteristiknya sendiri:
- kemampuan manuver;
- daya tahan;
- kekuatan senjata.
Juga diterapkan:
- Grafis warna 2D;
- nyalakan senjata;
- bos di akhir level;
- tingkat dengan asteroid (dan animasi rotasi mereka);
- perubahan warna latar pada level (dan bukan hanya ruang hitam);
- pergerakan bintang di latar belakang dengan kecepatan yang berbeda (untuk efek kedalaman);
- penilaian dan penghematan dalam EEPROM;
- suara yang sama (tembakan, ledakan, dll.);
- lautan lawan yang identik.
Platform
Kembalinya hantu.
Saya akan mengklarifikasi terlebih dahulu bahwa platform ini harus dianggap sebagai konsol game lama generasi ketiga pertama (80-an, shiru8bit ).
Juga, modifikasi perangkat keras atas perangkat keras asli dilarang, yang memastikan peluncuran pada papan identik lainnya langsung dari kotak.
Game ini ditulis untuk papan Arduino Esplora, tetapi mentransfer ke GBA atau platform lain, saya pikir, tidak akan sulit.
Namun demikian, bahkan pada sumber daya ini papan ini hanya dibahas beberapa kali, dan papan lainnya tidak layak disebut sama sekali, meskipun masing-masing komunitasnya cukup besar:
- GameBuino META:
- Pokitto;
- pembuatBuino;
- Arduboy;
- UzeBox / FuzeBox;
- dan banyak lainnya.
Untuk memulainya, apa yang tidak ada di Esplora:
- banyak memori (ROM 28kb, RAM 2.5kb);
- daya (8 bit CPU pada 16 MHz);
- DMA
- generator karakter;
- area memori yang dialokasikan atau register khusus. tujuan (palet, ubin, latar belakang, dll.);
- mengontrol kecerahan layar (oh, begitu banyak efek di tempat sampah);
- address extender ruang (pemetaan);
- debugger (
tetapi siapa yang membutuhkannya ketika ada seluruh layar! ).
Saya akan melanjutkan dengan fakta bahwa ada:
- perangkat keras SPI (dapat berjalan pada kecepatan F_CPU / 2);
- layar berdasarkan ST7735 160x128 1,44 ";
- sejumput penghitung waktu (hanya 4 pcs);
- sejumput GPIO;
- beberapa tombol (5 pcs. + joystick dua sumbu);
- beberapa sensor (pencahayaan, akselerometer, termometer);
- pemicu
iritasi piezo buzzer.
Ternyata hampir tidak ada apa-apa di sana. Tidak mengherankan bahwa tidak ada yang ingin melakukan apa pun dengannya kecuali klon Pong dan beberapa tiga pertandingan selama ini!
Mungkin faktanya adalah bahwa menulis di bawah pengontrol ATmega32u4 (dan sejenisnya) mirip dengan pemrograman untuk Intel 8051 (yang hampir berusia 40 tahun pada saat publikasi), di mana Anda perlu mengamati sejumlah besar kondisi dan menggunakan berbagai trik dan trik.
Pemrosesan perangkat
Satu untuk semuanya!
Setelah melihat sirkuit, tampak jelas bahwa semua periferal terhubung melalui GPIO expander (74HC4067D multiplexer lanjut MUX) dan diganti menggunakan GPIO PF4, PF5, PF6, PF7 atau PORTF senior, dan output MUX dibaca pada GPIO - PF1.
Sangat mudah untuk mengganti input hanya dengan memberikan nilai ke port PORTF dengan mask dan tidak melupakan nibble minor:
uint16_t getAnalogMux(uint8_t chMux) { MUX_PORTX = ((MUX_PORTX & 0x0F) | ((chMux<<4)&0xF0)); return readADC(); }
Jajak pendapat klik tombol:
#define SW_BTN_MIN_LVL 800 bool readSwitchButton(uint8_t btn) { bool state = true; if(getAnalogMux(btn) > SW_BTN_MIN_LVL) {
Berikut ini adalah nilai untuk port 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
Dengan menambahkan sedikit lebih banyak:
#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)
Anda dapat mewawancarai salib kanan dengan aman:
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; }
Harap perhatikan bahwa kondisi sebelumnya tidak diatur ulang, jika tidak Anda dapat melewatkan fakta menekan tombol (ini juga berfungsi sebagai perlindungan tambahan terhadap obrolan).
Sfx
Sedikit berdengung.
Bagaimana jika tidak ada DAC, tidak ada chip dari Yamaha, dan hanya ada persegi panjang PWM 1-bit untuk suara?
Pada awalnya, tampaknya tidak begitu banyak, tetapi, meskipun demikian, PWM yang licik digunakan di sini untuk menciptakan kembali teknik "PDM audio" dan dengan bantuannya Anda dapat melakukan
ini.Sesuatu yang serupa disediakan oleh perpustakaan dari Gamebuino dan yang diperlukan hanyalah mentransfer generator popping ke GPIO lain dan timer ke Esplora (output timer4 dan OCR4D). Untuk operasi yang benar, timer1 juga digunakan untuk menghasilkan interupsi dan memuat ulang register OCR4D dengan data baru.
Mesin Gamebuino menggunakan pola suara (seperti dalam musik pelacak), yang menghemat banyak ruang, tetapi Anda perlu melakukan semua sampel sendiri, tidak ada perpustakaan dengan yang sudah jadi.
Perlu disebutkan bahwa mesin ini terikat pada periode pembaruan sekitar 1/50 detik atau 20 frame / detik.
Untuk membaca pola suara, setelah membaca Wiki dalam format audio, saya membuat sketsa GUI sederhana pada Qt. Ini tidak menghasilkan suara dengan cara yang sama, tetapi memberikan konsep perkiraan tentang bagaimana pola akan berbunyi dan memungkinkan Anda memuat, menyimpan, dan mengeditnya.
Grafik
Pixelart Abadi.
Layar mengkodekan warna dalam dua byte (RGB565), tetapi karena gambar dalam format ini akan memakan banyak, semuanya telah diindeks oleh palet untuk menghemat ruang, yang telah saya jelaskan lebih dari sekali dalam artikel saya sebelumnya.
Tidak seperti Famicom / NES, tidak ada batas warna untuk gambar dan ada lebih banyak warna yang tersedia di palet.
Setiap gambar dalam game adalah array byte di mana data berikut disimpan:
- lebar, tinggi;
- mulai penanda data;
- kamus (jika ada, tetapi lebih lanjut tentang itu nanti);
- muatan;
- akhir penanda data.
Misalnya, gambar seperti itu (diperbesar 10 kali):

dalam kode itu akan terlihat seperti ini:
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, };
Di mana tanpa kapal dalam genre ini? Setelah ratusan sketsa uji dengan perbedaan piksel, hanya kapal-kapal ini yang tersisa untuk pemain:

Patut dicatat bahwa kapal-kapal tidak memiliki nyala di ubin (ini untuk kejelasan), itu diterapkan secara terpisah untuk membuat animasi knalpot dari mesin.
Jangan lupa tentang pilot dari setiap kapal:

Variasi kapal musuh tidak terlalu besar, tetapi izinkan saya mengingatkan Anda, tidak ada terlalu banyak ruang, jadi di sini ada tiga kapal:

Tanpa bonus kanonik dalam bentuk meningkatkan senjata dan memulihkan kesehatan, pemain tidak akan bertahan lama:

Tentu saja, dengan meningkatnya kekuatan senjata, jenis peluru yang dipancarkan berubah:

Seperti yang ditulis di awal, permainan memiliki tingkat dengan asteroid, itu terjadi setelah setiap bos kedua. Sangat menarik karena ada banyak benda bergerak dan berputar dengan ukuran berbeda. Selain itu, ketika seorang pemain memukul mereka, mereka sebagian runtuh, menjadi lebih kecil ukurannya.
Petunjuk: Asteroid besar mendapat lebih banyak poin.



Untuk membuat animasi sederhana ini, 12 gambar kecil sudah cukup:

Mereka dibagi menjadi tiga untuk setiap ukuran (besar, sedang dan kecil) dan untuk setiap sudut rotasi Anda perlu 4 lebih diputar 0, 90, 180 dan 270 derajat. Dalam permainan, cukup untuk mengganti pointer ke array dengan gambar pada interval yang sama sehingga menciptakan ilusi rotasi.
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); ...
Ini dilakukan hanya karena kurangnya kemampuan perangkat keras, dan implementasi perangkat lunak seperti transformasi Affine akan mengambil lebih dari gambar itu sendiri dan akan sangat lambat.
Sepotong satin untuk mereka yang tertarik.
Anda dapat melihat bagian dari prototipe dan apa yang muncul hanya di kredit setelah melewati permainan.
Selain grafis sederhana, untuk menghemat ruang dan menambahkan efek retro, mesin terbang huruf kecil dan semua mesin terbang yang hingga 30 dan setelah 127 byte ASCII dikeluarkan dari font.
Penting!
Jangan lupa bahwa const dan constexpr pada AVR tidak berarti sama sekali bahwa data akan ada dalam memori program, di sini untuk ini Anda perlu tambahan menggunakan PROGMEM.
Hal ini disebabkan oleh fakta bahwa inti AVR didasarkan pada arsitektur Harvard, sehingga kode akses khusus untuk CPU diperlukan untuk mengakses data.
Meremas galaksi
Cara termudah untuk berkemas adalah RLE.
Setelah mempelajari data yang dikemas, Anda dapat melihat bahwa bit paling signifikan dalam byte payload dalam rentang dari 0x00 hingga 0x50 tidak digunakan. Ini memungkinkan Anda untuk menambahkan data dan penanda awal untuk awal pengulangan (0x80), dan byte berikutnya untuk menunjukkan jumlah pengulangan, yang memungkinkan Anda untuk mengemas serangkaian 257 (+2 dari kenyataan bahwa RLE dua byte bodoh) dengan byte identik hanya dalam dua.
Implementasi dan tampilan unpacker:
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;
Hal utama adalah tidak menampilkan gambar di luar layar, jika tidak akan menjadi sampah, karena tidak ada pemeriksaan perbatasan di sini.
Gambar uji dibongkar dalam ~ 39ms. pada saat yang sama, menempati 3040 byte, sementara tanpa kompresi dibutuhkan 11.200 byte atau 22.400 byte tanpa pengindeksan.
Gambar uji (diperbesar 2 kali):

Pada gambar di atas Anda dapat melihat interlace, tetapi pada layar itu dihaluskan oleh perangkat keras, menciptakan efek yang mirip dengan CRT dan pada saat yang sama secara signifikan meningkatkan rasio kompresi.
RLE bukan obat mujarab
Kami diperlakukan untuk deja vu.
Seperti yang Anda tahu, RLE berjalan baik dengan pengepak seperti LZ. WiKi datang ke penyelamatan dengan daftar metode kompresi. Dorongannya adalah video dari "GameHut" tentang analisis
intro yang mustahil
di Sonic 3D Blast.Setelah mempelajari banyak pengepak (LZ77, LZW, LZSS, LZO, RNC, dll.), Saya sampai pada kesimpulan bahwa mereka membongkar:
- membutuhkan banyak RAM untuk data yang tidak dibongkar (setidaknya 64kb. dan lebih banyak);
- tebal dan lambat (beberapa perlu membangun pohon Huffman untuk setiap subunit);
- memiliki rasio kompresi yang rendah dengan jendela kecil (persyaratan RAM yang sangat ketat);
- memiliki ambiguitas dengan perizinan.
Setelah berbulan-bulan adaptasi yang sia-sia, diputuskan untuk memodifikasi paket yang ada.
Dengan analogi dengan pengemas mirip LZ, untuk mencapai kompresi maksimum, akses kamus digunakan, tetapi pada tingkat byte - pasangan byte yang paling sering digantikan diganti dengan satu byte pointer dalam kamus.
Tapi ada yang menarik: bagaimana membedakan byte "berapa banyak pengulangan" dari "kamus"?
Setelah lama duduk dengan selembar kertas dan permainan ajaib dengan kelelawar, ini muncul:
- "Kamus penanda" adalah penanda RLE (0x80) + byte data (0x50) + nomor posisi dalam kamus;
- batasi byte "berapa banyak pengulangan" ke ukuran penanda kamus - 1 (0xCF);
- kamus tidak dapat menggunakan nilai 0xff (ini untuk penanda untuk akhir gambar).
Menerapkan semua ini, kami mendapatkan ukuran kamus tetap: tidak lebih dari 46 byte pasangan dan pengurangan RLE menjadi 209 byte. Jelas, tidak semua gambar dapat dikemas seperti ini, tetapi mereka tidak akan menjadi lagi.
Dalam kedua algoritma, struktur gambar yang dikemas adalah sebagai berikut:
- 1 byte per lebar dan tinggi;
- 1 byte untuk ukuran kamus, itu adalah penanda marker ke awal data yang dikemas;
- dari 0 hingga 92 byte kamus;
- 1 hingga N byte data yang dikemas.
Utilitas packer yang dihasilkan pada D (pickoPacker) cukup untuk dimasukkan ke dalam folder dengan file * .png yang diindeks dan dijalankan dari terminal (atau cmd). Jika Anda butuh bantuan, jalankan dengan opsi "-h" atau "--help".
Setelah utilitas berjalan, kami mendapatkan file * .h, yang isinya nyaman untuk ditransfer ke tempat yang tepat dalam proyek (oleh karena itu, tidak ada perlindungan).
Sebelum membongkar, layar, kamus, dan data awal disiapkan:
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];
Potongan data yang sudah dibaca dapat dikemas dalam kamus, jadi kami memeriksa dan membukanya:
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;
Sekarang dari buffer yang diterima kami membongkar RLE dengan cara yang akrab dan menampilkannya di layar:
inline void printBuf_RLE(uint8_t *pData) { uint16_t repeatColor; uint8_t repeatTimes, tmpByte; while((tmpByte = *pData) != PIC_DATA_END) {
Anehnya, mengganti algoritma tidak secara signifikan mempengaruhi waktu pembongkaran dan ~ 47ms. Ini hampir 8 ms. lebih lama, tetapi gambar uji hanya membutuhkan 1.650 byte!
Sampai langkah terakhir
Hampir semuanya bisa dilakukan lebih cepat!
Meskipun ada perangkat keras SPI, inti AVR memberikan banyak sakit kepala saat menggunakannya.
Sudah lama diketahui bahwa SPI pada AVR, selain berjalan pada kecepatan F_CPU / 2, juga memiliki register data hanya 1 byte (tidak mungkin memuat 2 byte sekaligus).
Selain itu, hampir semua kode SPI pada AVR yang saya temui berfungsi sesuai dengan skema ini:
- Unduh data SPDR
- menginterogasi bit SPIF dalam SPSR dalam satu lingkaran.
Seperti yang Anda lihat, pasokan data yang berkelanjutan, seperti yang dilakukan pada STM32, tidak berbau di sini. Tetapi, bahkan di sini Anda dapat mempercepat output dari kedua unpacker dengan ~ 3ms!
Dengan membuka lembar data dan melihat bagian "Instruction set clocks", Anda dapat menghitung biaya CPU saat mengirimkan byte melalui SPI:
- 1 siklus untuk memuat register dengan data baru;
- 2 denyut per bit (atau 16 denyut per byte);
- 1 bilah per baris sihir jam (sedikit kemudian tentang "NOP");
- 1 jam untuk memeriksa bit status di SPSR (atau 2 jam di cabang);
Secara total, untuk mengirimkan satu piksel (dua byte), 38 siklus clock atau ~ 425600 siklus clock untuk gambar uji (11.200 byte) harus dihabiskan.
Mengetahui bahwa F_CPU == 16 MHz kita mendapatkan
0,0000000625 62,5 nanosecond per clock cycle (
Process0169 ), dengan mengalikan nilainya, kita mendapatkan ~ 26 milidetik. Muncul pertanyaan: “Dari mana saya menulis sebelumnya bahwa waktu pembongkaran 39ms. dan 47ms. "? Semuanya sederhana - logika unpacker + penanganan interupsi.
Ini adalah contoh dari interupsi output:

dan tanpa gangguan:

Grafik menunjukkan bahwa waktu antara pengaturan jendela alamat di layar VRAM dan awal transfer data dalam versi tanpa gangguan lebih sedikit dan hampir tidak ada kesenjangan antara byte selama transmisi (grafik seragam).
Sayangnya, Anda tidak dapat menonaktifkan interupsi untuk setiap output gambar, jika tidak suara dan inti dari seluruh permainan akan pecah (lebih lanjut tentang itu nanti).
Itu ditulis di atas tentang "sihir NOP" tertentu untuk garis jam. Faktanya adalah bahwa untuk menstabilkan CLK dan mengatur flag SPIF, dibutuhkan siklus 1 jam dan saat flag ini dibaca, flag tersebut sudah diset, yang menghindari percabangan menjadi 2 bar pada instruksi BREQ.
Berikut ini adalah contoh tanpa NOP:

dan bersamanya:

Perbedaannya tampaknya tidak signifikan, hanya beberapa mikrodetik, tetapi jika Anda mengambil skala yang berbeda:
NOP besar:

dan dengan itu terlalu besar:

maka perbedaannya menjadi jauh lebih terlihat, mencapai ~ 4.3ms.
Sekarang mari kita lakukan trik kotor berikut:
Kami menukar urutan memuat dan membaca register dan Anda tidak bisa menunggu setiap byte kedua dari bendera SPIF, tetapi memeriksanya hanya sebelum memuat byte pertama dari piksel berikutnya.
Kami menerapkan pengetahuan dan menyebarkan fungsi "pushColorFast (repeatColor);":
#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("");
Meskipun ada gangguan dari timer, menggunakan trik di atas memberikan keuntungan hampir 6ms.:

Inilah cara pengetahuan besi yang sederhana memungkinkan Anda memeras sedikit lebih banyak darinya dan menghasilkan sesuatu yang serupa:

Tabrakan colosseum
Pertempuran kotak.
Untuk mulai dengan, seluruh rangkaian objek (kapal, kerang, asteroid, bonus) adalah struktur (sprite) dengan parameter berikut:
- koordinat X, Y saat ini;
- koordinat baru X, Y;
- arahkan ke gambar.
Karena gambar menyimpan lebar dan tinggi, tidak perlu menduplikasi parameter ini, apalagi, organisasi seperti itu menyederhanakan logika dalam banyak aspek.
Perhitungannya sendiri dibuat sederhana untuk dangkal - berdasarkan persimpangan dari segi empat. Meskipun tidak cukup akurat dan tidak menghitung konflik di masa depan, ini lebih dari cukup.
Verifikasi berlangsung secara bergantian pada sumbu X dan Y. Karena ini, tidak adanya persimpangan pada sumbu X mengurangi perhitungan tabrakan.
Pertama, sisi kanan dari persegi panjang pertama dengan sisi kiri dari persegi panjang kedua diperiksa untuk bagian umum dari sumbu X. Jika berhasil, pemeriksaan serupa dilakukan untuk sisi kiri dari sisi pertama dan sisi kanan dari persegi panjang kedua.
Setelah berhasil mendeteksi persimpangan di sepanjang sumbu X, pemeriksaan dilakukan dengan cara yang sama untuk sisi atas dan bawah dari persegi panjang di sepanjang sumbu Y.
Di atas terlihat jauh lebih mudah daripada yang terlihat:
bool checkSpriteCollision(sprite_t *pSprOne, sprite_t *pSprTwo) { auto tmpDataOne = getPicSize(pSprOne->pPic, 0); auto tmpDataTwo = getPicSize(pSprTwo->pPic, 0); 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;
Tetap menambahkan ini ke permainan:
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);
Kurva Bezier
Rel ruang.
Seperti dalam game lain dengan genre ini, kapal musuh harus bergerak di sepanjang kurva.
Diputuskan untuk menerapkan kurva kuadrat sebagai yang paling sederhana untuk controller dan tugas ini. Tiga poin sudah cukup untuk mereka: awal (P0), final (P2) dan imajiner (P1). Dua yang pertama menentukan awal dan akhir garis, titik terakhir menggambarkan jenis kelengkungan.
Artikel bagus tentang kurva.Karena ini adalah kurva parametrik Bezier, itu juga membutuhkan satu parameter lagi - jumlah titik antara antara titik awal dan titik akhir.
Jadi kita mendapatkan struktur ini:
typedef struct {
Di dalamnya, position_t adalah struktur dua byte koordinat X dan Y.
Menemukan titik untuk setiap koordinat dihitung menggunakan rumus ini (thx Wiki):
B = ((1,0 - t) ^ 2) P0 + 2t (1,0 - t) P1 + (t ^ 2) P2,
t [> = 0 && <= 1]
Untuk waktu yang lama, implementasinya diselesaikan secara langsung tanpa matematika titik tetap:
... 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; ...
Tentu saja, ini tidak bisa dibiarkan. Bagaimanapun, menyingkirkan float tidak hanya dapat memberikan peningkatan dalam kecepatan, tetapi juga membebaskan ROM, sehingga implementasi berikut ditemukan:
- avrfix;
- stdfix;
- libfixmath;
- fixedptc.
Yang pertama tetap menjadi kuda hitam, karena merupakan perpustakaan yang dikompilasi dan tidak ingin mengacaukan pembongkaran.
Kandidat kedua dari bundel GCC juga tidak berhasil, karena avr-gcc yang digunakan tidak ditambal dan tipe "short _Accum" tetap tidak tersedia.
Pilihan ketiga, meskipun memiliki sejumlah besar mat. fungsi, memiliki operasi bit kode keras pada bit tertentu di bawah format Q16.16, yang membuatnya tidak mungkin untuk mengontrol nilai-nilai Q dan I.
Yang terakhir dapat dianggap sebagai versi yang disederhanakan dari "fixedmath", tetapi keuntungan utama adalah kemampuan untuk mengontrol tidak hanya ukuran variabel, yang secara default adalah 32bit dengan format Q24.8, tetapi juga nilai Q dan I.
Hasil pengujian pada pengaturan yang berbeda:
Jenis | IQ | Bendera tambahan | Byte ROM | Tms. * |
---|
mengapung | - | - | 4236 | 35 |
fixedmath | 16.16 | - | 4796 | 119 |
fixedmath | 16.16 | FIXMATH_NO_OVERFLOW | 4664 | 89 |
fixedmath | 16.16 | FIXMATH_OPTIMIZE_8BIT | 5036 | 92 |
fixedmath | 16.16 | _NO_OVERFLOW + _8BIT | 4916 | 89 |
fixedptc | 24.8 | FIXEDPT_BITS 32 | 4420 | 64 |
fixedptc | 9.7 | FIXEDPT_BITS 16 | 3490 | 31 |
* Cek dilakukan pada pola: "195.175.145.110.170.170.170" dan kunci "-O".
Dapat dilihat dari tabel bahwa kedua perpustakaan mengambil lebih banyak ROM dan menunjukkan diri mereka lebih buruk daripada kode yang dikompilasi dari GCC saat menggunakan float.
Terlihat juga bahwa revisi kecil untuk format Q9.7 dan penurunan variabel ke 16bit memberikan akselerasi 4ms. dan membebaskan ROM pada ~ 50 byte.
Efek yang diharapkan adalah penurunan akurasi dan peningkatan jumlah kesalahan:

yang dalam hal ini tidak kritis.
Mengalokasikan sumber daya
Selasa dan Kamis hanya bekerja satu jam.
Dalam kebanyakan kasus, semua perhitungan dilakukan setiap frame, yang tidak selalu dibenarkan, karena mungkin tidak ada cukup waktu dalam frame untuk menghitung sesuatu dan Anda harus mengelabui dengan bergantian, menghitung frame atau melewatkannya. Jadi saya melangkah lebih jauh - benar-benar meninggalkan staf.
Setelah memecah semuanya menjadi tugas-tugas kecil, baik itu: menghitung tabrakan, memproses suara, tombol dan menampilkan grafik, itu sudah cukup untuk melakukan mereka pada interval tertentu, dan kelambanan mata dan kemampuan untuk memperbarui hanya bagian layar akan melakukan trik.Kami mengelola semua ini tidak hanya dengan OS, tetapi dengan mesin negara yang saya buat beberapa tahun yang lalu, atau, lebih tepatnya, bukan task manager SM yang kecil.Saya akan mengulangi alasan untuk menggunakannya daripada RTOS:- persyaratan ROM yang lebih rendah (~ 250 byte inti);
- persyaratan RAM yang lebih rendah (~ 9 byte per tugas);
- prinsip kerja yang sederhana dan mudah dipahami;
- determinisme perilaku;
- lebih sedikit waktu CPU yang terbuang;
- meninggalkan akses ke zat besi;
- platform independen;
- ditulis dalam C dan mudah dibungkus dalam C ++;
membutuhkan sepeda saya sendiri.
Seperti yang pernah saya jelaskan, tugas untuknya diatur ke dalam array pointer ke struktur, di mana pointer ke fungsi dan interval panggilannya disimpan. Pengelompokan ini menyederhanakan deskripsi gim dalam tahap-tahap terpisah, yang juga memungkinkan Anda untuk mengurangi jumlah cabang dan secara dinamis mengganti serangkaian tugas.Misalnya, selama layar mulai, 7 tugas dilakukan, dan selama permainan sudah ada 20 tugas (semua tugas dijelaskan dalam file gameTasks.c).Pertama, Anda perlu mendefinisikan beberapa makro untuk kenyamanan Anda: #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
Deklarasi tugas sebenarnya membuat struktur, menginisialisasi bidangnya dan menempatkannya di ROM: TASK(updateBtnStates, 25);
Setiap struktur tersebut menempati 4 byte ROM (dua per pointer dan dua per interval).Bonus bagus untuk makro adalah tidak berfungsi untuk membuat lebih dari satu struktur unik untuk setiap fungsi.Setelah mendeklarasikan tugas yang diperlukan, kami menambahkannya ke array dan juga meletakkannya di 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 };
Saat mengatur flag USE_DYNAMIC_MEM ke 0 untuk memori statis, hal utama yang perlu diingat adalah menginisialisasi pointer ke task store di RAM dan mengatur jumlah maksimumnya yang akan dieksekusi: ... tasksContainer_t tasksContainer; taskFunc_t tasksArr[MAX_GAME_TASKS]; ... initTasksArr(&tasksContainer, &tasksArr[0], MAX_GAME_TASKS); …
Mengatur tugas untuk dieksekusi: ... addTasksArray_P(gameTasksArr); …
Perlindungan overflow dikendalikan oleh flag USE_MEM_PANIC, jika Anda yakin tentang jumlah tugas, Anda dapat menonaktifkannya untuk menyimpan ROM.Tinggal menjalankan handler saja: ... runTasks(); ...
Di dalam adalah loop tak terbatas yang berisi logika dasar. Setelah masuk, tumpukan juga dikembalikan berkat "__attribute__ ((noreturn))".Dalam loop, elemen-elemen array dipindai secara bergantian untuk keperluan memanggil tugas setelah interval berakhir.Penghitungan interval dibuat berdasarkan timer0 sebagai sistem dengan kuantum 1ms ...Meskipun distribusi tugas berhasil dalam waktu, mereka terkadang tumpang tindih (jitter), yang menyebabkan pemudaran jangka pendek dari segala sesuatu dan segala sesuatu dalam permainan.Pasti harus diputuskan, tetapi bagaimana? Tentang bagaimana semuanya diprofilkan di waktu berikutnya, tetapi untuk sekarang coba cari telur Paskah di sumbernya.Akhirnya
Jadi, menggunakan banyak trik (dan banyak lagi yang belum saya jelaskan), semuanya ternyata sesuai dengan ROM 24kb dan 1500 byte RAM. Jika Anda memiliki pertanyaan, saya akan dengan senang hati menjawabnya.Bagi mereka yang tidak menemukan atau tidak mencari telur Paskah:gali ke samping: void invadersMagicRespawn(void) { for(auto &alien : aliens) { if(!alien.alive) { alien.respawnTime = 1; } } }
Tidak ada yang luar biasa, bukan?Raaaaazvorachivaem macro 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.
Untuk membangun dan menjalankan, cukup letakkan folder (atau tautan) "esploraAPI" di "/ arduino / libraries /".Referensi:
NB. Anda dapat melihat dan mendengar bagaimana tampilannya nanti setelah saya membuat video yang dapat diterima.