Dacă aveți nelămuriri, puteți să ne contactați pe forumul dedicat temei 2. Nu se acceptă întrebări în ultimele 24 de ore înainte de deadline.
Minecraft e un joc video open-world în care jucătorii pot ridica construcții într-o lume formată din blocuri (cuburi) fixate într-o grilă tridimensională.
Fiindcă Minecraft este din nou în vogă, un arhitect și-a propus să construiască în joc clădirile pe care le proiectează și vă solicită ajutorul: are nevoie de un program care generează automat structuri arhitectonice grandioase.
Numim chunk o matrice tridimensională de blocuri (reprezentate de tipul char
), fiecare bloc având trei coordonate:
width-1
)height-1
)depth-1
)Un exemplu de chunk este ilustrat în imagine:
Un chunk este reprezentat printr-un vector cu width
elemente. Fiecare element chunk[x]
reprezintă un plan paralel cu planul $yOz$, și este pointer la un vector cu height
elemente. Mai departe, chunk[x][y]
reprezintă un rând – din planul chunk[x]
– paralel cu axa $Oz$, și este pointer la un vector cu depth
elemente. În sfârșit, chunk[x][y][z]
descrie blocul aflat la coordonatele $(x, y, z)$, conform următoarei encodări pentru care avem constante simbolice:
0
= aer (BLOCK_AIR
)1
= iarbă (BLOCK_GRASS
)2
= lemn (BLOCK_WOOD
)3
= piatră (BLOCK_STONE
)
Toate funcțiile din fișierele chunk_gen.c
, chunk_transform.c
și chunk_process.c
primesc ca parametru un chunk care se presupune că a fost deja alocat (dinamic) și returnează chunkul modificat. Dacă aplicați procesările direct pe chunkul primit ca parametru, este suficient să îl returnați tot pe el. Însă dacă alocați un nou chunk și aplicați procesările pe chunkul nou creat, chunkul original primit ca parametru trebuie dezalocat în aceeași funcție!
Atenție: pentru funcțiile care necesită schimbarea dimensiunii chunkului (e.g. rotate
), este obligatoriu ca la finalul funcției să obținem un chunk cu exact atâta memorie alocată cât este necesar.
Pentru această temă trebuie să porniți de la scheletul de cod de aici: tema2-schelet-v7.zip.
Scheletul include instrumente utile pentru a compila și valida tema. În fișierul USAGE.md
regăsiți instrucțiunile de utilizare a instrumentelor.
Pentru a rezolva tema, trebuie să completați funcțiile definite în fișierele din directorul libchunk
.
Pentru Partea A, veți implementa funcțiile definite în fișierul libchunk/chunk_gen.c
.
Implementați funcția chunk_place_block
care primește ca parametri un chunk de dimensiuni width
, height
, și depth
, trei coordonate și un tip de bloc.
char*** chunk_place_block( char*** chunk, int width, int height, int depth, int x, int y, int z, char block);
Funcția întoarce chunkul obținut prin amplasarea blocului precizat la coordonatele primite ca parametru.
Atenție! Coordonatele pot depăși chunkul. Spre exemplu, coordonata x
poate fi în afara intervalului [0, width-1]
. Blocul se introduce numai dacă x
, y
și z
se încadrează în limitele chunkului, altfel chunkul rămâne neschimbat.
Implementați funcția chunk_fill_cuboid
:
char*** chunk_fill_cuboid( char*** chunk, int width, int height, int depth, int x0, int y0, int z0, int x1, int y1, int z1, char block);
Funcția primește coordonatele a două puncte – $(x_0, y_0, z_0)$ și $(x_1, y_1, z_1)$ – ce reprezintă colțuri opuse ale unui paralelipiped dreptunghic și un tip de bloc, și întoarce chunkul obținut prin umplerea paralelipipedului cu blocuri de tipul precizat.
Atenție! Și această funcție poate primi coordonate ce depășesc limitele chunkului. Numai partea din paralelipiped care se încadrează în dimensiunile chunkului va fi umplută.
Implementați funcția chunk_fill_sphere
care introduce un bloc de un anumit tip la toate pozițiile aflate strict în interiorul sferei de centru și rază primite ca parametru. Funcția trebuie să întoarcă noul chunk.
char*** chunk_fill_sphere( char*** chunk, int width, int height, int depth, int x, int y, int z, double radius, char block);
Spre exemplu, dacă sfera are centru $(1, 1, 2)$ și rază $1.2$, atunci blocul de la $(1, 1, 3)$ va fi modificat de funcție, având distanța $1$ de centrul sferei. Însă blocul de la $(0, 0, 2)$ va rămâne intact, fiindcă stă la distanța $\sqrt{2} > 1.2$ de centrul sferei.
Atenție! La fel ca funcțiile anterioare, această funcție introduce blocuri numai la pozițiile aflate în limitele chunkului. Sfera descrisă de parametri nu se va încadra întotdeauna în dimensiunile chunkului.
Pentru a rezolva Partea B veți completa funcțiile din fișierul libchunk/chunk_process.c
.
Trebuie implementată funcția chunk_shell
:
char*** chunk_shell( char*** chunk, int width, int height, int depth, char target_block, char shell_block);
În jurul fiecărui bloc de tipul target_block
, funcția construiește un înveliș de tipul shell_block
. Blocurile de tip target_block
nu se înlocuiesc cu shell_block
, însă celelalte blocuri se pot înlocui, dacă este cazul.
Învelișul unui bloc e format din pozițiile vecine directe — spre exemplu, $(x+1, y, z)$ sau $(x, y-1, z)$ — și pozițiile vecine de “muchie”, precum $(x+1, y, z+1)$ sau $(x, y-1, z-1)$, dar nu și pozițiile vecine de “colț”, precum $(x+1, y+1, z+1)$.
Atenție! La fel ca funcțiile anterioare, această funcție introduce blocuri numai la pozițiile aflate în interiorul chunkului.
Veți implementa funcția chunk_fill_xz
:
char*** chunk_fill_xz( char*** chunk, int width, int height, int depth, int x, int y, int z, char block);
Dacă la poziția $(x, y, z)$ se află un bloc de tip $B$, atunci funcția amplasează block
pe toate pozițiile din planul paralel cu $xOz$ la care se poate ajunge prin deplasare cu câte un bloc, paralel cu axele $Ox$ și $Oz$, începând de la poziția $(x, y, z)$ și trecând numai prin blocuri de tip $B$.
Trebuie implementată funcția chunk_fill
având același prototip ca chunk_fill_xz
:
char*** chunk_fill( char*** chunk, int width, int height, int depth, int x, int y, int z, char block);
Funcția amplasează block
pe pozițiile din spațiu la care se poate ajunge prin deplasare cu câte un bloc pe orice axă, pornind de la $(x, y, z)$, trecând numai prin blocuri identice cu cel de pe poziția $(x, y, z)$.
Pentru Partea C, veți lucra în fișierul libchunk/chunk_transform.c
.
Veți implementa funcția chunk_rotate_y
, care primește un chunk și întoarce chunkul rotit la 90 de grade în jurul axei $y$ (sau în planul $xOz$). Privită de sus, rotația se realizează în sens trigonometric.
char*** chunk_rotate_y( char*** chunk, int width, int height, int depth);
Atenție! Rotirea poate produce un chunk cu dimensiuni diferite de cel original. Dimensiunile chunkului returnat de funcție sunt width_nou = depth
, height_nou = height
și depth_nou = width
.
Pentru acest task, aveți de implementat funcția chunk_apply_gravity
:
char*** chunk_apply_gravity( char*** chunk, int width, int height, int depth, int* new_height);
Numim un corp o mulțime de blocuri de același fel (dar diferite de aer), conectate, în spațiu. Cu alte cuvinte, dacă aplicăm funcția chunk_fill
cu coordonatele unui bloc (diferit de aer), se vor umple exact blocurile corespunzătoare corpului din care face parte blocul.
Funcția chunk_apply_gravity
are ca efect coborârea tuturor corpurilor prezente în chunk, până când niciun corp nu se mai poate deplasa în jos — fie pentru că atinge baza chunkului ($y=0$), fie pentru că alt corp se află imediat dedesubt.
După această transformare, pot rămâne spații goale (aer) de forma unor plane paralele cu $xOz$ în partea de sus a chunkului. Aceste spații goale vor fi eliminate. Spre exemplu, dacă cele două plane paralele cu $xOz$ situate cel mai sus pe axa $y$ sunt alcătuite doar din blocuri de aer, chunkul întors nu le va include, și va avea new_height = height - 2
care trebuie transmis apelantului prin parametrul de ieșire new_height
.
În exemplul de mai jos există șapte corpuri (patru cuburi, două inele și un baston). După aplicarea gravitației, cele 7 plane superioare paralele cu $xOz$ se vor elimina fiindcă rămân neocupate cu corpuri.
Pentru Partea D veți completa cele două funcții din fișierul libchunk/chunk_compress.c
.
În reprezentarea cu matrici tridimensionale, chunkurile largi ocupă cantități mari de memorie. Un chunk de dimensiuni $20,20,20$ necesită memorarea tipului de bloc pentru 8000 de blocuri.
Putem reduce consumul de memorie observând că blocurile alăturate sunt adesea identice. În loc să stocăm același bloc de multe ori, vom păstra tipul de bloc și numărul de apariții consecutive, conform schemei de lossless compression numite run-length encoding (RLE).
Pentru început, vom aplatiza (flatten) matricea tridimensională numerotând blocurile de la 0 la width*height*depth - 1
în funcție de poziția fiecăruia, în ordinea $y,z,x$. Un exemplu este ilustrat mai jos (am separat straturile pentru claritate).
Parcurgând blocurile în ordinea precizată numărăm aparițiile consecutive (runs) ale aceluiași bloc. În exemplul anterior, există 5 runs: $(\textrm{iarba}, 4)$, $(\textrm{piatra}, 5)$, $(\textrm{aer}, 8)$, $(\textrm{iarba}, 1)$, $(\textrm{aer}, 9)$.
Dorim să memorăm într-un vector de unsigned char
(fiecare unsigned char
are 8 biți) informațiile despre aceste runs, una după alta, pentru a putea ulterior să reconstruim chunkul.
Dacă $\textrm{bloc}$ e unul dintre cele patru blocuri posibile, atunci îi corespunde un număr de la 0 la 3, deci poate fi reprezentat de doi biți $b_1b_0$ (cifrele numărului corespunzător în baza doi). De asemenea, numărul de apariții $n$ dintr-un run, în baza 2, poate fi reprezentat de $q$ biți. Așadar memorăm fiecare run de forma $(\textrm{bloc}, n)$, individual, în următorul format, unde bb
sunt biții ce specifică blocul iar n...nn
biții ce specifică numărul de apariții:
Valoare $n$ | Primul unsigned char | Al doilea unsigned char |
---|---|---|
$1 \le n \lt 32 \implies q=5$ | bb0nnnnn | nu există |
$32 \le n \lt 4096 \implies q=12$ | bb10nnnn | nnnnnnnn |
În cazul în care $32 \le n \lt 4096$, cei mai semnificativi 4 biți din $n$ sunt stocați în primul unsigned char
, iar ceilalți biți în al doilea. Dacă un run are $n \ge 4096$, se împarte într-un prim run cu $n_1 = 4095$ și un al doilea run cu $n_2 = n - 4095$, iar procesul se repetă.
Spre exemplu, dacă un chunk este descris de trei runs, $(\textrm{piatra}, 6)$, $(\textrm{lemn}, 501)$ și $(\textrm{iarba}, 5)$, îl reprezentăm prin vectorul de patru unsigned char
:
Un chunk descris de un singur run $(\textrm{lemn}, 8192)$ îl reprezentăm prin vectorul de cinci unsigned char
:
Pentru acest task trebuie să implementați funcția chunk_encode
:
unsigned char* chunk_encode( char*** chunk, int width, int height, int depth, int* length);
Funcția primește un chunk și întoarce un vector de unsigned char
ce reprezintă un șir de runs conform descrierii de mai sus. Prin length
furnizează lungimea vectorului (numărul de unsigned char
s).
Odată encodate cu RLE, chunkurile ocupă mai puțină memorie, însă dacă vrem să le vizualizăm sau să aplicăm operații asupra lor, trebuie convertite înapoi în forma matricială.
Funcția de implementat, chunk_decode
, primește printr-un un vector de unsigned char
un chunk encodat conform schemei de la Task 9 și dimensiunile chunkului. Funcția întoarce chunkul original sub forma unei matrici tridimensionale alocate dinamic, în formatul din introducerea temei.
char*** chunk_decode( unsigned char* code, int width, int height, int depth);
Pentru acest task, trebuie să aveți punctajul maxim pe taskurile 7, 8, 9 și 10 și să nu aveți scurgeri de memorie la verificarea lor folosind valgrind.
Se va rula valgrind folosind următoarea comandă:
valgrind --tool=memcheck --leak-check=full --error-exitcode=1 <program>
Pentru depanarea problemelor de memorie puteți apela în src/main.c
funcțiile implementate și executa valgrind cu comanda de mai sus asupra programului build/main
.
Există o depunctare de până la -20p pentru coding style inadecvat. Checkerul verifică automat coding style-ul.
Checkerul local se regăsește în scheletul temei. Consultați fișierul USAGE.md
pentru instrucțiunile de utilizare.
Tema va fi trimisă prin Moodle, cursul Programarea Calculatoarelor (CB & CD), activitatea Tema 2.
Toate temele sunt testate în mod automat pe Moodle.
Arhiva temei se va încărca prin formularul de submisie (butonul Add submission).
Rezultatele vor fi disponibile în secțiunea Feedback – nota apare la linia Grade, iar outputul checkerului și erorile apar la sectiunea Feedback comments. Dacă apare un buton albastru în formă de plus, trebuie să dați click pe el pentru a afișa întregul output al checkerului.
Citiți cu atenție informațiile afișate în Feedback pentru a vă asigura că tema a fost rulată cu succes. Asigurați-vă că conținutul arhivei respectă structura dorită (ex. fișierele sunt în directorul corect).
Conținutul arhivei trebuie să fie următorul:
libchunk/
Makefile
Nu includeți fișierele checkerului în arhivă.
Lista nu este exhaustivă.
Minecraft is a trademark of Mojang Studios. This page contains copyrighted materials owned by Mojang Studios. All rights reserved by Mojang Studios. These materials are used under fair use for educational purposes. This assignment is not affiliated with, endorsed by, or associated with Mojang Studios or Microsoft.