Alexandru ŞORODOC - DOOM

Autorul poate fi contactat la adresa: Login pentru adresa

Introducere

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:

  • Miscare inainte/inapoi/stanga/dreapta
  • Rotatie stanga/dreapta
  • Trage
  • Schimba arma

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.

Descriere generala

Hardware Design

Componente

  • PCB 2016
  • OLED monocrom 0.96” 128×64 pe SPI
  • Speaker
  • 8 x Butoane control + 1 x enconder incremental

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.

Scheme

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>

Software Design

Setul minim de caracteristici

Proiectul promite sa includa urmatoarele:

  • <fc #0000FF>• Desenarea de sprite-uri scalate la distanta</fc>
  • <fc #00FF00>✔ Schema de control (la fel ca cea din introducere)</fc>
  • <fc #00FF00>✔ Control rotativ</fc>
  • <fc #FF0000>✘ O arma functionala (pistol)</fc>
  • <fc #FF0000>✘ Efecte sonore</fc>

Caracteristici planificate

  • <fc #FF0000>✘ Nivele simple 3D simple randate</fc>
  • <fc #00FF00>✔ Silky smooth 30fps for a truly cinematic experience</fc>
  • <fc #FF0000>✘ Mixer audio in software</fc>
  • <fc #FF0000>✘ Nivele multiple</fc>
  • <fc #FF0000>✘ Arme multiple</fc>
  • <fc #FF0000>✘ Efecte vizuale (sange, explozii)</fc>

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).

Software folosit

Descriere Cod

Bucla joc

  1. Citeste input
  2. Updateaza player, scena si alte entitati
  3. Transpune fiecare entitate in view space
  4. Randare sprite-uri intr-un buffer local
  5. Trimite buffer la OLED

Organizare cod

  • Functii de initializare si desenare pentru oled: oled.h si oled.c
  • Functii de citit starea input-ului: input.h si input.c
  • Functii si structuri care tin de joc si mecanica: game.h si game.c
  • Functii matematice: fastmath.h si fastmath.c
  • Main loop: main.c

Programare OLED

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.

Citire input

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.

Functii matematice

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

  • <fc #0000FF>varianta matematica</fc>
  • <fc #FF0000>seria taylor cu 2 termeni</fc>
  • <fc #008000>seria taylor doar cu operatii intregi</fc>

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;
}

Randare

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;
}

Rezultate Obtinute

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.

Concluzii

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/

Poze placa

Download

Jurnal

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

Bibliografie/Resurse

pm/prj2016/ddragomir/alexandrusorodoc.txt · Last modified: 2021/04/14 17:07 (external edit)
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0