Lekce 21 - Přímky, antialiasing, časování, pravoúhlá projekce, základní zvuky a jednoduchá herní logika

První opravdu rozsáhlý tutoriál - jak už plyne z gigantického názvu. Doufejme, že taková spousta informací a technik dokáže udělat šťastným opravdu každého. Strávil jsem dva dny kódováním a kolem dvou týdnů psaním tohoto HTML souboru. Pokud jste někdy hráli hru Admiar, lekce vás vrátí do vzpomínek. Úkol hry sestává z vyplnění jednotlivých políček mřížky. Samozřejmě se musíte vyhýbat všem nepřátelům.

Námět této lekce je vcelku složitý. Vím, že spousta z vás je unavena studiem základů. Každý by zemřel pro zvláštnosti 3D objektů, multitexturingu a podobně. Těmto lidem se omlouvám, protože chci zachovat postupné nabalování znalostí. Po velkém skoku vpřed není u krůčku zpět snadné udržet zájem čtenářů. Já osobně preferuji konstantní tempo. Možná jsem ztratil několik z vás, ale doufám, že ne příliš mnoho. Do dneška se ve všech mých tutoriálech objevovaly polygony, obdélníky a trojúhelníky. Pravděpodobně jste si všimli neúmyslné diskriminace :-) čar, přímek, linek a podobných jednorozměrných útvarů. O několik hodin později začal vznikat Line Tutoriál. Vypadal v klidu, ale totálně nudný! Linky jsou skvělé, ale v porovnání s některými efekty nic moc. Shrnuto: rozhodl jsem se napsat multi-tutoriál. Na konci lekce bychom měli mít vytvořenu jednoduchou hru typu 'Admiar'. Misí bude vyplnit políčka mřížky. Hráče nesmí chytit nepřátelé - jak jinak. Implementujeme levely, etapy, životy, zvuky a kódy - k průchodu skrz levely, když se věci stanou příliš obtížnými. Ačkoli hru spustíte i na Pentiu 166 s Voodoo 2, rychlejší procesor nebude na škodu.

Rozšíříme standardní kód z lekce jedna. Přidáme potřebné hlavičkové soubory - stdio.h pro souborové operace a stdarg.h kvůli výstupu proměnných (level, obtížnost ap.).

#include <windows.h>// Hlavičkový soubor pro Windows

#include <stdio.h>// Hlavičkový soubor pro standardní vstup/výstup

#include <stdarg.h>// Hlavičkový soubor pro funkce s proměnným počtem parametrů

#include <gl\gl.h>// Hlavičkový soubor pro OpenGL32 knihovnu

#include <gl\glu.h>// Hlavičkový soubor pro Glu32 knihovnu

#include <gl\glaux.h>// Hlavičkový soubor pro Glaux knihovnu

HDC hDC = NULL;// Privátní GDI Device Context

HGLRC hRC = NULL;// Trvalý Rendering Context

HWND hWnd = NULL;// Obsahuje Handle našeho okna

HINSTANCE hInstance;// Obsahuje instanci aplikace

bool keys[256];// Pole pro ukládání vstupu z klávesnice

bool active = TRUE;// Ponese informaci o tom, zda je okno aktivní

bool fullscreen = TRUE;// Ponese informaci o tom, zda je program ve fullscreenu

Deklarujeme proměnné. Pole vline ukládá záznamy o 121 vertikálních linkách, které tvoří mřížku. 11 přímek zleva doprava a 11 čas ze shora dolů. Hline ukládá 121 horizontálních přímek. Ap používáme ke zjištění stisku klávesy A. Filled je nastaveno na FALSE, jestliže mřížka není kompletně vyplnění a TRUE pokud je. Gameover ukončuje hru. Pokud se anti rovná TRUE je zapnut antialiasing objektů.

bool vline[11][10];// Ukládá záznamy o vertikálních linkách

bool hline[10][11];// Ukládá záznamy o horizontálních linkách

bool ap;// Stisknuto 'A'?

bool filled;// Bylo ukončeno vyplňování mřížky?

bool gameover;// Konec hry?

bool anti = TRUE;// Antialiasing?

Přicházejí na řadu celočíselné proměnné. Loop1 a loop2 užíváme k označení bodů v herní mřížce, zjištění zda do nás nepřítel nevrazil a k vygenerování randomové pozice. Zastavení pohybu nepřátel je implementováno čítačem delay. Po dosažení určité hodnoty se začnou znovu hýbat a delay se zpátky vynuluje.

Proměnná adjust je speciální. I když program obsahuje timer, tento timer pouze zjišťuje, zda je počítač (průběh programu) příliš rychlý a v takovém případě ho zpomalíme. Na grafické kartě GeForce hra běží hodně rychle. Po testu s PIII/450 s Voodoo 3500 TV si nelze nevšimnout extrémní lenosti. Problém spočívá v kódu pro časování, který hru pouze zpomaluje. Zrychlení jím nelze provést. Vytvořil jsem proměnnou adjust, která může nabývat nuly až pěti. Čím vyšší hodnota, tím rychleji se objekty pohybují - podpora starších systémů. Nicméně nezáleží, jak rychlá je hra, absolutní rychlost provádění programu se nikdy nezvýší. Nastavením adjust na trojku vytvoříme kompromis pro pomalé i rychlé systémy. Více o časování dále.

Lives ukládá počet životů, level užíváme k zaznamenávání obtížnosti. Není to level, který se zobrazuje na monitoru. Level2 začíná se stejnou hodnotou, ale může být inkrementován donekonečna - záleží na obratnosti hráče. Pokud dokáže dosáhnout třetího levelu, proměnná level se přestane zvyšovat. určuje pouze vnitřní obtížnost hry. Stage definuje konkrétní etapu hry.

int loop1;// Řídící proměnná cyklů

int loop2;// Řídící proměnná cyklů

int delay;// Doba zastavení nepřátel

int adjust = 3;// Rychlostní kompenzace pro pomalé systémy

int lives = 5;// Počet životů hráče

int level = 1;// Vnitřní obtížnost hry

int level2 = level;// Zobrazovaný level

int stage = 1;// Etapa/fáze hry

Definujeme strukturu objektu - hráč, nepřítel ap. Vnitřní proměnné fx a fy ukládají pomocnou polohu pro plynulý pohyb (fx = fine x). X a y definují pozici na mřížce. Mohou nabývat hodnot od nuly do deseti. Kdybychom se s hráčem po scéně pohybovali pomocí těchto dvou proměnných měli bychom jedenáct pozic vodorovně a jedenáct svisle. Hráč by přeskakoval z jednoho místa na druhé. Proto při pohybu používáme upřesňující fx a fy. Poslední proměnnou spin používáme pro otáčení objektů okolo osy z.

struct object// Struktura objektu ve hře

{

int fx, fy;// Pohybová pozice

int x, y;// Absolutní pozice

float spin;// Otáčení objektu dokola

};

Na základě struktury vytvoříme hráče, devět nepřátel a jeden speciální objekt - skleněné přesýpací hodiny, které se sem tam objeví. Pokud je stihnete sebrat, nepřítel se na chvíli zastaví.

struct object player;// Hráč

struct object enemy[9];// Nepřátelé

struct object hourglass;// Skleněné hodiny

Abychom proměnné pro časovač měli pohromadě, sloučíme je do struktury. Frekvenci časovače deklarujeme jako 64-bitové celé číslo. Resolution je perioda (obrácená hodnota frekvence). Mm_timer_start a mm_timer_elapsed udržují počáteční a uplynulý čas. Používáme je pouze tehdy, pokud počítač nemá performance counter (v překladu: čítač provedení nebo výkonu, zůstanu u anglického termínu). Logická proměnná performance_timer bude nastavena na TRUE pokud program detekuje, že počítač má performance counter. Pokud ho nenajde budeme pro časování používat méně přesný, ale celkově dostačující multimediální timer. Poslední dvě proměnné jsou opět 64-bitové integery, které ukládají čas spuštění a uplynulý čas performance counteru. Proměnnou na bázi této struktury pojmenujeme timer.

struct // Informace pro časovač

{

__int64 frequency;// Frekvence

float resolution;// Perioda

unsigned long mm_timer_start;// Startovní čas multimediálního timeru

unsigned long mm_timer_elapsed;// Uplynulý čas multimediální timeru

bool performance_timer;// Užíváme Performance Timer?

__int64 performance_timer_start;// Startovní čas Performance Timeru

__int64 performance_timer_elapsed;// Uplynulý čas Performance Timeru

} timer;// Struktura se jmenuje timer

Následující pole si můžeme představit jako tabulku rychlostí. objekt ve hře se může pohybovat rozdílnými rychlostmi. Vše závisí na proměnné adjust (výše). Pokud se její hodnota rovná nule pohybující se o pixel za určitý čas, pokud pěti, rychlost činí dvacet pixelů. Inkrementováním adjust se na pomalých počítačích zvýší rychlost (ale i "trhanost") hry. Počet pixelů kroku je v tabulce. Adjust používáme jako index do tohoto pole.

int steps[6]={ 1, 2, 4, 5, 10, 20 };// Krokovací hodnota pro přizpůsobení pomalého videa

Deklarujeme pole dvou textur - pozadí a bitmapový font. Base ukazuje na první display list fontu (viz. minulé tutoriály). Funkce pro nahrávání a vytváření textur nebudu opisovat, byly tu už tolikrát, že je musíte znát na zpaměť (překladatel).

GLuint texture[2];// Dvě textury

GLuint base;// Základní display list pro font

Inicializujeme časovač. Začneme vynulováním všech proměnných. Potom zjistíme, zda budeme moci používat performance counter. Pokud ano,uložíme frekvenci do timer.frequency, pokud ne budeme používat multimediální timer - nastavíme timer.performance_timer na FALSE a načteme do počáteční hodnoty aktuální čas. Timer.resolution definujeme na 0.001 (Překladatel: dělení je celkem zbytečné) a timer.frequency na 1000. Protože ještě neuplynul žádný čas, přiřadíme uplynulému času startovní čas.

void TimerInit(void)// Inicializace timeru

{

memset(&timer, 0, sizeof(timer));// Vynuluje proměnné struktury

// Zjistí jestli je Performance Counter dostupný a pokud ano, bude načtena jeho frekvence

if (!QueryPerformanceFrequency((LARGE_INTEGER *) &timer.frequency))

{

// Performance Counter není dostupný

timer.performance_timer = FALSE;// Nastaví Performance Timer na FALSE

timer.mm_timer_start = timeGetTime();// Získání aktuálního času

timer.resolution = 1.0f/1000.0f;// Nastavení periody

timer.frequency = 1000;// Nastavení frekvence

timer.mm_timer_elapsed = timer.mm_timer_start;// Uplynulý čas = počáteční

}

Má-li počítač performance counter projdeme touto větví. Nastavíme počáteční hodnotu a oznámíme, že můžeme používat performance counter. Poté spočítáme periodu pomocí frekvence získané v if() výše. Perioda je převrácená hodnota frekvence. Nakonec nastavíme uplynulý čas na startovní. Všimněte si, že místo sdílení proměnných obou timerů, jsem se rozhodl použít různé. Obě cesty by pracovaly, ale tato je přehlednější.

else

{

// Performance Counter je možné používat

QueryPerformanceCounter((LARGE_INTEGER *) &timer.performance_timer_start);// Počáteční čas

timer.performance_timer = TRUE;// Nastavení Performance Timer na TRUE

timer.resolution = (float) (((double)1.0f)/((double)timer.frequency));// Spočítání periody

timer.performance_timer_elapsed = timer.performance_timer_start;//Nastaví uplynulý čas na počáteční

}

}

V následující funkci načteme timer a vrátíme uplynulý čas v milisekundách. Deklarujeme 64-bitové celé číslo, do kterého načteme současnou hodnotu čítače. Opět větvíme program podle přítomnosti performance timeru. První řádkou v if() načteme obsah čítače. Dále od něj odečteme počáteční čas, který jsme získali při inicializaci časovače. Získaný rozdíl násobíme periodou čítače. Abychom výsledek v sekundách převedli na milisekundy násobíme ho tisícem. Tuto hodnotu vrátíme. Nepoužíváme-li performance counter, provede se větev else, která dělá analogicky to samé. Načteme současný čas, odečteme od něj počáteční, násobíme periodou a poté tisícem. Opět získáme uplynulý čas v milisekundách a vrátíme ho.

float TimerGetTime()// Získá čas v milisekundách

{

__int64 time;// Čas se ukládá do 64-bitového integeru

if (timer.performance_timer)// Performance Timer

{

QueryPerformanceCounter((LARGE_INTEGER *) &time);// Načte aktuální čas

// Vrátí uplynulý čas v milisekundách

return ((float)(time - timer.performance_timer_start) * timer.resolution)*1000.0f;

}

else// Multimediální timer

{

// Vrátí uplynulý čas v milisekundách

return ((float)(timeGetTime() - timer.mm_timer_start) * timer.resolution)*1000.0f;

}

}

V další funkci se resetuje pozice hráče na levý horní roh a poloha nepřátel na randomové body. Levý horní roh scény má souřadnice [0;0]. Přiřadíme je hráčově x a y. Protože je na začátku linek, nepohybuje se, takže i upřesňující pohybové pozice nastavíme na nulu.

void ResetObjects(void)// Reset hráče a nepřátel

{

player.x = 0;// Hráč bude vlevo nahoře

player.y = 0;// Hráč bude vlevo nahoře

player.fx = 0;// Pohybová pozice

player.fy = 0;// Pohybová pozice

Přejdeme k inicializaci polohy nepřátel. Jejich aktuální počet (zobrazených) je roven vnitřnímu levelu násobenému jeho současnou obtížností/etapou. Zapamatujte si, že maximální počet levelů je tři a maximální počet etap v levelu je také tři. Z toho plyne, že můžeme mít nejvíce devět nepřátel. V cyklu nastavíme x pozici každého nepřítele na pět až deset a y pozici na nula až deset. Nechceme, aby se pohybovali ze staré pozice na novou, takže se ujistíme, že se fx a fy budou rovnat x krát délka linky (60) a y krát výška linky (40).

for (loop1=0; loop1<(stage*level); loop1++)// Prochází nepřátele

{

enemy[loop1].x = 5 + rand() % 6;// Nastaví randomovou x pozici

enemy[loop1].y = rand() % 11;// Nastaví randomovou y pozici

enemy[loop1].fx = enemy[loop1].x * 60;// Pohybová pozice

enemy[loop1].fy = enemy[loop1].y * 40;// Pohybová pozice

}

}

Funkce glPrint() se moc nezměnila. Narozdíl od minulých tutoriálů jsem přidal možnost výpisu hodnot proměnných. Zapneme mapování textur, resetujeme matici a přesuneme se na určenou pozici. Pokud je zvolena první (nultá) znaková sada, změníme měřítko tak, aby byl font dvakrát vyšší a jeden a půl krát širší. Pomocí této finty budeme moci vypsat titul hry většími písmeny. Na konci vypneme mapování textur.

GLvoid glPrint(GLint x, GLint y, int set, const char *fmt, ...)// Výpis textů

{

char text[256];// Bude ukládat výsledný řetězec

va_list ap;// Ukazatel do argumentů funkce

if (fmt == NULL)// Nebyl předán řetězec

return;// Konec

va_start(ap, fmt);// Rozdělí řetězec pro proměnné

vsprintf(text, fmt, ap);// Konvertuje symboly na čísla

va_end(ap);// Výsledek je uložen v text

if (set>1)// Byla předána špatná znaková sada?

{

set=1;// Pokud ano, zvolí se kurzíva

}

glEnable(GL_TEXTURE_2D);// Zapne texturové mapování

glLoadIdentity();// Reset matice

glTranslated(x,y,0);// Přesun na požadovanou pozici

glListBase(base-32+(128*set));// Zvolí znakovou sadu

if (set==0)// Pokud je určena první znaková sada font bude větší

{

glScalef(1.5f,2.0f,1.0f);// Změna měřítka

}

glCallLists(strlen(text),GL_UNSIGNED_BYTE, text);// Výpis textu na monitor

glDisable(GL_TEXTURE_2D);// Vypne texturové mapování

}

Implementace změny velikosti okna je nová. Namísto perspektivní scény použijeme pravoúhlou projekci (ortho view). Její hlavní charakteristikou je, že se při změně vzdálenosti pozorovatele (translace do hloubky) objekty nezmenšují - vypnutá perspektiva. Osa z je méně užitečná, někdy dokonce ztrácí význam. V tomto tutoriálu s ní nebudeme pracovat vůbec.

Začneme nastavením viewportu, úplně stejně, jako při perspektivní scéně. Poté zvolíme projekční matici (analogie filmovému projektoru; obsahuje informace, jak se zobrazí obrázek) a resetujeme ji.

Inicializujeme pravoúhlou projekci. První parametr 0.0f určuje pozici levé hrany scény. Druhá předávaná hodnota označuje polohu pravé hrany. Pokud by mělo okno velikost 640 x 480, tak ve width bude uložena hodnota 640. Scéna by začínala na ose x nulou a končila 640 - přesně jako okno. Třetím parametrem označujeme spodní okraj scény. Bývá záporný, ale protože chceme pracovat s pixely určíme spodek okna rovnu jeho výšce. Nula, čtvrtý parametr, definuje horní okraj. Poslední dvě hodnoty náleží k ose z. V této lekci se o ni nestaráme, takže nastavíme rozmezí od -1.0f do 1.0f. Všechno budeme vykreslovat v hloubce nula, takže uvidíme vše.

Po nastavení pravoúhlé scény, zvolíme matici modelview (informace o objektech, lokacích, atd.) a resetujeme ji.

GLvoid ReSizeGLScene(GLsizei width, GLsizei height)// Inicializace a změna velikosti okna

{

if (height==0)// Proti dělení nulou

{

height=1;// Výška se rovná jedné

}

glViewport(0,0,width,height);// Reset Viewportu

glMatrixMode(GL_PROJECTION);// Zvolí projekční matici

glLoadIdentity();// Reset projekční matice

glOrtho(0.0f,width,height,0.0f,-1.0f,1.0f);// Vytvoří pravoúhlou scénu

glMatrixMode(GL_MODELVIEW);// Zvolí matici modelview

glLoadIdentity();// Reset matice modelview

}

Při inicializaci se vyskytne několik nových příkazů. Začneme klasicky loadingem textur a kontrolou úspěšnosti této akce, poté vygenerujeme znakovou sadu fontu. Zapneme jemné stínování, nastavíme černé pozadí a vyčistíme hloubku jedničkou.

glHint() oznamuje OpenGL, jak má vykreslovat. V tomto případě požadujeme, aby všechny linky byly nejhezčí, jaké OpenGL dokáže vytvořit. Tímto příkazem zapínáme antialiasing. Také zapneme blending a zvolíme jeho mód tak, abychom umožnili, již zmíněný, antialiasing linek. Blending je potřeba, pokud chceme pěkně skombinovat (smíchat, zprůhlednit - blend with) s obrázkem na pozadí. Pokud chcete vidět, jak špatně budou linky vypadat, vypněte blending. Je důležité poukázat na fakt, že antialiasing se nemusí zobrazovat správně(? překl.). Objekty ve hře jsou docela malé, takže si nemusíte všimnout, že něco není v pořádku. Podívejte se pořádně. Všimněte si, jak se linky na nepřátelích zjemní pokud je antialiasing zapnutý. Hráč a hodiny by měli vypadat mnohem lépe.

int InitGL(GLvoid)// Nastavení OpenGL

{

if (!LoadGLTextures())// Loading textur

{

return FALSE;

}

BuildFont();// Vytvoření fontu

glShadeModel(GL_SMOOTH);// Zapne jemné stínování

glClearColor(0.0f, 0.0f, 0.0f, 0.5f);// Černé pozadí

glClearDepth(1.0f);// Nastavení hloubkového bufferu

glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);// Nastavení antialiasingu linek

glEnable(GL_BLEND);// Zapne blending

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);// Typ blendingu

return TRUE;

}

Na řadu přichází vykreslování. Smažeme obrazovku a hloubkový buffer a zvolíme texturu fontu - texture[0]. Abychom slova "GRID CRAZY" vypsali purpurovou barvou nastavíme R a G naplno, G s poloviční intenzitou. Nápis vypíšeme na souřadnice [207;24]. Použijeme první (nultou) znakovou sadu, takže bude text velkými písmeny. Poté zaměníme purpurovou barvu za žlutou a vypíšeme "Level" s obsahem proměnné level2. Dvojka v %2i určuje maximální počet číslic. Pomocí i oznamujeme, že se jedná o celočíselnou proměnnou (integer). O trochu níže, tou samou barvou, zobrazíme "Stage" s konkrétní etapou hry.

int DrawGLScene(GLvoid)// Všechno kreslení

{

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// Smaže obrazovku a hloubkový buffer

glBindTexture(GL_TEXTURE_2D, texture[0]);// Zvolí texturu fontu

glColor3f(1.0f,0.5f,1.0f);// Purpurová barva

glPrint(207,24,0,"GRID CRAZY");// Vypíše logo hry

glColor3f(1.0f,1.0f,0.0f);// Žlutá barva

glPrint(20,20,1,"Level:%2i",level2);// Vypíše level

glPrint(20,40,1,"Stage:%2i",stage);// Vypíše etapu

Zkontrolujeme konec hry. Pokud je gameover rovno TRUE zvolíme náhodnou barvu. Používáme glcolor3ub(), protože je mnohem jednodušší vygenerovat číslo od 0 do 255 než od 0.0f do 1.0f. Doprava od titulku hry vypíšeme "GAME OVER" a o řádek níže "PRESS SPACE". Upozorňujeme hráče, že zemřel a že pomocí mezerníku může hru resetovat.

if (gameover)// Konec hry?

{

glColor3ub(rand()%255,rand()%255,rand()%255);// Náhodná barva

glPrint(472,20,1,"GAME OVER");// Vypíše GAME OVER

glPrint(456,40,1,"PRESS SPACE");// Vypíše PRESS SPACE

}

Pokud mu však nějaké životy zbyly, zobrazíme doprava od titulku hry animované obrázky hráče. Vytvoříme cyklus, který jde od nuly do aktuálního počtu životů mínus jedna. Jedničku odečítáme, protože jeden obrázek se zobrazuje do hracího pole.

for (loop1=0; loop1<lives-1; loop1++)// Cyklus vykreslující životy

{

Uvnitř cyklu resetujeme matici a provedeme translaci doprava na pozici, kterou získáme výpočtem: 490 plus řídící proměnná krát 40. Tímto způsobem budeme moci vykreslit každý animovaný život hráče o 40 pixelů doprava od minulého. Poté orotujeme pohled proti směru hodinových ručiček v závislosti na hodnotě uložené v player.spin. Záporným znaménkem způsobíme, že se budou životy otáčet opačným směrem než hráč.

glLoadIdentity();// Reset matice

glTranslatef(490+(loop1*40.0f),40.0f,0.0f);// Přesun doprava od titulku

glRotatef(-player.spin,0.0f,0.0f,1.0f);// Rotace proti směru hodinových ručiček

Zvolíme zelenou barvu a začneme zobrazovat. Kreslení linek je úplně stejné, jako kreslení polygonů. Začneme s glBegin(GL_LINES). Tím oznámíme OpenGL, že chceme kreslit přímky. Pro jednu stačí pouze dva body. My zadáváme body pomocí glVertex2d(), protože nepotřebujeme hloubku, ale samozřejmě lze použít i glVertex3f() pro plnohodnotný bod ve 3D prostoru.

glColor3f(0.0f,1.0f,0.0f);// Zelená barva

glBegin(GL_LINES);// Začátek kreslení životů

glVertex2d(-5,-5);// Levý horní bod

glVertex2d( 5, 5);// Pravý dolní bod

glVertex2d( 5,-5);// Pravý horní bod

glVertex2d(-5, 5);// Levý dolní bod

glEnd();// Konec kreslení

Po vykreslení X (X - tvar hráče), znovu natočíme scénu, ale tentokrát pouze o polovinu úhlu. Zadáme tmavší zelenou barvu a vykreslíme +, ale trochu větší než X. Protože je + pomalejší a tmavší, X vypadá, jako by se otáčelo na jeho vrcholu.

glRotatef(-player.spin*0.5f,0.0f,0.0f,1.0f);// Rotace proti směru hodinových ručiček

glColor3f(0.0f,0.75f,0.0f);// Tmavší zelená barva

glBegin(GL_LINES);// Pokračování kreslení životů

glVertex2d(-7, 0);// Levý středový bod

glVertex2d( 7, 0);// Pravý středový bod

glVertex2d( 0,-7);// Horní středový bod

glVertex2d( 0, 7);// Dolní středový bod

glEnd();// Konec kreslení

}

Nakreslíme herní mřížku. Nastavením proměnné filled na TRUE oznámíme programu, že už byla mřížka kompletně vyplněná (více dále). Určíme šířku čáry na 2.0f - linky ztloustnou a mřížka bude opticky více definovaná. Přestože se zhorší kvalita grafického výstupu, vypneme antialiasing. Velmi zatěžuje procesor a pokud nemáte hodně dobrou grafickou kartu, zaznamenáte obrovské zpomalení. Vyzkoušejte si a konejte, jak uznáte za vhodné.

filled=TRUE;// Před testem je všechno vyplněné

glLineWidth(2.0f);// Širší čáry

glDisable(GL_LINE_SMOOTH);// Vypne antialiasing

glLoadIdentity();// Reset matice

Po resetu matice deklarujeme dva vnořené cykly. Prvním procházíme mřížku zleva doprava a druhým ze shora dolů. Nastavíme barvu na modrou a pokud je právě kreslená linka již přejetá hráčem, přebijeme modrou barvu bílou. Dále zkontrolujeme, zda se nechystáme kreslit příliš vpravo. Pokud ano přeskočíme kreslení.

for (loop1=0; loop1<11; loop1++)// Cyklus zleva doprava

{

for (loop2=0; loop2<11; loop2++)// Cyklus ze shora dolů

{

glColor3f(0.0f,0.5f,1.0f);// Modrá barva

if (hline[loop1][loop2])// Byla už linka přejetá?

{

glColor3f(1.0f,1.0f,1.0f);// Bílá barva

}

if (loop1<10)// Nekreslit úplně vpravo

{

Otestujeme, jestli už byla horizontální linka přejetá. Pokud ne, přiřadíme do filled FALSE a tím oznámíme, že ještě nejméně jedna linka nebyla vyplněná, a tudíž ještě nemůžeme tento level opustit.

if (!hline[loop1][loop2])// Nebyla linka ještě přejetá?

{

filled=FALSE;// Všechno ještě není vyplněno

}

Poté konečně vykreslíme horizontální linku. Protože je vodorovná, přiřadíme y-ové hodnotě obou bodů stejnou velikost. Přičítáme sedmdesátku, aby nad hracím polem zůstalo volné místo pro informace o počtu životů, levelu ap. Hodnoty na ose x se liší tím, že druhý bod je posunut o šedesát pixelů doprava (80-20=60). Opět přičítáme konstantu, v tomto případě dvacítku, aby hrací pole nebylo namačkáno na levý okraj a vpravo nebyla zbytečná mezera. Všimněte si, že linky jsou kresleny zleva doprava. Toto je důvod, proč nechceme kreslit jedenáctou - nevešla by se na obrazovku.

glBegin(GL_LINES);// Začátek kreslení horizontálních linek

glVertex2d(20+(loop1*60),70+(loop2*40));// Levý bod

glVertex2d(80+(loop1*60),70+(loop2*40));// Pravý bod

glEnd();// Konec kreslení

}

Na řadu přicházejí vertikální linky. Kód je téměř stejný, takže text popisu nebudu zbytečně opisovat. Linky se kreslí ze shora dolů namísto zleva doprava - jediná odlišnost.

glColor3f(0.0f,0.5f,1.0f);// Modrá barva

if (vline[loop1][loop2])// Byla už linka přejetá?

{

glColor3f(1.0f,1.0f,1.0f);// Bílá barva

}

if (loop2<10)// Nekreslit úplně dolů

{

if (!vline[loop1][loop2])// Nebyla linka ještě přejetá?

{

filled=FALSE;// Všechno ještě nebylo vyplněno

}

glBegin(GL_LINES);// Začátek kreslení vertikálních linek

glVertex2d(20+(loop1*60),70 +(loop2*40));// Horní bod

glVertex2d(20+(loop1*60),110+(loop2*40));// Dolní bod

glEnd();// Konec kreslení

}

Scéna je dohromady seskládaná z obdélníků o velikosti jedné desetiny obrázku scény. Na každý z nich je namapovaná určitá část velké textury, proto musíme zapnout mapování textur. Protože nechceme, aby měl kreslený obdélník barevný nádech, nastavíme barvu na bílou. Také nesmíme zapomenout zvolit texturu.

Textura hrací plochy

glEnable(GL_TEXTURE_2D);// Zapne mapování textur

glColor3f(1.0f,1.0f,1.0f);// Bílá barva

glBindTexture(GL_TEXTURE_2D, texture[1]);// Zvolí texturu

Dále prověříme, jestli aktuální obdélník ve scéně ještě existuje (není za hranou hrací plochy). Nacházíme se v cyklech, které postupně vykreslují 11 linek vodorovně a 11 svisle. Nicméně nevykreslujeme 11 obdélníků, ale pouze 10! Ověříme, jestli se nechystáme kreslit na jedenáctou pozici - loop1 i loop2 musí být menší než deset (0-9).

if ((loop1<10) && (loop2<10))// Pouze pokud je obdélník v hrací ploše

{

Zjistíme přejetí všech okolních linek obdélníku. Kraje testujeme v pořadí: horní, dolní, levý a pravý. Po každém průchodu vnitřním cyklem se inkrementuje loop1 a tím se z pravého okraje stává levý okraj následujícího obdélníku. V případě průchodu vnější smyčkou se ze spodních hran obdélníků v řádku stávají horní okraje nových obdélníků v řádku o jedno níže. Vše by mělo být zřejmé z diagramu.

Diagram

Pokud jsou všechny okraje projeté (rovnají se TRUE), můžeme namapovat texturu a vykreslit obdélník. Děláme to stejným stylem, jako jsme rozřezávali texturu znakové sady na jednotlivá písmena. Ani teď se neobejdeme bez matematiky. Dělíme loop1 i loop2 deseti, protože chceme rozdělit texturu mezi sto obdélníků (10x10). Koordináty jsou v rozmezí od nuly do jedné s krokem jedné desetiny (1/10=0,1).

Takže abychom dostali pravý horní roh, vydělíme hodnotu proměnných loop deseti a přičteme 0,1 k x-ovému koordinátu. Levý horní roh získáme dělením bez žádných dalších komplikací. Levý dolní bod spočívá opět v dělení deseti a přičtení 0,1 k ypsilonové složce. Dostáváme se k pravému dolnímu rohu, u kterého se po vydělení přičítá 0,1 k oběma souřadnicovým složkám. Doufám, že to dává smysl (Já taky - překl.).

Pokud budou oba loopy rovny devíti, ve výsledku dostaneme kombinaci 0,9 a 1,0, které dosadíme do parametrů funkce glTexCoord2f(x,y). souřadnice vrcholů obdélníků pro glVertex2d(x,y) získáme analogicky jako okraje linek mřížky. Přičítáme k nim, ale ještě konstanty (1, 59, 1, 39), které zajišťují zmenšení obdélníků - aby se vešly do políček mřížky a přitom nic nepřekryly.

// Jsou přejety všechny čtyři okraje obdélníku?

if (hline[loop1][loop2] && hline[loop1][loop2+1] && vline[loop1]loop2] && vline[loop1+1][loop2])

{

glBegin(GL_QUADS);// Vykreslí otexturovaný obdélník

glTexCoord2f(float(loop1/10.0f)+0.1f,1.0f-(float(loop2/10.0f)));

glVertex2d(20+(loop1*60)+59,(70+loop2*40+1));// Pravý horní

glTexCoord2f(float(loop1/10.0f),1.0f-(float(loop2/10.0f)));

glVertex2d(20+(loop1*60)+1,(70+loop2*40+1));// Levý horní

glTexCoord2f(float(loop1/10.0f),1.0f-(float(loop2/10.0f)+0.1f));

glVertex2d(20+(loop1*60)+1,(70+loop2*40)+39);// Levý dolní

glTexCoord2f(float(loop1/10.0f)+0.1f,1.0f-(float(loop2/10.0f)+0.1f));

glVertex2d(20+(loop1*60)+59,(70+loop2*40)+39);// Pravý dolní

glEnd();// Konec kreslení

}

}

V závěru vypneme mapování textur a po opuštění obou cyklů vrátíme šířku čáry na původní hodnotu.

glDisable(GL_TEXTURE_2D);// Vypne mapování textur

}

}

glLineWidth(1.0f);// Šířka čáry 1.0f

V případě, že je anti rovno TRUE, zapneme zjemňování linek (antialiasing).

if (anti)// Má být zapnutý antialiasing?

{

glEnable(GL_LINE_SMOOTH);// Zapne antialiasing

}

Abychom usnadnili hru, přidáme speciální objekt - přesýpací hodiny, jejichž sebráním se nepřátelé na chvíli zastaví. Pro jejich umístění v hracím poli používáme proměnné x a y, nicméně protože se nebudou pohybovat, můžeme využít nepotřebné fx jako přepínač (0 jsou viditelné, 1 nejsou, 2 hráč je sebral). Fy implementujeme pro čítač, jak dlouho by měly být viditelné.

Začneme testem viditelnosti. Pokud se nemají zobrazit, přeskočíme vykreslení. Pokud ano, resetujeme matici a translací je umístíme. Protože mřížka začíná na dvacítce, přičteme tuto hodnotu k x*60. Ze stejného důvodu na ose y přičítáme 70. Dále orotujeme matici okolo osy z o úhel uložený v hourglass.spin. Před vykreslením ještě zvolíme náhodnou barvu.

if (hourglass.fx==1)// Hodiny se mají vykreslit

{

glLoadIdentity();// Reset Matice

glTranslatef(20.0f+(hourglass.x*60),70.0f+(hourglass.y*40),0.0f);// Umístění

glRotatef(hourglass.spin,0.0f,0.0f,1.0f);// Rotace ve směru hodinových ručiček

glColor3ub(rand()%255,rand()%255,rand()%255);// Náhodná barva

Pomocí GL_LINES oznámíme kreslení linek. Horní levý bod získáme odečtením pěti pixelů v obou směrech. Konec přímky leží pět pixelů směrem vpravo dolů od aktuální pozice. Druhou linku začneme vpravo nahoře a skončíme vlevo dole. Tvar písmene X doplníme o horní a dolní uzavírací linku.

glBegin(GL_LINES);// Vykreslení přesýpacích hodin

glVertex2d(-5,-5);// Levý horní bod

glVertex2d( 5, 5);// Pravý dolní bod

glVertex2d( 5,-5);// Pravý horní bod

glVertex2d(-5, 5);// Levý dolní bod

glVertex2d(-5, 5);// Levý dolní bod

glVertex2d( 5, 5);// Pravý dolní bod

glVertex2d(-5,-5);// Levý horní bod

glVertex2d( 5,-5);// Pravý horní bod

glEnd();// Konec kreslení

}

Dále vykreslíme hráče. Opět resetujeme matici a určíme pozici ve scéně. Všimněte si, že pro jemný neskokový pohyb používáme fx a fy. Natočíme matici o uložený úhel, zvolíme světle zelenou barvu a pomocí linek vykreslíme tvar písmene X.

glLoadIdentity();// Reset Matice

glTranslatef(player.fx+20.0f,player.fy+70.0f,0.0f);// Přesun na pozici

glRotatef(player.spin,0.0f,0.0f,1.0f);// Rotace po směru hodinových ručiček

glColor3f(0.0f,1.0f,0.0f);// Zelená barva

glBegin(GL_LINES);// Vykreslení hráče

glVertex2d(-5,-5);// Levý horní bod

glVertex2d( 5, 5);// Pravý dolní bod

glVertex2d( 5,-5);// Pravý horní bod

glVertex2d(-5, 5);// Levý dolní bod

glEnd();// Konec kreslení

Aby nevypadal až tak nudně, přidáme ještě tvar znamínka +, které se otáčí trochu rychleji, má tmavší barvu a je o dva pixely větší.

glRotatef(player.spin*0.5f,0.0f,0.0f,1.0f);// Rotace po směru hodinových ručiček

glColor3f(0.0f,0.75f,0.0f);// Tmavší zelená barva

glBegin(GL_LINES);// Pokračování kreslení hráče

glVertex2d(-7, 0);// Levý středový bod

glVertex2d( 7, 0);// Pravý středový bod

glVertex2d( 0,-7);// Horní středový bod

glVertex2d( 0, 7);// Dolní středový bod

glEnd();// Konec kreslení

Ještě zbývá vykreslit nepřátele, takže se do nich pustíme. Deklarujeme cyklus procházející všechny nepřátele, kteří jsou viditelní v konkrétním levelu. Tento počet získáme vynásobením levelu s obtížností. Jejich maximální počet je devět. Uvnitř smyčky resetujeme matici a umístíme právě vykreslovaného nepřítele pomocí fx a fy (může se pohybovat). Zvolíme růžovou barvu a pomocí linek vykreslíme čtverec postavený na špičku, který nerotuje.

for (loop1=0; loop1<(stage*level); loop1++)// Vykreslí nepřátele

{

glLoadIdentity();// Reset matice

glTranslatef(enemy[loop1].fx+20.0f,enemy[loop1].fy+70.0f,0.0f);// Přesun na pozici

glColor3f(1.0f,0.5f,0.5f);// Růžová barva

glBegin(GL_LINES);// Vykreslení nepřátel

glVertex2d( 0,-7);// Horní bod

glVertex2d(-7, 0);// Levý bod

glVertex2d(-7, 0);// Levý bod

glVertex2d( 0, 7);// Dolní bod

glVertex2d( 0, 7);// Dolní bod

glVertex2d( 7, 0);// Pravý bod

glVertex2d( 7, 0);// Pravý bod

glVertex2d( 0,-7);// Horní bod

glEnd();// Konec kreslení

Přidáme krvavě červené X, které se otáčí okolo osy z a poté ukončíme obrovskou vykreslovací funkci.

glRotatef(enemy[loop1].spin,0.0f,0.0f,1.0f);// Rotace vnitřku nepřítele

glColor3f(1.0f,0.0f,0.0f);// Krvavá barva

glBegin(GL_LINES);// Pokračování kreslení nepřátel

glVertex2d(-7,-7);// Levý horní bod

glVertex2d( 7, 7);// Pravý dolní bod

glVertex2d(-7, 7);// Levý dolní bod

glVertex2d( 7,-7);// Pravý horní bod

glEnd();// Konec kreslení

}

return TRUE;// Konec funkce

}

Změn ve funkci WinMain() bude také trochu víc. Protože se jedná o hru, musíme ošetřit ovládání klávesnicí, časování a vše ostatní, co jsme dosud neudělali.

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)

{

MSG msg;

BOOL done=FALSE;

if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start FullScreen?", MB_YESNO|MB_ICONQUESTION) == IDNO)

{

fullscreen=FALSE;

}

Změníme titulek okna na "NeHe's Line Tutorial" a přidáme volání funkce ResetObjects(), která inicializuje pozici hráče na levý horní roh a nepřátelům předělí náhodné umístění, nejméně však pět políček od hráče. Poté zavoláme funkci pro inicializaci timeru.

if (!CreateGLWindow("NeHe's Particle Tutorial",640,480,16,fullscreen))

{

return 0;

}

ResetObjects();// Inicializuje pozici hráče a nepřátel

TimerInit();// Zprovoznění timeru

while(!done)

{

if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))

{

if (msg.message==WM_QUIT)

{

done=TRUE;

}

else

{

TranslateMessage(&msg);

DispatchMessage(&msg);

}

}

else

{

Teď zajistíme, aby pracoval kód pro časování. Předtím než vykreslíme scénu, nagrabujeme aktuální čas a uložíme jej do desetinné proměnné nazvané start. Potom vykreslíme scénu a prohodíme buffery.

float start=TimerGetTime();// Nagrabujeme aktuální čas

if ((active && !DrawGLScene()) || keys[VK_ESCAPE])

{

done=TRUE;

}

else

{

SwapBuffers(hDC);

}

Vytvoříme časové zpoždění a to tak, že vkládáme prázdné příkazy tak dlouho, dokud je aktuální hodnota časovače (TimerGetTime()) menší než počáteční hodnota sečtená s rychlostí kroky hry krát dva. tímto velmi jednoduše zpomalíme OPRAVDU rychlé systémy.

Protože používáme krokování rychlosti (určené proměnnou adjust) program vždy poběží stejnou rychlostí. Například, pokud je hodnota kroku rovna jedné, měli bychom čekat dokud timer nebude větší nebo roven dvěma (2*1). Ale pokud zvětšíme rychlost kroku na dva (způsobí, že se hráč bude pohybovat o dvakrát tolik pixelů najednou), zpoždění se zvětší na čtyři (2*2). Ačkoli se pohybujeme dvakrát tak rychle, zpoždění trvá dvakrát déle a tudíž hra běží stejně rychle (ale více trhaně).

Spousta lidí jde ale jinou cestou než my. Je třeba brát v úvahu čas který uběhl mezi jednotlivými cykly ve kterých se renderuje. Na začátku každého cyklu se uloží aktuální čas, od kterého se odečte čas v minulém cyklu a tímto rozdílem se vydělí rychlost, kterou se má objekt pohybovat. Například: máme auto, které má jet rychlostí 10 jednotek za sekundu. Víme, že mezi tímto a předchozím cyklem uběhlo 20 ms. Objekt musíme tedy posunout o (10*20)/1000 = 0,2 jednotek. Bohužel v tomto programu to takto provést nemůžeme, protože používáme mřížku a ne např. otevřenou krajinu. Hodnoty fx a fy musí být přesně určené. Pokud hráčova fx bude řekněme 59 a počítač rozhodne posunout hráče o dva pixely doprava, tak po stisku šipky nahoru hráč nepůjde po "šedesátých pixelech", ale o kousek vedle.

Překl.: Nicméně i naše metoda má jeden velký error - okno nemůže v čekacích cyklech zpracovávat žádné zprávy. Řekněme, že bude (poněkud přeženu) časové zpoždění 5 sekund. Okno není aktivní a uživateli připadá, že v programu nastala fatální chyba. Pokusí se ho ukončit, ale i to se mu podaří až za těchto pět sekund. A pokud se bude zpomalovací kód volat častěji (např. po každém překreslení)... chápete? I u nás je tento problém trochu znatelný. Pokud se pokoušíte zatočit do určité linky, někdy se strefíte až na několikátý pokus - program nezareaguje včas. Proč vlastně vzniklo vícevláknové programování? Aby odstranilo zdánlivě "spadnuté programy" při náročných a dlouho trvajících výpočtech. Já osobně, bych se takovémuto časování za každou cenu vyhnul.

// Plýtvá cykly procesoru na rychlých systémech

while(TimerGetTime() < start + float(steps[adjust] * 2.0f))

{

}

if (keys[VK_F1])

{

keys[VK_F1]=FALSE;

KillGLWindow();

fullscreen =! fullscreen;

if (!CreateGLWindow("NeHe's Line Tutorial",640,480,16,fullscreen))

{

return 0;

}

}

Přejdeme k ovládání klávesnicí. Po stisku 'A' znegujeme proměnnou anti a tím oznámíme kódu pro kreslení, že má nebo nemá používat antialiasing.

if (keys['A'] && !ap)// Stisk A

{

ap = TRUE;// Nastaví příznak

anti=!anti;// Zapne/vypne antialiasing

}

if (!keys['A'])// Uvolnění A

{

ap=FALSE;// Vypne příznak

}

Teď pohyb a logika nepřátel. Chtěl jsem udržet kód opravdu jednoduchý, takže nečekejte žádné zázraky. Pracuje tak, že nepřátelé zjistí, kde je hráč a poté se vydají jeho směrem (na pozici x, y). Mohou například vidět, že je v hracím poli nahoře, ale v čase, kdy testovali pozici x, hráč už může být díky fx úplně někde jinde. Častokrát se dostanou tam, kde byl o krok předtím. Někdy vypadají opravdu zmateně.

Začneme ujištěním se, jestli už není konec hry a jestli je okno aktivní. Pokud se například minimalizovalo, nepřátelé se nebudou na pozadí pohybovat.

Vytvoříme cyklus, který i tentokrát prochází všechny nepřátele.

if (!gameover && active)// Není-li konec hry a okno je aktivní

{

for (loop1=0; loop1<(stage*level); loop1++)// Prochází všechny nepřátele

{

V případě, že bude x pozice nepřítele menší než x pozice hráče a zároveň se také musí rovnat y*40 pozici y (jsme v průsečíku vertikální a horizontální linky) posuneme nepřítele doprava. Analogickým způsobem implementujeme i pohyb doleva, nahoru a dolů.

Poznámka: po změně pozic x a y nelze vidět žádný pohyb, protože při vykreslování objekty umísťujeme pomocí proměnných fx a fy. Změnou x a y jenom určujeme požadovaný směr pohybu.

if ((enemy[loop1].x < player.x) && (enemy[loop1].fy==enemy[loop1].y*40))

{

enemy[loop1].x++;// Přesun o políčko doprava

}

if ((enemy[loop1].x > player.x) && (enemy[loop1].fy==enemy[loop1].y*40))

{

enemy[loop1].x--;// Přesun o políčko doleva

}

if ((enemy[loop1].y < player.y) && (enemy[loop1].fx==enemy[loop1].x*60))

{

enemy[loop1].y++;// Přesun o políčko dolů

}

if ((enemy[loop1].y > player.y) && (enemy[loop1].fx==enemy[loop1].x*60))

{

enemy[loop1].y--;// Přesun o políčko nahoru

}

Následující kód provádí opravdový pohyb. Zjistíme, zda je proměnná delay větší než tři mínus level. Pokud jsme v levelu jedna, program pojde cyklem dvakrát (3-1=2), předtím než se nepřítel opravdu pohne. V levelu tři (nejvyšší možný) se nepřátelé budou pohybovat stejnou rychlostí jako hráč - tedy bez zpoždění. Také ověřujeme, jestli se hourglas.fx nerovná dvěma. Tato proměnná označuje hráčovo sebrání přesýpacích hodin. V takém případě nepřítelem nepohybujeme.

Pokud je zpoždění vyšší než tři mínus level a hráč nesebral hodiny, pohneme nepřítelem úpravou proměnných fx a fy. Nejprve vynulujeme zpoždění, takže ho budeme moci znovu počítat a potom opět deklarujeme cyklus, který prochází všechy viditelné nepřátele.

if (delay > (3-level) && (hourglass.fx!=2))// Hráč nesebral přesýpací hodiny

{

delay=0;// Reset delay na nulu

for (loop2=0; loop2<(stage*level); loop2++)// Prochází všechny nepřátele

{

Nepřítel se vždy pohybuje pomocí fx/fy směrem k x/y. V prvním if zjistíme jestli je fx menší než x*60. V takovém případě ho posuneme doprava o vzdálenost steps[adjust]. Také změníme jeho úhel natočení, aby vznikl dojem rolování doprava.

Úplně stejně provedeme pohyby doleva, dolů a nahoru.

if (enemy[loop2].fx < enemy[loop2].x*60)// Fx je menší než x

{

enemy[loop2].fx+=steps[adjust];// Zvýšit fx

enemy[loop2].spin+=steps[adjust];// Rotace ve směru hodinových ručiček

}

if (enemy[loop2].fx > enemy[loop2].x*60)// Fx je větší než x

{

enemy[loop2].fx-=steps[adjust];// Snížit fx

enemy[loop2].spin-=steps[adjust];// Rotace proti směru hodinových ručiček

}

if (enemy[loop2].fy < enemy[loop2].y*40)// Fy je menší než y

{

enemy[loop2].fy+=steps[adjust];// Zvýšit fy

enemy[loop2].spin+=steps[adjust];// Rotace ve směru hodinových ručiček

}

if (enemy[loop2].fy > enemy[loop2].y*40)// Fy je větší než y

{

enemy[loop2].fy-=steps[adjust];// Snížit fy

enemy[loop2].spin-=steps[adjust];// Rotace proti směru hodinových ručiček

}

}

}

Pohyb tedy máme. nyní potřebujeme vyřešit náraz nepřátel do hráče. V případě, že se obě fx i obě fy rovnají... hráč zemře. Dekrementujeme životy a v případě jejich nulové hodnoty prohlásíme hru za skončenou. Resetujeme všechny objekty a necháme zahrát úmrtní skladbu.

Zvuky jsou v našich tutoriálech novinkou. rozhodl jsem se použít tu nejzákladnější dostupnou rutinu... PlaySound(). Předáváme jí tři parametry. První určuje cestu k souboru se zvukem. Druhý parametr pomocí nulového ukazatele ignorujeme. Třetí parametr je flag stylu. Dva nejčastěji používané jsou: SND_SYNC, který zastaví provádění programu, dokud přehrávání zvuku neskončí. Druhá možnost, SND_ASYNC, přehrává zvuk nezávisle na běhu programu. Dáme přednost maličkému zpoždění, takže funkci předáme SND_SYNC.

Na začátku tutoriálu jsem zapomněl na jednu věc: Abychom mohli používat funkci PlaySound(), potřebujeme inkludovat knihovnu WINMM.LIB (Windows Multimedia Library). Ve Visual C++ to lze provést v nabídce Project/Setting/Link.

// Setkání nepřítele s hráčem

if ((enemy[loop1].fx==player.fx) && (enemy[loop1].fy==player.fy))

{

lives--;// Hráč ztrácí život

if (lives==0)// Nulový počet životů

{

gameover=TRUE;// Konec hry

}

ResetObjects();// Reset pozice hráče a nepřátel

PlaySound("Data/Die.wav", NULL, SND_SYNC);// Zahraje umíráček

}

}

Ošetříme stisk kurzorových kláves. Vyřešíme šipku doprava, ostatní směry jsou zcela analogické. Abychom nevypadli pryč z hracího pole musí být player.x menší než deset (šířka mřížky). Nechceme, aby mohl změnit směr uprostřed přesunu a tak kontrolujeme, zda se fx==player.x*60 a fy==player.y*40. Nastanou-li obě rovnosti, můžeme s určitostí říci, že se nachází v průsečíku rovnoběžné se svislou linkou a tedy dokončil svůj pohyb. Platí-li všechny podmínky, označíme linku pod hráčem jako přejetou a posuneme jej na následující pozici.

if (keys[VK_RIGHT] && (player.x<10) && (player.fx==player.x*60) && (player.fy==player.y*40))

{

hline[player.x][player.y]=TRUE;// Označení linky

player.x++;// Doprava

}

if (keys[VK_LEFT] && (player.x>0) && (player.fx==player.x*60) && (player.fy==player.y*40))

{

hline[player.x][player.y]=TRUE;// Označení linky

player.x--;// Doleva

}

if (keys[VK_DOWN] && (player.y<10) && (player.fx==player.x*60) && (player.fy==player.y*40))

{

vline[player.x][player.y]=TRUE;// Označení linky

player.y++;// Dolů

}

if (keys[VK_UP] && (player.y>0) && (player.fx==player.x*60) && (player.fy==player.y*40))

{

vline[player.x][player.y]=TRUE;// Označení linky

player.y--;// Nahoru

}

Hráče máme, dá se říci, přesunutého - ale pouze v programu! Je viditelný stále na stejném místě, protože ho vykreslujeme pomocí fx a fy. Provnáme, polohu fx vzhledem k x a pokud se nerovnají, snížíme vzdálenost mezinimi o přesně daný úsek. Po několika překresleních se začnou obě hodnoty rovnat, což značí, že dokončil pohyb a nyní se nachází v průsečíku linek. Při následném stisku klávesy můžeme začít hráče znovu posunovat (viz. kód výše).

if (player.fx<player.x*60)// Fx je menší než x

{

player.fx+=steps[adjust];// Zvětší fx

}

if (player.fx>player.x*60)// Fx je větší než x

{

player.fx-=steps[adjust];// Zmenší fx

}

if (player.fy<player.y*40)// Fy je menší než y

{

player.fy+=steps[adjust];// Zvětší fy

}

if (player.fy>player.y*40)// Fy je větší než y

{

player.fy-=steps[adjust];// Zmenší fy

}

}

Nastane-li konec hry, projde program větví else. V ní je pouze test stisku mezerníku, který znovu spustí hru. Nastavíme filled na TRUE a díky tomu si program bude myslet, že je mřížka kompletně vyplněná - resetuje se pozice hráče i nepřátel. Abychom byli přesní, program si vlastně myslí, že jsme dokončili level, a proto inkrementuje do stage přiřazenou nulu na jedna. Přesně tohle chceme. Život vrátíme na počáteční hodnotu.

else// Jinak (if (!gameover && active))

{

if (keys[' '])// Stisknutý mezerník

{

gameover = FALSE;// Konec hry

filled = TRUE;// Mřížka vyplněná

level = 1;// Level

level2 = 1;// Zobrazovaný level

stage = 0;// Obtížnost hry

lives = 5;// Počet životů

}

}

Následující část testuje, zda je mřížka kompletně vyplněná. Filled může být nastaveno na TRUE celkem dvěma způsoby. Buď je mřížka úplně vyplněná, nebo skončila hra (zabitím hráče; nula životů) a uživatel stiksl mezerník, aby ji restartoval.

if (filled)// Vyplněná mřížka?

{

Ať už to způsobil kterýkoli případ je nám to celkem jedno. Vždy zahrajeme zvuk značící ukončení levelu. Už jsme jednou vysvětloval, jak PlaySound() pracuje. Předáním SND_SYNC vytvoříme časové zpoždění, kdy program čeká až zvuk dohraje.

PlaySound("Data/Complete.wav", NULL, SND_SYNC);// Zvuk ukončení levelu

Potom inkrementujeme stage a zjistíme, jestli není větší než tři. Pokud ano, vrátíme ho na jedno, zvětšíme vnitřní i zobrazovaný level o jedničku.

stage++;// Inkrementace obtížnosti

if (stage > 3)// Je větší než tři?

{

stage=1;// Reset na jedničku

level++;// Zvětší level

level2++;// Zvětší zobrazovaný level

Pokud bude vnitřní level větší než tři, vrátíme ho zpět na trojku a přidáme hráči jeden život, ale pouze do maximálních pěti. Více živý nikdy nebude.

if (level>3)// Je level větší než tři?

{

level=3;// Vrátí ho zpátky na tři

lives++;// Život navíc

if (lives > 5)// Má víc životů než pět?

{

lives = 5;// Maximální počet životů pět

}

}

}

Resetujeme všechny objekty ve hře (hřáč, nepřátelé) a vynulujeme flag projetí všech linek na FALSE. Pokud bychom to neudělali, další level by byl předčasně ukončen - program by opět skočil do tohoto kódu. Mimochodem, je úplně stejný jako kód pro vykrelsování mřížky.

ResetObjects();// Reset pozice hráče a nepřátel

for (loop1=0; loop1<11; loop1++)// Cyklus skrz x koordináty mřížky

{

for (loop2=0; loop2<11; loop2++)// Cyklus skrz y koordináty mřížky

{

if (loop1 < 10)// X musí být menší než deset

{

hline[loop1][loop2] = FALSE;// Nulování

}

if (loop2 < 10)// Y musí být menší než deset

{

vline[loop1][loop2] = FALSE;// Nulování

}

}

}

}

Pokusíme se umplementovat hráčovo sebrání přesýpacích hodin. Že si musí polohy odpovídat je, myslím si, jasné. Nicméně přidáváme ještě podmínku hourgalss.fx==1. Nejedná se o žádnou polohu. Fx používáme jako indikátor toh, že jsou zobrazené na monitoru.

// Hráč sebral přesýpací hodiny

if ((player.fx==hourglass.x*60) && (player.fy==hourglass.y*40) && (hourglass.fx==1))

{

Necháme zahrát zvuk zmrazení. Aby zvuk zněl na pozadí, používáme SND_ASYNC. Díky OR-ování se symbolickou konstantou SND_LOOP docílíme toho, že se po dokončení přehrávání zvuku sám znovu spustí. Zastavit ho můžeme buď požadavkem na zastvení, nebo přehráním jiného zvuku.

Aby hodiny nebyly dále zobrazené nastavíme fx na dva. Také přiřadíme do fy nulu. Fy je něco jako čítač, který inkrementujeme do určité hodnoty, po jejímž přetečení změníme hodnotu fx.

PlaySound("Data/freeze.wav", NULL, SND_ASYNC | SND_LOOP);// Zvuk zmrazení

hourglass.fx=2;// Skryje hodiny

hourglass.fy=0;// Nuluje čítač

}

Následující kód zajišťuje narůstání rotace hráče o polovinu nižší rychlostí než má hra. V případě, že bude hodnota vyšší než 360° odečteme 360. Tím zajistíme, aby nebyla moc vysoká.

player.spin += 0.5f * steps[adjust];// Rotace hráče

if (player.spin>360.0f)// Úhel je větší než 360°

{

player.spin -= 360;// Odečte 360

}

Aby se hodiny točily opačným směrem než hráč, namísto zvyšování, úhel snižujeme. Rychlost je čtvrtinová oproti rychlosti hry. Opět ošetříme podtečení proměnné.

hourglass.spin-=0.25f*steps[adjust];// Rotace přesýpacích hodin

if (hourglass.spin < 0.0f)// Úhel je menší než 0°

{

hourglass.spin += 360.0f;// Přičte 360

}

Zvětšíme hodnotu čítače přesýpacích hodin, o které jsme mluvili před chvílí. Opět podle rychlosti hry. Dále zjistíme, jestli se hourglass.fx rovná nule (nejsou zobrazené) a zároveň jelsti je čítač větší než 6000 děleno level. V takovém případě přehrajeme zvuk zobrazení, vygenerujeme novou pozici a přes fx=1 hodiny zobrazíme. Vynulujeme čítač, aby mohl počítat znovu.

hourglass.fy+=steps[adjust];// Zvětšení hodnoty čítače přesýpacích hodin

if ((hourglass.fx==0) && (hourglass.fy > 6000/level))// Hodiny jsou skryté a přetekl čítač

{

PlaySound("Data/hourglass.wav", NULL, SND_ASYNC);// Zvuk zobrazení hodin

hourglass.x = rand()%10+1;// Náhodná pozice

hourglass.y = rand()%11;// Náhodná pozice

hourglass.fx = 1;// Zobrazení hodin

hourglass.fy = 0;// Nulování čítače

}

Překl-li čítač v době, kdy jsou hodiny viditelné (fx==1), schováme je a opět vynulujeme čítač.

if ((hourglass.fx==1) && (hourglass.fy>6000/level))// Hodiny jsou zobrazené a přetekl čítač

{

hourglass.fx = 0;// Skrýt hodiny

hourglass.fy = 0;// Nulování čítače

}

Při hráčově sebrání hodin jsme zmrazili všechny nepřátele. Nyní je rozmrazíme. Fx==2 indikuje, že byly hodiny sebrány. Fy porovnáváme s vypočtenou hodnotou. Jsou-li obě podmínyk pravdivé, vypneme zvuk, který zní ve smyčce na pozadí a to tak, že přehrajeme nulový zvuk. Zneviditelníme hodiny a vynulujeme jejich čítač.

if ((hourglass.fx==2) && (hourglass.fy>500+(500*level)))// Nepřátelé zmrazení a přetekl čítač

{

PlaySound(NULL, NULL, 0);// Vypne zvuk zmrazení

hourglass.fx = 0;// Skrýt hodiny

hourglass.fy = 0;// Nulování čítače

}

Na samém konci hlavní smyčky programu inkrementujeme proměnnou delay. To je, myslím si, vše.

delay++;// Inkrementuje čítač zpoždění nepřátel

}

}

KillGLWindow();// Zruší okno

return (msg.wParam);// Ukončí program

}

Psaním tohoto tutoriálu jsem strávil spoustu času. Začínal jako zcela jednoduchý tutoriál o linkách, který se úplně nečekaně rozvinul v menší hru. Doufejme, že budete moci ve svých programech využít vše, co jste se zde naučili. Vím, že se spousta z vás ptala po hře s kostičkami a políčky. Nemohli jste dostat více kostičkovatější a více políčkovatější hru než je tato. Ačkoli lekce nevysvětluje mnoho nových věcí o OpenGL, myslím si, že časování a zvuky jsou také důležité - zvlášť ve hrách. Co ještě napsat? Asi nic...

napsal: Jeff Molofee - NeHe <nehe (zavináč) connect.ab.ca>
přeložil: Michal Turek - Woq <WOQ (zavináč) seznam.cz>

Zdrojové kódy

Lekce 21

<<< Lekce 20 | Lekce 22 >>>