Copyright © 1997-1999 George Foot
Preklad (Translation) © 2002 Anino Belan
Táto dokumentácia sa môže šíriť bezo zmeny v ľubovoľnej podobe bez obmedzenia. V prípade šírenia zmenených kópií je nutné uviesť, že došlo ku zmene.
Toto je siedma verzia, posledná zmena v origináli bola urobená 26 Augusta 1999
Tento dokument môžete bezo zmien voľne šíriť v akejkoľvek forme. Nechcem za to nič, ale e-mail poteší.
Môžete tiež šíriť zmenené kópie v akejkoľvek forme, ak sa postaráte o to, aby bolo zrejmé, že tieto kópie sú zmenené.
Neprijímam žiadnu zodpovednosť za akúkoľvek škodu, ktorú použitie alebo zneužitie tohto tutoriálu môže komukoľvek spôsobiť. Nie je zaručené, že príklady, ktoré sú súčasťou tohto balíka budú fungovať ani nie je zaručené, že sú bezpečné. Vo všeobecnosti by ste v žiadnom prípade nemali kompilovať programy, ktorým nerozumiete.
Tento tutoriál je dostupný v niekoľkých verziach - textový súbor, Info formát (ktorý funguje aj s RHIDE) a v zdrojovom tvare ako Texinfo. Zdrojová distribúcia obsahuje nástroje na vytvorenie iných formátov včítane DVI a Postscript-u, ktoré môžu byť vytlačené. Podpora pre HTML sa plánuje. (pozn. prekl. V slovenčine existuje len HTML verzia, ak z toho spravite aj nejaké iné, nech sa páči.)
K tutoriálu patrí aj súbor ukážkových programov, väčšinou napísaných Grzegorzom Adamom Hanciewiczom. Demonštrujú funkcie a techniky o ktorých sa hovorí v texte tutoriálu a sú hodnotným zdrojom informácií, ak sa vám nedarí niečo pochopiť.
Všetky časti Allegro Vivace - tutoriál v hlavných formátoch, zdrojová distribúcia tutoriálu a ukážkové programy - môžu byť stiahnuté jednotlivo alebo po skupinách z webovej stránky:
http://www.canvaslink.com/gfoot/vivace/
Ak ma chcete kontaktovať, moja e-mailová adresa je:
gfoot@users.sourceforge.net
Komentáre, návrhy a sťažnosti sú, ako vždy, veľmi cenené.
Zámerom tohto tutoriálu je viesť nováčikov k programovaniu hier a Allegru s pomocou procesu vytvárania jednoduchej hry.
Ako bolo povedané vyššie, tento tutoriál je určený pre nováčikov v programovaní hier a Allegre. Nie je určený pre úplných začiatočníkov v jazyku C. Tým myslím, že sa predpokladá istá znalosť a skúsenosti s používaním jazyka C a očakávam, že už máte funkčný kompilátor, ktorý podporuje Allegro.
Pod funkčným kompilátorom myslím taký, ktorý vie kompilovať programy v jazyku C a urobiť z nich programy spustiteľné na vašej platforme. Tiež samozrejme musí byť schopný pracovať s knižnicou Allegro, ale tomu sa budeme venovať nižšie.
Ak váš kompilátor nefunguje správne, potrebujete problém odstrániť a toto nie
je miesto, kde by som túto problematiku mal rozoberať do detailov. Ak používate
djgpp, prečítajte si readme.1st
a djgpp FAQ; ak to nepomôže,
konajte podľa inštrukcií, ktoré sú vo FAQ ohľadom poslielania otázok na mailing
list a usenetovú skupinu venovanú djgpp. Ak používate verziu djgpp v2.00, vrelo
odporúčam upgrade.
Tento tutoriál bol poslednýkrát zmenený, keď bola na svete verzia Allegro 4.0, ale nepokrýva ešte úplne všetky nové vlastnosti. Napriek tomu je Allegro 4.0 hlavnou verziou, pre ktorú je tento tutoriál písaný a čoskoro ho budem aktualizovať.
Väčšina vecí bude fungovať aj pre staršie verzie, ale niektoré nemusia --
taký je život. Urobím, čo budem môcť, aby som opísal zmeny potrebné pre Allegro
3.x. Aby mohli byť skompilované verzie Allegra staršie než 3.0, potrebujú C++.
Nemusíte v C++ programovať, ale váš počítač ho musí podporovať. Ak nepodporuje,
buď nainštalujte najnovšiu verziu Allegra (odporúča sa, sú samozrejme veľmi
stabilné) alebo nainštalovať podporu C++ (pre djgpp pozrite readme.1st
)
Ak nenainštalujete novú verziu allegra, budete musieť zmeniť časti kódu
ukážok z tohto tutoriálu.
Dúfame, že novšie verzie zmienených balíkov sú spätne kompatibilné so staršími; ak nie, je opäť na vás, aké zmeny urobíte.
Budete potrebovať program GNU make, napriek faktu, že je v súbore
readme.1st
z djgpp označený ako nepovinný. Je to náramne užitočný
balík a aj Allegro aj príklady sprevádzajúce tento tutoriál sú navrhnuté tak,
aby sa dali (bezbolestne) skompilovať s jeho pomocou. Najnovšia verzia pre djgpp
(počas písania) je mak3761b.zip
, ktorá patrí k verzii djgpp 2.01 a
nefunguje správne s verziou 2.00.
Väčšina ľudí pri programovaní používa jeden z troch spôsobov: niekto používa IDE ako napríklad RHIDE, niekto má radšej Emacs a iní používajú jednoduché textové editory a utilitu Make. Tento tutoriál sa nesnaží vnútiť vám niektorý z týchto systémov; programujte v tom prostredí, ktoré máte najradšej. Príklady môžu byť skompilované utilitou Make, ale jednoducho pre ne môžete vytvoriť RHIDE projekty.
Tento tutoriál je určený ľuďom s rôznymi programátorskými schopnosťami, od tých, ktorí sa ešte len učia až po tých, ktorí už majú množstvo skúseností. Ak máte pocit, že niektorá časť zachádza priveľmi do podrobností, tak to všetko nečítajte -- táto časť je najskôr určená ľuďom s nižším štandardom, než máte vy a stačí, keď ju preletíte, aby ste sa zoznámili s príkazmi, ktoré sú v nej uvedené.
Nadpisy niektorých sekcií som uzavrel medzi [ a ]. Znamená to, že kapitoly obsahujú témy pre pokročilých, komplikované, ktoré je namáhavé programovať alebo sú len okrajové. Ak nechcete, nečítajte to.
Veľkým problémom pri písaní hier je inšpirácia -- ak nemáte v hlave nič, čo by ste chceli napísať, tak to ani nenapíšete. V prvom momente vás pravdepodobne buď nenapadne nič, alebo dostanete spústu nápadov, ktoré ale budú vyzerať príliš zložito na to, aby sa dali naprogramovať.
Ak vás nenapadne nič, relaxujte a čítajte tento tutoriál. Použite ukážkové príklady ako základ, pridajte k nim nové veci a prispôsobte si ich podľa svojej potreby. Počas čítania získate lepší prehľad o tom, čo sa dá urobiť a čo nie. Mrknite sa aj na komerčné hry -- skúste, či viete na prvý pohľad povedať, ako robia to, čo robia a potom to skúste vylepšiť. Nehovorte si, že je to niečo, čo nikdy nezvládnete -- ako budete získavať skúsenosti, budete prichádzať na rôzne spôsoby, ako veci spraviť. Pamätajte si, že ak to autori nejako napísali, musí sa to dať urobiť.
Ak sú vaše nápady príliš zložité, to čo bolo napísané vyššie platí tiež -- čím viac hier napíšete, tým lepší v tom budete a tým viac hier budete schopní napísať. V takmer každom prípade je pre začiatočníkov najlepšie, keď začnú s jednoduchými vecami a urobia rýchle fungujúcu hru a potom na tom základe stavajú. Táto technika má svoje nevýhody, ale nateraz bude celkom dobrá.
A nakoniec -- hry musia prinášať zábavu; je to ich cieľom. Nemôžete napísať peknú hru, ak sa netešíte z toho, že ju píšete. Ak sa vám bude tento tutoriál zdať nudný, tak som ho napísal zle a treba ho zmeniť, dajte mi, prosím, vedieť. Tento tutoriál je pre vás a vám podobných; užite si ho!
Allegro je knižnica určená na programovanie hier, ktorej hlavným autorom a koordinátorom je Shawn Hargreaves. Je to veľmi dobrá knižnica; je rýchla, má množstvo vlastností a od verzie 4 predstavuje skvelú multiplatformnú vrstvu, ktorá vám umožňuje písať kód, ktorý bude fungovať aj na systémoch, ktoré ste v živote nevideli. Ak chcete podrobnejšie informácie, pozrite si domovskú stránku projektu:
http://www.talula.demon.co.uk/allegro/
Allegro je dostupné v prvom rade na linkách uvedených na jeho domovskej
stránke (pozri Čo je to Allegro?) a tiež ako
súčasť distribúcie djgpp na ftp.simtel.net
. Verzie, ktoré sú
pribalené k
djgpp sú zvyčajne posledné stabilné verzie. Ak chcete najnovšiu vývojovú verziu,
musíte ísť na stránku allegra.
Odporúčam vám stiahnuť si vývojovú verziu, pretože tento tutoriál bol písaný s prihliadnutím na ňu. Je celkom stabilná. Počas písania tohto tutoriálu bola vonku už niekoľko týždňov verzia 3.9.25 a musím povedať, že som s ňou nemal žiadne väčšie problémy. (pozn.prekl. Už pomerne dlho sú na svete aj stabilné verzie radu 4. Ja osobne som mal lepšie skúsenosti so stabilnými, než s vývojovými verziami.)
Tieto inštrukcie majú slúžiť len ako stručný návod - postup sa líši od platformy k platforme a je vysvetlený oveľa podrobnejšie v zodpovedajúcich readme súboroch, ktoré sú vo verzii, ktorú ste si stiahli.
Vytvorte pre Allegro adresár kdekoľvek chcete a rozbaľte do neho Allegro tak,
aby ste zachovali jeho adresárovú štruktúru (parameter -d
pre
pkunzip
pod DOSom; Unixové rozpakovávače to spravia automaticky a
žiadne prepínače nie sú nutné). Uistite sa, že bol vytvorený napríklad adresár s
názvom docs
.
Keď to bude hotové, choďte do adresára kde Allegro máte. Ak používate vývojovú verziu, treba spustiť fix, ktorý distribúciu upraví pre vašu platformu. Pre DOS a djgpp napíšte:
fixdjgpp
Pre Unixové systémy napíšte:
chmod 700 fixunix.sh ./fixunix.sh
Potom bude treba skonfigurovať kompiláciu. Pozrite si readme súbor pre vašu verziu a platformu, aby ste sa dozvedeli detaily.
Aby ste skompilovali knižnicu a nástroje, zadajte 'make':
make
To by potom malo bežať samo a skončiť krátkou správou, ktorá vám povie, že to
funguje. Ak sa tak nestane, prečítajte si dokumentáciu k Allegru ak ste tak ešte
nespravili (v adresári v ktorom máte Allegro súbory allegro.txt
,
faq.txt
a čiastočne aj readme.txt
).
Nakoniec potrebujete nainštalovať knižnicu a hlavičkové súbory, aby ich vaše programy mohli jednoducho používať:
make install
Pod Unixom budete pravdepodobne potrebovať práva roota, aby ste to mohli urobiť.
Ak ste niekde zamrzli, skúste sa popýtať na Allegrovskej konferencii --
allegro@canvaslink.com
. Ak potrebujete informáciu o tom, ako sa do
konferencie prihlásiť, pozrite si časť Contact Info na
konci Allegrovského súboru readme.txt
. Väčšina ľudí píše odpovede
iba do konferencie, takže ak nie ste prihlásení, v maili to uveďte a poproste
ľudí, aby poslali Cc: aj vám.
Inštalácia je v podstate samotestujúca, pretože sa pri nej kompilujú všetky
Allegrovské vzorové príklady a nástroje. Napriek tomu som pre úplnosť zaradil
krátky program na kontrolu toho, či nie sú problémy s nájdením hlavičkového
súboru a knižnice. Ak chcete test spustiť, vojdite do adresára
examples
a napíšte:
make test
Ak tam nie sú žiadne chyby, skvelé - môžete skompilovať ostatné vzorové
programy z Vivace znovuspustením make
bez argumentu
test
.
Ak ale kompilátor nemôže nájsť allegro.h
,
liballeg.a
alebo -lalleg
, inštalácia neprebehla
správne a vy budete musieť znovu podrobnejšie prečítať dokumentáciu.
Táto časť stručne opakuje veci, ktoré sú v dokumentácii Allegra
Za prvé, každý zdrojový kód, ktorý používa funkcie alebo procedúry z Allegra,
musí obsahovať #include <allegro.h>
. Dajte to až za
všetky štandardné hlavičkové súbory -- konkrétne musí ísť až za #include
<stdio.h>
-- aby sa predišlo kolíziam medzi hlavičkovými súbormi.
Za druhé treba zavolať allegro_init()
krátko po štarte vášho
programu. Inicializuje to niektoré dôležité všeobecné veci, ktoré sú pre
knižnicu podstatné. Tiež budete potrebovať inicializovať jednotlivé subsystémy,
ktoré budete používať, ale k tomu sa vrátime neskôr. Odporýčam, aby bola
allegro_init()
prvá vec, ktorú spustíte vo vašej funkcii
main
.
Za tretie, musíte [V Allegro 4 a vývojových verziach] napísať
END_OF_MAIN()
za uzatváraciu zátvorku vašej funkcie main. Ak to
nespravíte, váš program sa nebude správne linkovať s knižnicou Allegro.
A nakoniec, pri linkovaní musíte prilinkovať knižnicu Allegro. Spraví sa to
tak, že na koniec vášho linkovacieho príkazu pridáte
-lalleg
. (pozn. prekl. V Linuxe treba pridať aj
-lalleg_unsharable
) Linkovací príkaz je ten, ktorý vám vyrobí
spustiteľný súbor; môže to byť jediný príkaz, ktorý používate. Ale počas tohto
tutoriálu vás budem povzbudzovať, aby ste vaše projekty rozdelili na viacero
súborov.
Ako príklad tohto všetkého si pozrite súbor test.c
v podadresári
examples/test/ex_1
hlavného adresára tutoriálu.
V každom prípade dosť už o tom -- poďme sa mrknúť na zaujímavejšie veci.
Väčšina hier je postavená na rovnakom základnom princípe. Sú samozrejme výnimky a ja nie som profesionály programátor hier, ale podľa mojich skúseností nasledovný systém funguje veľmi dobre.
Na začiatku potrebujeme dostať hru do nejakého známeho stavu a zakaždým sa to deje tým istým spôsobom. Napríklad môžete chcieť, aby hráč začínal vždy v strede obrazovky, vytvoriť bludisko alebo vygenerovať náhodnú mapu. Všetko toto sa udeje tu.
Hra potrebuje zabezpečiť, aby sa veci diali znova a znova kým hráč neumrie, nevyhrá, nevzdá sa alebo niečo také. To sa nám deje v hlavnom cykle hry. Ten má tri hlavné zložky:
Keď hra skončí, môžme potrebovať urobiť ešte nejaké ďalšie veci ako napríklad povedať hráčovi, prečo hra skončila, zmeniť tabuľku najlepších výsledkov alebo niečo na ten spôsob.
V skutočnosti je zvyčajne vyššie uvedená postupnosť súčasťou väčšieho cyklu, takže hra sa môže hrať znova a znova bez toho, aby ste sa medzitým vždy museli vracať do OS a pred vonkajším cyklom je tiež nejaká inicializácia a po ňom nejaký upratovací kód, napr. nahrávanie tabuľky najlepších výsledkov z disku na začiatku a jej znovuuloženie na konci. Pre začiatok ale budeme robiť len jeden beh hry za spustenie.
Takto vyzerá vyššie opísaná štruktúra v jazyku C:
#include <allegro.h> #include "game.h" int end_game; /* príznak, ktorý nastavíme, ak chceme skončiť */ int main (void) { allegro_init(); /* inicializuje knižnicu Allegro */ init(); /* inicializuje hru */ end_game = 0; /* príznak, že končíme */ do { /* cyklus */ input(); /* vezmi vstup */ process(); /* spracuj ho */ output(); /* vyrob výstup */ } while (end_game == 0); /* kým je príznak v poriadku */ shutdown(); /* zhoď veci, ktoré potrebuješ */ allegro_exit(); /* pre istotu */ return 0; /* povedz operačnému systému, že je všetko O.K. */ } END_OF_MAIN()
V uvedenom príklade je game.h
hlavičkový súbor, v ktorom sú
deklarácie funkcií init
, input
, process
,
output
a shutdown
, ktoré musia byť definované niekde
inde v projekte.
Už som spomínal, že vás budem povzbudzovať k tomu, aby ste svoj zdrojový kód rozdelili na viacero súborov. Mnohým ľuďom sa to zo začiatku zdá ťažké a nevedia pochopiť, na čo je to dobré. V tejto sekcii tutoriálu mám v úmysle vyjasniť to a ukázať, ako má podľa mňa vyzerať dobrý sytém organizácie projektu.
Takže najprv, prečo je projekt rozdelený do viacerých súborov dobrá vec? Veď to vyzerá, že len pribúdajú problémy -- vyžaduje to vytvárať hlavičkové súbory, robiť externé deklarácie a máte pocit, že musíte prehľadať viacero súborov, kým nájdete funkciu, ktorú hľadáte.
V skutočnosti má ale tento prístup mnohé pozitíva. Keď vo svojom kóde zmeníte jeden riadok, gcc, aby vytvorilo nový spustiteľný súbor, prekompiluje všetko. Ale ak je váš projekt rozdelený do viacerých súborov a vy zmeníte jeden z nich, gcc už má k dispozícii objektové súbory, ktoré vygenerovalo z vašich zdrojových súborov, keď ste ich kompilovali posledne a teraz potrebuje prekompilovať iba súbor, ktorý ste zmenili. Pri väčších projektoch to môže znamenať rozdiel v dĺžke (5 minút alebo viac, v závislosti na rýchlosti počítača) rekompilácie a dvadsaťsekundovou úpravou.
Pri troche organizáce môže rozdelenie projektu do súborov značne uľahčiť hľadanie kusu kôdu, ktorý práve zháňate. Je to jednoduché -- rozdeľte kód medzi súbory podľa toho, čo robí. Ak potom hľadáte funkciu, ktorá kreslí hráča, viete, že bude v zdrojovom súbore s grafikou.
Okrem toho, ak je váš program dostatočne modulárny a zdieľánie medzi súbormi je obmedzené na minimum, ľahšie sa hľádajú chyby. Ak majú k danej premennej prístup iba niektoré programy, vadná hodnota musí byť spôsobená chybou v jednom z týchto súborov.
To, čo bude nasledovať je len môj skromný názor; rôzni ľudia si môžu chcieť zariadiť veci rôzne. Ale keďže som zistil, že nasledujúce pravidlá sú užitočné, odporúčam vám dodržiavať ich.
#include
-nite jeden do druhého, ale nepíšte tú istú informáciu do
hlavičkových súborov dvakrát. Ak budete totiž informáciu chcieť zmeniť, stačí
vám ju zmeniť raz a vyhnete sa zdĺhavému hľadaniu duplicít, ktoré majú byť
zmenené tiež.
Dúfam, že ste pochopili výhody rozdelenia vášho projektu, ale ak nechcete, rozhodne ho nechajte v jednom súbore. Príklady, ktoré budem používať neskôr, budú štandardne mnohosúborové, založené na odporúčaniach podaných vyššie a na skôr diskutovanej štruktúre hry (pozri Návrh štruktúry), takže budete mať veľá možností oceniť výhody tohto prístupu.
Ak ste z toho stále zmätení, pozrite sa, prosím, na článok, ktorý som napísal pre elektronický magazín C-Scene:
http://www.cscene.org/
Článok je v časti 2 a volá sa Multi-file projects and the GNU Make utility. Opakuje informácie spomenuté tu, lenže podrobnejšie a vysvetľuje začiatočníkom v tejto technike niektoré bežné problémy.
Táto časť predpokladá, že ste oboznámení s pojmami programovania v grafike ako napríklad súradnicový systém, rozlíšenie atď. Ak nie ste, aj tak si ju skúste prečítať; ak nebudete rozumieť, mali by ste si pozrieť nejakú knižku o počítačovej grafike.
Aby ste dostali počítač do grafického režimu, použite túto funkciu:
int set_gfx_mode (int card, int w, int h, int v_w, int v_h);
Miesto card dajte jednu z GFX_*
konštánt, bežne
GFX_AUTODETECT
. w a h je minimálna šírka a
výška obrazovkového priestoru, ktorú požadujete; v závislosti na režime, ktorý
zvolíte, môžete dostať kúsok väčšiu plochu. Ako v_w a v_h
dajte zatiaľ 0.
Grafické funkcie Allegra nie sú obmedzené na zápis na obrazovku; zapisujú do
bitmáp. Bitmapa môže byť obrazovka, môže to byť blok pamäte alebo to
môže byť podbitmapa (časť inej bitmapy). Keď voláte grafické funkcie
Allegra, dáte im ako parameter smerník na štruktúru BITMAP
, ktorý
obsahuje informácie o bitmape, do ktorej chcete zapisovať.
Najlepšie bude, keď si zvyknete na váš smerník pozerať jednoducho ako na odkaz na bitmapu -- je to niečo ako identifikačne číslo. Allegro na začiatku určí jeho hodnotu a vy ho potom uvádzate grafickým procedúram, aby ste im dali vedieť, kde majú kresliť.
Jediné zaujímavé časti štruktúry BITMAP
aspoň pokiaľ sme sa
zaujímali sú polia w
a h
fields; je v nich uložená
šírka a výška bitmapy v pixeloch.
Allegro definuje premennú screen
ako smerník na štruktúru
BITMAP
reprezentujúcu obrazovku, takže napríklad:
clear_to_color (screen, 5);
vyplní obrazovku farbou 5 (často fialová).
Na nakreslenie pixelu použite funkciu Allegra:
void putpixel (BITMAP *bmp, int x, int y, int color);
Toto nastaví pixel bitmapy bmp, ktorý sa nachádza na súradniciach x,y na farbu color. Jednoduché.
Ako príklad použitia set_gfx_mode
, clear_to_color
a putpixel
si pozrite ukážkový program
examples/chap_04/ex_2_2
.
Niektoré ďalšie jednoduché grafické funkcie sú:
void vline(BITMAP *bmp, int x, int y1, int y2, int color); void hline(BITMAP *bmp, int x1, int y, int x2, int color); void line(BITMAP *bmp, int x1, int y1, int x2, int y2, int color); void triangle(BITMAP *bmp, int x1, y1, x2, y2, x3, y3, int color); void rect(BITMAP *bmp, int x1, int y1, int x2, int y2, int color); void rectfill(BITMAP *bmp, int x1, int y1, int x2, int y2, int color); void circle(BITMAP *bmp, int x, int y, int radius, int color); void circlefill(BITMAP *bmp, int x, int y, int radius, int color);
Skúste pozmeniť ukážkový program z predošlej sekcie
(examples/chap_04/ex_2_2
) tak, aby ste niektoré z nich
použili. Informácie o nich nájdete v súbore allegro.txt
v
podadresári docs
vášho adresára obsahujúceho Allegro.
Ak by ste s tým mali problémy, pozrite sa na príklad
examples/chap_04/ex_2_3
.
Na písanie textu na obrazovku použite nasledovné funkcie:
void textout(BITMAP *bmp, FONT *f, unsigned char *s, int x, y, int c); void textout_center(BITMAP *bmp, FONT *f, unsigned char *s, int x, y, int c); void textprintf(BITMAP *bmp, const FONT *f, int x, y, int c, const char *fmt, ...);
bmp
je samozreme bitmapa, na ktorú chcete písať.
Štruktúra FONT
je definovaná v allegro.h
a používa
sa na odkazovanie na rôzne fonty načítané v pamäti. Väčšinou ju budete používať
rovnako, ako používate štruktúru BITMAP
-- poväčšine len budete
smerníky na ne dávať rôznym funkciam ako parametre a nebudete potrebovať vedieť
nič o tom, čo sa vo vnútri štruktúry nachádza.
Môžete si vytvoriť vlastné fonty, použiť fonty z distribúcie GRX (iná
grafická knižnica), ktoré pôvodne pochádzajú z XFree86 (okienkoidný systém pre
Unix), alebo skonvertovať fonty z Windowsovského TTF formátu. V každom z týchto
prípadov musíte vedieť používať allegrovský Grabber a datasúbory. Ak chcete viac
informácií môžete si pozrieť súbor grabber.txt
, ale o práci s
datasúbormi budeme hovoriť v tomto tutoriáli až oveľa neskôr. Zatiaľ môžete do
fukcií písať font
; je to FONT *
deklarovaný v
allegro.h
a odkazuje na 8x8 BIOS font.
s
je reťazec, ktorý chcete vypísať, x
a
y
sú súradnice a c
je farba. Ak c
je -1 a
font je proporcionálny, Allegro použije informáciu o farbe obsiahnutú vo fonte,
takto sa dajú použiť viacfarebné fonty.
Funkcia textout_center
je takmer rovnaká; jediný rozdiel je, že
text bude vycentrovaný na súradnicu x
.
Funkcia textprintf
je vhodne obalený textout
, ktorý
prijíma formátovací reťazec tým istým spôsobom ako printf
.
textout
je rýchlejší, ale textprintf
univerzálnejší.
Farba pozadia textového výstupu sa dá nastaviť funkciou
void text_mode(int mode);
kde mode
je nová farba pozadia. Ak je mode
záporný,
pozadie sa nekreslí a bude viditeľný podklad, ktorý tam bol predtým.
Pohrajte sa s tým sami, alebo sa pozrite na
examples/chap_04/ex_2_4
, kde nájdete ukážku toho, ako sa to
používa.
V prvom rade treba podotknúť, že palety majú význam iba v 8-bitových (256 farieb) režimoch. Môžete nastaviť režimy aj s vyššími farebnými hĺbkami; ak to urobíte, obsah tejto časti pre vás nebude mať význam. Ak chcete informáciu o vyšších farebných hĺbkach, pozrite si dokument Pot of Gold od Shawna Hargreavesa na ktorý je linka z allegrovských webových stránok odkazujúcich na dokumentáciu.
Keď chcete kresliť na obrazovku v 8-bitovom režime, musíte určiť číslo farby od 0 do 255. Zvyčajne prvých šestnásť farieb býva: čierna, modrá, zelená, tyrkysová, červená, fialová, hnedá, svetlošedá, tmavošedá, ..., svetlofialová, žltá, biela (ale nemusí to tak byť vždy).
Zvyšných 240 farieb nemusí myť vždy rovnakých. Pred tým, než ich použijete, môžete počítaču povedať, aká farba sa má objaviť na obrazovke, keď nastavíte pixelom určité číslo farby. Najbezpečnejšie je nastaviť všetky farby, ktoré budete používať, včetne prvých šestnástich; tak si budete istí, že skutočne budú vyzerať tak, ako chcete, aby vyzerali.
Čísla farieb, ktoré budete uvádzať grafickým funkciam budem nazývať logické farby a farby k ním priradené, ktoré sa objavia na obrazovke fyzické farby. A teda potrebujeme funkcie, ktoré priradia logickým farbám fyzické.
Logické farby sú vyjadrené číslami od 0 do 255. Každá možná fyzická farba je
vyjadrená tromi číslami, každé z nich z intervalu od 0 do 63. Tieto čísla
reprezentujú veľkosť červenej, zelenej a modrej zložky vo farbe. 63 je
samozrejme maximálna veľkosť. Na reprezentáciu fyzických farieb sa používa
štruktúra RGB definovaná v allegro.h
:
typedef struct RGB { unsigned char r, g, b; } RGB;
Netreba hovoriť, že r
, g
and b
sú
intenzity červenej, zelenej a modrej vo farbe, ako je to popísané vyššie.
Ak chcete zmeniť fyzický význam logickej farby, použite funkciu
void set_color (int index, RGB *p);
index
je číslo logickej farby a p
je smerník na RGB
záznam, ako je popísané vyššie.
Ukážkový program examples/chap_04/ex_3_2
ukazuje použitie tejto
funkcie - ako stláčate klávesy, farba 0 (farba pozadia) nadobúda rôzne hodnoty.
Ak chcete skončiť, stlačte <ESC>.
Pripomeňme, že je tu jedna drobná komplikácia -- ak sa pokúšate meniť farby
rýchle za sebou, mali by ste medzi zmenami volať funkciu Allegra
vsync
(ktorá tiež pridá isté zdržanie). Hlavný dôvod je ten, že
niektoré grafické karty nemajú radi, ak im brnkáte po palete, keď sa snažia
kresliť po obrazovke, niekedy potom zobrazia nesprávne farby a vyzerá to ako
zasnežené. vsync
počká, kým monitor dokreslí obrazovku, čo problém
odstráni. O vsync
si neskôr ešte niečo povieme.
Ak chceme zmeniť celú paletu naraz, mohli by sme opakovane volať
set_color
, ale bolo by to pomalé a nešikovné. Miesto toho
allegro.h
definuje typ PALETTE
ako pole 256 RGB
záznamov, jeden pre každú logickú farbu. V tomto poli môžete vytvoriť celú
vlastnú paletu a potom ju nastaviť jedným volaním funkcie
void set_palette (PALETTE p);
Príklad examples/chap_04/ex_3_3
demonštruje túto funkciu tak,
že nakreslí dúhu farieb a potom prepne paletu na čiernobielu.
V málofarebných režimoch (napr. 256 farieb) je to jednoduché: ak chcete spraviť stmievanie, spravte paletu tmavšiu a tmavšiu až bude všetko čierne, pri rozsvecovaní to urobte naopak.
Allegro má niekoľko funkcií, ktoré to robia; najzákladnejšie sú:
void fade_in (PALETTE p, int speed); void fade_out (int speed);
Poznamenajme, že fade_in
chce, aby ste jej povedali, do akej
palety treba rozsvietiť; fade_out
to nepotrebuje, pretože
predpokladá, že sa bude stmievať z aktuálnej palety.
speed
je rýchlosť stmievania v intervale od 1 do 64, kde 64
znamená okamžitú zmenu.
examples/chap_04/ex_3_4
ukazuje použitie týchto funkcií.
Animácia je, keď spôsobíte, že to vyzerá, že sa veci hýbu. Môže to znamenať, že vám po obrazovke pobehuje pixel, ale zvyčajne sa tým myslí, že dookola meníte výzor niečoho tak, že vyzerá, že sa to hýbe.
Teraz sa pozrieme iba na to, ako pohybovať po obrazovke s jednoduchými vecami; druhá interpretácia bude rozobratá neskôr, keď sa pozrieme na sprity.
Najjednoduchší spôsob, ako vytvoriť zdanie, že sa niečo hýbe, je zmazať to na tom mieste, kde to je a znovu to vykresliť na obrazovku niekde inde. Je otázkou pre filozofov, či to je skutočne pohyb; ale keďže to aj tak neexistuje, nie je veľmi dôvod trápiť sa tým. A tak budeme hovoriť, že sa to pohlo.
Na to, aby vyplnený kruh prešiel cez obrazovku, môžete urobiť niečo takéto:
int x; install_timer(); /* potrebuje to `rest' */ circlefill (screen, 0, 100, 5, 15); /* nakreslí prvýkrát */ for (x = 1; x < 320; x++) { /* cyklus cez obrazovku */ rest (10); /* spomaľ */ circlefill (screen, x - 1, 100, 5, 0); /* vymaž tam, kde to bolo */ circlefill (screen, x, 100, 5, 15); /* nakresli na novom mieste */ }
Vyskúžajte, či to funguje. Teraz to skúste s väčším polomerom (povedzme 50) a pozrite sa, čo sa stane.
No áno - začalo to trochu blikať. Ak nezačalo, skúste to s väčším kruhom. Ak stále nezačalo, hmm, začne ak použijete režim obrazovky s vyšším rozlíšením (vyskúšajte si to). Ak to ani tak nezačalo blikať, potom gratulujem, máte superpočítač.
My, obyčajní smrteľníci, ale s tým blikaním niečo budeme musieť urobiť. Je viacero možných prístupov; niektoré sú komplikovanejšie než iné, niektoré efektívnejšie a niektoré sú jednoducho dobré, pretože nežerú toľko procesorového času (a teda hry môžu bežať s vyššou obnovovacou frekvenciou).
Toto je najdôležitejšia vec, ktorú treba urobiť. Zatemnenie je časový úsek, počas ktorého sa váš monitor pripravuje na vykreslenie nového rámca. Počas tohto času sa nič na obrazovke nedeje, takže ak stihneme vymazať objekt a znovu ho nakresliť, kým monitor začne s novým rámom, je po probléme.
Funkcia vsync
čaká, kým sa nezačne ďalšie zatemnenie. Keď to
nastane, treba prekresliť obrazovku tak rýchlo, ako je to možné. Takže nahraďte
rest(10)
funkciou vsync()
a skúste to znovu. Oveľa
lepšie.
Pripomeňme, že zatemnenie je svojim spôsobom niečo ako časovač. Objavuje sa v pravidelných intervaloch (pre daný grafický režim; medzi jednotlivými režimami sa to líši). Z toho vyplýva, že (okrem toho, že ho aj tak používame na synchronizáciu) to môže byť užitočný spôsob, ako regulovať rýchlosť hry. V kapitole 9 sa ale dajú nájsť aj lepšie spôsoby, ako sa to dá urobiť - budeme tam hovoriť o systéme časovačov, ktorý Allegro obsahuje.
Ak toho máme na kreslenie príliš veľa, tak to všetko nezvládneme počas jedného zatemnenia. Potrebujeme teda blikanie znížiť nejakým iným spôsobom. Predstavte si situáciu, že máme množstvo kruhov, a predtým, než ich prekreslíme, ich všetky vymažeme. Budú vymazané pomerne dlhý čas; je preto veľká ôravdepodobnosť, že sa monitor bude snažiť zobraziť časť obrazovky vtedy, keď ešte nebudú vykreslené.
Tento problém môžeme vyriešiť tak, že ich budeme prekreslovať jeden po druhom. Môže to pomôcť znížiť blikanie, ale môže to viesť k iným problémom. To, čo je podstatné ale je, že je rozumné snažiť sa o to, aby boli veci vykreslené čo najdlhšie.
Ak napríklad zmažete všetky vaše objekty a až potom zisťujete, kde budú vykreslené, nie sú na obrazovke po celý čas výpočtu. Ak spravíte výpočty pred tým, než ich zmažete, budú na obrazovke dlhšie a čas, keď sú preč, sa skráti.
Ako sme poznamenali vyššie, niekedy nestihneme celé vykreslenie počas jedného zatemnenia. Čo sa stane potom? Monitor začne prekreslovať obrazovku z hora dole. Takže ak máme nakreslenú iba časť, je lepšie, ak je horná časť obrazovky v poriadku, keď ju monitor začne prekreslovať.
V podstate ide o to, aby práve nebolo vymazané niečo, čo sa monitor pokúša zobraziť. Žiaľ, ak nemáme k dispozícii časovač s veľmi vysokým rozlíšením, nevieme povedať, ktorá časť obrazovhy sa práve prekresľuje. A ak taký časovač máme, má to zas iné tienisté stránky.
Rozumné môžu byť rôzne poradia vykresľovania. Môžete vykresľovať veci od hora nadol. Toto okrem iného urýchli prekresľovanie rámcov v režimoch s vyšším rozlíšením/farebnou hĺbkou zredukovaním prepínaní rozsahu videa. (??? This also improves the frame redraw speed in higher resolution/colour depth modes by reducing video bank switches.) Tiež môžete zvážiť, aké zložité sú jednotlivé objekty a začať prekresľovanie od jednoduchších.
Iná technika je vykreslovanie zdola nahor. Môže to znieť zvláštne, ale ak to tak spravíte, zatemnenie stihnete len raz a to na kratučký moment. Ak budete používať tento systém, dôjde asi k nejakému blikaniu, ale pravdepodobne to nebude až také tragické.
Môžete si to predstaviť ako rýchlu jazdu autom na diaľnici, počas ktorej sa pozeráte bočným oknom. (Uistite sa, že šoféruje niekto iný.) Snažíte sa mať dobrý výhľad na živý plot. Autá, ktoré idú tým istým smerom ako vy, majú nízku relatívnu rýchlosť a teda stoja v ceste vášmu výhľadu na živý plot dlhšiu dobu. Zato autá ktoré idú v protismere blokujú váš výhľad len veľmi krátko.
Toto je veľmi populárny a jednoduchý systém, ktorý funguje dobre v rýchlych grafických režimoch (teda tých s nízkym rozlíšením). Novšie grafické karty (AGP a v menšej miere PCI) ho môžu použiť aj pri vyšších rozlíšeniach.
Technika dvojitého bufferingu znamená, že celý grafický výstup sa zapisuje do dočasnej bitmapy, kým nie je prekreslený celý rámec a výsledný obraz sa potom skopíruje do reálnej videopamäte ako jeden veľký kus.
Toto môže byť efektívne z viacerých dôvodov. Po prvé, znamená to, že z obrazovky nemusíme nič mazať -- iba nahradíme jeden obraz (na ktorom je všetko vykreslené) iným (na ktorom je tiež všetko vykreslené).
Po druhé, videopamäť je o dosť pomalšia. Ak mienime počas vykresľovania rámca častokrát prepisovať jednotlivé oblasti, bude rýchlejšie, ak to všetko zapíšeme do (rýchlej) systémovej pamäte a potom to skopírujeme do (pomalej) videopamäte, než keby sme zapisovali priamo na obrazovku. Navyše čítanie z videopamäte je dokonca ešte pomalšie; a teda ak budeme chcieť čítať z obrázka, je oveľa lepšie čítať z kópie v systémovej pamäti. Je to niečo podobné ako vyrovnávacia pamäť disku, kde sa časti disku uchovávajú v systémovej pamäti aby sa vyhlo priamemu prístupu na disk.
Tak teda namiesto kreslenia na obrazovku, kreslíme do inej bitmapy v pamäti.
Potom, keď sme hotoví, zavoláme vsync()
a potom celý zásobník
(buffer) skopírujeme na obrazovku. V režime 320x200x256 sa to s rezervou vojde
do prekreslovacieho intervalu, takže k žiadnemu blikaniu nedôjde. Pri vyšších
rozlíšeniach alebo farebných hĺbkach je treba samozrejme skopírovať viac
obrazových dát a teda to trvá dlhšie. Dôsledky môžu byť rôzne, počnúc rozdelením
(kde je na hornej časti obrazovky vykreslený jeden rámec a na dolnej druhý) a
končiac znížením frekvencie vykresľovania (pretože vykresľovanie trvá pridlho,
takže zmeškáme ďalšiu prekresľovaciu periódu, takže vsync
bude
čakať až na tú, ktorá nasleduje po nej).
Pozrite si ukážkový prgram examples/chap_04/ex_4
. Nechceme
zatiaľ rozoberať príliš podrobne pamäťové bitmapy a funkciu blit; vrátime sa k
tomu podrobnejšie v kapitole 7.
Toto je varianta dvojitého bufferingu, ktorá nevyžaduje taký rýchly grafický režim, ale je ťažšia na programovanie. Teoreticky to vyzerá tak, že spravíte presne to isté, ako pri dvojitom bufferingu, teda všetko vykreslíte do pamäťovej bitmapy, ale potom na obrazovku skopírujete iba tie oblasti, ktoré sa zmenili.
To ale znamená, že treba sledovať, ktoré oblasti sa zmenili; Najjednoduchší spôsob je označiť obdĺžniky ako špinavé keď do nich píšete. Potom môžete ľahko tieto obdĺžniky skopírovať na obrazovku.
Tento prístup je lepší než dvojitý buffering, ak sa toho veľa nezmenilo. Ak bolo zmien veľa, môže sa ukázať ako horší, čiastočne preto, lebo jednorázové prekreslenie veľkej plochy je efektívnejšie, než mnoho prekreslení menších oblastí a čiastočne preto, že sa jednotlivé špinavé obdĺžniky môžu prekrývať; pokiaľ sa nepostaráte o to, aby sa tak nestalo, budú sa niektoré časti obrazu prekresľovať dvakrát. Avšak ak situácie v ktorých bude jednoduchšie použiť dvojitý buffering viete rozpoznať, môžete jednoducho skopírovať celý obraz naraz, ako pri dvojitom bufferingu. Tieto dve techniky majú mnoho spoločné.
Špinavé obdĺžniky sú náročnejšie na realizáciu a ak spravíte malú chybičku, môžete sa dočkať podivných výsledkov. V kapitole 7 sa vrátime aj k tejto téme.
Toto je ďalšia varianta dvojitého bufferingu. Hlavná myšlienka je kopírovať na obrazovku iba každý druhý riadok. Môžete kopírovať iba párne riadky a nepárne nechať čierne. Dostanete tak trochu tmavší obraz s efektívne zníženým zvislým rozlíšením. Tiež môžete skopírovať párne riadky v jednom rámci a nepárne v ďalšom. Takto nestratíte jas obrazu, ale niektoré riadky budú "staršie", než iné, čo spôsobí malé rozmazanie obrazu. Tieto techniky sa uplatňujú najlepšie vo veciach ako video.
Znie to celkom jednoducho a aj môže byť, ale znova ukážeme viacero spôsobov, ako sa to dá urobiť. Našim cieľom bude, aby sa to hodilo do štruktúry hry, ktorá bola prezentovaná skôr.
Na to, aby sme mali pohromade všetky údaje, ktoré popisujú každý kruh -- jeho umiestnenie, farbu, polomer, rýchlosť, ktorou sa pohybuje v každom smere, budeme používať štruktúru. Tiež bude výhodné zapamätať si, kde bol kruh nakreslený, aby sme si zjednodušili mazanie. Takže začnime definíciou štruktúry:
struct circle_t { int x,y,r; /* x, y, polomer */ int col; /* farba */ int xs,ys; /* x rýchlosť, y rýchlosť */ int dx,dy; /* nakreslene x, nakreslene y */ };
Ďalej, dobrý spôsob, ako písať modulárny kód ako tento je najprv popremýšľať trochu o rozhraní; rozhodnite sa, čo má vrstva, ktorá bude modul používať, vidieť. Potom môžete napísať funkcie, zatiaľ stačí len zhruba určiť, čo budú robiť -- alebo môžete napísať programy, ktoré ich volajú, dokonca bez toho, že by ste urobili volané funkcie. Výhoda tohto prístupu je, že ak máte viac než jedného človeka, ktorý na hre pracuje, jeden z vás môže písať obslužné programy objektu a druhý program, ktorý ich volá.
Ďalšou výhodou, ktorú uvidíme neskôr je, že do tohoto typu systému je jednoduché pridať nové objekty -- všetky objekty budú používať tie isté funkcie rozhrania. A tak človek, ktorý píše program, ktorý volá všetky obslužné programy objektu môže začať s použitím fiktívnych objektov, ktoré toho veľa nerobia, aby skontroloval, či obsluha vlasne funguje; reálne objekty môžu byť pridané neskôr.
Poďme rozhodnúť, aké funkcie by každý objekt mohol mať. V ideálnom prípade chceme mať pre kruh k dispozícii nasledovné funkcie: funkciu, ktorá ho nakreslí, funkciu, ktorá ho zmaže a funkciu, ktorá upraví vnútorné premenné. Tieto funkcie budú často volané z vnútorného cyklu hry. Okrem toho potrebujeme funkciu, ktorá ho vytvorí a inicializuje jeho vnútorné premenné, spolu s funkciou, ktorá ho zlikviduje. Táto funkcia v tomto konkrétnom prípade nebude robiť veľa, ale mohla by, ak by napríklad bolo nutné uvoľniť dynamicky alokovanú pamäť potrebnú pre kruh.
Prototypy našich funkcií môžu vyzerať nasledovne:
void circle_draw (struct circle_t *circle, BITMAP *bmp); void circle_erase (struct circle_t *circle, BITMAP *bmp); void circle_update (struct circle_t *circle); struct circle_t *circle_init (int new_x, int new_y, int new_radius, int new_col, int new_xs, int new_ys); void circle_destroy (struct circle_t *circle);
Všimnite si, že som mená funkcií zvolil tak, aby začínali na
circle_
. Je užitočné urobiť to, pretože tieto funkcie budú globálne
a nechceme, aby dochádzalo ku kolíziam s funkciami, ktoré obsluhujú iné typy
objektov. Začiatok názvu nás tiež upozorní, že patria do modulu "kruh". Keby sme
si tým neboli istí, mohli by sme sa samozrejme pozrieť do hlavičkového súboru,
aby sme zistili, do ktorého modulu vlastne patria, ale je rýchlejšie, keď to
viete zistiť iba pohľadom na meno funkcie.
Budeme mať pole smerníkov na štruktúry circle_t
a naša hlavná
funkcia init
ich všetky inicializuje s pomocou volania
circle_init
s rôznymi parametrami.
Keď už sú inicializované, tak ich ešte predtým, než odídeme z funkcie
init
vykreslíme.
Funkcia input
zatiaľ nebude robiť nič okrem toho, že nastaví
príznak "koniec hry" (premenná end_game
), ak bolo stlačené
<ESC>. Použitie klávesnice bude vysvetlené v ďalšej časti.
Vo funkcii process
vypočítame novú polohu všetkých kruhov.
Použijeme na to for cyklus, ktorý prejde pole kruhov a zavolá
circle_update
pre každý z nich.
Vo funkcii output
budeme potrebovať podobný cyklus, ktorý pre
každý kruh zavolá circle_draw
. Vo väčšine prípadov (záleží od toho,
ktorú animačnú techniku použijeme) budeme najprv musieť zavolať funkciu
circle_erase
.
Na záver, keď skončíme, je slušné uvoľniť z pamäte kruhy vytvorené s pomocou
circle_init
funkciou circle_destroy
.
Teraz by som vám navrhol, aby ste sa pokúsili naprogramovať to sami pre
jeden kruh a potom program rozšíriť tak, aby sa ich hýbalo viacero. Ukážkové
programy examples/chap_05/1_a
a examples/chap_05/1_b
demonštrujú, ako sa to robí. Ak by ste mali problémy s vytvorením programu,
ktorý hýbe len jedným kruhom, navrhujem vám pozrieť si prvý príklad a pokúsiť sa
ho upraviť tak, aby hýbal s viacerými kruhmi.
Všimnite si, že v prípade, že hýbete viacerými kruhmi, ak ich mažete a vykreslujete jeden po druhom, mazanie ďalších môže vygumovať kúsok z už vykreslených. Rieši sa to tak, že vymažete všetky kruhy naraz a potom ich všetky nakreslíte. Toto ale môže zväčšiť blikanie obrazovky (pamätáte sa na maximalizáciu času vykreslenia?), ak nepoužívate dvojitý buffering.
Vyššie opísaná technika je vhodná, ak máme mnoho podobných objektov. Ale v reálnej hre je zvyčajne mnoho rôznych typov objektov. Tak sa teda pozrime na nejaké spôsoby, ako narábať napríklad so štvorcami súčasne s kruhmi.
Jedno riešenie je pridať do štruktúry informácie, ktoré jej umožnia popísať, či sa jedná o kruh alebo o štvorec tak, že pridáme premennú, ktorá to rozlíši. Potom môžeme modifikovať funkcie tak, aby rozlišovali oba prípady a kreslili ich róznym spôsobom. Niektoré funkcie dokonca nebude treba meniť.
Iné riešenie je vytvoriť novú štruktúru pre štvorce a nový súbor funkcií, ktoré s nimi budú narábať.
Obe tieto techniky majú svoje výhody. Prvá je vhodná vtedy, keď sú dva typy objektov veľmi podobné; v takýchto prípadoch nebude nutné mnoho funkcií meniť. Všetky objekty môžu byť uložené v jednom poli a bude stačiť jeden cyklus na to, aby boli vykreslené, aby sa pohli atď. Navyše podobnosť môže byť využitá tak, že môžeme vytvoriť spoločné procedúry pre veci ako zisťovanie kolízie.
Druhá technika je vhodná, ak sú objekty úplne rôzne. V takom prípade je nešikovné robiť štruktúru, ktorá bude udržiavať dáta pre všetky z niekoľkých objektov a funkcie nebudú mať príliš veľa kódu spoločného. Pri použití tejto techniky potrebujete zvláštne pole pre každý typ objektu a ak chcete vykresľovať/mazať/meniť objekty, musíte prezerať polia jedno po druhom.
V praxi sada viacerých rôznych typov objektov môže byť rozdelená do tried typov objektov. Kritériom napríklad môže byť správanie, výzor alebo to, ktorému hráčovi objekt patrí. S každou triedou sa potom môže zaobchádzať prvým zo zmienených systémov a bude mať spoločnú štruktúru, ktorá bude schopná popísať každý objekt v tejto triede. Rozdielne triedy sú potom spravované ako rôzne objekty druhým systémom.
Ak ste tomu porozumeli, pokúste sa vyššie uvedený program na pohyb viacerých
kruhov upraviť tak, aby sa tam hýbali aj štvorce s použitím každej z uvedených
dvoch metód. V prípade, že zatuhnete, sú tu ukážky, na ktoré sa môžete pozrieť:
examples/chap_05/ex_2_a
a examples/chap_05/ex_2_b
. Ak
sa vám nepodarí urobiť to vlastným spôsobom, pozrite si ich a pokúste sa do
každej pridať ďalší typ objektov.
Ako cvičenie založené na zmiešanej technike sa pokúste urobiť program, kde sa
budú hýbať štvorce, obdĺžniky a... hmmm...
trojuholníky (ktoré budú prechádzať hranicu!), pričom štvorce a obdĺžniky sa
budú hýbať tak, ako predtým, ale trojuholníky sa nebudú odrážať; spravte to tak,
že sa vrátia na plochu z opačnej strany, než je tá, kadiaľ odišli. Príklad
tohoto programu je examples/chap_05/ex_2_c
; môžete ho spustiť, aby
ste videli, čo to robí a potom sa pokúste napodobniť jeho správanie vašim
vlastným programom.
Predpokladajme, že chceme počas behu programu meniť počet útvarov. Vo všetkých predošlých príkladoch bol počet útvarov jednotlivých tvarov dopredu daný. Môžeme počet útvarov znížiť tak, že skrátime cyklus, ale takto vždy odstránime posledný útvar v zozname a okrem toho nemáme možnosť útvary pridať (pokiaľ predtým nejaké nezničíme). Skrátka to, čo hra môže robiť, je limitované v čase kompilácie veľkosťou polí.
Sú dve techniky, ktoré by som tu chcel predstaviť: dynamická alokácia a zreťazené zoznamy. Zreťazené zoznamy závisia od dynamickej alokácie, takže túto sekciu budete pravdepodobne čítať ako prvú.
V tejto sekcii uvidíme, čo to dynamická alokácia je a pozrieme sa, ako ju môžeme použiť na rozšírenie systému lineárnych polí, ktorý sme používali doteraz.
Dynamická alokácia nám dovoľuje vyžiadať si pamäť počas behu programu (teda keď už program beží, ako protiklad k času kompilácie). To napríklad znamená, že sa na začiatku behu programu môžeme spýtať užívateľa, koľko kusov z každého útvaru budeme animovať a potom si vytvoríme polia také dlhé, ako potrebujeme. A ešte lepšie je, že môžeme meniť veľkosť polí počas behu hry, takže ak by sme mali mať málo miesta, tak ich len trochu zväčšíme.
Na to, aby sme to urobili, použijeme funkciu malloc
. Táto
štandardná funkcia jazyka C dostane parameter typu size_t
(v
podstate číslo) a alokuje daný počet bytov pamäte. Návratová hodnota je
void *
, ktorá ukazuje na blok alokovanej pamäte.
Takže chceme vytvoriť pole kruhov dĺžky num_circles
. Každý kruh
bude reprezentovaný struct circle_t
. Môžeme použiť kľúčové slovo
sizeof
, s pomocou ktorého zistíme, koľko bytov potrebujeme pre
každú štruktúru a alokovať pamäť takto:
struct circle_t *circles; ... circles = (struct circle_t *) malloc (num_circles * sizeof (struct circle_t));
Pretypovanie na struct circle_t *
v jazyku C nie je nutné;
kompilátor C++ si ale bude sťážovať, ak ho nepoužijete. Jeho použitie je
väčšinou vecou vkusu.
Ak alokácia zlyhá (napr. pre nedostatok pamäte), malloc
vráti
NULL
. Za určitých okolností to môžete chcieť zistiť a informovať
užívateľa. Je dobrý zvyk robiť to, ale ľudia sa o to často nestarajú, pretože
nemusí existovať zmysluplný spôsob, ako z toho von. Ak používate djgpp s nejakým
robustným DPMI klientom (napr. CWSDPMI), tento ukončí váš program, kedykoľvek sa
pokúsite použiť smerník NULL. Niektorí iní DPMI klienti nechajú program bežať,
čo pred vami problém skryje; najlepšie je tomu vyhnúť sa počas vývoja, alebo
aspoň občas použiť CWSDPMI (teda z čistého DOSu, nie z Windows), aby ste
skontrolovali, či veci vyzerajú byť v poriadku.
Po alokácii sa môžeme na kruhy odvolávať spôsobom circles[0], circles[1], atď., ako keby sme mali skutočné pole. To, čo máme, nie je celkom presne pole v jazyku C; je to len smerník na blok pamäte. Predsa sa to ale v mnohých ohľadoch správa ako pole.
Na záver, ak sme s používaním pamäte skončili, mali by sme ju uvoľniť, aby ju malloc mohol ak to bude potrebné znovu použiť:
free (circles);
Teraz sa pokúste zmeniť váš program z časti 5.1 (iba kruhy, štvorce a
trojuholníky netreba) tak, aby si užívateľ mohol na začiatku programu zvoliť
počet kruhov. Ak zatuhnete, pozrite sa na examples/chap_05/ex_3_1
.
Zmeniť príklad examples/chap_05/ex_2_a
tým istým spôsobom bude
celkom jednoduché. Zmena examples/chap_05/ex_2_b
bude vyžadovať
viac mallocov.
A čo takto dovoliť hre zväčšovať pole počas behu? To pri skutočných poliach nie je možné, ale ako sme spomenuli predtým, naše "pole" nie je skutočné pole. Bolo alokované dynamicky, takže môžeme použiť funkciu realloc(), aby sme ho realokovali s inou veľkosťou. Údaje, ktoré v ňom boli predtým, tam budú stále aj potom, keď alokáciu rozšírime. Ak ho skracujeme, údaje, ktoré sú aj v novom bloku budú samozrejme zachované, ale dáta, ktoré sú na konci odrezané sa stratia. Dokonca ak blok pamäte znovu zväčšíte, nedá sa spoliehať na to, že dáta znovu získate.
Problém so znovualokovaním takýchto blokov je, že ak je oblasť pamäti, ktorá sa nachádza za našim blokom už obsadená, realloc potrebuje skopírovať celý blok niekde inde. Toto nie je zvlášť pomalé (operácie kopírovania pamäte sú skutočne celkom rýchle) ale nie je to vec, ktorú by ste chceli, aby sa vám diala príliš často.
Väčšina implementácií malloc
nechávajú nejaké voľné miesto na
konci každého bloku, ale nemôžeme sa na to príliš spoliehať. Je lepšie znížiť
počet realokácií nejakým iným spôsobom. Najjednoduchší spôsob, ako to spraviť je
vždy si zapýtať veľa miesta navyše --- potom nemusíme volať realloc
veľmi často.
Takže potrebujeme sledovať niekoľko vecí:
int num_objs; /* koľko je ich aktívnych */ int alloced_objs; /* koľko je ich alokovaných */ int block_size; /* koľko ich budeme na jeden krát alokovať */
Keď potom chceme pridať nový objekt, najprv skontrolujeme, či máme dosť
miesta (je alloced_objs > num_objs
?). Ak áno, zväčšíme iba
num_objs
a použijeme ten priestor. Inak musíme zväčšiť
alloced_objs
o block_size
a znovualokovať blok
napríklad takto:
objects = (struct obj_t *) realloc (objects, alloced_objs * sizeof (stuct obj_t));
Všimnite si, že syntax realloc je podobná ako malloc až na to, že musíte
zadať starú hodnotu smerníka. Po zavolaní realloc už nemôžete používať jeho
starú hodnotu -- funkcia realloc blok presunula, stará hodnota je teraz
neplatná. Ak ste nastavili iný smerník na hodnotu objects
a realloc
posunula blok niekde inde, druhý smerník bude ukazovať na nesprávny blok.
Po znovualokovaní novej veľkosti bloku môžeme samozrejme zväčšiť
num_objs
a použiť novoalokované objekty.
Zreťazené zoznamy sú extrémne užitočnou dátovou štruktúrou. Nanešťastie sú z istých dôvodov pre mnohých ľudí ťažko pochopiteľné. Ak si však o nich vytvoríte správnu predstavu, sú skutočne veľmi jednoduché a keď ich niekoľkokrát použijete, získate intuitívny vhľad do toho, ako ich používať.
V tejo časti sa pozrieme na tri typy zreťazených zoznamov a vyskúšame použiť ich v krúžkovo/štvorčekovo/trojuholníkových programoch vytvorených predtým.
V prvom rade potrebujete byť dôverne oboznámení so smerníkmi. Smrníky sú jednoducho objekty, ktoré môžu ukazovať na iné objekty. Snažte sa nemyslieť na ne presne ako na adresy v pamäti; je to bežná analógia, ale nemusí byť nutne správna a ak o nich budete uvažovať takto, smerníková aritmetika sa bude zdať mätúcou.
Takže smerníky môžu ukazovať na iné dátové objekty. Väčšinou sú smerníky navrhnuté tak, aby ukazovali na istý konkrétny typ dát. Konkrétne môžu ukazovať na užívateľom definované štruktúry:
struct little_struct { int number; } *ptr;
Smerník ptr
môže ukazovať na ľubovoľný objekt typu struct
little_struct
. Ak chceme pristupovať k poľu number
objektu,
na ktorý ptr
ukazuje, môžeme písať (*ptr).number
alebo
použiť skrátený zápis ptr->number
, ktorý znamená to isté.
Všimnite si, že definícia uvedená vyššie nevytvára hneď objekt, na ktorý by
ptr
ukazoval; vytvorí len objekt ptr
, ktorý má možnosť
ukazovať na objekty typu struct little_struct
ale na začiatku
neukazuje nikam (smozrejme niekam ukazuje, ale kam presne záleží na iných
okolnostiach; a teda sa nemôžete spoliehať, že je to platný smerník). Teraz sa
pozrite na toto:
struct linked_list_node { int number; struct linked_list_node *next; } *ptr;
Tu štruktúra linked_list_node
neobsahuje len celé číslo
number
, ale aj smerník next
, ktorý môže ukazovať na
objekt typu struct linked_list_node
. Takže ak vytvoríme objekt
tohto typu a nasmerujeme na neho ptr
, smerník
ptr->next
môže ukazovať na iný objekt tohto typu. A
ptr->next->next
môže ukazovať na ďalší... ako
vidíte, môžeme mať celý zoznam takýchto štruktúr, pričom alokujeme každú zvlášť
(radšej ako v jednom veľkom bloku) a môžeme k nemu pristupovať s použitím
jediného smerníka na začiatok zoznamu. Aby sme označili koniec zoznamu, môžeme
nastaviť smerník next
posledného uzla na NULL.
ptr --> +------------+ +------------+ +---------------+ | number : 7 | | number : 3 | | number : 285 | | next : ----> | next : ----> | next : NULL | +------------+ +------------+ +---------------+
Na diagrame vyššie vidíte ptr
, ktorý ukazuje na prvú zo zoznamu
troch položiek. Prvá položka s hodnotou number
7 má svoj smerník
next
nastavený na druhú s hodnotou number
3. Smerník
next
tejto položky ukazuje na tretiu položku, ktorá má hodnotu
number
285, a smerník next
tretej položky je NULL,
pretože je to posledný prvok v zozname. Celý zoznam obsahuje čísla 7, 3 a 285 v
uvedenom poradí.
Pravdepodobne sa čudujete, prečo sme sa trápili s takýmto systémom. Ak ho porovnáme s lineárnym dynamickým blokovým zásobníkom zmieneným vyššie, má niekoľko výhod a niekoľko nevýhod.
V prvom rade narábať s vecami v zreťazenom zozname je veľmi jednoduché. Nové
položky môžu byť pridané kdekoľvek do zoznamu, staré položky vymazané,
existujúce pložky môžu byť preusporiadané alebo presúvane zo zoznamu do zoznamu
a zoznamy sa môžu deliť alebo spájať s minimálnou námahou. Stačí zmeniť hodnoty
niektorých smerníkov next
. Každá z týchto operácií by bola
nepríjemná a zdĺhavá, keby mala byť vykonaná v lineárnom bloku -- museli by sme
presúvať veľké množstvo dát.
Predstavte si napríklad vymazanie druhej dátovej položky v dlhom sekvenčnom
zozname -- museli by sme posunúť celý zvyšok zoznamu o jedno miesto. V
zreťazených zoznamoch stačí len nastaviť smerník next
prvej položky
na to, na čo ukazuje smerník next
druhej položky (teda buď na
tretiu položku, alebo na NULL, ak žiadna tretia položka neexistuje) a potom
môžeme druhú položku funkciou free
uvoľniť.
Podobne je jednoduché pridať novú položku. Predpokladajme, že máme smerník na
položku v zozname pred miestom, kde chceme našu novú položku pridať. Stačí
nastaviť smerník next
našej položky na rovnakú hodnotu ako má
smerník next
položky zo zoznamu a potom nastaviť smerník
next
položky zo zoznamu na našu novú položku.
Nevýhodami zreťazených zoznamov je využitie pamäte a neefektívnosť
vyhľadávania
položiek podľa čísla. Použitá pamäť je väčšia kvôli všetkým smerníkom v zozname
a pretože je väčšinou alokované veľké množstvo malých kúskov (konkrétne v čase
písania tohto textu bola minimálna veľkosť bloku alokovaného funkciou
malloc
v kompilátore djgpp 4096 bajtov). Vyhľadávanie podľa čísla
je neefektívne, pretože musíme prejsť celý zoznam od začiatku, aby sme danú
položku našli, zatiaľ čo v sekvenčnom zozname môžeme skoči priamo na ňu
jednoduchým pripočítaním.
Ako vždy, ktorý spôsob použijete, záleží na tom, na čo ho chcete použiť.
Často kód zjednoduší, ak prvá položka v zreťazenom zozname je atrapa (niekedy sa nazýva aj hlavička zoznamu), ktorej dáta nemajú žiadny význam. Robí sa to preto, aby manipulácia s prázdnymi zoznamami bola jednoduchšia -- stanú sa zoznamami s jedinou položkou, s atrapou.
Teraz by som vám navrhol, aby ste sa pozreli do
examples/chap_05/ex_3_2a
na nejaké príklady vkladania, mazania a
vyhľadávania položiek v zreťazených zoznamoch.
Myslím, že teraz pravdepodobne rozumiete, prečo zreťazené zoznamy môžu byť veľmi užitočné -- môžeme vytvoriť toľko útvarov koľko chceme, kedy chceme a pridávať ich do zoznamu veľmi efektívnym spôsobom. V skutočnosti sa nestaráme o to, koľko ich je v zozname; ak sa potrebujeme z nejakých dôvodov odkazovať na istú konkrétnu položku, použijeme miesto jeho poradového čísla smerník.
Ak predpokladáme, že ste pochopili predošlú lekciu, toto bude zvládnuteľné jednoducho. V jednoducho zreťazených zoznamoch vyššie má každá položka jeden smerník na ďalšiu položku v zozname. V dvojmo zreťazených zoznamoch má každá položka dva smerníky -- jeden na ďalšiu položku v zozname a jeden na predošlú položku v zozname.
struct dllnode { struct dllnode *next; struct dllnode *prev; int number; } *ptr;
ptr --> +--------+ +--------+ +--------+ | next -----> | next -----> | next ---> NULL NULL <--- prev | <----- prev | <----- prev | | number | | number | | number | +--------+ +--------+ +--------+
Teraz stačí mať smerník na ľubovoľný prvok zoznamu a je možné pristupovať ku
každému prvku. Stále je často užitočné mať ako prvý prvok v zozname (ten, ktorý
má prev == NULL
) atrapu, ktorej dáta sa nepoužívajú. Dôvody sú
rovnaké ako predtým. Môže slúžiť ako rukoväť k zoznamu -- niečo, čoho
sa dá zachytiť, čo bude stále súčasťou zoznamu. Pamätajte, že ostatné prvky sa
budú priebežne pridávať a mazať; hlavička je jediný prvok o ktorom bude
zaručené, že je tam celý čas.
Jednou z výhod dvojmo zreťazeného zoznamu je to, že môžete zmazať prvok zoznamu bez toho, že by ste potrebovali vedieť, v ktorom zozname sa vlastne nachádza. Dalšou výhodou je, že ak nejaký prvok zoznamu potrebuje prejsť celý zoznam, môže to urobiť bez toho, že by poznal smerník na hlavičku zoznamu. Okrem toho ak máte daný ľubovoľný prvok zoznamu, môžete pridať prvok na obe jeho strany bez toho, aby ste museli celý zoznam prejsť. Dvojité zoznamy sa vo všeobecnosti jednoduchšie (a efektívnejšie) spravujú, ale samozrejme za cenu trocha vyšších režijných nákladov.
examples/chap_05/ex_3_2b
je príklad dvojmo zreťazeného zoznamu a
funkcií, ktoré s ním narábajú.
Znovu, ak ste porozumeli zreťazeným zoznamom až potiaľto, toto nebude príliš
náročné. V našom jednoducho zreťazenom zozname sme mali smerník
next
posledného prvku nastavený na NULL, aby sme tak označili
koniec zoznamu. V kruhovo zreťazenom zozname smerník next
posledného prvku ukazuje späť na začiatok zoznamu -- teda to nie je v
skutočnosti posledný prvok. Predstavte si to ako kruh prvkov, kde každý ukazuje
na nasledujúci.
Ak je kruhový zoznam dvojmo zreťazený, smerník prev
prvého prvku
ukazuje na posledný prvok.
Prázdne kruhové zoznamy sú tiež problematické; použitie atrapového prvku je užitočné aj tu, ale musíte pamätať na to, že ho treba preskočiť, keď budete prechádzať okolo kruhu. Môžete to spraviť aj tak, že ak je zoznam úplne prázdny, váš smerník na neho bude NULL (ako to môžete spraviť aj s ostatnými zreťazenými zoznamami), ale stále budete musieť ošetrovať nejaké špeciálne prípady.
Kruhové zoznamy sú užitočné na iné veci, než lineárne zreťazené zoznamy; ak
programujete hru, vo všeobecnosti chcete počas herného cyklu spravit niečo s
každým prvkom zoznamu a na tieto účely sa často viac hodia lineárne zoznamy.
Kruhové zoznamy som tu spomenul pre úplnosť a pretože sa od lineárnych príliš
nelíšia. Ako príklad si pozrite examples/chap_05/ex_3_2c
.
Pokúste sa prerobiť váš program na pohyb útvarov tak, že útvary uložíte do
zreťazeného zoznamu a animujete ich odtiaľ. Tiež môžete každých 100 herných
cyklov pridať nový útvar, alebo niektorý zmazať. Pozrite si
examples/chap_05/ex_3_2d
ako ukážku toho všetkého.
Táto metóda, keď všetky objekty hry vložíte do zreťazeného zoznamu, je veľmi užitočná. Môžete tam tiež vložiť hráčovu raketu/postavu/potvoru; ak hráčova postava vykonáva podobné akcie ako nepriatelia, môže to fungovať celkom dobre.
Táto časť zatiaľ nebola napísaná, sorry.
Vstupné zariadenie je čokoľvek, čo použijete, aby ste dali počítaču informácie. U štandardných PC je to najmä klávesnica, aj keď väčšina má myš a mnohé joystick. Podľa uvedenej definície disketová jednotka je tiež istým druhom vstupného zariadenia rovnako ako mikrofóny, skenery a modemy, ale to väčšinou nie sú tie najlepšie veci na ovládanie hry.
Pozrieme sa na každé z hlavných troch zariadení podrobnejšie, potom premyslíme abstrakciu nášho vstupného systému. Nakoniec sa pozrieme na rôzne spôsoby použitia vstupnej informácie.
Štandardné rozhranie PC klávesnice je takmer, aj keď nie celkom, úplne nepoužiteľné ako zariadenie na ovládanie hry. Je na to niekoľko dôvodov; v prvom rade kedykoľvek stlačíte klávesu alebo len na ňu klepnete, hra určitý čas (zvyčajne štvrtinu sekundy) bude vidieť len jednu stlačenú klávesu. Potom, ak držíte klávesu stlačenú, naplníte zásobník mnohými stlačeniami, čo znamená, že hra ich stále bude čítať aj potom, čo klávesu pustíte. Navyše ak stlačíte jednu klávesu a klepnete na inú, bude to vyzerať, ako by ste prvú pustili.
Toto všetko dohromady robí štandardné PC klávesnicové funkcie nepoužiteľné pre riadenie akčných hier. Našťastie Allegro poskytuje sadu oveľa lepších funkcií.
Vo vašich allegrovských programoch treba počas inicializácie zavolať
install_keyboard()
. Potom môžete používať klávesnicové funkcie
Allegra.
Predovšetkým je tam pole charov nazvané key
ktoré obsahuje jeden
prvok pre každú klávesu na klávesnici. Je indexované podľa scankódov (nie ASCII
kódov). Jednotlivé scankódy môžete získať s použitím KEY_*
konštánt
definovaných v allegro.h
. Napríklad, ak chcete zistiť, či je práve
stlačený medzerník, zistite, či je key[KEY_SPACE]
nenulové.
Pravdepodobne si pamätáte, že ste vstupnú funkciu už videli, keď bolo potrebné
skontrolovať, či bola alebo nebola stlačená klávesa <ESC>.
Potom, čo inštalujete klávesnicový ovládač Allegra, funkcie BIOSu prestanú
fungovať. To sa týka getch
z conio, getchar
,
gets
, scanf
z stdio a všetko ďalšie, čo používa stdin.
Allegro poskytuje náhradu za getch
nazvanú readkey
,
ktorá čaká, kým bude stlačená klávesa a vráti celé číslo zložené zo scankódu
klávesy a ASCII hodnoty. Aby ste ich rozdelili, musíte urobiť niečo takéto:
int value, scancode, ascii; value = readkey(); scancode = value >> 8; ascii = value & 0xff;
Podobne keypressed
je náhrada coniovskej funkcie
kbhit
, ktorá vracia nenulovú hodnotu, ak je vo vstupnom zásobníku
stlačená klávesa.
Ďalšie informácie nájdete v dokumentácii k Allegru.
Je niekoľko spôsobov, ktorými môžu objekty v hre reagovať na vstup. Nateraz budeme objektami hýbať konštantnou rýchlosťou v smere, ktorý užívateľ určí. Iné modely spomenieme neskôr.
Takže čo chceme je niečo takéto:
if (key[KEY_LEFT]) x--; /* ak užívateľ stlačí ľavú šipku, ideme vľavo */ if (key[KEY_RIGHT]) x++; /* to isté vpravo */ if (key[KEY_UP]) y--; /* to isté hore; pozn. aby sa išlo hore, y sa zmenšuje */ if (key[KEY_DOWN]) y++; /* to isté dole (y sa zväčšuje) */
kde x
a y
sú súradnice objektu.
Pokúste sa pozmeniť (jednoduchý) kolieskový program, aby užívateľ mohol
kolieskom hýbať. Pokúste sa urobiť program tak, aby vyhovoval navrhnutej
štruktúre. Ukážkový program examples/chap_06/ex_1_3
demonštruje,
ako sa to má spraviť.
S použitím explicitných KEY_*
konštánt vo vašej hre je niekoľko
problémov. Vo všeobecnosti je najlepšie použiť pole key
vtedy, keď
sa nestaráte, čo klávesa normálne značí. Ak od niekoho chcete, aby zadal
<Q> potom na testovanie toho, či to spravil nemôžete použiť pole
key
, pretože mapovanie konštánt KEY_*
na fyzickú
klávesnicu je nezávislé od softwarového mapovania klávesnice. KEY_Q
odkazuje na klávesu vedľa <TAB>, ale inojazyčné a alternatívne mapovania
klávesnice tam nemusia mať <Q>. To znamená, že môžete KEY_*
konštanty použiť na výber pohodlnej rozlohy ovládacích kláves, ale nemôžete ich
použiť, ak je symbol na klávese dóležitý (napr. Q ako Quit)
Keďže klávesnica na PC nebola v skutočnosti navrhnutá ako zariadenie na
ovládanie hier, nemôže sledovať každú klávesu nezávisle. Dokonca sa k tomu ani
nepribližuje. Existujú isté kombinácie kláves, ktorých stlačenie spôsobí, že iné
klávesy už nebudú snímané -- dokonca aj keď máte dobré klávesnicové funkcie ako
v Allegre. Je to hardwarové obmedzenie. Príklad
examples/chap_06/ex_1_4
je jednoduchý program, ktorý opakovane
vypisuje zoznam všetkých kláves, ktoré Allegro registruje ako stlačené; spustite
ho a vyskúšajte stláčať klávesy, kým nezistíte, že ďalšie stlačenie nie je
detekované. Na mojej klávesnici spôsobí súčasné stlačenie kláves <Z>,
<X> a <C> to, že ak sa s nimi stlačí <F>, zostane
nepovšimnuté.
Čo je ešte väčšia smola, nie všetky klávesnice majú takéto prekrytia na tých istých miestach. Ako potom chcete vybrať klávesy, ktorými budete ovládať hru, ak nie je isté, že budú fungovať na všetkých systémoch?
Odpoveďou je samozrejme nechať na užívateľoch, nech si definujú svoje vlastné klávesy. Ak dôjde ku klávesovému prekrytiu, môžu si zvoliť iné klávesy.
Ak to budete chcieť spraviť, bude treba konštanty KEY_*
vo
vašich vstupných procedúrach nahradiť premennými, ktoré inicializujete na nejaké
vstupné hodnoty a potom užívateľom dovoliť meniť ich ak to budú chcieť.
Prirodzene nemôžete očakávať, že užívatelia budú vedieť, čo sú to scankódy;
mali by ste napísať procedúru, ktorá to pre nich zistí.
Použitie poľa key
je celkom jednoduché; jednoducho necháte bežať
cyklus od 1 do KEY_MAX
, najvyššieho scankódu a zistíte si, ktorá
klávesa je stlačená. Vložíte ju do vašej premennej klavesa_vlavo
,
počkáte, kým užívateľ pôjde ďalej, spýtate sa na klávesu "vpravo" atď. Môžete
predpokladať, že prvá stlačená klávesa, ktorú nájdete, je jediná; ktorý praštený
idiot by stláčal dve klávesy naraz, keď je požiadaný o to, aby stlačil jednu? :)
Ukážkový program, ktorý užívateľovi dovolí predefinovať ovládacie klávesy uvidíme neskôr.
Digitálne snímanie joysticku je jednoduché. Uistite sa, či je joystick
centrovaný (t.j. povedzte im, aby dali joystick do stredu a počkajte, kým vám
dajú vedieť, že to spravili) a potom zavolajte
initialise_joystick()
. Teraz kedykoľvek chcete snímať stav
joysticku, zavolajte poll_joystick()
. Táto funkcia zistí stav
joysticku a popri iných veciach obnoví stav globálnych premenných joy_left
, joy_right
,
joy_up
a joy_down
, z ktorých každá ukazuje, či je
alebo nie je joystick tlačený v danom smere.
Príklad vstupného kódu uvedený v klávesnicovej sekcii vyššie bude teraz vyzerať:
poll_joystick(); if (joy_left) x--; /* if the joystick is pushed to the left, go left */ if (joy_right) x++; /* same for right */ if (joy_up) y--; /* same for up; note that y _decreases_ to go up */ if (joy_down) y++; /* and for down y _increases_ */
Predsa len sa uistite, že joystick je centrovaný, keď ho inicializujete.
Funkcia initialise_joystick
vráti nulu iba vtedy, keď je joystick
pripojený, takže to môžete skontrolovať, potom požiadať užívateľa, aby joystick
vycentroval a nakoniec znovu zavolať funkciu initialise_joystick
,
aby sa správne nastavila stredová pozícia.
Pokúste sa zmeniť predošlý príklad tak, aby čítal vstup z joysticku miesto z
klávesnice. Ak zatuhnete, pozrite sa na ukážkový program examples/chap_06/ex_2_1
.
Je jednoduché čítať ich; priemerný joystick ich má dve a ich stav je uložený
v premenných joy_b1
a joy_b2
ako TRUE (nenulová
hodnota), kéď sú tlačidlá stlačené. Ako pri všetkých premenných typu
joy_*
, ich hodnota sa obnovuje iba vtedy, keď zavoláte funkciu
poll_joystick
. Ak ju nikdy nezavoláte, ich hodnota bude stále
rovnaká; nemusíte ju ale volať príliš často. Raz za herný cyklus úple stačí, ak
náhodou nekontrolujete nasledujúci prípad...
Podľa všetkého väčšina joystickov má palebné tlačidlá s odskokom, takže by ste mali vedieť, že stlačenie palebného tlačidla vyvolá niekoľko zmien z "vypnyté" na "zapnuté" a späť; mne to nikdy nespôsobilo žiadne problémy, ale možno sa môj joystick skrátka o to nestará. Vo väčšine hier, ktorých sa to týka, je problém v nastavení maximálnej palebnej rýchlosti (t.j. užívateľ nemôže streliť druhýkrát, kým povedzme neprejde 20 herných cyklov.) Jediná vec, v ktorej odskakovanie spôsobí skutočne veľký chaos sú dvojkiky na tlačítku joysticku; jedno stlačenie môže vyzerať ako dve (prípadne viac) stlačení. Poznamenajme, že to je problém iba vtedy, ak čas medzi odozvami z joysticku je veľmi krátky.
Pokúste sa zmeniť príklad z digitálneho snímania tak, aby ktuh smenil farbu, keď stlačíte palebné tlačidlo. Pokúste sa to urobiť tak, aby pri každom stlačení zmenil farbu len raz (tak, že budete zakaždým čakať až dovtedy, kým sa joystick neuvoľní -- nezabudnite načítať stav joysticku v každom cykle, inak v prenemmých ostanú staré hodnoty). Ak sa farby nemenia správnym spôsobom, tlačidlo môže mať odskakovanie, pokúste sa to zrušiť. Teraz (ťažšie) urobte, aby sa mohol kruh pohybovať, aj keď je ltačidlo stlačené.
Príklady na všetko uvedené vyššie sú v examples/chap_06/ex_2_2
;
pozrite sa špeciálne na techniku použitú v poslednom prípade.
[Poznámka: táto informácia je zastaralá. Stále to bude fungovať ale pre Allegro 4 to chce zmenu.]
Predpokladá sa, že PC joystick je analógové zariadenie; inými slovami, môžete
zistiť nielen ktorým smerom páka ukazuje, ale aj ako ďaleko tým smerom je
naklonená. Ale ako zdôrazňuje Shawn v allegro.txt
:
...presné pravidlá sa môžu líšiť v závislosti na type joysticku, rýchlosti vášho počítača, teplote v miestnosti a fáze mesiaca. Ak chcete získať zmysluplný vstup, musíte joystick pred použitím kalibrovať, čo je trest za programátorove hriechy.
Kalibrácia nie je v skutočnosti taká ťažká, len trocha irituje užívateľov. Aj keď väčšina PC hráčov je na klaibráciu joysticku zvyknutá.
V kalibrácii ide o to, že joystickovské procedúry potrebujú vedieť, aký
rozsah hodnôt v smere X aj Y joystick vracia. Allegro to robí s použitím troch
funkcií: initialise_joystick
, ako sme povedali vyššie, zaznačí
hodnoty v strede; calibrate_joystick_tl
zaznačí hodnoty, keď je
páka čo najďalej vľavo hore a calibrate_joystick_br
robí to isté,
keď je páka vpravo dole.
Znova, obyčajne budete volať initialise_joystick
raz na
začiatku, aby ste zistili, či je alebo nie je joystick pripojený. Ak je a vy ho
chcete neskôr kalibrovať, postupujte takto:
initialise_joystick
, aby si poznačila
stredovú pozíciu.
calibrate_joystick_tl
calibrate_joystick_br
Teraz má Allegro všetko, čo potrebuje vedieť, aby vám bolo schopné dať analógovú pozíciu. Z estetických dôvovdov je vhodné teraz joystick vrátiť do stredovej pozície; inak môžete spustiť niečo dôležité s joystickom, ktorý je stále nastavený vpravo dole. Buď užívateľa požiadajte aby joystick vycentroval a potvrdil to ešte raz, alebo sledujte stav joysticku, kým to neurobí (pozrite nižšie).
Ako ukážku takéhoto kódu si pozrite examples/chap_06/ex_2_3a
.
Keď máme joystick zkalibrovaný, môžeme konečne jeho pozíciu čítať
detailnejšie. Premenné joy_x
a joy_y
vrátia jeho X a Y
súradnice od -128 do 128, kde (-128,-128) je ľavý horný roh, (0,0) je stred a
(128,128) je pravý dolný roh.
Teraz zmeňte program na pohyb kruhom tak, aby sa kruh hýbal tak rýchlo, ako
ďaleko potiahnete páku. examples/chap_06/ex_2_3b
ukazuje ako na to
v prípade, že to nezvládnete. Všimnite si #define
neďaleko
začiatku; ovláda maximálnu rýchlosť kruhu, ktorá zodpovedá úplnému nakloneniu
páky. Použitie #define
alebo premennej typu constant
namiesto čísla zjednoduší zmenu rýchlosti v budúcnosti.
Na myš môžete pozerať jedným z dvoch spôsobov -- buď je to "ukáž a klikni" zariadenie, alebo čosi ako analógový joystick, ktorý poskytuje priamu kontrolu nad postavou.
Prístup "ukáž a klikni" znamená, že na obrazovke je nejaký druh ukazovátka a s pomocou myši sa ním pohybuje. Keď sa stlačí tlačítko, spravíte niečo, čo záleží od toho, kam ukazovátko ukazovalo. Príkladom takého niečoho sú WIMP GUIčka (ako Windows), Dune 1 a 2, Warcraft 1 a 2, spústa rozhraní (rôzne menu) atď. Je to veľmi populárna technika.
Na inicializáciu myši zavolajte install_mouse()
. Ako sa píše v
allegro.txt, funkcia vráti -1, ak inicializácia zlyhá alebo vráti počet
tlačidiel na myši, ak uspeje.
Teraz môžete získať X-ovú a Y-ovú súradnicu ukazovateľa myši v premenných
mouse_x
a mouse_y
(kde sú uložené súradnice na
obrazovke) a stav tlačidiel myši uložený v premennej mouse_b
,
ktorá je bitovým poľom s týmto významom:
Bity: 2 1 0 . . X = príznak ľavého tlačidla . X . = príznak pravého tlačidla X . . = príznak stredného tlačidla
Ostatné bity sú v súčasnosti nepoužité. Príznakové bity troch tlačidiel sú nastavené, ak je zodpovedajúce tlačidlo stlačené, nulové, ak stlačené nie je. Ak myš nemá stredné tlačidlo, patričný príznak je stále nulový.
Takže na zistenie stavu jednotlivých tlačidiel použijete na premennú
mouse_b
binárne AND (&) s číslami 1, 2 alebo 4 nasledujúcim
spôsobom:
if (mouse_b & 1) printf ("Ľavé "); if (mouse_b & 2) printf ("Pravé "); if (mouse_b & 4) printf ("Stredné "); printf ("\n");
Poznamenajme, že hodnota premennej mouse_b
sa môže meniť
(a aj sa mení) "vám za chrbtom"; nasledujúci spôsob testovania môže vrátiť
pochybné výsledky:
if (mouse_b) { printf ("Stlačené tlačidlá: "); if (mouse_b & 1) printf ("Ľavé "); if (mouse_b & 2) printf ("Pravé "); if (mouse_b & 4) printf ("Stredné "); printf ("\n"); }
Predstavte si, čo sa stane, ak je na začiatku stlačené jedno tlačidlo,
takže prvá podmienka if
sa vyhodnotí, ale potom sa tlačidlo
uvoľní; reťazec "Stlačené tlačidlá: " sa vypíše, ale pretože už žiadne
tlačidlo stlačené nie je, žiadna z možností "Ľavé ", "Pravé ", "Stredné "
sa nevypíše.
Ak by podobná nekalosť mala robiť problémy, môžete si vytvoriť kópiu
premennej mouse_b
a pracovať potom s ňou; mimochodom je to
niečo podobné, ako čítanie z joysticku.
my_mouse_b = mouse_b; if (my_mouse_b) { printf ("Stlačené tlačidlá: "); if (my_mouse_b & 1) printf ("Ľavé "); /* atď */ }
Ak chcete zobraziť ukazovateľ na obrazovke, jednoducho zavolajte funkciu
show_mouse
ktorej poviete, na ktorú bitmapu ho chcete ktesliť,
napríklad:
show_mouse (screen);
Kedykoľvek sa myš pohne, Allegro obnoví ukazovateľ myši (ledaže by ste
mu povedali, aby to nerobilo -- pozrite si dokumentáciu k Allegru). Preto
predtým, než do bitmapy čokoľvek zapíšete, treba myš skryť, inak sa na
monitore budú diať zlé veci. Spraví sa to tak, že zavoláte
show_mouse(NULL)
.
Poznamenajme, že funkcie, ktoré priamo vykresľujú ukazovateľ myši (podobne ako niektoré iné časti Allegra) vyžadujú, aby boli nainštalované procedúry časovača -- je to čiastočne vecou toho, ako veci fungujú na pozadí. Kvôli ďalším informáciam si pozrite časovače a čiastočne aj Inicializácia systému časovačov.
Ak potrebujete upraviť oblasť myši, môžete zavolať:
set_mouse_range (min_x, min_y, max_x, max_y);
kde min_x
a min_y
je minimálna hodnota X a Y
a max_x
a max_y
sú maximálne hodnoty.
Ak z nejakého dôvodu chcete posunúť ukazovateľ myši na určité miesto
na obrazovke, zavolajte funkciu position_mouse
, ktorej
odovzdáte ako parametre novú X-ovú a Y-ovú súradnicu.
Ukážkový program examples/chap_06/ex_3_1
demonštruje
prístup "ukáž a klikni".
Táto technika znamená, že zistíme, ako ďaleko sa v každom hernom cykle v jednotlivých smeroch pohla myš a potom s údajmi zaobchádzame ako s analógovými dátami z joysticku. Príklady takýchto hier sú:
Nemyslím si, že je zákonitosť v tom, že v tomto zozname sú iba hry v prvej osobe (teda hry, kde je zobrazená situácia z pohľadu hráča). Nehovorím, že je táto metóda nepoužiteľná v iných hrách; myslím si, že je bežnejšie, že sa v tomto type hier daný prístup využíva.
Keď používate tento systém, myš inicializujete rovnako ako v systéme "ukáž a klikni" (pozrite Inicializácia myši).
Pri použití tohto spôsobu ovládania žiadate ovládač myši o obnovu
hodnoty mickey. Mickey je veľmi jemná miera pohybu myši a je
určovaná relatívne k momentu, kedy ste ju poslednýkrát zisťovali. Allegro
má na to funkciu get_mouse_mickeys
:
void get_mouse_mickeys (int *mickeyx, int *mickeyy);
Dajte funkcii dva smerníky na premenné typu int
a ona
naplní premenné, na ktoré ukazujú hodnotami, o ktoré sa zmenila X-ová a
Y-ová súradnica myši odvtedy čo sme naposledy volali túto funkciu.
Napríklad, ak dx
a dy
sú premenné typu
int
, tak volanie funkcie
get_mouse_mickeys (&dx, &dy);
vloží mickey rozdiely do dx
a dy
.
Poznamenajme, že jednotky nie sú rovnaké, ako jednotky
v mouse_x
and mouse_y
--
mickey má oveľa jemnejšie jednotky.
Ukážka k tejto časti, examples/chap_06/ex_3_2
, používa
túto techniku na pohyb nášho skvelého kruhu, pričom pohyb myši spôsobuje
zrýchľovanie. Ako je [ed: bude] zmienené neskôr, toto je všeobecný model
aj pre realistickejšie hry -- pohyb myši je zviazaný so silou, ktorá
pôsobí na objekt. Pozrite sa na ukážku, aspoň aby ste vedeli, ako to
funguje.
Keď máme k dispozícii uvedené typy vstupov, môžeme písať programy, v ktorých môžeme ovládať veci ktorýmkoľvek z nich. Ako ale napísať hru, ktorá dovoľuje užívateľovi použiť zariadenie, ktoré si vyberie?
Môžeme napísať verzie reagujúceho kódu, ktoré budú interpretovať každý
typ informácie a potom použijeme ten, ktorý korešponduje so zariadením,
ktoré vybral užívateľ, ale bolo by lepšie minimalizovať počet styčných
bodov medzi vstupným kódom a reagujúcim kódom. Príklad
examples/chap_06/ex_4
ukazuje spôsob, ako sa to dá spraviť;
odporúčam vám pozerať sa do neho, kým budete čítať túto časť. Pozrite sa
hlavne na input.c
a input.h
.
Potrebujeme všeobecný formát, ktorý budeme posielať reagujúcemu kódu. Potrebujeme informáciu o tom, ako ďaleko je vstupné zariadenie v každom smere, a aký je stav palebných tlačidiel. Na uschovanie týchto informácií som vytvoril nasedovnú štruktúru:
struct input_t { int dx, dy; int fire1, fire2; };
Nech sú dx
a dy
čísla z rozsahu od -128 do 128
(ako pri joystickových procedúrach), ktoré ukazujú, ktorým smerom zariadenie
ukazuje a ako ďaleko. fire1
a fire2
budú nenulové,
ak je stlačené zodpovedajúce palebné tlačidlo. Ich počet sme obmedzili na
dve, pretože väčšina joystickov má iba dve palebné tlačidlá; ak píšete kód,
ktorý má fungovať pod viacerými systémami, často je rozumné obmedziť sa na
to, čo je spločné.
Vstupná procedúra bude musieť získať informáciu zakaždým, keď ju
zavolajú. Pre vstup z joysticku je to jednoduché; stačí skopírovať patričné
hodnoty, keďže rozsah je rovnaký. Ak je joystick digitálny, budeme ale
musieť skontrolovať joy_left
, joy_right
, atď.
pričom použijeme rovnaký systém, aký opíšeme o chvíľu pre klávesnicu.
Pre klávesnicu musíme nejakým spôsobom určiť, kde vlastne ukazuje.
Ak máme určené klávesy left_key
a right_key
,
môžeme určiť vychýlenie vpravo (rightness) nasledovne:
rightness = (key[left_key] ? -1 : 0) + (key[right_key] ? 1 : 0);
Pozrite sa, čo to spraví v jednotlivých situáciach: Ak žiadna z kláves nie je stlačená, hodnota oboch zátvoriek je nulová a výsledok je 0. Ak sú stlačené obe, prvá zátvorka vráti -1 a druhá 1; takže výsledok je znovu 0. Ak je stlačená iba ľavá klávesa, prvá zátvorka bude -1 a druhá 0, akže dokopy -1 a ak je stlačená iba pravá klávesa, výsledok bude 0+1, čo je 1. Teraz to môžeme prispôsobiť našej štruktúre napríklad takto:
dx = (key[left_key] ? -128 : 0) + (key[right_key] ? 128 : 0); dy = (key[up_key] ? -128 : 0) + (key[down_key] ? 128 : 0); fire1 = key[fire1_key]; fire2 = key[fire2_key];
Každý vstup z klávesnice tu má rovnaký účinok ako úplné vychýlenie joysticku.
Čo sa týka myši, použijeme priame ovládanie; ak by sme chceli použiť rozhranie "ukáž a klikni", boli by sme príliš závislí na užívateľovi, ktorý myš používa. Tento systém môžete simulovať s pomocou vstupných procedúr na ľubovoľnom vstupnom zariadení, ale na tomto mieste to nechceme rozoberať.
Aby sme mohli zobraziť premenné dx
a dy
z myšieho kódu do rozsahu [-128,128] ktorý tu používame, potrebujeme ich
vydeliť max_mousespeed
a vynásobiť 128, pričom
max_mousespeed
je premenná ovládajúca citlivosť myši. Spravíme
to ale v opačnom poradí, lebo potom to môže zostať ako výpočet v celočíselnej
aritmetike. Pretypovaniu medzi celočíselnými a desatinnými typmi by sme
sa mali vyhnúť všade, kde je to možné -- je pomalé.
Celý kód obsahujúci to, o čom sme sa až doteraz bavili je vo funkcii
input_getinput
.
Predtým, než to začneme používať, je ale potrebné vstupnému modulu
povedať, aký druh vstupu očakávame. To môžete spraviť zavolaním funkcie
input_create_*
, ktorá môže mať nejaké parametre (v závislosti
na tom, čo *
v skutočnosti je). Táto funkcia musí byť urobená
tak, aby vytvorila vstupnú štruktúru a vrátila smerník na ňu. Vstupná
funkcia joysticku žiadnu informáciu naviac nepotrebuje, ale vstupná funkcia
myši potrebuje vedieť, akú hodnotu použiť pre max_mousespeed
,
a vstupná funkcia klávesnice potrebuje vedieť, ktoré klávesy použiť. Mohli
by sme použiť jedinú funkciu s premenlivým počtom parametrov, ale to je pre
potreby tohto tutoriálu trocha komplikované.
Takže definujme funkcie"
input_t *input_create_joystick (); input_t *input_create_mouse (int max_mousespeed); input_t *input_create_keybd (int left_key, int right_key, int up_key, int down_key, int fire1_key, int fire2_key);
Tiež budeme potrebovať funkcie pre inicializáciu vstupného modulu, funkciu, ktorá ho zruší a funkciu, ktorá zruší už zaregistrované vstupné zariadenie (napr. ak zmeníte klávesy); ich deklarácia môže vyzerať takto:
void input_init (); void input_shutdown (); void input_destroy (input_t *what);
Takže ak to zhrnieme, programy na začiatku zavolajú input_init
aby nastavili modul. Potom zavolajú niektoré z input_create_*
funkcií, aby zaregistrovali vstupné zariadenia hráčov; input_create_keybd
sa môže volať viac než raz s rôznymi klávesami, aby mohol hrať na klávesnici
viac než jeden hráč. Funkcie input_create_*
vrátia smerníky na
štruktúry input_t
, ktorých členské premenné budú obnovené
pri každom zavolaní input_getinput
. Zaregistrované zariadenia
sa môžu odregistrovať zavolaním funkcie input_destroy
, ktorá
ako parameter dostane smerník na struct input_t
; táto funkcia
súčasne dealokuje štruktúru, na ktorú smerník ukazuje. Funkcia
input_shutdown
odregistruje všetky zariadenia a uvoľní všetku
pamäť, ktorá je ešte alokovaná.
Štruktúra input_t
obsahuje polia dx
, dy
,
fire1
a fire2
. dx
and dy
nadobúdajú hodnoty od -128 do 128, ktoré ukazujú množstvo pohybu v jednotlivých
smeroch a fire1
a fire2
sú nenulové, ak je stlačené
zodpovedajúce palebné tlačidlo.
Ešte raz, pozrite si program examples/chap_06/ex_4
, aby ste
videli, ako tento systém používať.
Táto časť zatiaľ nie je napísaná. Keď bude, bude obsahovať informácie o fyzikálnych modeloch (nie, nie je to taká nuda, ako to znie!) a ako môžete vstupné dáta použiť.
Táto časť zatiaľ nie je napísaná. Keď bude, bude hovoriť o:
Aby ste nainštalovali ovládače zvuku, treba zavolať funkciu:
install_sound (digi_driver, midi_driver, NULL);
kde na miesto parametra digi_driver
vložíte konštantu
DIGI_*
a na miesto parametra midi_driver
konštantu MIDI_*
. Tretí parameter je zastaralý, dajte
tam skrátka NULL.
Ak je funkcia úspešná, vráti nulu. Nenulová návratová hodnota
pravdepodobne znamená, že ovládač, ktorý požadujete, nie je prístupný.
Ak pre niektorý ovládač používate *_AUTODETECT
, môže
to znamenať, že konfiguračný súbor špecifikuje, ktorý ovládač presne
treba použiť a tento ovládač je nepoužiteľný.
Myslím si, že sa málokedy vyskytne rozumný dôvod nepoužiť
DIGI_AUTODETECT
a MIDI_AUTODETECT
. Moje dôvody
sú také, že ak určíte zariadenie explicitne, hra nebude fungovať, ak toto
zariadenie nie je dostupné a nebude schopná použiť lepšie zariadenie,
ak toto dostupné bude. Dokonca ak máte v počítači nejaký hardwarový
konflikt, ktorý spôsobuje, že táto voľba normálne zlyháva, nie je to dôvod
na to, aby ste zrušili funkčnosť vašej hry na počítačoch iných ľudí --
môžete si vytvoriť konfiguračný súbor (pozrite nižšie). Konfiguračné súbory
majú vždy prednosť pred autodetekciou, takže konflikt zmizne -- môžete sa
spoľahnúť na to, že rovnako to bude fungovať aj u koncových užívateľov;
ak sa vyskytnú problémy, môžu ich odstrániť. Ak parametre zadáte napevno,
ľudia nebudú môcť nastavenia zmeniť, kým si to neprekompilujú -- čo je
zlá novinka!
Takže ak nemáte veľmi dobrý dôvod (žiaden ma nenapadá), používajte
konštanty *_AUTODETECT
. Dokonca ukážkový program
play.exe
v adresári tests
Allegra je pre mňa
hraničným prípadom -- používa pevne určenú tabuľku čísel a mien ovládačov.
To je dosť zlé; výhoda tohto prístupu (aspoň myslím) spočíva v tom,
že sa ovládače otestujú rýchlo. Nie ktovieaká výhoda...
Treba poznamenať ešte jednu vec -- MIDI prehrávač Allegra poučíva pre svoju činnosť procedúry časovača, takže predtým, ako budete prehrávať MIDI súbory ho musíte inštalovať. Viac podrobností nájdete v kapitole Inicializácia systému časovača.
Allegro v konfiguračných súboroch podporuje široký okruh oblastí;
konfigurácia zvuku je len časťou štandardných dát a vy môžete pridávať
pre vnútornú potrebu vašej hry dáta podľa vlastného výberu. Čo sa všetkých
detailov použitia konfiguračných súborov týka, pozrite si dokumentáciu
k Allegru (ak používate info, napíšte v príkazovom riadku
info allegro config
). Tu bude zmienka iba o tom, čo sa týka
ovládačov zvuku.
Ak nepoviete inak, program skontroluje vo svojom domovskom adresári
súbory allegro.cfg
a sound.cfg
. Ak chcete použiť
iné meno súboru, dajte ho ako parameter funkcii set_config_file
.
Informácia o konfigurácii zvuku je v časti [sound]
súboru a obsahuje okrem iného informácie o tom, ktoré ovládače
s akými nastaveniami sa majú použiť, ak zažiadate o autodetekciu.
Allegro potom autodetekciu spracuje tak, že ak je ovládač explicitne určený
v konfiguračnom súbore, tak sa ten ovládač použije. Ak žiaden ovládač
neurčíte, Allegro sa vám začne hrabať v mašine, aby zistilo, čo tam
vlastne je; toto môže mať vedľajšie efekty a navyše z technických
dôvodov niektoré veci (ako napr. externé MIDI zariadenie) nikdy nebudú
zistené
Najbezpečnejšia a najjednoduchšia cesta, ako vytvoriť konfiguračné
súbory je program setup
. Je navrhnutý tak, aby sa dal pribaliť
k vašim hrám a aby si s jeho pomocou mohol užívateľ vybrať (alebo
si nechať zistiť) a vyskúšať nastavenia. Setup potom uloží nastavenia do
konfiguračného súboru, ktorý môže váš program čítať.
Teda najjednoduchší spôsob ako to distribuovať, je pribaliť súbor
setup.exe
do toho istého adresára, ako spustiteľný súbor
vášho programu, takže výstup z programu setup bude presne tam, kde ho
váš program bude očakávať. Je veľa vecí, ktoré s pomocou programu
setup môžete nastaviť (alebo nechať tak). Úplnú informáciu nájdete
v súbore setup.txt
(je v tom istom adresári ako zvyšok
programu setup).
Allegro vie čítať digitálny zvuk z WAV a VOC súborov. Oba typy súborov musia byť mono. WAV súbory môžu byť 8 alebo 16 bitové; VOC súbory musia byť 8 bitové.
Načítanie súboru nemôže byť jednoduchšie -- meno WAV alebo VOC súboru
(musí mať správnu koncovku) dáte ako parameter funkcii load_sample
a ona vráti SAMPLE *
ktorý ukazuje na zvuk alebo NULL, ak sa zvuk
nepodarilo nahrať (teda nebola rozlíšená koncovka, nebol nájdený súbor alebo
formát súboru je nesprávny alebo nepodporovaný).
Môžete tiež rovnakým spôsobom priamo zavolať load_voc
alebo
load_wav
. Výhodou tohto prístupu môže byť, že sa spracuje aj
súbor, ktorý nemá koncovku .WAV alebo .VOC.
Allegrovský prehrávč zvukov môže počas prehrávania narábať so zvukmi rôznymi spôsobmi. Parametre, ktoré na základnej úrovni môžete nastaviť sú hlasitosť, balanc a frekvencia. Zvukovému prehrávaču tiež môžete povedať, či chcete alebo nechcete vždy spustiť zvuk od začiatku, keď sa dohrá do konca.
Keď chcete prehrať zvuk, treba zavolať funkciu play_sample
.
Má nasledujúce parametre:
SAMPLE *
)
Poznamenajme, že zmena frekvencie môže viesť k nesprávnemu prehraniu -- Allegro je knižnica na programovanie hier, nie súbor rafinovaných programov na úpravu zvukov. Tiež prehrávanie zvukov na nižšej hlasitosti, ale s ovládačom hlasitosti na vašej stereosúprave vytočeným nadoraz doprava bude viesť k skresleniu, nehovoriac o šume ;). Snažte sa, pokiaľ je to možné, využívať celý rozsah hodnôt a používať zvuky, ktoré tiež vnútorne používajú celý prípustný rozsah.
Typické volanie funkcie play_sample potom bude vyzerať takto:
play_sample (gun_sound, 255, 128, 1000, 0);
čo prehrá gun_sound
(ktorý ste predtým načítali) na
maximálnej hlasitosti, zo stredu, s jeho pôvodnou frekvenciou a bez
cyklenia.
play_sample (some_sound, 192, 96, 1200, 1);
bude hrať some_sound
na 3/4 hlasitosti, posunutý jemne vľavo,
trochu vyššie ako obyčajne a vždy, keď príde na koniec, začne sa hrať od
začiatku.
Na zastavenie prehrávania zvuku slúži funkcia stop_sample
:
stop_sample (some_sound);
To je zvlášť užitočné pri zacyklených zvukoch.
Tieto parametre môžete upraviť aj počas prehrávania súboru. Používa
sa na to funkcia adjust_sample
. Má rovnaké parametre ako
play_sample
a v zozname prehrávaných zvukov hľadá ten,
ktorý ste zadali. Takže ak už spomínaný zvuk some_sound
stále
hrá, môžete zrušiť cyklenie takto:
adjust_sample (some_sound, 192, 96, 1200, 0);
Samozrejme môžeme zmeniť aj iné parametre. Ak chcete upravovať zvuky,
kým sú prehrávané, dbajte na to, aby súčasne nebolo prehrávaných viacero
kópií daného zvuku -- adjust_sample
zmení iba prvú kópiu,
ktorú nájde a ostatné sa nezmenia.
Rafinovanejší spôsob, ako prehrávať a ovládať zvuky, ktorý nemá spomínané problémy nájdete v kapitole Hlasové funkcie.
Keď už zvuk nepotrebujete, môžete ho odstrániť z pamäti funkciou
destroy_sample
nasledujúcim spôsobom:
destroy_sample (some_sound);
Ak zvuk ešte stále hrá, bude automaticky zastavený.
Táto časť ešte nebola napísaná. Bude obsahovať informácie o tom, ako používať hlasové funkcie priamo, aby ste získali nad zvukmi väčšiu kontrolu.
Je tu ale ukážkový program: examples/chap_08/ex_2_5
V súčasnosti podporuje Allegro iba MIDI súbory, z ktorých môže nahrať buď typ 0 alebo 1 (teda väčšinu).
Na načítanie MIDI súboru do pamäti zavolajte funkciu load_midi
,
ktorá ako parameter dostane meno súboru a vráti MIDI *
alebo
NULL, ak sa načítanie nepodarí.
Ak chcete začať s prehrávaním MIDI objektu, jednoducho zavolajte
play_midi
:
play_midi (title_theme, 0);
Druhý parameter je príznak cyklenia; 0 znamená necykliť, čokoľvek iné spôsobí, že keď sa súbor dohrá, spustí sa od začiatku. Ak chcete, aby sa váš súbor opakoval od iného miesta (napríklad ak má úvod, ktorý chcete pri opakovaní preskočiť), môžete zavolať:
play_looped_midi (background_music, loop_start, loop_end);
Keď je dosiahnutá pozícia loop_end
, prehrávač skočí späť
na loop_start
. Ak je loop_end
-1, návratový bod
bude až na konci skladby. Jednotky pre loop_start
a
loop_end
sú takty, tak ako sú odmeriavané premennou
midi_pos
zmienenou nižšie.
Inak môžete hudbu zastaviť volaním stop_midi
:
stop_midi();
Keďže Allegro môže naraz prehrávať iba jednu hudbu, táto funkcia
nepotrebuje parametre. Začiatok prehrávania hudby zatiaľ, čo hrá nejaká
iná, spôsobí zastavenie prehrávania predošlej. Volanie funkcie
stop_midi
má rovnaký efekt ako volanie play_midi
s prvým parametrom NULL.
Počas prehrávania MIDI súboru Allegro zväčšuje raz za takt globálnu
premennú midi_pos
. Ak sa žiaden MIDI súbor neprehráva
(alebo necyklický súbor už skončil), je nastavená na -1.
Poznamenajme, že zväčšenie nemusí nutne nastať v každom takte -- ak sa v nejakom takte nič nehrá, premenná sa zmení až pri hraní najbližšej noty. Ak chcete s premennou synchronizovať udalosti, uistite sa, či je v každom takte nejaká nota; môžete napríklad vložiť notu s nulovou hlasitosťou, ktorá bude slúžiť len na spustenie udalosti.
Prehrávanie môžete zastaviť a znovu spustiť s pomocou funkcii
midi_pause
a midi_resume
. Môžete skočiť na určitú
pozíciu v súbore ak cieľovú midi_pos
hodnotu odovzdáte
ako parameter funkcii midi_seek
. Pamätajte na to, že
vyhľadávanie dozadu spôsobí previnutie súboru na začiatok a potom
hľadanie dopredu; teda nebude dobrý nápad robiť za sebou niekoľko malých
vyhľadávaní dozadu.
Nakoniec, ak sa súbor prehráva cyklicky, môžete za jazdy zmeniť miesta
cyklenia. Dve premenné, ktoré ich určujú, sú midi_loop_start
a midi_loop_end
a sú merané v midi_pos
jednotkách.
Hodnota -1 pre midi_loop_start
alebo midi_loop_end
znamená začiatok resp. koniec súboru.
Spomenul som už predtým, že časť MIDI súboru môže tvoriť predohru
ktorú v cykle nechcete hrať zakaždým a funkcia play_looped_midi
tento problém rieši; nastavenie bodov cyklenia vám dovoľuje spraviť
podobnú vec, ak MIDI súbor obsahuje záver, ktorý nechcete hrať počas
opakovania -- teda chcete raz zahrať úvod, potom nekoľkokrát cykliť hlavnú
časť a nakoniec zahrať záver.
Aby ste to dosiahli, začnete prehrávať hudbu s použitím funkcie
play_looped_midi
kde ako parameter začiatku cyklu uvediete
začiatok hlavnej časti a ako koniec cyklu koniec hlavnej časti. Potom
necháte hudbu hrať; zakaždým, keď hlavná časť skončí, spustí sa znovu
od začiatku. Neskôr môžete parameter konca cyklu nastaviť na koniec skladby.
Sú tam ale niektoré zádrhele:
midi_loop_start
musí byť vždy skôr, než
midi_loop_end
; ak je vaša nová štartovacia hodnota väčšia, než
stará koncová, musíte najprv zmeniť koncovú hodnotu a ak je vaša nová
koncová hodnota menšia než stará štartovacia, treba najprv zmeniť
štartovaciu.
midi_loop_end
a až potom midi_loop_start
ak sa
koncový bod posúva dozadu a v opačnom poradí, ak sa koncový bod posúva
dopredu. To zmenšuje pravdepodobnosť skokov počas zmeny.
Pravdepodobnosť, že niektorá z uvedených vecí bude robiť vážne problémy je pomerne malá, ale je lepšie to zabezpečiť, než ľutovať. (Týka sa to najmä prvej.)
Na uvoľnenie MIDI súboru z pamäti použite funkciu destroy_midi
:
destroy_midi (background_music);
Ak je súbor práve prehrávaný, hudba sa najprv zastaví.
V tomto zozname nájdete rôzne použitie časovačov. Samozrejme nie je úplný -- jeho cieľom je poskytnúť vám predstavu, na čo sa väčšinou časovače používajú. Niektoré, prípadne všetky z nich sa neskôr objavia v príkladoch.
Toto je pravdepodobne najbežnejšie použitie. Časovač môžete použiť na to, aby ste zistili, ako dlho hráč zotrváva v jednej úrovni aby ste mu pridali body, ak to zvládne rýchlo, alebo ho zamordovali, ak je tam pridlho.
Ak zväčšíte globálnu premennú zakaždým, keď prekreslíte obrazovku, môžete nastaviť časovač tak, aby túto hodnotu niekam raz za sekundu skopíroval. Miesto, kde to skopírujete, bude potom ukazovať počet prekreslení za poslednú sekundu -- je to skrátka číslo FPS (frames per second, rámcov za sekundu) alebo obnovovacia frekvencia hry. Toto je veľmi užitočné, keď zisťujete, ako dobre hra funguje na počítačoch iných ľudí; ak je číslo vysoké, majú peknú hladkú grafiku; ak nie, grafika je trhaná.
Možno sa pamätáte, že keď sme sa kedysi skôr pokúšali získať
konštantné zdržanie pri pohybe kruhom krížom cez obrazovku, tak sme na
ovládanie rýchlosti použili vsync
.
Predošlý systém mal zásadný nedostatok v tom, že rýchlosť cyklu stále závisí od rýchlosti počítača -- ak čakáme 10 ms medzi vykresleniami a vykreslenie trvá 5 ms, budeme vykresľovať raz za 15 ms; ale ak vykreslenie trvá 10 ms, budeme vykresľovať raz za 20 ms. Na reguláciu rýchlosti priveľký rozdiel.
Použitie vsync
je lepšie; vertikálne zatemnenie prichádza
v pravidelných intervaloch, takže ak zariadime, že naše prekreslovane a
pohyb zaberie menej času, než jeden zatemňovací cyklus (alebo presnejšie,
vždy zaberie rovnaký počet zatemňovacích cyklov), dosiahneme konštantnú
rýchlosť. Ak ale ostaneme na niektorom rámci trocha dlhšie, hra bude
vyzerať, akoby na zlomok sekundy zastala, pretože vsync
zmešká jeden VBI (interval vertikálneho zatemnenia) a musí čakať na ďalší.
To sa deje pomerne často pod určitými polo-viacúlohovými operačnými
systémami, kde systém prevezme kontrolu nad počítačom tesne pred VBI
-- vsync
potom ten VBI zmešká a musí čakať na ďalší.
Výsledný efekt je pekne drsný (hra je strašne trhaná a podľa mňa najhoršie
je omeškávanie sa v čase).
Lepší spôsob na udržanie konštantnej rýchlosti hry sú časovače. Aby sa
znížilo blikanie, prekreslovanie musí byť stále ovládané s pomocou
vsync
, ale náš časovač vie efektívne zistiť, ako dlho musel
vsync
čakať a zdržanie môžeme neskôr vyrovnať tak, že
patričný počet krát prejdeme cez logiku hry.
Ako presne sa to spraví, uvidíme potom, ako sa pozrieme, ako časovače fungujú. Alebo môžete rovno skočiť na danú časť teraz -- to je na vás. Pozrite sa na Ovládanie rýchlosti hry.
Ak nastavíme časovač tak, aby behal dosť často, môžeme získať vstup v pravidelných intervaloch. Načo je to dobré? Nuž, niektoré herné systémy (ako napríklad ten opísaný vyššie) majú sklon prebehnúť niekoľkokrát za sebou cez herný cyklus a potom zbehnúť dlhú funkciu na obnovovanie grafiky. Strávia veľkú časť svojho času kreslením a keď s tým skončia, znova sa rýchlo niekoľkokrát preženú herným cyklom. Ak by sme čítali vstup vždy počas herného cyklu, tak by sme prečítali zakaždým ten istý stav.
Ako extrémny príklad si skúste predstaviť, že vykonanie vykresľovacej funkcie zaberie celú sekundu a funkcia herného cyklu sa za ten čas má vykonať desaťkrát. Ak užívateľ stlačí tlačidlo počas behu grafickej funkcie, alebo ho dokonca väčšinu času drží stlačené, funkcia herného cyklu si to nevšimne, pretože vstup kontroluje, pretože vstup sa kontroluje až po skončení grafickej funkcie. Ale ak užívateľ stlačí klávesu po skončení grafickej funkcie, všetkých 10 obnovovaní pozície bude vidieť tlačidlo stlačené, čo je tiež zle. To, čo potrebujeme je, aby každé obnovenie stavu hry videlo iný stav vstupu, získaný v pravidelných intervaloch.
Pred tým, než môžete použiť niektorú z funkcií časovača, musíte inicializovať systém časovačov Allegra. Ako už bolo povedané, potrebujú to aj mnohé iné časti Allegra (ako napríklad kód na vykreslovanie ukazovateľa myši alebo prehrávač MIDI súborov), takže ho už pravdepodobne budete mať nainštalovaný.
Na inicializáciu systému časovačov zavolajte:
install_timer();
Po tomto volaní vám prestane fungovať funkcia delay
z knižnice libc; môžete použiť funkciu rest
z Allegra,
ktorá robí takmer to isté.
Allegrovské funkcie časovača sú ovládané cez prerušenia. Prerušenia sú vyvolávané istými udalosťami a prerušia vykonávanie vášho programu. V tomto momente procesor začne vykonávať kód ISR (interrupt service routine - funkcia obsluhy prerušenia). Allegro má rôzne mechanizmy, s pomocou ktorých sa dozvie o tom, že sa určité prerušenie vyskytlo -- v tomto prípade prerušenie od časovača.
Allegrovský ovládač prerušenia volá rôzne funkcie, ktoré určíte, nazývané obslužné funkcie (anglicky callbacks), v intervaloch, ktoré tiež môžete určiť. Dôležité je, že tieto funkcie sa volajú vo vnútri prerušenia.
Z rôznych technických dôvodov je veľmi nešťastný nápad pokúšať sa počas prerušenia presúvať veci na disk a z disku. To znamená, že sa musíte uistiť, že vaše obslužné funkcie nesmú byť počas behu programu dočasne odložené z pamäte na disk -- ak by boli, museli by sa počas prerušenia načítavať späť do pamäte. Tiež sa musíte uistiť, že všetky dáta, ktoré menia -- globálne premenné atď. -- sa nikdy neocitnú v swapovacom priestore na disku. Toto urobíte zamknutím pamäte, kde sú uložené. Djgpp má funkcie, ktoré to vedia urobiť a Allegro ich má pekne zabalené vo forme nasledovných makier:
END_OF_FUNCTION (function_name); LOCK_FUNCTION (function_name); LOCK_VARIABLE (variable_name);
LOCK_VARIABLE
zamyká statickú alebo globálnu premennú
a LOCK_FUNCTION
zamyká funkciu. LOCK_VARIABLE
funguje aj sama o sebe, ale LOCK_FUNCTION
potrebuje,
aby ste koniec zamykanej funkcie označili s pomocou
END_OF_FUNCTION
. Ak END_OF_FUNCTION
uviesť
zabudnete, dostanete spústu nebezpečne vyzerajúcich (ale celkom slušne
vysvetľujúcich) varovaní kompilátora a chýb linkera.
Ďalšia vec, s ktorou sa stretneme, keď je reč o prerušeniach, je
volatilita (premenlivosť). Každý slušný kompilátor sa snaží optimalizovať
kód, ktorý generuje. Obyčajne pri tom predpokladá, že premenná, do ktorej
nezapisujete, sa nemení. To je väčšinou pravda, ale ak niektorá funkcia
volaná cez prerušenie túto premennú zmení, o tejto zmene nemusí
kompilátor nič vedieť. Aby ste tento problém vyriešili, musíte všetky
premenné, ktorých sa to týka deklarovať ako volatile.
volatile
je kľúčové slovo jazyka C, ktoré povie kompilátoru,
aby nerobil žiadne predpoklady ohľadom obsahu premennej -- a navyše
nebude dočasne uchovávať obsah premennej v registri.
Samozrejme nedeklarujte všetky vaše premenné ako volatile. Keby ste to spravili, značne by ste tým obmedzili schopnosť kompilátora optimalizovať váš kód a výsledný program by pravdepodobne bežal pomalšie. Ako volatile označte len tie premenné, do ktorých zapisujete alebo z ktorých čítate v súvislosti s prerušením (teda vo vnútri obslužnej funkcie časovača alebo iného prerušenia).
Keď ste nainštalovali systém časovačov, zamkli obslužné funkcie a
všetky dáta s ktorými narábajú a označili všetky takéto dáta ako
volatile
, môžete konečne povedať Allegru aby obslužné
funkcie začalo volať.
install_int (callback, msecs);
Nahraďte callback
menom funkcie, ktorá vracia void a nemá
parametre (v jazyku C) alebo neurčený počet parametrov (v jazyku C++).
msecs
je interval, v akom sa bude funkcia volať.
Tu je príklad funkcie pre C:
volatile int counter = 0; /* Budeme to v obslužnej funkcii meniť, takže to musí byť volatile. */ void my_callback_func() { counter++; /* Pekné a jednoduché */ } END_OF_FUNCTION (my_callback_func); /* Všimnite si syntax */
Poznamenajme, že obslužná funkcia musí byť veľmi jednoduchá. Nesmie trvať príliš dlho, nesmie volať žiadne knižničné funkcie jazyka C, nesmie si dovoliť žiadny prepych ako napr. volanie funkcií DOSu. Ak volá procedúry Allegra, musia spĺňať rovnaké pravidlá.
Ak použivame C++, musíme zmeniť definíciu funkcie na:
void my_callback_func(...)
inak dostaneme chybu.
Ďalej je tu kód, ktorý použijeme na zamknutie vecí zmienených vyššie:
LOCK_FUNCTION (my_callback_func); /* Zamkneme funkciu */ LOCK_VARIABLE (counter); /* Mení sa v obslužnej funkcii, takže ju zamkneme tiež. */
Nakoniec nasledujúci riadok povie Allegru, aby našu funkciu volalo desaťkrát za sekundu (raz za sto milisekúnd):
install_int(my_callback_func, 100);
Všimnite si, že aj keď my_callback_func
je funkcia, za jej
menom nepíšeme zátvorky ((
a )
) -- keby sme to
spravili, funkcia by sa zavolala a jej (neexistujúca) návratová hodnota
by sa zadala install_int
. My ale potrebujeme zadať miesto, kde
v pamäti začína kód funkcie, takže zátvorky nepíšeme.
Mimochodom, ak chcete zmeniť interval volania funkcie, môžete zavolať
install_int
neskôr znova.
Ak chcete mať lepšiu kontrolu nad obslužnými funkciami, môžete použiť
funkciu install_int_ex
. Parameter tejto funkcie sa zadáva
v tikoch hardwarových hodín, ale môžete použiť makrá, ktoré na tento formát
vedia skonvertovať bežnejšie jednotky -- sekundy alebo milisekundy na jedno
volanie, alebo počet volaní za sekundu alebo minútu. Čo sa týka detailov
k tejto funkcii a ďalších informácií o tom, čo obslužné funkcie môžu alebo
nemôžu urobiť, pozrite si Allegrovkú dokumentáciu.
Odinštalovanie obslužnej funkcie je jednoduché -- stačí zavolať funkciu:
remove_int(my_callback_proc);
Teoreticky by ste teraz mohli odomknúť pamäť, ktorú funkcia zaberá, ale prakticky to nehrá žiadnu rolu.
Tu je zoznam niektorých vecí, ktoré musíte a ktoré nesmiete, keď píšete a používate obslužné funkcie časovačov (a vo všeobecnosti aj iných prerušení).
Musíte:
Nesmiete:
Tento príklad je celkom jednoduchý. Nastavíme globálnu premennú tak, aby obsahovala uplynulý počet sekúnd. Naša obslužná funkcia bude toto číslo zväčšovať. Zamkneme počítadlo aj obslužnú funkciu. Potom, keď chceme spustiť hodiny, nechháme obslužnú funkciu volať raz za sekundu. Potom môžeme z (volatile!) premennej zistiť, koľko času uplunulo.
Keď čas, ktorý stopujeme uplynie, môžeme si počítadlo skopírovať, alebo len odinštalovať obslužnú funkciu. Ak ju odinštalujeme, počítadlo sa už meniť nebude.
Ako variáciu môžeme naprogramovať časový limit. V tomto prípade nastavíme počítadlo na počet sekúnd, ktoré sú k dispozícií (napríklad na uhratie levelu). Obslužná funkcia bude túto hodnotu zmenšovať. V hlavnom cykle hry budeme kontrolovať, či je v počítadle kladná hodnota. Ak nie je, hráčovi vypršal čas. V tomto momente môžete obslužnú funkciu odinštalovať, aby ste zastavili ďalšie zmenšovanie počítadla.
Pozrite si examples/chap_09/ex_4_1
.
Toto je tiež celkom jednoduché
Najprv si urobíme dve volatile globálne premenné -- jednu nazvanú
last_fps
a druhú frame_counter
. Potom spravíme
obslužnú funkciu časovača, ktorá jednoducho skopíruje frame_counter
do last_fps
a potom nastavíte frame_counter
na nulu. Zamkneme obe premenné a obslužnú funkciu a potom obslužnú funkciu
necháme vyvolať raz za sekundu.
Teraz môžeme premennú last_fps
zobraziť na monitore
alebo zapísať do logu alebo čokoľvek. Na chvíľu to bude nula. Všetko, čo
musíme urobiť, aby sme získali počet vykreslených rámcov za sekundu je,
aby vykresľovacia funkcia po každom vykreslení rámca premennú
frame_counter
zväčšila o 1.
Funguje to preto, lebo obslužná funkcia sa volá každú sekundu a kopíruje
hodnotu do last_fps
, pričom vynuluje frame_counter
.
Potom vykreslíme nejaké rámce a kým to robíme, zväčšuje sa premenná
frame_counter
. Sekundu po predošlom volaní sa obslužná funkcia
zavolá znovu a skopíruje počet vykreslených rámcov z frame_counter
do last_fps
.
Pozrite si examples/chap_09/ex_4_2
.
Táto téma je mojou nomináciou na "často pýtanú otázku" roka v mailingliste Allegra. Je veľmi dôležitá. Hry, ktoré nebežia rovnako rýchlo na rôznych počítačoch alebo za rôznych podmienok ma štvú. Nemyslím tým, že obnovovacia frekvencia musí byť na všetkých počítačoch rovnaká -- to samozrejme nie je možné. Hovorím o rýchlosti hry -- napríklad o rýchlosti pohybu postáv.
Aby ste to dosiahli, mohli by ste v ideálnom prípade chcieť hýbať postavami v pravidelných intervaloch. Žeby vo vnútri obslužnej funkcie časovača? V žiadnom prípade. Je to príliš komplikované; pamätajte, že obslužné funkcie musia byť jednoduché. Mimochodom, predstavte si, koľko problémov by narobilo snažiť sa zamknúť všetko, čoho sa takáto obslužná funkcia dotkne!
Najlepšia vec, ktorú môžeme reálne urobiť je časovač zväčšujúci premennú, ktorá obsahuje počet herných cyklov, ktoré by mali doteraz uplynúť. Potom môžeme upraviť herný cyklus tak, aby túto premennú porovnával s počtom herných cyklov, ktoré skutočne uplynuli. Ak sa omeškávame, musíme zavolať funkciu obsluhujúcu logiku hry ešte raz (prípadne niekoľkokrát), kým to nezrovnáme. Inak herným cyklom momentálne prejsť netreba.
Ak sme patričný počet cyklov prešli, môžeme si dať pohov a robiť iné
veci, ako zobraziť jeden rámec grafiky. Zobrazovanie grafiky je často
pomalá vec, čiastočne preto, lebo vykreslenie nejaký čas trvá a čiastočne
preto, lebo sa s použitím vsync
čaká na VBI. Pokiaľ kreslíme
grafiku a čakáme na VBI, naša obslužná funkcia časovača je stále
v pravidelných intervaloch volaná. Samozrejme s tým momentálne nerobíme nič;
ono to počká, kým s grafikou skončíme. Ale keď sme s grafikou hotoví, vieme
presne, koľko herných cyklov musíme prebehnúť, aby sme sa opäť dostali tam,
kde chceme byť.
Takže ak to zhrnieme -- treba si spraviť dve počítadlá. Jedno sa bude zväčšovať v pravidelných intervaloch (cieľový interval cyklu hry) s pomocou obslužnej funkcie časovača a druhé bude zväčšovať raz za aktuálny herný cyklus funkcia, ktorá ho vykonáva. Na začiatku tieto počítadlá nastavíme na nulu. Pri každom prechode cez hlavný herný cyklus najprv nakreslíme jeden rámec grafiky. To vo väčšine hier zaberie relatívne dlhý čas. Potom zistíme, či je cieľový počet herných cyklov väčší, než aktuálny (to pravdepodobne bude) a robíme herné cykly, kým to nedoženieme. Potom začneme od začiatku.
Pozrite si examples/chap_09/ex_4_3
.
Súbor grabber.txt
v podadresári tools
Allegra
obsahuje dobrý popis takmer všetkých aspektov datasúborov. Ak je niektorý
tu zmienený bod nejasný, ak chcete viac informácií alebo ak tu nie je niečo
popísané do podrobností, je to miesto, kde by ste mali hľadať ďalšie
informácie.
Doteraz ste mohli vidieť, že hra vyžaduje mnoho rôznych súborov; je tam spustiteľný súbor, ktorý spúšťajú používatelia, potom grafika buď v niekoľkých veľkých súboroch z ktorých každý jej obsahuje spústu, alebo každý obrázok vo svojom vlastnom súbore (ja radšej používam prvý prístup). Tiež máme zvukové efekty (jeden súbor pre každý) a hudbu (jeden súbor pre každú časť).
To sú len štandardné súbory, ktoré potrebujú takmer všetky hry. Okrem toho môže každá hra obsahovať súbory s popisom levelov, umelej inteligencie protivníkov a iné potrebné dáta. Dohromady to môže byť značný počet súborov.
Vďaka spôsobu clusteringu pod DOSom musí byť každý súbor uchovávaný
v blokoch určitej dĺžky. Na partíciach menších, než 1Gb majú tieto
clustre (čítať "klastre") veľkosť 16K. Väčšina súborov nezaplní
úplne posledný cluster a zostávajúci priestor je premrhaný -- v priemernom
prípade je strata 8K na súbor, ale v praxi to pravdepodobne býva viac.
Takto premrhaný priestor môže narastať, zvlášť ak používate veľa súborov,
z ktorých každý je oveľa menší než veľkosť jedného clustra. Ak máte
Windows 95, skúste napísať vo vašom adresári djgpp\zoneinfo
príkaz dir /s/v
a porovnajte "bytes" a "bytes allocated".
Keď to spravíte, možno sa rozhodnete adresár vymazať, alebo aspoň zozipovať
-- pravdepodobne ho nebudete potrebovať.
Množstvo súborov s koncovkami .PCX
, .WAV
alebo .MID
tiež ľudí zvádza k tomu, aby si ich "požičali"
pre svoje vlastné účely, alebo ich modifikovali a tak zmenili hru.
Ak im to nechcete dovoliť, je rozumné nenechať tam tie súbory ležať len tak.
Odhliadnuc od predošlých dvoch dôvodov, je celkom chaos, ak máte všetky súbory uložené jednotlivo a vaša hra ich má na začiatku všetky nahrať. Datasúbory poskytujú spôsob, ako zabaliť všetky alebo niektoré vaše súbory do jedného veľkého datasúboru, ktorý môže byť načítaný naraz, prípadne môže byť skomprimovaný a dáta v ňom zašifrované.
Datasúbory sa vytvárajú zo súborov na disku. Allegro na narábanie s nimi
poskytuje dva hlavné prostriedky: Grabber a nástroj DAT. Oba sú dobre
zdokumentované v súbore grabber.txt
, v adresári tools
Allegra, ale spravím tu aspoň stručný popis.
(nebolo zatiaľ napísané)
(nebolo zatiaľ napísané)
Je viacero spôsobov, ako môžete prečítať dáta z datasúboru. V prvom rade môžete prečítať celý datasúbor, všetko naraz. Za druhé môžete prečítať jednu zložku datasúbosu zvlášť. A nakoniec môžete otvoriť zložku datasúboru, ako keby to bol normálny súbor.
Toto je zvyčajný spôsob, akým sa veci robia. Pri tomto prístupe stačí
zavolať jednu funkciu a Allegro načíta všetky objekty, ktoré ste uložili
v datasúbore. Funkcia, ktorú treba zavolať je load_datafile
:
DATAFILE *load_datafile(char *filename);
Dáte jej meno vášho datasúboru a vráti smerník na pole štruktúr DATAFILE,
pre každý objekt je tam jedna. Takže ak data
je
DATAFILE *
a vy napíšete:
data = load_datafile("datafile.dat");
potom data[0]
bude prvý objekt datasúboru, data[1]
druhý, atď.
Najdôležitejšia položka v štruktúre DATAFILE je položka dat
.
Je to smerník, ktorý ukazuje na aktuálne dáta objektu. Ak je objekt bitmapa,
potom toto pole ukazuje na štruktúru BITMAP -- takže môžete písať:
blit(data[0].dat, screen, ...);
Ak je objekt zvuk, tak je to štruktúra SAMPLE:
play_sample(data[1].dat, 255, 128, 1000, 0);
Rovnako to funguje pre MIDI súbory, palety, animácie a niektoré
ďalšie veci. V grabberi alebo nástroji DAT môžete vidieť, akého typu
dát sú jednotlivé objekty, je to uvedené v zozname vľavo od mena objektu
(napíšte dat -l <filename>
, alebo sa pozrite na ľavú
stranu obrazovky grabbera).
Tiež sa dá povedať, aký typ dát sa v objekte nachádza podľa položky
type
. Táto je nastavená na konštantu ako
DAT_BITMAP
, DAT_SAMPLE
or DAT_MIDI
.
Ak dáta nie sú v zvláštnom formáte, je to DAT_DATA
--
znamená to, že dat
iba ukazuje na blok dát. Veľkosť tohto
bloku môžete zistiť z položky size
štrultúry DATAFILE:
load_map_data(data[2].dat, data[2].size);
odovzdá smerník na binárny blok a jeho veľkosť funkcii
load_map_data
(ktorú si sami napíšete).
Ďalej, ako zistiť, aké číslo objektu v datasúbore korešponduje s jednotlivými objektami, ktoré ste vložili do datasúboru? Sú tri spôsoby, ako to zistiť a ako vždy, je na vás, ktorý z nich zvolíte. Podľa okolností môžete zmixovať všetky tri dohromady.
Prvý spôsob je použiť hlavičkový súbor, ktorý vám môže vytvoriť grabber alebo nástroj DAT. Tento súbor bude obsahovať jedno makro pre každý súbor, ktoré #definuje meno objektu ako jeho index v datasúbore. Takže miesto použitia toho, čo som písal vyššie, môžete povedať:
blit(data[MY_BITMAP].dat, screen, ...); play_sample(data[MY_SAMPLE].dat, 255, 128, 1000, 0); load_map_data(data[MY_MAP].dat, data[MY_MAP].size);
Ak pri tomto prístupe pridáte do datasúboru nové objekty a uložíte ich pred staré, nemusíte v celom programe meniť všetky indexy.
Text každého #define je presne to, čo zadáte grabberu ako meno objektu. Ak na
vytváranie objektu použijete nástroj DAT, jeho meno bude pravdepodobne pôvodné
meno súboru napísané veľkými písmenami s tým, že znak .
bude
nahradený znakom _
. Táto náhrada sa robí preto, aby výsledné meno
objektu mohlo byť použiteľné v #define v Cčku -- bodky povolené nie sú.
V tejto súvislosti majte na pamäti, že keď vyberiete svojim objektom mená,
tak budú použité vo vašom programe ako makrá v #define -- nevyberajte teda mená,
ktoré môžu byť použité na ňiečo iné (napr.main
,
BITMAP
, atď. ) V grabberi alebo nástroji dat môžete použiť
nastavenie Prefix
, čo spôsobí, že sa pred každé meno v hlavičkovom
súbore niečo vloží. Ak ho napr. nastavíte na DATAFILE
, dostanete:
blit(data[DATAFILE_BITMAP].dat, screen, ...);
ak bolo meno objektu BITMAP
.
Ďalší spôsob, ako sa dá zistiť, kde sa objekt v datasúbore nachádza je
založený na fakte, že sú uložené v abecednom poradí. Podľa autora je to pravda
teraz a vždy aj bude. Neodporúčam používať túto vlastnosť pričasto, ale môže byť
užitočná, ak napríklad máte viacero bitmáp (napríklad rámce jedného spritu),
ktoré sú nazvané ENEMY_000
, ENEMY_001
,
ENEMY_002
, atď. Aby ste sa na ne odvolali, môžete samozrejme
používať mená z #define, ale je výhodnejšie môcť vybrať niektorú z týchto
premenných s pomocou čísla uloženého v premennej.
Kvôli spôsobu, akým preprocesing funguje (pre
= pred, t.j. pred
kompiláciou) nie je možné zmeniť počas behu programu ENEMY_000
na
ENEMY_xxx
. Ale keďže sú objekty uložené v abecednom poradí, viete,
že ENEMY_000
bude uložená v súbore ako prvá a
ENEMY_001
bude tesne za ňou -- teda n-tá bitmapa s nepriateľom bude
objekt číslo ENEMY_000 + n
(ak ako každý normálny programátor
začínate číslovať od nily, teda ENEMY_000
je nultý objekt.) Potom
nasledujúca vec bude fungovať:
void draw_nth_enemy (int n, BITMAP *where, int x, int y) { draw_sprite(where, data[ENEMY_000 + n], x, y); }
Tretí spôsob nájdenia objektu v datasúbore znamená prehľádávanie datasúboru,
kým nenájdete objekt so správnym menom. Mená objektov sú uložené ako
properties (vlastnosti) objektov. Všetky detaily nájdete v súbore
grabber.txt
. Stručne ale, ak chcete získať meno objektu, napíšte:
name = get_datafile_property(&dat[obj_number], DAT_ID ('N','A','M','E'));
Všimnite si toho &
-- odovzdávate smerník na prvok poľa. Ak
je reťazec, ktorý sa vráti, prázdny (teda name[0] == '\0', nie name ==
NULL), potom objekt nemá vlastnosť NAME
. To pravdepodobne znamená,
že ste z datasúboru vysekali vlastnosti; v tom prípade musíte na vyhľadanie
objektu použiť niektorú z predošlých dvoch metód.
Je spústa ďalších vecí, o ktorých by bolo dobré sa tu zmieniť. V prvom rade,
ak používate C++, potom sa kompilátoru nebude páčiť, že dávate pole
dat
(smerník na void) ako parameter funkciam, ktoré očakávajú iný
typ smerníka, takže ho musíte explicitne pretypovať na to, čo funkcia očakáva:
play_midi( (MIDI *) data[MUSIC].dat, 0);
Po druhé (a so vzťahom k predošlému) je často výhodné zriadiť si aliasy k objektom v datasúbore tak, že vytvoríte globálne premenné alebo polia, niečo ako:
BITMAP *enemy_bitmap; MIDI *background_music; SAMPLE *crash_sound;
potom ich nastavíte, aby ukazovali na určité miesta v datasúbore:
data = load_datafile("DATAFILE.DAT"); if (!data) barf(); /* loading failed */ enemy_bitmap = (BITMAP *) data[ENEMY_BITMAP]; background_music = (MIDI *) data[MUSIC]; crash_sound = (SAMPLE *) data[CRASH_SOUND];
a konečne ich použijete v hre:
draw_sprite(screen, enemy_bitmap, x, y); play_midi(background_music, 1); play_sample(crash_sound, 255, 128, 1000, 0);
Výhody tohto prístupu sú, že sa zbavíte obtiažneho pretypovávania potrebného v C++, výsledný kód bude krajší a trochu rýchlejší a tiež to zjednoduší prechod z kódu založeného na datasúboroch na kód založený na jednotlivých súboroch alebo naopak.
Po tretie, pole datasúboru je zakončené objektom typu DAT_END
.
Môžete a nemusíte to pokladať za veľmi užitočné; je to temer zbytočná
informácia, čosi ako že argv[argc]
je NULL. Ak chcete celý
datasúbor z nejakého dôvodu prehľádať (napríklad, aby ste našli objekt s určitou
vlastnosťou), môžete tento fakt použiť, aby ste vedeli, kde máte prestať.
Nakoniec, keď prácu s datasúborom skončíte, môžete ho odstrániť z pamäte
použitím funkcie unload_datafile
:
unload_datafile(data);
Je zbytočné hovoriť, že keď to spravíte, nemôžete viac pole data
používať a ak máte nejaké aliasy pre objekty v poli (ako vyššie), nemôžete
používať ani tie.
Pozrite si príklad: examples/chap_10/ex_3_1
Je možné z datasúboru načítať aj jednotlivé objekty, za predpokladu, že v
datasúbore sú informácie o menách (teda, že ste v grabberi nevysekali všetky
vlastnosti, alebo ste nepoužili -s2
v nástroji dat). Funkcia, ktorá
to spraví, je load_datafile_object
a používa sa spôsobom:
data_object = load_datafile_object ("datafile.dat", "object_name");
data_object
bude DATAFILE *
, rovnako ako keby ste
načítali celý datasúbor. Na prístup k objektu len dereferencujete smerník:
music_object = load_datafile_object("datafile.dat", "MUSIC"); play_midi(music_object->dat); /* alebo music_object[0].dat */
Keďže je načítaný len jeden objekt, funkcia vráti smerník na jedinú štruktúru
DATAFILE, nie na celé pole. Takže nemôžete použiť index objektu z hlavičkového
súboru a za vráteným objektom nenasleduje žiaden DAT_END
objekt.
Na odstránenie objektu načítaného týmto spôsobom z pamäti použijete funkciu
unload_datafile_object
:
unload_datafile_object(data_object);
Pozrite príklad: examples/chap_10/ex_3_2
Príjemná vlastnosť Allegrovských funkcií na prácu s pakovanými súbormi je
schopnosť čítať objekt dátového súboru, ako by to bol súbor na disku. Stačí na
to použiť kódovaný formát mena súboru meno_datasúboru#meno_objektu
,napríklad:
fp = pack_fopen("datafile.dat#MUSIC", F_READ);
Ak potom fp
nie je NULL, môžete z neho čítať ako z normálneho
pakovaného súboru. Nemôžete do neho zapisovať. Keď ich chcete zatvoriť,
použijete pack_fclose
ako zvyčajne.
Pozrite si príklad: examples/chap_10/ex_3_3