Evo nakon nekog vremena vraćamo se u ovaj zapušteni topic... Jeste li spremni da krenemo u osnove DirectX programiranja?
No, kao prvo moramo postaviti neki starting point da ne krenemo skroz ispočetka. Na kraju krajeva, trebali biste imati neko znanje C++a (pokazivači, polja, OOP, strukture). Tijekom vašeg "obrazovanja" ne očekuje se da sve prototipe funkcija učite napamet niti išta drugo po tom pitanju, to bi bilo suludo. Bilo bi fino ako uhvatite čemu što služi, a za ostalo imate ovaj tutorial, dokumentaciju DirectXa te Google naravno ( i nas ovdje). Kao što znate, bez alata, nema ni zanata. A alate koje ćemo koristiti su: Visual C++ (Express Edition) sa DirectX SDK... I to je to. Spremni ste za lansiranje!
---------------------------------------------------------------------------
Osnove Win32 API-ja
Svi znamo što je DirectX iz prošle lekcije, također logično je zaključiti da se vrti na Windowsu (i kako Microsoft implicira, samo na njemu). Windows je zasnovan na konceptu prozora(windowsa) koji se grade uz pomoć Win32 API-ja. Vjerovali ili ne, DirectX pogonjene aplikacije su također prozori (no shit, Einstein!) pa nam nema druge, nego da krenemo od osnovama Win32 API-ja. Evo jedan primjer console aplikacije sa kojima smo se tako dugo družili:
Code:
#include <iostream> // uobicajeni input/output header file
int main() // program pocinje main funkcijom
{
cout << "Pozz" << endl; // ispisi "Pozz" na zaslon
return 0; // program/funkcija izvrsena uspješno
}
Vjerojatno znate već da je main() funkcija ulazna točka u svaki program. Ovaj program jednostavno ispisuje dosadnom bijelom bojom "Pozz" na crnu pozadinu Command Prompta. Ništa posebno, zabavno i teško da će ikoga sa mozgom impresionirati (možda neku pijanu plavušu). Ovo gore što vidite je primjer najednostavnijeg programa u C++u. Windows aplikacija, na drugu stranu, je različita po tome što ima dvije funkcije koje joj omogućuju rad sa Windowsom.
Jedna od tih je podjednaka već navedenoj main() funkciji. Druga omogućuje aplikaciji da bude pokretana događajima (iliti event-driven). Ti eventovi, ako ne znate, su interakcija korisnika i aplikacije poput klikanja štakorom, pritisak na tipku i tako dalje. Kada se takav neki event dogodi, Windows ga naravno dokumentira u poruku koju odašilje vašem programu da je procesira i interpretira. Dakle, drugo-spomenuta funkcija rukuje tim porukama i sadrži kôd koji treba pokrenuti pri zaprimanju određene poruke. Mi ćemo sada baciti se na pisanje na našeg prvog prozora i kao prvu funkciju definirati ćemo... WinMain()!
WinMain() funkcija je ekvivalent main funkciji u konzolnim programima. To je mjesto gdje aplikacija započinje i gdje se radi početna inicijalizacija. Inače bih definirao posebnu funkciju gdje bih napravio prozor no za potrebe ovog tutoriala, to ću napraviti izravno u WinMain() funkciji da dodatno ne kompliciramo stvari. Prvo ćemo proći kroz prototip WinMain() funkcije:
Code:
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow);
Ovako ga sveta dokumentacija deklarira... Idemo malo secirati ga na manje dijelove da skužite ovaj naizgled strašni komad kôda. U duši je to obrijani pekinezer ; )
WINAPI
Koji li je sad ovo vrag, pitate se? Da, zalegao je baš između povratnog tipa funkcije i imena WinMain. Radi se o metodi dodjeljivanja parametara, kraće rečeno, redoslijedu čitanja parametara. Normalno to ide od desno prema lijevo, ali WINAPI ga obrne pa ide lijevo-nadesno. Zašto je to tako, pa to baš i nije najbitnija stvar na svijetu ali Windows želi tako da bude. So it shall be then!
HINSTANCE hInstance
Ovo je prvi parametar i stoji za "handle to an instance". Handle je 32-bitni integer koji indentificira nešto, poput objekta. Instanca je kopija aplikacije. Zbog multitaskinga i mogućnosti da se program vrti u više kopija istodobno, Windows treba način da ih indentificira i "žiki" ko' je ko'. To radi tako da dodijeli svakoj instanci "ručicu iliti handle", tj. integer koji omogućuje razlikovanje kopije aplikacija.
HINSTANCE hPrevInstance
Drugi parametar je sličan prvom, stoji za "handle to the previous instance". Što ovo znači? Npr. ako ima više kopija otvoreno, hPrevInstance će sadržavati ručicu ili handle zadnje instance aplikacije koja je napravljena. U biti, ovo je sve što trebate znati o njoj...
LPSTR lpCmdLine
Možda ste zamijetili po nazivu, ovo je dugački pokazivač iliti Long Pointer. On pokazuje na string koji sadržava zapovjednu liniju koja poziva aplikaciju. Ne kužite? Ni ja isto (joke). Evo primjer: ako ste imali aplikaciju nazvanu "MačkeiKerovi.exe" i pokrenuli je sa Run "programom" iz Start Menua, mogli biste dodati na bazu imena "MačkeiKerovi.app devMode" ili "MačkeiKerovi.app cheatMode". U svakom slučaju, sada bi program tražio za dodatne parametre. Shvaćate sada?
int nCmdShow
Ovaj parametar pokazuje kako će se prozor prikazati kada se stvori. Npr, ovo bi moglo pozvati prozor da bude minimiziran, maximiziran i drugi -miziran... Ovo vam neće biti potrebno, ali tu su ako vam zatrebaju.
-------------------------------------------------------------------
V-v-v-v-vrijeme je za d-d-d-d-dvoboj... Dosta teorije...
Dosta smo zjakali o prototipima funkcije WinMain(). 'Ajmo nešto napraviti. Nešto novo, nešto divlje... Predlažem da napravimo napokon taj naš prozor. Može? Naravno da može :)
Ovo dolje je naša aplikacija, kao što vidite, više nismo na običnim console aplikacijama.
Code:
// osnovni header fileove koji su nam potrebni
#include <windows.h>
#include <windowsx.h>
//prototip WindowProc funkcije (sjećate se, ona koja rukuje sa porukama)
LRESULT CALLBACK WindowProc(HWND hWnd,
UINT message,
WPARAM wParam,
LPARAM lParam);
// ulazna točka u svaku windows aplikaciju
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
// "ručica za prozor", ispuni se funkcijom
HWND hWnd;
// struktura prozora
WNDCLASSEX wc;
// očisti strukturu prozora
ZeroMemory(&wc, sizeof(WNDCLASSEX));
// ajmo popuniti članove ove strukture
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = (WNDPROC)WindowProc;
wc.hInstance = hInstance;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
wc.lpszClassName = L"WindowClass1";
// registriraj klasu koristeći informacije date wc-u
RegisterClassEx(&wc);
// napravi prozor i koristi rezultat kao ručicu (hWnd)
hWnd = CreateWindowEx(NULL,
L"WindowClass1", // Ime windows klase
L"PCPlay Ucionica", // Naziv prozora
WS_OVERLAPPEDWINDOW, // stil prozora
300, // x-pozicija prozora
300, // y-pozicija prozora
500, // širina prozora
400, // visina prozora
NULL, // nema roditeljskog prozora
NULL, // ne koristimo izbornik
hInstance, // ručica aplikacije (remember)
NULL); // ne koristi se sa više prozora
// napokon ju prikaži:
ShowWindow(hWnd, nCmdShow);
// ulaz u glavni loop
// ova struktura sadrži predivne poruke koje nam Windows šalje
MSG msg;
// dakle, čekamo dok dobijemo poruku od Windowsa
while(GetMessage(&msg, NULL, 0, 0))
{
// prevedi u programu razumljiv format
TranslateMessage(&msg);
// pošalji WindowProc() funkciji da ju procesira
DispatchMessage(&msg);
}
// vrati ovaj dio WM_QUITa Windowsu
return msg.wParam;
}
// ova funkcija rukuje sa svim porukama koje naša aplikacija prima
// dakle, poštar :)
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
// traži određenu akciju koja treba biti provedena pri dobivanju poruke
switch(message)
{
// ovo se pročita kada korisnik pritisne crveni X
case WM_DESTROY:
{
// zatvori aplikaciju
PostQuitMessage(0);
return 0;
} break;
}
// rukuj sa svakom porukom koju WindowProc nije procesirao osobno
return DefWindowProc (hWnd, message, wParam, lParam);
}
Sada kada bi ovo pokrenuli, dobili bi fini prozorček. Iscrtan vama pod nosom:
Uf, koliko toga ima... Komentari su na mjesto ali definitivno niste sve pohvatali... Idemo, top to bottom:
Osnovno što kod svake windows aplikacije je potrebno napraviti jest registrirati njenu klasu, napraviti prozor i prikazati ga naravno na zaslonu korisnika. Najjednostavnije postavljeno, windows klasa ili klasa prozora je osnovna struktura koju Windows koristi da bi rukovao osobinama i akcijama različitih prozora. Nećemo i ne da mi se ulaziti u detalje svega ovoga.. Za detalje uvijek imate MSDN. Uglavnom, napravite WNDCLASSEX strukture, date podatke njoj i onda koristeći to registrirate klasu prozora.
WNDCLASSEX wc;
Ovo je struktura koja sadrži informaciju o windows klasi. Nećemo ići skroz kroz njen sadržaj, pošto neki neće ni biti potrebni u game programiranju ali vidi se uglavnom kroz kontekst isto. Kao što sam rekao, MSDN je biblija za svakog Windows programera. A biblija kao biblija ima odgovor na sva vaša pitanja...
ZeroMemory(&wc, sizeof(WNDCLASSEX));
ZeroMemory je funkcija koja pokreće cijelu strukturu ili bolje da kažem briše sve iz strukture i postavlja na NULL. Cijeli blok memorije dakle. Adresa je dodijeljena kroz prvi parametar (&wc). Drugi parametar potreban je da kaže funkciji koliko je struktura velika. Dakle, smisao je da se pokrene struktura wc na NULL.
wc.cbSize = sizeof(WNDCLASSEX);
Ovo služi da postavi strukturu na potrebnu veličinu. A to radimo sa sizeof() operatorom.
wc.style = CS_HREDRAW |CS_VREDRAW;
U ovom članu definiramo kakav je stil našeg prozora. Ima mnoštvo vrijednosti koje ovdje možemo ubaciti, ali većinu nećemo koristiti za naše potrebe. Moguće vrijednosti pronađite u MSDNu pod WNDCLASSEX. Za sada koristit ćemo CS_HREDRAW i logički ili (|) sa CS_VREDRAW. Što ovo dvoje radi jest da kažu Windowsu da iscrta ponovno prozor u slučaju da se pomakne vertikalno ili horizontalno. Uglavnom, korisno samo kod aplikacija nevezanih uz igre.
wc.lpfnWndProc = (WNDPROC)WindowProc;
Što ova vrijednost radi jest da govori windows klasi koju funkciju koristiti kada dospije neka poruka od Windowsa. U našem programu, ova funkcija odgovara WindowProc(), ali također može imati drugačiji naziv poput WinProc(), get the point? Nije bitno kako se zove ako je pravilno damo kao vrijednost u član lpfnWndProc strukturi wc u ovom slučaju.
wc.hInstance = hInstance;
S ovim smo upoznati. Ovo je ručica kopiji naše aplikacije. Jednostavno treba postaviti vrijednost koju smo dali WinMain().
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
Ovaj član pohranjuje uobičajeni kursor miša za windows klasu. Ovo je napravljeno kroz return vrijednost funkcije LoadCursor(), koja sadrži dva parametra. Prvi je hInstance aplikacije koja pohranjuje kursor. Nećemo ulaziti u ovo, nema smisla, samo puknite NULL. Drugi parametar odnosi se na vrijednost koja sadrži uobičajeni kursor. Ako želite laprdekati i gledati druge mogućnosti... MSDN, mensch!
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
Ovaj član sadrži "brush" u nazivu koji se koristi da oboji pozadinu. Ovo nam neće biti potrebno u DirectXu, nego ćemo ga ostrakizmom protjerati iz naše aplikacije. Čitaj - iskomentirat ćemo ga :)
wc.lpszClassName = L"WindowsClass1";
Ovo je ime klas koju gradimo. Nazivamo ju "WindowClass1" jer nemamo mašte. Nije bitno kako se zove. Može biti "SečenovaBaka" ako se osjećate posebno kreativno. ^^ Što se tiče onog čudnovatog L ispred stringa. To govori compileru da string bi trebao biti napravljen od 16-bitni Unicode charactera, a ako ne znate što je unicode... GOOGLE!
RegisterClassEx(&wc);
Ova funkcija se praktički sama objašnjava. Ona konačno registrira sva ova čudesa iznad. Ide jedan parametar, a to je adresa wc strukture koju smo maloprije popunili. Komplicirano, zar ne?
-----------------------------------------------------------
Vjerovali ili ne, sve ovo dosad je bila registracija klase samo... Baf XD
Hajdemo sada napraviti taj prozorček. Kada je naša klasica fino napravljena možemo napraviti taj prokleti prozor. Samo ćemo opet morati proći kroz mnoštvo patnje i boli... Dakle prototip CreateWindowEx():
Code:
HWND CreateWindowEx(DWORD dwExStyle,
LPCTSTR lpClassName,
LPCTSTR lpWindowName,
DWORD dwStyle,
int x,
int y,
int nWidth,
int nHeight,
HWND hWndParent,
HMENU hMenu,
HINSTANCE hInstance,
LPVOID lpParam);
Divno, zar ne? Ah, ne prigovarajte, niste vi taj koji piše :P
Gladan sam, žedan sam, a u pozadini slušam Nightwish... Jesam li spomenuo da sam gladan? Hehe, dosta cviljenja... NA POSAO!
DWORD dwExStyle,
Ovo je nadodatak na četvrti parametar, dwStyle pa se nećemo previše bakćati sa detaljima, jednostavno puknite NULL i haj'mo dalje.
LPCTSTR lpClassName,
Naizgled dugačko, no ipak tako jednostavno. Ovo je ime klasu koju naš prozor koristi. Pošto imamo samo jednu klasu, koristit ćemo L"WindowsClass1". Ovaj string također koristi 16-bitne Unicode charactere i to je razlog L-a ispred njega.
LPCTSTR lpWindowName,
Ovo je ime prozora, ono što piše u gornjem lijevom kutu. Također koristi Unicode.
DWORD dwStyle,
Ovo je mjesto gdje deklariramo različite opcije za naš prozor. Možemo stilizirati ga u biti, izbaciti stvari koje smatramo da nam ne trebaju ili ne smiju biti omogućene. Više detalja o mogućim opcijama, naravno, po 3.14159265 put, MSDN pod CreateWindowEx(). Mi ćemo koristiti WS_OVERLAPPEDWINDOW, što je najjednostavniji način da dođemo do osnovnih fičra prozora.
int x, int y, int nWidth, int nHeight,
Ovo vjerojatno svi već kužite... Dakle pozicija prozora na zaslonu i širina te visina istog.
HWND hWndParent,
Ovo je parametar koji govori windowsu da li ima neki roditeljski /parent\ prozor koji sadrži druge prozorčiće. Nema ga, dakle null. PaintShop Pro npr sastoji se od jednog parent prozora i malih prozorčića. Svi povezani u jedan app.
HMENU hMenu,
Ovo je ručica za izbornik. Nemamo ga, dakle NULL.
LPVOID lpParam
Ovo je parametar koji bi koristili da tvorimo više prozora. Mi ih ne radimo, opet NULL postavljamo kao vrijednost.
Povratna vrijednost
Sve ovo što unutar ove funkcije uradimo (CreateWindowsEx()) spremamo direktno u našu hWnd varijablu. I to je sve što se tiče kreiranja prozora.
----------------------------------------------------------
Sljedeći zadatak jest da prikažemo taj prozor koji cijeli dan radimo. To je prilično jednostavan zadatak. Koristimo funkciju ShowWindow() sa povratnom vrijednosti BOOL.
Kako smo se navikli, sve započinjemo prototipom:
Code:
BOOL ShowWindow(HWND hWnd,
int nCmdShow);
HWND hWnd,
Ovaj parametar je ručica prozoru kojeg smo toliko mukotrpno stvarali. Ovdje vraćamo vrijednost koju nam je Windows vratio nakon funkcije CreateWindowEx().
int nCmdShow
Ako zavirite u sjećanje, zadnji parametar u WinMain() funkciji... Ovo je mjesto gdje ga koristimo. Naravno, ne moramo, ali u ovom slučaju ići ćemo prema pravilima i dati Windowsu što želi. Ovo u fullscreenu neće biti bitno. Dakle, 'nough chit-chat.
---------------------------------------------------------
Iako smo napravili prve tri faze, ovo još nije gotovo ( OMG!!)... Hehe, ima još jedna mala fazica i funkcijica koja je prijeko potrebna. Prvo ćemo se pozabaviti glavnim loopom. On glasi u našem programu ovako:
Code:
// ova struktura sadrži predivne poruke koje nam Windows šalje
MSG msg;
// dakle, čekamo dok dobijemo poruku od Windowsa
while(GetMessage(&msg, NULL, 0, 0))
{
// prevedi u programu razumljiv format
TranslateMessage(&msg);
// pošalji WindowProc() funkciji da ju procesira
DispatchMessage(&msg);
}
Ajmo od vrha prema dolje:
MSG msg;
MSG je struktura koja sadrži sve podatke potrebne za zasebnu poruku. Mislim da nećemo direktno pristupati sadržaju njenom. Tako da napravite to i jednostavno nastavite :)
while(GetMessage(&msg, NULL, 0, 0))
GetMessage() je funkcija koja preuzima svaku poruku iz reda čekanja i ubacuje ju u našu msg strukturu. Uvijek vraća true, osim kada program se gasi, tada vraća naravno, false. Na taj način, naš while() loop je razbijen samo kada je program izvršen uspješno.
GetMessage() funkcija neće biti nama od velike koristi jer ću vas već u sljedećoj lekciji upoznati sa boljim načinom.
WindowProc() funkcija
Sada kad imamo većinu toga dovršeno, ostaje nam još poštar da se sredi. Gore spomenuta GetMessage() funkcija dobiva poruku, prevodi ju u format razumljiv programu i šalje ju ovdje. Kada je WindowProc() pozvan četiri podatka moraju stići da bi bilo uspješno.
Code:
LRESULT CALLBACK WindowProc(HWND hWnd,
UINT message,
WPARAM wParam,
LPARAM lParam);
U ovom slučaju ćemo se usredotočiti na sadržaj ove funkcije, izostavljajući parametre koje ću objasniti prilikom sljedećeg tutoriala kada vam predstavim zamjenu za GetMessage() funkciju. Kada poruka uđe u WindowProc, možemo koristiti uMsg argument da ustanovimo koja je to poruka. 99% programera koristi switch() izjavu da ustanove o čemu se radi.
Code:
// traži određenu akciju koja treba biti provedena pri dobivanju poruke
switch(message)
{
// ovo se pročita kada korisnik pritisne crveni X
case WM_DESTROY:
{
// kôd ide ovdje
} break;
}
Ovako izgleda switch izjava za kôd koji treba vrtiti. Naravno, imamo ovdje samo WM_DESTROY, što znači da će se druge poruke ignorirati. WM_DESTROY poziva se samo na kraju kada se prozor zatvara. Ovo činimo kada želimo zatvoriti potpuno program:
Code:
// traži određenu akciju koja treba biti provedena pri dobivanju poruke
switch(message)
{
// ovo se pročita kada korisnik pritisne crveni X
case WM_DESTROY:
{
// zatvori aplikaciju
PostQuitMessage(0);
return 0;
} break;
}
PostQuitMessage() funkcija šalje vrijednost '0' WM_QUITu. Ako se prisjetite opet, u vašem glavnom loopu. GetMessage() vraća samo false ako se program gasi. 0 = false. Dakle, omogućuje da se WinMain() završi uspješno.
Evo ga... Ovo je tek mali uvod u Win32, ali moram reći ako ste naučili sve što sam vam ovdje prepričao da niste loši... Polako napredujete i uskoro ćete imati priliku baciti se u Direct3D.