Proiectul meu consta in crearea unei platforme hardware si scrierea unei clone minimaliste a jocului DOOM din 1993 care sa ruleze pe aceasta.
DOOM este un FPS cu o grafica pseudo 3D (level-ul este 3D iar personajele, viewmodel-urile si efectele sunt sprite-uri 2D). Controlurile jocului sunt simple:
Se observa ca nu exista rotatie sus/jos deoarece din punct de vedere logic toate entitatile jocului sunt plasate pe un plan 2D. Engine-ul nu permite existenta etajelor (cel putin 2 camere sau platforme una deasupra celeilalte) dar permite zone cu inaltime diferita (de ex. scari, platforme elevate).
Pentru ca pe acea vreme lumea folosea DOS nu exista suport pentru mouse pentru rotatie, insa pentru ca pot face ce vreau cu hardware-ul voi include un control rotativ (aka encoder incremental) pentru a emula mouse-ul.
Am optat pentru un display OLED deoarece acestea au un timp de raspuns foarte mic, de ordinul microsecundelor, fata de un LCD care are un timp de raspuns de ordinul a zeci-sute de milisecunde. Acest lucru este foarte important pentru o aplicatie real time.
Schema pentru legarea display-ului si a encoder-ului incremental (PORTC):
<WRAP left 100% >
</WRAP>
Schema pentru legarea speaker-ului (PORTD):
<WRAP left 100% >
</WRAP>
Schema pentru legarea butoanelor de control (PORTA):
<WRAP left 100% >
</WRAP>
Proiectul promite sa includa urmatoarele:
Din pacate timpul nu mi-a permis sa fac tot ce imi doream la acest proiect. Am reusit sa termin partea de de control (miscarea si rotirea camerei FPS) si partial partea de desenare (mai exact implementarea unei proiectii ortografice din motive de simplitate).
oled.h
si oled.c
input.h
si input.c
game.h
si game.c
fastmath.h
si fastmath.c
main.c
OLED-ul foloseste 2 pini pentru date SPI (D0 ca SCLK si D1 ca SDIN), un pin de reset si un pin DC care selecteaza tipul de input (commanda sau data). Driver-ul citeste cate 8 biti pe SDIN si daca DC e low se vor interpreta ca o comanda sau parametrul unei comenzi iar daca DC e high cei 8 biti se vor scrie la adresa curenta in memoria GDDRAM. Spatiul de desenare este impartit in 128 de coloane si 7 pagini (randuri) a cate 8 biti inaltime fiecare. Fiecare bit controleaza valoarea unui pixel de pe ecran.
In fisierele oled.h
si oled.c
se afla functiile de control si de setup ale OLED-ului care initializeaza parametrii cum ar fi contrastul, modul de avansare la scriere, row si column remapping (aka flip vertical si orizontal) si frecventa oscilatorului intern. Functia oled_send_buffer(const uint8_t buffer[OLED_BUFFER_SIZE])
primeste un buffer ce va fi scris in GDDRAM-ul display-ului pentru a inlocui imaginea afisata. Aceasta va fi folosita la etapa de desenare a jocului.
Jocul este controlat prin 8 butoane ce ocupta intreg port-ul A si un encoder incremental care e conectat la pinii PC0 si PC1. Encoder-ul functioneaza ca o pereche de switch-uri care cicleaza prin secventa LL → LH → HH → HL → LL
la rotire in sensul acelor de ceas si prin secventa inversa la rotire in sensul opus acelor de ceas.
Pentru butoane este suficienta citirea starii (PINA) la inceputul fiecarui cadru fara a implementa debouncing sau intreruperi. Insa encoderul genereaza 24 de tranzitii per rotatie asa ca este posibil ca microcontroller-ul sa scape cateva tranzitii in timpul randarii cadrelor. Solutia este instalarea unui ISR pentru pinii PC0 si PC1. Starea encoder-ului si numarul de tranzitii parcurse de la cadrul anterior se vor salva in structura internal_input_state
iar main-ul va citi la inceputul fiecarui cadru starea acesteia prin functia input_get
.
Deoarece orice joc cu perspectiva 3D necesita folosirea extensiva a functiilor trigonometrice am implementat niste versiuni de sin
si cos
rapide si cat de cat precise ce folosesc numai numere intregi pe 16 biti. Inputul lor reprezinta un numar intre -512 si 512 unde -512 reprezinta -π si 512 reprezinta π si returneaza un numar intre -512 si 512 ce reprezinta o valoare intre -1 si 1. Aproximarile folosesc serii taylor cu 2 termeni centrate in 0 pentru sin
si π / 2 (adica 256) pentru cos
. Functiile sunt optimizate pentru folosirea numai a operatiilor cu nr. intregi.
Mai jos este un grafic cu 3 variante ale functiei mele sin
Se observa ca inmultirea cu π a fost inlocuita cu 50 * x / 16 (50 / 16 = 3.125). Toate impartirile la puteri ale lui 2 vor fi inlocuite cu shiftari in cod.
Pentru ca seriile taylor isi pierd precizia foarte repede am wrap-at valorile mai mici ca -256 si mai mari ca 256 pentru sin
si cele negative pentru cos
. De exemplu sin(257)
se va calcula ca sin(255)
.
inline int16_t sin_fast(int16_t x) { int16_t temp; // wrap input if (x > 256) x = 512 - x; else if (x < -256) x = -512 - x; temp = (50 * x) >> 10; return ((50 * x) >> 4) - temp * temp * temp / 6; } inline int16_t cos_fast(int16_t x) { int16_t temp; // wrap input if (x < 0) x = -x; // offset by pi / 2 x -= 256; temp = (50 * x) >> 10; return -((50 * x) >> 4) + temp * temp * temp / 6; }
Acum avem toate cele necesare pentru a implementa o proiectie ortografica. Pentru a desena un sprite trebuie ca punctul sau central sa fie adus in screen space, dar mai intai trebuie sa treaca prin view space. View space-ul este spatiul in care camera (jucatorul) se afla la origine uitandu-se spre -Z cu axa +X la dreapta si +Y in sus (in cazul nostru nu exista axa Y). Acest lucru se face folosind o structura de transformare care deriva din pozitia si orientarea jucatorului:
// game.h typedef struct { int16_t x, z; } point_t; typedef struct { point_t pos; } entity_t; typedef struct { point_t pos; int16_t angle; } player_t; typedef struct { point_t translation; int16_t sin_ang, cos_ang; int16_t scale; } transformation_t; typedef struct { player_t player; entity_t entity; } game_t; // main.c game_t game; transformation_t transf; transf.translation.x = -game.player.pos.x; transf.translation.z = -game.player.pos.z; transf.sin_ang = sin_fast(-game.player.angle); transf.cos_ang = cos_fast(-game.player.angle); transf.scale = 1; // not yet implemented for (...) { point_t p = game_entity_transform(entity.pos, &transf); if (p.x + entity.sprite.width / scale / 2 > -64 && p.x - entity.sprite.width / scale / 2 < 64 && p.z > 0) draw_sprite_centered_at(&entity.sprite, p, scale); } // game.c point_t game_entity_transform(point_t p, const transformation_t *t) { point_t q; p.x += t->translation.x; p.z += t->translation.z; q.x = (p.x * t->cos_ang - p.z * t->sin_ang) / 511; q.z = (p.x * t->sin_ang + p.z * t->cos_ang) / 511; // not yet implemented q.x /= t->scale; q.z /= t->scale; return q; }
OLED-ul merge extraordinar de bine, a ajuns cam la 100 de cadre/secunda cu un program simplu (desena o linie verticala care se misca pe orizontala, parcurgea 128 de coloane in 1.5 secunde).
Encoder-ul incremental merge foarte bine, ma asteptam ca 24 de tranzitii/rotatie sa fie cam putin dar sunt arhisuficiente.
Aproximarile mele ale functiilor sin si cos nu sunt de laudat, se comporta foarte urat cand cand returneaza valori aproape de -512 si 512. Din pacate arhitectura nu permite precizie foarte buna.
Proiectul a fost fun si am avut multe de invatat din el. Limitarile acestui micrcontroller cum ar fi lipsa de suport hardware pentru intregi pe 32 de biti, lipsa de suport hardware pentru numere in virgula modila si memoria redusa nu il fac un candidat bun pentru a rula un joc cu perspectiva 3D, insa cu putin efort se poate obtine ceva functional/
22 Apr 2016 - Descriere simpla si schema bloc
06 Mai 2016 - Scheme electrice
22 Mai 2016 - Program pentru desenare pe OLED complet
26 Mai 2016 - Hardware-ul (fara speaker) finalizat
27 Mai 2016 - Input handling, fundatia pentru codul jocului si prezentare la PM Fair