                            
    ۲      ۲    ۱    ۰  ۲     
    ۱      ۱    ۰       ۱   ۰ 
    ۰     ۱   ۰   ۰ ۱   ۱ 
         ۰      ۱    ۱  
    ۰       ۰۱ ۲    ۲ ۱ 
    ۱     ۰۰ ۱ ۲ ۱ ۱ ۱  ۱ ۰ 
    ۲    ۰۱ ۱ ۱ ۰ ۰ ۰  ۰  
    ۱ ۱  ۲     ۰  
    ۰       ۱۰   
            ۲۱ ۰  
    ۰    ۱  ۱ ۲ ۱ ۱ 
               
      Laaman tie DJGPP-peliohjelmointiin versio 1.30. By Jokke / BAD KARMA
	 Copyright (C) Joonas Pihlajamaa 1997. All rights reserved.

         LUE TIEDOSTO READJUST.NOW ENNEN KUIN LUET MERKKIKN PIDEMMLLE!
         SE SISLT TRKE TIETOA KYTTOIKEUKSISTA, LEVITTMISEST JA
         MUISTA RAJOITUKSISTA. JOS KYTT MITN TMN PAKETIN SISLT
         HYVKSYT SAMALLA EHDOITTA KAIKEN MIT READJUST.NOW -TIEDOSTON
         ALUSSA SANOTAAN!

			    SISLLYSLUETTELO:
-----------------------------------------------------------------------------
	   DJGPP - vaikea, suuri, monimutkainen, omituinen, hidas?
-----------------------------------------------------------------------------
			  Grafiikkaa - mit se on?
-----------------------------------------------------------------------------
		    Paletti - hrhelhameita ja tanssia?
-----------------------------------------------------------------------------
		Kaksoispuskuri - luonnonoikku, horoskooppi?
-----------------------------------------------------------------------------
		PCX-kuvien lataus - vain vhn oikaisemalla
-----------------------------------------------------------------------------
		    Bitmapit - eikai vain suunnistusta?
-----------------------------------------------------------------------------
				Animaatiot
-----------------------------------------------------------------------------
		 Pitk spriten trmt? Ent coca-colan?
-----------------------------------------------------------------------------
	     Nppimistn ksittely - ja nyt meill on hauskaa
-----------------------------------------------------------------------------
			 Fixed point matematiikka
-----------------------------------------------------------------------------
		Lookup-tablet ja muita optimointivinkkej
-----------------------------------------------------------------------------
                       Vliaikatulokset ja fontteja
-----------------------------------------------------------------------------
                            Maskatut spritet
-----------------------------------------------------------------------------
                   Hiirulainen, jokanrtin oma lemmikki
-----------------------------------------------------------------------------
                       Tekstitilan ksittely suoraan
-----------------------------------------------------------------------------
                   Projektien hallinta - useat tiedostot
-----------------------------------------------------------------------------
          Useiden tiedostojen projektit - kntminen ja hallinta
-----------------------------------------------------------------------------
                     Hieman automaatiota - tapaus Rhide
-----------------------------------------------------------------------------
                   Todellista guruutta - salaperinen make
-----------------------------------------------------------------------------
                   Ammattimaista meininki - enginen teko
-----------------------------------------------------------------------------
                   Vauhtia peliin - ulkoisen assyn kytt
-----------------------------------------------------------------------------
                           PIT - aikaa ja purkkaa
-----------------------------------------------------------------------------
               Miten peli toimii yht nopeasti kaikilla koneilla
-----------------------------------------------------------------------------
                                Saatteeksi
-----------------------------------------------------------------------------


DJGPP - vaikea, suuri, monimutkainen, omituinen, hidas?
-------------------------------------------------------

Tutoriaali sivuaa koko ajan DJ Delorien ilmaista Gnu-kntj
DOS:ille, eli DJGPP:t, erityisesti sen kakkosversiota. Itse siirryin
puolessa vliss tt tutoriaalia 2.0 -versiosta versioon 2.01 ja
luulisin, ett esimerkit toimivat molemmilla nist versioista ja
luultavasti uudemmillakin. Vanhemmat versiot eivt luultavastikaan
toimi niden lhdekoodien kanssa. 

Tmn mahtavan ilmaiskntjn lydt esimerkiksi internetist
osoitteesta ftp://x2ftp.oulu.fi jostain
pub/msdos/programming-hakemiston alihakemistosta. Sen saa mys
MBnetist, tarvittavat tiedostot ovat alueella PC-Ohjelmointi (area
8), tiedostoja on useita, ja ne lytyvt ko. alueelta lytyvst
MBNETDJ2.TXT:st. Mys kaikille Mikrobitin tilaajille tullut Huvi &
Hytyromppu sislt tmn kntjn hakemistossa MIKROBIT\DJGPP201\,
tosin sielt puuttuu LGP2721B.ZIP (tarvitaan C++ koodin kntmisess),
jonka Kpyaho unohti laittaa mukaan. Halutessasi voit hakea puuttuvan
tiedoston MBnetist.

DJGPP:n asennukseen purat vain kaikki tarvitsemasi paketit haluamaasi
hakemistoon (esim. D:\OHJELMAT\DJGPP) PKUNZIP:in -d parametrill. Sen
jlkeen list polkuun tuon hakemiston alihakemiston BIN (esim.
D:\OHJELMAT\DJGPP\BIN), ja viel lopuksi teet uuden environment-muuttujan
DJGPP, joka osoittaa DJGPP:n juurihakemistossa olevaan DJGPP.ENV
-tiedostoon. Eli esim.:

SET DJGPP=D:\OHJELMAT\DJGPP\DJGPP.ENV

Nyt voit kokeilla toimivuutta tekemll pienen C-ohjelman (vaikka
koe.c) ja kirjoittamalla:

GCC koe.c -o koe.exe

Lis infoa GCC:n knnsoptioista ja kntjst saat kirjoittamalla:

INFO GCC

Suosittelisin ett lueskelet DJGPP:n dokumentaatiota ja teet tss
vaiheessa paljon testiohjelmia ja opettelet kyttmn
info-lukijaa. Hydyllinen hankinta on mys Rhide, joka on IDE
DJGPP:lle. Ohjelma lytyy MBnetist alueelta 8 (ETSI RHIDE) sek
H&H-Rompulta. Kun tunnet osaavasi kytt vaivattomasti kntj
palaa takaisin dokumentin pariin.

Jos et viel C:t osaa, niin hanki jostain, esimerkiksi kirjastosta hyv
kirja ja opettele sen avulla C-ohjelmointi. En aio alkaa
selittmn kaikkein yksinkertaisimpia asioita esimerkkikoodeissa
taikka kommentoimaan liiemmlti koodia.


Grafiikkaa - mit se on?
------------------------

No olet siis pttnyt edet seuraavaan aiheeseen, joka nyttisi
olevan grafiikan ohjelmointi DJGPP:ll. Aloittakaamme siis! Tiedoksi
nyt etukteen, ett muistiosoitteet ovat heksoina, vaikkei sit 
ilmoitetakaan.

Esimerkkin kytn VGA:n perusmoodia, 13h (heksaluku, desimaalina
19), joka on erittin helppokyttinen. Kun tarvitset muita moodeja
sinulla on varmasti jo tarpeeksi taitoa hankkia itse informaatiota,
mutta tmn neuvon ihan alusta alkaen.

Eli olipa kerran PC, jossa oli 16-bittinen muistivyl, joka salli
vain 64 kilon osoittamisen kerralla, sill 16-bittisell osoitteella
voidaan maksimissaan osoittaa 2^16=65536 tavua muistia. PC:n oli
suunnitellut Intel, mutta PC:hen oli luvattu yli 64 kilotavua muistia
ja 32-bittinen muistivyl oli niihin aikoihin kovin kallis. Joten
joku sai suorastaan neronleimauksen: Jaetaan koko muisti 64 kilon
palasiin!

En syvenny tekniikkaan sen kummemmin, vaan totean vain, ett 8088
prosessoriin perustuvassa PC:ss muodostettiin muisti SEGMENTIST ja
OFFSETISTA (SEG:OFF, esim B800:0000). Todellinen osoite muistissa 
saatiin kertomalla SEGMENTTI kuudellatoista ja lismll siihen
OFFSET. (B800:0000 = B800*16+0000 = B8000) Ja kun kummatkin olivat
16-bittisi lukuja saatiin nin 20-bittinen siirrososoite. Ja koska 20
bitill voi ilmoittaa tsmlleen kaksi potensiin 20 eri arvoa oli
maksimimr mit voidaan osoittaa 1 megatavu. Kymmenen ensimmist
segmentti (eli 0000 1000 2000 3000 4000 5000 6000 7000 8000 ja 9000)
omistettiin ohjelmille ja nimettiin perusmuistiksi, jota oli siis
10*64=640 kilotavua. Sitten segmentist A000 alkoi grafiikkamuisti.

No tietokoneet kehittyivt ja esiteltiin suojattu tila, eli PROTECTED
MODE (PM), joka ksitteli koko muistia selektoreilla ja offseteilla,
jotka olivat entisen 16 bitin sijasta 32-bittisi (selektorit ovat
kuitenkin yh 16-bittisi). Vanhat segmenttien varastoimiseen tarkoitetut
SEGMENTTIREKISTERIT varattiin nyt selektoreille, jotka kertoivat
prosessorille, mit LOOGISTA muistialuetta ksiteltiin. DJGPP, joka on
suojatun tilan kntj esim. antaa ohjelmalle alussa 2 selektoria, toinen
osoittaa dataan ja toinen koodiin. Tst pidemmlle en tied tarkasti,
mutta riitt tiet, ett selektorin osoittaessa dataan ei offset 1234
todellakaan ole muistissa kohdassa 1234, vaan se on ohjelman oman 
data-alueen 1234. tavu.

Ja mik meit kiinnostaa, on perusmuistin 11. segmentti, jonka osoite
siis oli A000:0000. Siirrososoite on siis A000*16+0000 = A0000. Mutta,
kuten muistamme, ei onnistu, ett vain tekisimme pointterin, joka osoittaa
tuonne osoitteeseen, sill ohjelman datahan on aivan toisessa
selektorissa kuin perusmuisti. Meidn tytyy ensin lyt oikea
selektori, jonka osoittama looginen muistialue vastaisi PC:n
perusmuistia. Ja tllainen lytyykin nimell _dos_ds. Tmn selektorin
osoittaman muistialueen 0. tavu on perusmuistin 0. tavu, 1. tavu on
perusmuistin 1. tavu ja niin jatkuu edelleen, kunnes tavu numero A0000
on ensimminen VGA:n grafiikkamuistin tavu.

Nyt meill on siis tiedossa segmentin A000, eli VGA-kortin
muistialueen siirrososoite, A0000 ja oikea selektori, _dos_ds. Mutta
miten laitamme tavun tuonne? Hyv kysymys. Se onnistuu vhintn 5:ll
eri tavalla, mutta perehdymme helpompaan. Kirjaston sys/farptr.h
funktioon _farpokeb(selektori, siirrososoite, tavu), jolla psemme
ksiksi tuonne. Normaalin pointterin tekohan ei onnistu, vaan meill
pit olla funktio, joka kykenee osoittamaan toisen selektorin
alueelle.

Ninollen esimerkkiohjelma, joka asettaa VGA-muistin 235. tavun arvoon
100 on tmn nkinen (PIXEL1.C):

#include <go32.h> /* muistathan, _dos_ds on mritelty tll! */
#include <sys/farptr.h> /* tlt lytyy _farpokeb */

void main() {
    int selektori=_dos_ds, 
      siirros=0xA0000 + 235, 
      arvo=100;

    _farpokeb( selektori, siirros, arvo );
}

Arvaan, ett ehk menit ja kokeilit tuota ja petyit, kun mitn ei
tapahtunutkaan. Ei se mitn, niin pitkin tapahtua, sill olimme
tekstitilassa. Jotta jotain tapahtuisi meidn pit olla oikeassa
tilassa, joka oli siis 0x13 (heksanumero 13 C:ss, desimaalimuodossa
19). Tmn tilan rakenne onkin seuraava mihin perehdymme. Ole huoleti,
valitsin tmn tilan, sill se on KAIKKEIN yksinkertaisin tila
PC-yhteensopivalla tietokoneella. Resoluutio on 320 rivi vaakatasossa
ja 200 pystytasossa. Jokaista pikseli merkitn yhdell tavulla, eli
sill voi olla 256 erilaista arvoa. Nytt alkaa aivan ruudun
vasemmasta ylkulmasta (miksi? sit ei kukaan oikein tied, menee
filosofiaksi) ja jatkuu tavu tavulta (pikseli pikselilt) pttyen
lopulta oikeaan alakulmaan. Eli ensimmiset 320 tavua ovat ensimmisen
rivin kaikki vaakatasossa olevat pikselit, sitten seuraavat 320 ovat
toisen rivin pikselit, kunnes lopulta ollaan ruudun alakulmassa.

Ja kun muistamme, ett ensimminen tavu on kohdassa A0000 (heksa siis
tmkin), eli 0 tavua alusta eteenpin, niin me voimmekin tehd hienon
kaavion:

Pikselit:        Sijainti:
..........................
0...319          1. rivi
320...639        2. rivi
...
63680...63999    200. rivi

Nin meill onkin hieno kaava, jolla saamme selville pikselin
sijainnin:

alkio = rivi * 320 + sarake    eli:
offset = y*320+x

Muista, ett C:ss 1. rivi olisi tietenkin rivi numero 0!

Nyt yhdistmme tietomme: VGA:n muisti sijaitsee selektorissa _dos_ds,
alkaen osoitteesta A0000 (heksa, C:ss 0xA0000) ja siit lhtee 64000
tavua, joka on nyttmuisti. Pikselin osoite tss muistissa voidaan
laskea kaavalla y*320+x. Selektorin kanssa voidaan muistia asettaa
komennolla _farpokeb(selektori, siirros, arvo). Tarvittava moodi on
0x13 ja siin on 256 vri ja resoluutio 320 x 200.

Mutta miten psemme sinne? Vastaus on helppo: conio.h:n funktiolla
textmode(moodi)! Ja kun viel yhdistmme thn funktion getch(), joka
odottaa napinpainallusta (lytyy myskin kirjastosta conio.h), sek
palaamme lopuksi tekstitilaan (0x3, eli heksa 3, eli desimaali 3) on
meill jo aika kiva ohjelma kasassa (PIXEL2.C):

#include <go32.h> /* _dos_ds ! */
#include <sys/farptr.h> /* _farpokeb(selektori, siirros, arvo) */
#include <conio.h> /* textmode(moodi), getch() */

void main() {
    int selektori=_dos_ds, siirros=0xA0000, y=100, x=160, 
		  graffa=0x13, texti=0x3, color=100;
    
    textmode(graffa);
    _farpokeb(selektori, siirros+y*320+x, color);
    getch();
    textmode(texti);
}

Tietenkin olisi ollut helpompaa sijoittaa arvo suoraan parametrin
kohdalle:

textmode(0x13);
_farpokeb(_dos_ds, 0xA0000+100*320+160, 100);
getch();
textmode(0x3);

Mutta katsoin aiemman tavan havainnollisemmaksi. Kaiken tekemiseksi
oikein helpoksi teemme tst pikselinsytytyksest makron
#define-komennolla. Tm ei hidasta ohjelmaa yhtn, mutta varmasti
selvent koodia. Se mrittelee makron putpixel(x, y, c), jonka
kntj muuttaa knnsvaiheeksa _farpokeb-funktioksi. x tarkoittaa
saraketta vlilt 0-319 ja y rivi vlilt 0-199, sek c vri vlilt
0-255. Muista, ett vaikka teetkin makron sinun pit silti
sisllytt mukaan kirjastot sys/farptr.h ja go32.h! Sulut makron
farpokeb-funktion muuttujien x ja y ymprill selittyvt sill, ett
koska makro puretaan suoraan kutsukohtaan niin esim. komento:
putpixel(50, 40+a, 100) purkautuisi muotoon: _farpokeb( _dos_ds,
0xA0000+40+a*320+50, 100), joka ei tietenkn ole haluttu tulos, sill
40+a pit ksitell ennen sijoitusta, eli sulut vain ymprille! Tss
se siis on:

#define putpixel(x, y, c) _farpokeb( _dos_ds, 0xA0000+(y)*320+(x), c)

Kun haluat kytt sit, niin teet vaikka seuraavanlaisen
koodinptkn (PIXEL3.C):

#include <sys/farptr.h>
#include <go32.h>
#include <conio.h> /* textmode(moodi) ja getch() lytyvt tlt! */

#define putpixel(x, y, c) _farpokeb( _dos_ds, 0xA0000+y*320+x, c)

void main() {
    textmode(0x13);
    putpixel(319, 199, 150);
    getch();
    textmode(0x3);
}

Ohjelma sytytt pikselin aivan ruudun alareunaan. Jos et en muista,
miten ohjelma knnettiin DJGPP:ll, on tmn kokeilemiseksi
tarvittava komento: "GCC PIXEL3.C -o PIXEL3.EXE" ja sitten kokeilu
komennolla "PIXEL3".

Painu nyt kokeilemaan ohjelmaa ja muuntelemaan sit! Laita se tekemn
ruksi, pystyviiva, vaakaviiva, tai vaikka ympyr jos osaat, tai
yhdist se randomin kanssa ja tee nytnsstj! Kokeilemalla tulet
parhaiten sinuiksi uuden asian kanssa. Ja kun olet valmis, siirrymme 
seuraavaan aiheeseen, palettiin.


Paletti - hrhelhameita ja tanssia?
------------------------------------

Kuten edellisess luvussa opimme, voi tilassa 13h olla 256 erilaista
vri. Teit ehk jo ohjelman, joka piirt pikselin jokaisella vrill
viivaa ja huomasit, ett kytss olevat vrit ovat huonoja,
puuttelisia, kirkkaita, tummia tai muuten vain inhottavia. Mutta ei
ht - niit voi muuttaa! Ja vaikka paletissa ei mielestsi olisikaan
mitn vikaa haluat ehk tehd sellaisia efektej kuten hivytys,
plasma, "crossfade" (toinen kuva ilmestyy toisen alta pikkuhiljaa)...
Niss kaikissa tarvitaan enemmn tai vhemmn itse tehty palettia ja
siksi meidn pitkin opetella nm asiat ennenkuin menemme pidemmlle.
Kaiken ytimen on VGA ja sen paletti, etenkin sen asettaminen, mutta
ehk mys sen lukeminen. Tss luvussa teemme funktiot, yhden tai
useamman vrin, asettamiseen ja lukemiseen, sek tutustumme
paletinpyritykseen (palette rotation).

Ensin taas vhn teoriaa efektien ja paletin takana. Kuten ehk
tiedtkin, valo voidaan koostaa komponenteista. Tietokoneella
jokaisella vrill on yleens kolme komponenttia: punainen, vihre ja
sininen (red, green, blue). Tt kutsutaan nimell RGB. Itseasiassa
jokainen moodin 13h vri on vain osoite taulukkoon, jonka jokainen
alkio sislt vrin punaisen, virhen ja sinisen komponentin mrn,
eli vahvuuden.

Jos meill olisi puhtaan punainen vri, sen arvot olisivat seuraavat:
r=63, g=0 ja b=0. Sininen taas olisi 0,0 ja 63. Violetti, joka on
sinisen ja punaisen yhdistelm, voisi olla vaikkapa 63,0 ja 63 (eli
tysi mr punaista ja sinist). Jos taas haluaisimme tumman punaisen
vrin, olisivat sen vriarvot vaikka 30, 0, 0. Koska 30 on vhemmn
kuin puolet kirkkaan punaisen puna-arvosta, on tm vri siis yli
puolet tummempi! Helppoa! Ja miksi maksimimr on vain 63? Siksi,
koska VGA:n rekistereiss vrille on varattuna vain 6 tavua, jolla
voidaan esitt numerot vlill 0...63. Tm joudutaan huomioimaan
esim. PCX:n paletin latauksessa, sill siin vrit ovat vlill
0...255. Tss joudutaan jakamaan vriarvot neljll, jotta saadaan
toimiva luku.

Eli ymmrrmme nyt, ett jokaisella vrill on itseasiassa punainen,
vihre ja sininen komponentti, mutta mit siit? Vastaus on helppo,
jos haluamme, voimme muuttaa mit tahansa tilan 0x13 (tai miksei
muunkin tilan) vri helpolla joukolla komentoja. Meidn tarvitsee
vain kirjoittaa asetettavan vrin numero porttiin 3C8h (h lopussa
siis tarkoittaa heksalukua, C:ss 0x3C8) ja sitten porttiin 3C9 ensin
punainen komponentti, sitten vihre komponentti ja lopuksi sininen
komponentti. Tmn jlkeen VGA korottaa vri-indeksi automaattisesti
yhdell, eli jos ensin sytmme porttiin 3C8h vrinumeron 5 ja sitten
punaisen, virhen ja sinisen porttiin 3C9h korottuu VGA:n sisinen
laskuri yhdell, ja voimme halutessamme tunkea heti seuraavan vrin
RGB arvot porttiin 3C9.

Nyt olemme jauhaneet teoriaa tarpeeksi. Menkmme pikkuiseen
esimerkkiin. Esittelemme tietorakenteen RGB, joka sislt vrin
RGB-arvot ja sitten funktion, jolle annetaan parametrin osoitin
tllaiseen rakenteeseen ja vrin numero jolle nm vriarvot
asetetaan. Myhemmin yhdistmme tmn pieneen esimerkkiohjelmaamme,
mutta (PALETTE.H):

typedef struct {
    char red;
    char green;
    char blue;
} RGB;

void setcolor(int index, RGB *newdata) {
    outportb(0x3C8, index);    
    outportb(0x3C9, newdata->red);    
    outportb(0x3C9, newdata->green);
    outportb(0x3C9, newdata->blue);
}

Huomiosi ehk kiinnittyy viel outoon funktioon outportb, jolle
annetaan ensimmisen portin numero ja sitten sinne sytettv
tavu. Funktion kyttmiseksi sisllytt mukaan kirjaston dos.h. 
Ehk sinua kiinnostaisi mys tmn kytt? No olkoon, tehkmme
esimerkkiohjelma kokonaisuudessaan. Kun edellinen pikku koodinptk on
nimell PALETTE.H, voimme helposti sisllytt sen seuraavaan
esimerkkiohjelmaamme kuten ihan tavallisen kirjaston. Muista vain,
ett kirjaston tytyy olla samassa hakemistossa ohjelman kanssa,
muuten ei esimerkki knny. Eli tss sitten itse koodiosa, joka
tuikkaa keskelle ruutua vrin 50. Sitten se odottaa napinpainallusta
ja muuttaa funktiollamme vrin punaiseksi. Huomaa, ett vain alussa
kajotaan nyttmuistiin. Toinen kohta hoidetaan vrinvaihdolla!
Eli (PAL1.C):

#include <conio.h>
#include <sys/farptr.h>
#include <go32.h>
#include <dos.h>

#include "palette.h"

#define putpixel(x, y, c) _farpokeb(_dos_ds, 0xA0000+y*320+x, c)

void main() {
    RGB newcolor;
 
    textmode(0x13);
    putpixel(160, 100, 50);
    getch();
    newcolor.red=63;
    newcolor.green=0;
    newcolor.blue=0;
    setcolor(50, &newcolor);
    getch();
    textmode(0x3);
}

Seuraavana huomionkohteenamme onkin sitten vriarvojen luku, joka on
yht suoraviivaista kuin edellinenkin. Erotuksena on, ett vriarvo
kirjoitetaankin porttiin 3C7h ja portista 3C9h _luetaan_ vrin
arvo. Jlleen tripletin (kolme alkiota, RGB) luvun jlkeen indeksi
kohoaa, joten voisimme lukea seuraavat vrit. Luku portista tapahtuu
funktiolla inportb(portti). Muuta tietoa emme tarvitsekaan.
Listkmme nyt kirjastoomme (PALETTE.H) kolme uutta funktiota.
getcolor(int index, RGB *color) lukee vrin <index> vriarvot ja
asettaa ne RGB-rakenteeseen <color>. setpal(char *palette) asettaa
koko paletin kerralla hyvksikytten automaattista indeksin korotusta
(indeksi nollataan aluksi ja sytetn koko data pern, indeksi
korottuu jokaisen rgb-arvon jlkeen). getpal(char *palette) taas lukee
vastaavasti koko paletin. Niiden kytst sitten
esimerkkiohjelmassamme, joka seuraa ajallaan. Eli uutuudet kirjastoon
PALETTE.H:

void getcolor(int index, RGB *color) {
    outportb(0x3C7, index);
    color->red=inportb(0x3C9);
    color->green=inportb(0x3C9);
    color->blue=inportb(0x3C9);
}

void setpal(char *palette) {
    int c;
    
    outportb(0x3C8, 0);
    for(c=0; c<256*3; c++)
	outportb(0x3C9, palette[c]);
}

void getpal(char *palette) {
    int c;
    
    outportb(0x3C7, 0);
    for(c=0; c<256*3; c++)
	palette[c]=inportb(0x3C9);
}

Kuten huomasit, ei viimeisiss funktiossa ole lainkaan en
RGB-rakennetta. Tm siksi, ett koko paletti on huomattavasti
helpompi ksitell nin. Jos olet sit mielt, ett RGB oli parempi
tai haluat muuttaa loputkin pointtereiksi, en sit
est. Char-pointteriversiossa on aina kolme tavua perkkin
ilmoittamassa RGB-tripletti. Toisen vrin r alkaa siis 4. tavusta,
eli indeksist 3. Jos haluat jonkin vrin r-arvon, niin lasket:
"palette[number*3+0]". Vihrell korotat tuota yhdell (number*3+1) ja
sinisen kanssa kahdella. Helppoa tmkin.

Nyt on kaikki trkein katettu VGA:n paletista, joten kysytkin ehk
(aina sin sitten olet kysymss ;) mihin nit nyt sitten voi
kytt. Itseasiassa paletilla on loputtomasti
kyttmahdollisuuksia. Ensimminen on 256-vristen kuvien paletin
asettaminen, sill vrll paletilla kuvat yleens nyttvt enemmn
tai vhemmn sotkulta. Toisena on hivytysefekti, sek feidaus
valkoiseen. Palettiliutuksesta kytetn usein termi feidaus, joka
tarkoittaa, ett palettia liutetaan svy svylt toiseen vriin,
jolloin saadaan vaikka hieno ruudun tummeneminen. Kokeilemmekin sit
ihan kohta, kunhan selitn viel yhden efektin, palettirotaation.

Palettirotaatiossa on paletti, jonka vriarvoja pyritetn
ympri. Eli kytnnss vri, joka ennen oli numerolla 5 onkin
rotaation jlkeen vrinumerossa 6. Tt jatketaan koko ajan, ja vri
matkaa koko paletin lvitse, ja kun se on lopussa niin se siirretn
paletin alkuun. Yleens vri 0 ei kuitenkaan siirret, sill se on
taustavri ja yleens musta. Usein kytetn mys palettia, jossa on
useampia vrej kuin 256, jolloin erona on vain se, ett ainoastaan
osa vreist nkyy ruudulla.

"JA MIHIN TT", kuulen sinun kysyvn. Olet kenties nhnyt plasman,
jonka vrit vaihtuvat koko ajan (kunnon plasmassa on kyll lisksi
mukana muutakin kuin pyriv paletti, mutta pyrityksell saadaan
kummasti liseloa muuten liikkuvaan plasmaan). Tai tunnelin, jossa
vrit siirtyvt kauemmaksi tai lhemmksi. Tllaisia efektej voidaan
helposti toteuttaa palettirotaatiolla. Ennenkuin ymmrrt voit ehk
tarvita pienen demonstraation. Kohta teemmekin esimerkin, joka piirt
vaakatasossa viivoja, jokainen eri vrill alkaen yhdest pttyen
255:teen. Sitten teemme hienon liukupaletin ja alamme pyrittmn
sit. Eli tehkmme viel funktio (listn kirjastoon PALETTE.H):

void rotatepal(int startcolor, int endcolor, char *pal) {
   char r, g, b;
   int c;

   r=pal[startcolor*3+0]; /* tallennamme ensimmiset vrit ja siirrmme */
   g=pal[startcolor*3+1]; /* ne lopuksi loppuun. Tm paletti pyrii siten, */
   b=pal[startcolor*3+2]; /* ett viimeinen vri kulkeutuu kohti alkua */

   for(c=startcolor*3; c<endcolor*3; c++)
       pal[c]=pal[c+3]; /* muista, ett uusi vri on kolmen vlein,
			   sill vlisshn on aina kolme tavua, r,
			   g ja b, joita ei saa sekoittaa, muuten
			   saisimme aikaan vaikkapa sinisen paloauton!
			   (kiinnostava tavoite sinns) */

   pal[endcolor*3+0]=r;
   pal[endcolor*3+1]=g;
   pal[endcolor*3+2]=b;
}

Viel ennen esimerkki tarvitsemme yhden rutiinin, joka tekee
efektistmme edes jotenkin siedettvn. Palettia pit nimittin
vaihtaa ennen kuin ruudulle aletaan piirt, tai muuten voi edess
olla aika huonolaatuinen efekti (normaalipaletissa ei ole mitn
vriliukuja). Varsinkin nin yksinkertaisessa ohjelmassa voi nopealla
nytnohjaimella/koneella nopeus olla liiankin suuri, joten hidastamme
vhn rutiinia odottamalla signaalia, jonka VGA antaa pstessn
ruudun loppuun ja lhtiessn palaamaan ylkulmaan aloittaakseen taas
piirron. Thn teemme funktion, joka odottaa kunnes piirto on valmis
ja kuvaruudulle voi kopioida pelkmtt kesken piirron muutoksia
tehdess aiheutuvia ongelmia. Listkmme seuraava funktio kirjastoon
PALETTE.H:

void waitsync() {
    while( (inportb(0x3DA)&8) != 0);
    while( (inportb(0x3DA)&8) == 0);
}

Nyt sitten hienoon esimerkkiohjelmaamme, joka piirsi niit viivoja ja
pyritti palettia. Huomaa funktio genpal(char *palette), joka asettaa
paletin liukuvreill tehdyksi, sek waitsync()-funktion kytt
(kokeile vaikka ilman waitsync():i, niin net eron)! Eli tss se
olisi (PAL2.C):

#include <conio.h>
#include <sys/farptr.h>
#include <go32.h>
#include <dos.h>

#include "palette.h"

#define putpixel(x, y, c) _farpokeb(_dos_ds, 0xA0000+y*320+x, c)

void genpal(char *palette) {
    char r=0, g=0, b=0;
    int c, color=0;
    
    for(c=0; c<64; c++) { // MUSTA (0,0,0) - PUNAINEN (63,0,0)
	palette[color++]=r;
	palette[color++]=g;
	palette[color++]=b;
	if(r<63) r++;
    }
    for(c=0; c<64; c++) { // PUNAINEN (63,0,0) - VIOLETTI (63,0,63)
	palette[color++]=r;
	palette[color++]=g;
	palette[color++]=b;
	if(b<63) b++;
    }
    for(c=0; c<64; c++) { // VIOLETTI (63,0,63) - VALKOINEN (63,63,63)
	palette[color++]=r;
	palette[color++]=g;
	palette[color++]=b;
	if(g<63) g++;
    }
    for(c=0; c<64; c++) { // VALKOINEN (63, 63, 63) - MUSTA (0,0,0)
	palette[color++]=r;
	palette[color++]=g;
	palette[color++]=b;
	if(r) r--;
	if(g) g--;
	if(b) b--;
    }
}

void main() {
    int x, y;
    char palette[256*3];

    textmode(0x13);
    genpal(palette);
    setpal(palette);
    for(y=0; y<200; y++) for(x=0; x<320; x++)
	putpixel(x, y, y);
    while(!kbhit()) {
	rotatepal(1, 255, palette);
	waitsync(); /* odotetaan ett piirto on valmis ennen uuden
		       paletin asettamista! */
	setpal(palette);
    }
    getch();
    textmode(0x3);
}

Huomasit varmaan, ett ruudun onnettoman geometrian takia kaikki vrit
EIVT mahtuneet ruudulle. No niin. Ja mits kivaa seuraavaksi?
Seuraavaksi tutustumme viimeiseen palettikikkaan, jonka periaatteen
olet jo voinut keksikin, eli feidauksen.

Genpal-funktio olisi voinut kytt mys erillist rutiinia jolle 
annetaan parametreina monenko vrin matkalla liu'utaan vrist toiseen.
Kuitenkin koska tuo oli yksinkertaisemman nkinen tein sen tuolla
tapaa.

Teemme minimaalisia lisyksi PALETTE.H:hon, sek pikkuisen
esimerkkiohjelman, joka demonstroi efekti kytnnss. Ideahan on
erittin yksinkertainen. Meill on paletti, jossa on sekailaisia
vrej ja haluamme hivytt sen. Miten? No tietenkin muuttamalla
ruudun mustaksi. Miten se tapahtuu? Nollaamme jokaisen vrin, mutta
emme kerralla, vaan vhennmme joka kierroksella ja asetamme uuden
paletin. Tst funktiosta voit tehd helposti muitakin efektej,
kuten feidauksen valkoiseen (korotetaan jokaista vri joka
kierroksella kunnes ollaan vriss 63) tai vaikka paletista toiseen
(jos kohdepaletin vastaava komponentti on suurempi niin korotetaan
arvoa, jos pienempi niin vhennetn). Esittelen tss vain
hivytyksen, mutta lydt kirjastosta PALETTE.H toteutettuna mys
valkoiseen ja toiseen palettiin feidauksen. Voit mys itse tehd
hauskoja efektej, kuten feidata valkoiseen, tehd valkoisen paletin
ja feidata sen mustaan. Kokeile! Mutta, tss rutiinimme:

void fadetoblack(char *palette) {
    char temppal[256*3];
    int c, color;

    memcpy(temppal, palette, 256*3);
    for(c=0; c<63; c++) { /* tarvitsemme maksimissaan 63 muutosta */
	for(color=0; color<256*3; color++)
	    if(temppal[color]) temppal[color]--;
	waitsync();
	setpal(temppal);
    }
}

Sitten yhdistmme efektin lopuksi edelliseen esimerkkiohjelmaamme
lismll sen juuri ennen tekstitilaan vaihtoa:

fadetoblack(palette);

Kokonaisuudessaan ja toimivana, vanhat osat mukana on esimerkkimme
tiedostossa PAL3.C. Siihen on tehty mys pari muuta muutosta, kuten
se, ett aluksi paletti feidataan valkoiseen, asetetaan oikeasti val-
koiseksi (muuten feidatessa mustaan paletti vlht hetken normaalivri-
sen, ttkin SAA kokeilla).

No niin. Pahin tiedonnlksi lienee tlt erlt tyydytetty! Viihdy
esimerkkien parissa ja tee mit vain mieleen juolahtaa niill. Muista,
ett palettifunktiot toimivat mys tekstitilassa. Tmn voit kokeilla
vaikka kyttmll fadetoblack-funktiota. Muista kuitenkin laittaa
loppuun textmode(0x3), vaikket moodia olisi vaihtanutkaan, sill et
vlttmtt pid DOS-kehotteestasi jokainen vri mustana...


Kaksoispuskuri - luonnonoikku, horoskooppi?
-------------------------------------------

No niin, olet nemm sulattanut jo kaiken edellisen tiedon. Mainiota!
Tnn psemme (tai miten nyt haluamme asian ilmaista) yhteen
peliohjelmoinnin perustempuista, kaksoispuskuriin. Periaate tmn
takana on aivan naurettavan yksinkertainen, ja itseasiassa min opin
tmn ern lehden lhdekoodia vilkaisemalla (Mikrobitin
grafiikkaohjelmointikurssi, numero 11/95). Eli thn asti olemme
tunkeneet grafiikkaamme suoraan nyttpuskuriin tavu
kerrallaan. Valitettavasti tss on haittoja. Ensimmisen on se, ett
meill on kiire. Nimittin kytss on vain lyhyt aika kun nytt ei
piirret monitorille ja jos siin ajassa ei ehd piirt nytt niin
nytt alkaa vlkkymn, ilmestyy lumisadetta (varsinkin paletinvaihdon
kanssa!) ja muitakin ei-toivottavia ilmiit esiintyy.

Lisksi on todettava valitettava tosiasia: Nyttmuisti on
HIDASTA. Jos haluamme tehd sen kaikkein tehokkaimmin niin kopioimme
kaiken tavaran kerralla nytlle. Eli sen sijaan, ett liskisimme
pikseleit sinne, toisia tnne kopioimme tavaran nytlle nytn
alusta loppuun neljn tavun (kaksoissana) kokoisina palasina. Mutta
miten saamme ruudulle pikseleit sinne tnne, kun kaikki pitisi
kopioida kerralla? Vastaus on, ett kytmme kaksoispuskuria!
Kaksoispuskuri, englanniksi doublebuffer on saman kokoinen kuin
nyttmuisti, mutta sille on varattu tilaa keskusmuistista, joten se
on nopeampaa kuin hidas, kortilla sijaitseva nyttmuisti (nin vain
on, uskokaa pois). Sinne pikselinpiirto tapahtuu huomattavasti
sutjakammin, ja kaiken lisksi meill ei ole mitn kiirett. Vaikka
piirrmme uuden pikselin, ei se ny nytll ennenkuin kaksoispuskuri
on kopioitu, eli flipattu nyttmuistiin.

DJGPP:ll nyttmuisti varataan vaikka malloc-kskyll ja vapautetaan
suorituksen loppuessa free-kskyll. Kokoa pit puskurilla olla
tilassa 13h 64000 tavua. Eroja oikeaan nyttmuistiin
kaksoispuskurissa on DJGPP:ll:

  - Se on nopeampaa.
  - Se sijaitsee omassa muistissa, joten se voidaan taulukoida. Ei
    en putpixel-makroja, vaan dblbuf[y*320+x]=color.
  - Se voidaan kopioida nopealla _dosmemputl-rutiinilla, joka on
    viimeiseen saakka optimoitu (hidas se on siltikin, mutta se on
    nyttkortin ja VGA:n rakenteen vika.)
  - Se ei ny ruudulla ennenkuin ksketn.
  - Se ei vilku.
  - Se silyy muistissa vaikka kytisiin tekstitilassa.
  - Paljon muuta kivaa.

Muttamutta, tarvitsisimme esimerkin. Mist saamme sellaisen? No tss
pieni esimerkki. Mukana on makro flip(char *buffer), joka kopioi 64000
tavua puskuria nyttmuistiin DJGPP:n _dosmemputl-komennolla, joka
lytyy kirjastosta sys/movedata.h ja tarvitsee mys _dos_ds: ja
siten kirjastoa go32.h. Eli tss tllaista (BUFFER1.C):

#include <go32.h>
#include <sys/movedata.h>
#include <time.h>
#include <stdlib.h>
#include <conio.h>
#include <dos.h>
#include <stdio.h>

#define flip(c) _dosmemputl(c, 64000/4, 0xA0000)

char *dblbuf;

void varaamuisti() {
    dblbuf=(char *)malloc(64000);
    if(dblbuf==NULL) {
	printf("Ei tarpeeksi muistia kaksoispuskurille!\n");
	exit(1);
    }
}

int main() {
    int x, y;
    
    varaamuisti();
    srand(time(NULL)); /* alustetaan satunnaislukugeneraattori */
    textmode(0x13);
    while(!kbhit()) {
	for(y=0; y<200; y++)
	for(x=0; x<320; x++)
	    dblbuf[y*320+x]=rand()%256;
    }
    getch();
    textmode(0x3);
    return 0;
}

Kokeile mys ohjelmaa BUFFER2.C, joka on toteutettu ilman
kaksoispuskurointia, jos eroa ei viel huomaa, tulee se
joka tapauksessa viel esiin, ja on muitakin hydyllisi asioita miss
kaksoispuskuri, tai kolmoispuskurikin on tarpeen. Mutta, kokeile tmn
kytt ja palaa tmn dokumentin pariin VASTA kun osaat tydellisesti
kaksoispuskurin kytn (oikeammin ymmrrt miten se toimii, miten sit
kytetn, mihin se perustuu ja miten siihen piirretn
pisteit). Sitten syksymmekin uuteen tuntemattomaan. Katsotaan nyt
mihin...


PCX-kuvien lataus - vain vhn oikaisemalla
-------------------------------------------

Noniin, kaikki wannabe gamekooderit. Nyt on aika menn vaikeimpaan
aiheeseemme, johon monen kooderin taidot ovat viimein tyssnneet ja
jota minkn en viel tysin ymmrr, enk tied osaanko sit
selitt. Se on hyvuskoisuus, sill PCX:n sislt lytyy looginen ja
helposti ymmrrettv rakenne. Ja vaikkei sitkn tysin ymmrr, voi
aina vain kytt samaa rutiinia (kuten min) PCX:n
lataamiseen. Esittelenkin tss kappaleessa lyhyesti tmn yhden
yleisimmist kuvaformaateista olevan tiedostotyypin
saloja. 256-vrisen tyypillisen PCX:n rakenne voidaan jakaa karkeasti 
neljn (4) osaan:

 - 128 ensimmist tavua headeria sislten info kuvasta
 - kuvadata RLE-pakattuna
 - salaperinen tavu 12
 - palettidata, viimeiset 768 tavua

Ensimmisen ja kaikkein vaikeimpana on headeri, jonka loikimme lhes
kokonaan yli, sill tosipelikooderi tiet lataavansa oikeaa
PCX-kuvaa, joka on oikeaa formaattia oikeankokoiseen puskuriin ja
jtt selittmttmt kaatumiset muiden harteille! Tai itseasiassa en
sit selit kun en siihen ole perehtynyt syvemmin. Kiinnostuneille
PCGPE:ss on tmkin formaatti selitettyn lahjakkaan kryptisesti
englannin kamalalla mongerruksella. Kaikki sit haluavat hankkivat sitten
tiedoston PCGPE10.ZIP, joka sislt kaikkea hydyllist
peliohjelmointiasiaa, englanniksi siis.

Headerista tahdomme tiet vain sen, ett PCX-kuvan koko lasketaan
seuraavasti:

 - Mennn offsettiin 8 (fseek(handle, 8, SEEK_SET)).
 - Luetaan kaksi tavua ja tehdn niist sana (unsigned short int,
   katsomme latauskoodia kohta) ja meill on koko vaakatasossa.
 - Luetaan toiset kaksi tavua ja tehdn niille samoin kuin
   edellisille, nyt meill on y-koko.

Sitten onkin vaikein pala PCX:n rakenteessa. Sit kutsutaan nimell
RLE-koodaus (run length encoding) ja se tarkoittaa sit, ett jos
meill on perkkin 10 pikseli vri 15 emme kirjoitakaan PCX:n
kymment kertaa numeroa 15, vaan kirjoitamme sinne tavun 192+10=202 ja
sen pern tavun 15. Nyt kun PCX-lukija lukee ensimmisen tavun se
katsoo, ett ahaa, nyt tulee toistoa ja toistaa seuraavaa tavua
puskuriin tavu-192 kertaa (202-192=10). Nin me teemmekin
yksinkertaisen pseudorungon:

  - Lue tavu1
  - Jos tavu1 on suurempi kuin 192 niin lue tavu2 ja toista tavua 2
    tavu1-192 kertaa.
  - Jos tavu1 ei ollut suurempi laita puskuriin tavu1.

Nin helppoa, nyt viel paletti. Sekin on helppoa, kunhan muistamme
kaksi seikkaa:

 1) Etsimme paletin tiedoston LOPUSTA pin (fseek(handle, -768, SEEK_END))
 2) Jaamme vrikomponentit neljll, sill PCX:ss vriarvot ovat
    vlilt 0-255, VGA:ssa 0-63 (255/4=63).

Nyt yhdistmme taas kaiken tietomme, ja teemme funktion, joka ottaa
argumenttinaan PCX:n nimen ja puskurin jonne se ladataan. Ohjelma EI
VARAA MUISTIA puskurille, vaan se pit varata etukteen. Voit itse
tehd muutokset ohjelmaan jos haluat. Yleens kuitenkin etukteen on
tiedossa kuvan koko, kun PCX:i kytetn
peleiss. Kuvankatseluohjelmaa tehdess pit kuitenkin koko ottaa
selville jo viimeistn sen vuoksi, ett kuva nytetn oikein, vaikka
puskurissa olisikin tilaa.

Eli tss meill on valmiiksi pureskeltu PCX-lataajan runko, teemme
sille oikein oman kirjaston PCX.H. Kirjasto tarvitsee stdio.h:n
tiedostonksittelyfunktioita ja niiden tietorakenteita:

void loadpcx(char *filename, char *buffer) {
    int xsize, ysize, tavu1, tavu2, position=0;
    FILE *handle=fopen(filename, "rb");

    if(handle==NULL) {
	printf("Virhe PCX-tiedoston avauksessa: Tiedostoa ei lydy!\n");
	exit(1);
    }
    fseek(handle, 8, SEEK_SET);
    xsize=fgetc(handle)+(fgetc(handle)<<8)+1;
    ysize=fgetc(handle)+(fgetc(handle)<<8)+1;
    fseek(handle, 128, SEEK_SET);
    while(position<xsize*ysize) {
	tavu1=fgetc(handle);
	if(tavu1>192) {
	    tavu2=fgetc(handle);
	    for(; tavu1>192; tavu1--)
		buffer[position++]=tavu2;
	} else buffer[position++]=tavu1;
    }
    fclose(handle);
}

void loadpal(char *filename, char *palette) {
    FILE *handle=fopen(filename, "rb");
    int c;

    if(handle==NULL) {
	printf("Virhe PCX-tiedoston palettia luettaessa:"
	       " Tiedostoa ei lydy!\n");
	exit(1);
    }

    fseek(handle,-768,SEEK_END);
    for(c=0; c<256*3; c++)
	paletti[c] =fgetc(handle)/4;
    fclose(handle);
}

Kuten jo varmasti huomasit ovat paletin ja PCX:n latausrutiinit
erillisin. Tm siksi, ett joskus on huomattavasti ktevmp ladata
vain kuva, jos palettia ei mihinkn tarvita. Seuraavaksi seuraa
kappaleen esimerkkiohjelma, joka kytt hyvkseen tutoriaalin
varrella esiteltyj rutiineja ja muodostaa pienen esityksen. Ohjelma
lataa PCX-kuvan PICTURE.PCX ja paletin siit. Sitten se liskisee sen
ruudulle. Lopuksi kuva himmenee tyhjyyteen ja palataan
tekstitilaan. Esimerkki olettaa kuvan olevan kokoa 320x200,
256-vrinen ja paletin sisltv PCX-kuva RLE-pakattuna. Voit korvata
kuvan mill haluat joko muuttamalla lhdekoodia tai kopioimalla oman
kuvasi PICTURE.PCX:n plle.

Huomaa, ett ohjelmassa luodaan kaksoispuskuri, johon kuva
ladataan. Nyttmuistin vnkminen parametriksi aiheuttaa 100%
varmasti kaatumisen, tai jos jotenkin sstyt silt niin ainakaan
mitn ei ilmesty nytlle. Mutta asiaan (PCX1.C):

#include <go32.h>
#include <conio.h>
#include <stdio.h>
#include <sys/movedata.h>
#include <dos.h>

#include "palette.h"
#include "pcx.h"

#define flip(c) _dosmemputl(c, 64000/4, 0xA0000)

void main() {
    char palette[256*3];
    char dblbuf[64000];

    textmode(0x13);
    loadpcx("PICTURE.PCX", dblbuf);
    loadpal("PICTURE.PCX", palette);
    setpal(palette);
    flip(dblbuf);
    getch();
    fadetoblack(palette);
    textmode(0x3);
}

Toivottavasti ymmrsit tst luvusta ainakin kyttperiaatteen. Eli
loadpcx(nimi, puskuri) lataa kuvan puskuriin ja flip(puskuri) laittaa
sen nytlle (jos kuva on kokoa 320x200). Paletti ladataan tarvittaessa
funktiolla loadpal(nimi, palettipuskuri) ja asetetaan aktiiviseksi
komennolla setpal(palettipuskuri). Huomaa, ett esimerkiss asetetaan oikea
paletti ENNEN kuvan laittamista ruudulle. Huomataksesi miksi vaihda
setpal- ja flip-funktioiden paikkaa ja lis vliin getch(), jotta ehdit kat-
sella rauhassa muutosta. Tllaista tss kappaleessa. Mene nyt kokeilemaan
PCX-kuvien latausta. Seuraavassa kappaleessa tutustummekin sitten johonkin
peliohjelmoijaa lhell olevaan asiaan...


Bitmapit - eikai vain suunnistusta?
-----------------------------------

Tnn siis teemme pienen bitmap-enginen C:ll. Itse olen aiemmin tehnyt
kaikki sprite- ja bitmap -rutiinini C++:ssalla, mutta tll kertaa
kytmme C:t, sill haluan niden esimerkkien toimivan ilman C++:ssakin.
Eli mit on bitmap?

Bitmap, eli bittikartta on mrtyn kokoinen suorakulmion muotoinen esine,
jolla on puskuri muistissa sislten sen vrit, kuten nyttpuskurinkin
kanssa on. Hydylliseksi bitmapin tekee se, ett laitamme siihen pyyhkimis-
ja piirtotoiminnot, sek liikutustoiminnot, joilla voimme siirrell bitmap-
piamme ympri ruutua. Lisksi teemme siihen vrin, joka tarkoittaa ettei
sit kohtaa bitmapista tarvitse kopioida ruudulle. Nin saamme tehty bit-
mappiimme reiki, eli teemme sen osittain lpinkyvksi. Mutta miten tm
kaikki sitten tehdn? Koko asia on, kuten kaikki asiat ohjelmoinnissa lo-
pulta ovat - naurettavan helppo.

Eli, menkmme takaisin kaksoispuskurin aikoihin. Siin meill on
puskuri, jonka koko on 320x200 pikseli ja se kopioidaan kokonaan nytn
plle. Bittikartassa on muutama selke ero:

 - Se voi alkaa mist tahansa kohdasta ruutua, vaikka koordinaateista
   15, 123.
 - Se voi olla mink kokoinen tahansa (yleens kuitenkin ruutua pienempi).
 - Sen peittm tausta tallennetaan ja palautetaan kun bittikartta
   pyyhitn pois, mik mahdollistaa liikuttelemisen.
 - Siin on lpinkyv vri, meill 0, jota ei piirret ruudulle. Jos siis
   koko bittikartta olisi vri 0, emme nkisi ruudulla mitn!

Eli itseasiassa bittikartta on pari puskuria, joille on varattu tilaa
siten, ett jokainen bittikartan vri voidaan sil
puskuriin. Puskureita on perusbittikartassa kaksi, eli itse kuvan
sisltv kartta, joka on jrjestelty aivan samoin kuin
esim. kaksoispuskuri, mutta koko on bittikartan mukainen. Toinen on
taustapuskuri, joka on muuten sama, mutta sinne vain siltn
piirrettess alle jneet pikselit, jotta ne voidaan bittikarttaa
ruudulta pyyhkiess palauttaa sielt.

Eli tllainen voisi olla 3x3 kokoinen bittikartta:

Bittikartta:      Taustapuskuri (mit bittikartan alle on
                  piirrettess jnyt):
30 20 19          0  0  0
19 23 42          0  0  0
12 32 43          0  0  0

Kuten huomaatte bittikartta on piirretty mustalle pohjalle, sill
taustapuskuri eli se mit bittikartan alle ji on tynn mustaa, eli
vri 0. Bittikartta on kaikkein helpointa mritell omaan
datarakenteeseensa, joka sislt tarvittavat tiedot kartan piirtelyyn
ja pyyhkimiseeen, nimetn se vaikka structiksi BITMAP.

Koordinaattien mrittely saavutetaan siten, ett meill on rakenteessamme
X-ja Y-koordinaatit, joista piirto kaksoispuskuriin aloitetaan. Koko
taas on helpompi. Jos kaksoispuskurin koko oli 320x200, niin kaava
oikean pikselin hakemiseksi oli y*320+x. Jos meill on bitmap kokoa
ysize * xsize, niin oikea koordinaatti on y*xsize+x. Piirrettess
loopataan X: ja Y:t siten, ett luemme yksi kerrallaan pikselin
bittikartasta, ja jos se on jokin muu kuin vri 0 (yleens musta, tm
oli siis lpinkyvksi sovittu vri), otamme ensin sen alle jvn
pikselin talteen taustapuskuriin ja laitamme sitten vasta bittikartan
vrin ruudulle oikeaan kohtaan (bittikartan vrit sisltvst
puskurista).

Eli tarvittavat tiedot bittikarttarakenteeseen ovat:

 - bittikartan vrit (char * -pointteri)
 - taustan vrit (char * -pointteri)
 - x-sijainti ruudulla (int)
 - y-sijainti ruudulla (int)
 - koko x-suunnassa (int)
 - koko y-suunnassa (int)

Lisksi meill on xspeed ja yspeed, joita kytetn esimerkeiss
silmn bittikartan liikenopeutta x- ja y-suunnassa. Nill
tempuilla meill on nyt teoria liikuteltavan bitmapin tekemiseksi.
Ensin mrittelemme rakenteen, joka sislt kaiken tarvittavan tiedon
bittikartastamme (BITMAP.H):

typedef struct {
    char *bitmap;
    char *background;
    int x;
    int y;
    int xsize;
    int ysize;
    int xspeed;
    int yspeed;
} BITMAP;

Sitten tehtvnmme on tehd "interface", eli kyttliittym
bitmap-engineemme. Siihen sisllytmme seuraavat funktiot:

  - bdraw(BITMAP *b) piirt bittikartan kohtaan BITMAP.x, BITMAP.y

  - bhide(BITMAP *b) tyhjent edellisell piirtokerralla piirretyn bitti-
    kartan. Huomaa, ett JOKAISEN PIIRRON JLKEEN ON TULTAVA TYHJENNYS
    ja ett BITTIKARTTAA EI LIIKUTETA SEN OLLESSA RUUDULLA (todellisuudessa
    tietenkin kaksoispuskurissa, joka kopioidaan ruudulle kun kaikki bitti-
    kartat ovat nkyviss, sanoinhan, ett hydymme viel siit!)

  - bmove(BITMAP *b) lis X-koordinaattiin muuttujan BITMAP.xspeed ja
    Y-koordinaattiin vastaavasti muuttujan BITMAP.yspeed.

  - bsetlocation(BITMAP *b, int x, int y) asettaa uudet X- ja
    Y-koordinaatit.

  - bsetspeed(BITMAP *b, int xspeed, int yspeed) asettaa uudet X- ja
    Y-nopeudet. Huomaa, ett liike yls saavutetaan negatiivisella
    Y-nopeudella ja vastaavasti liike vasemmalle negatiivisell
    X-nopeudella.

  - bload(BITMAP *b, int x, int y, int xspeed, int yspeed, int xsize,
    int ysize, char *bitmapbuffer, int bufferx, int buffery, 
    int bufferxs), jossa 8. parametrist lhtien kertoo
    latauspuskurista, jona tulemme kyttmn 320x200 kokoista PCX, kuvaa,
    sislten kaikki bitmapit mit pit ladata. Jos kuvan x-koko ja y-koko,
    sek aloituskoordinaatit kuvassa on ilmoitettu oikein, onnistuu lataus
    suorakulmion muotoiselta alueelta tysin onnistuneesti, eik lataus-
    rutiinin kytt vaadi kovin paljoa miettimist. Lis kytst ajal-
    laan tulevassa esimerkiss.

No niin. Lhtekmme tekemn kirjastoamme BITMAP.H yksi funktio kerrallaan.
Rakenne BITMAP on jo esitelty, joten alkakaamme kermn sen pern
ksittelyfunktioita. Ensimmisenhn oli vuorossa bdraw(), joka onkin
helpoimpia ja trkeimpi funktioita. Katsellaanpas esimerkkikoodia:

void bdraw(BITMAP *b) {
    int y=b->y,
	x=b->x,
	yy, xx;

    /* Eli loopataan koko suorakulman kokoinen alue. bitmap- ja
       ja background -puskureissahan lasketaan sijainti seuraavasti:
       y * b->xsize + x. */

    for(yy=0; yy<b->ysize; yy++) { 
    for(xx=0; xx<b->xsize; xx++) {

	/* eli vrill 0 tm vertailu alla ei ole tosi, joten vrill
	   0 merkittyj kohtia EI piirret! */

	if(b->bitmap[yy*b->xsize+xx]) {

	    /* doublebuffer muuttuja osoittaa kaksoispuskuriin. Huomaa, ett
	       ylkulma on y*320+x, mutta koska haluamme viel piirt useita
	       rivej, lismme yy-looppimme y-arvoon, kutenn mys xx-looppi
	       x-arvoon. Jos et ymmrtnyt niin poista vliaikaisesti kohdat
	       ja net mit tapahtuu */

	    b->background[yy*b->xsize+xx]=
		doublebuffer[ (y+yy) * 320 + (x+xx) ];


	    /* sitten vain asetetaan bittikartasta oikea kohta ruudulle,
	       alle peittyv osa on jo tallessa puskurin background vastaa-
	       valla kohdalla. */

	    doublebuffer[ (y+yy) * 320 + (x+xx) ]=
		b->bitmap[yy*b->xsize+xx];
	}
    }
    }
}

Koska joiltakin on esiintynyt valituksia siit, ett koodi j hmrn
peittoon, niin esittelen tss saman pseudona, jos se olisi hieman
selvemp:

funktio bdraw
    kokonaisluvun kokoiset kierroslaskurit a ja b

    looppaa a vlill 0 - <y-koko>
        looppaa b vlill 0 - <x-koko>

            bittikarttasijainti = a * <x-koko> + b
            ruutusijainti = ( <y-sijainti> + a ) * 320 + b + <x-sijainti>

            jos bittikartta(bittikarttasijainti) ei ole 0 niin

                tausta(bittikarttasijainti) = kaksois(ruutusijainti)
                kaksois(ruutusijainti) = bittikartta(bittikarttasijainti)

            end jos

        end looppi b
    end looppi a

end funktio

Kun lhdet korvaamaan a:n muuttujalla yy ja b:n muuttujalla xx ja
korvaat bittikartan sisiset muuttujat <y-koko>, <x-koko>,
<y-sijainti> ja <x-sijainti> BITMAP-rakenteen muuttujilla b->ysize,
b->xsize, b->y ja b->x sek tausta:n ja bittikartan:n
b->background:illa ja b->bitmap:illa, kaksois-muuttujan
kaksoispuskurisi nimell niin olet aikalailla ensimmisess,
alkuperisess sorsassa. Jos yhtn selvent niin voit poistaa
kommentit alkuperisest sorsasta kokonaan ja siirt sijainnin laskut
sielt []-sulkeiden sisst juuri tuollaisiin
bittikarttasijainti-tyylisiin apumuuttujiin, jolloin koodi selvenee
hieman. Olkoot, tss se on:

void bdraw(BITMAP *b) {
    int a, b, bitmapsijainti, ruutusijainti;

    for(a=0; a < b->ysize; a++) {
        for(b=0; b < b->xsize; b++) {
            bitmapsijainti=a * b->xsize + b;
            ruutusijainti = ( b->y + a ) * 320 + b + b->x;

            if(b->bitmap[bitmapsijainti] != 0) {

                b->background[bitmapsijainti] = doublebuffer[ruutusijainti];
                doublebuffer[ruutusijainti] = b->bitmap[bitmapsijainti];

            }
        }
    }
}

Varaa aikaa edellisten tutkimiseen, sill on trke, ett ymmrrt periaat-
teen. Tietenkin saat lisselvyytt kokeilemalla muuttaa noita kohtia, jol-
loin net muutoksen kntmll uudelleen esimerkkiohjelman, jonka
myhemmin esittelemme ja ajamalla muunnellun version. Seuraavana onkin
huomattavasti nopeammin tehty pyyhintfunktio, joka eroaa vain siten, ett
sen sijaan, ett silisimme taustan ja korvaisimme ruudun pikselin
bitmap-puskurin arvolla laitammekin background-puskuriin tallennetun pikse-
lin takaisin kaksoispuskuriin, joka on piilotusfunktion jlkeen samassa
kunnossa kuin ennen piirtoakin!

void bhide(BITMAP *b) {
    int y=b->y,
	x=b->x,
	yy, xx;

    /* Eli loopataan koko suorakulman kokoinen alue. bitmap- ja
       ja background -puskureissahan lasketaan sijainti seuraavasti:
       y * b->xsize + x. */

    for(yy=0; yy<b->ysize; yy++) { 
    for(xx=0; xx<b->xsize; xx++) {

	/* eli vrill 0 tm vertailu alla ei ole tosi, joten vrill
	   0 merkittyj kohtia EI piirret! */

	if(b->bitmap[yy*b->xsize+xx]) {
	    doublebuffer[ (y+yy) * 320 + (x+xx) ]=
		b->background[yy*b->xsize+xx];
	}
    }
    }
}

Tuohon ette varmaan en pseudoja tarvitse, koska sehn eroaa
edellisest vain tuon sijoituksen osalta, eli ensimminen sijoitus
draw-funktiosta knnetn vain toisinpin, niin alkup. tausta
palautuu.

Seuraavaksi kolme helponta funktiota heti riviss, sill niiden toteuttami-
nen on helppoa ja ymmrtminen viel helpompaa, muista, ett X-ja Y-koor-
dinaatteja vhennetn negatiivisill nopeuksilla, sill X+(-1)=X-1:

void bmove(BITMAP *b) {
    b->x+=b->xspeed;
    b->y+=b->yspeed;
}

void bsetlocation(BITMAP *b, int x, int y) {
    b->x=x;
    b->y=y;
}

void bsetspeed(BITMAP *b, int xspeed, int yspeed) {
    b->xspeed=xspeed;
    b->yspeed=yspeed;
}

Seuraava onkin vaikea pala, joten lisn koodia saadakseni siit vhn
selvemmksi. Idea siis on, ett otamme pikselin tuplapuskuriin ladatus-
ta ja laitamme sen bitmap-puskuriin. Eli oikeastaan knteisesti nyt-
tfunktioon nhden. Eli katsotaanpas:

void bload(BITMAP *b, int x, int y, int xspeed, int yspeed, int xsize,
    int ysize, char *bitmapbuffer, int bufferx, int buffery, 
    int bufferxs) {
    int yy, xx;

    bsetlocation(b, x, y);
    bsetspeed(b, xspeed, yspeed);
    b->xsize=xsize;
    b->ysize=ysize;

    b->bitmap=(char *)malloc(xsize*ysize);
    b->background=(char *)malloc(xsize*ysize);
    if(b->background==NULL || b->background==NULL) {
	printf("Ei tarpeeksi muistia bitmap-puskureille!\n");
	exit(1);
    }
    
    /* Eli loopataan koko suorakulman kokoinen alue. bitmap-
       puskurissahan lasketaan sijainti seuraavasti:
       y * b->xsize + x. */

    for(yy=0; yy<ysize; yy++) { 
    for(xx=0; xx<xsize; xx++) {

	/* doublebuffer muuttuja osoittaa kaksoispuskuriin. Huomaa, ett
	   ylkulma on y*320+x, mutta koska haluamme viel piirt useita
	   rivej, lismme yy-looppimme y-arvoon, kutenn mys xx-looppi
	   x-arvoon. Jos et ymmrtnyt niin poista vliaikaisesti kohdat
	   ja net mit tapahtuu */

	b->bitmap[yy*xsize+xx]=
	    bitmapbuffer[ (buffery+yy) * bufferxs + (bufferx+xx) ];
    }
    }
}

bload on itseasassa tysin sama kuin ensimminenkin funktio, mutta
alussa meill on pari alustusta jotta BITMAP-rakenne saadaan halutuksi
(muistinvarausta, sijainnin nollausta, koon alustus...). Vain
piirtofunktio on korvattu versiolla, joka ei piirr ruudulle, vaan
lataa ruudulta (bitmapbuffer tss tapauksessa, jottei tarvi oikeaa
kaksoispuskuria vlttmtt kytt) pikselit. Ei se loppujenlopuksi
ole sen vaikeampi.

Nyt kun lismme kaikki yhteen kirjastoomme BITMAP.H ja teemme lopuksi
viel pienen esimerkkiohjelman, joka liikuttelee palloa
ruudulla. Koska kirjastomme ei kykene estmn ruudun yli menemisi,
niin meidn pit knt liikkuvan pallon suuntaa ennenkuin alareuna
osuu ruudun alareunaan ja menee sitten siit yli (eli jos bittikartan
koko, sijainti ja nopeus yhteenlaskettuna on yli ruudun koon, tai
bittikartan sijainti ja nopeus yhteenlaskettuna on pienempi kuin
0). Eli kun jompikumpi edellisist ehdoista tyttyy niin knnetn
pallon suuntaa ja saadaan pallo "pomppimaan" reunoista.

Mutta, olemme taas puhuneet ihan tarpeeksi. Menkmme nyt esimerkkiohjel-
mamme pariin (BITMAP1.C). Siin lataamme bittikartan tiedostosta BITMAP.PCX
ja tausta tiedostosta BITBACK.PCX. Nin nemme lpinkyvyyden toiminnassa
(muutenhan pallo olisi nelinmuotoinen). Lisksi tietenkin kytmme jo va-
kioiksi muuttuneita palettifunktiota ohjelmamme koristukseksi:

#include <go32.h>
#include <sys/movedata.h>
#include <conio.h>
#include <stdio.h>
#include <dos.h>
#include <stdlib.h>

char *doublebuffer;

#include "palette.h"
#include "pcx.h"
#include "bitmap.h"

#define flip(c) _dosmemputl(c, 64000/4, 0xA0000)

int main() {
    char palette[768];
    BITMAP bitmap;

    doublebuffer=(char *)malloc(64000);
    if(doublebuffer==NULL) {
	printf("Ei tarpeeksi muistia kaksoipuskurin varaukseen!\n");
	return 1;
    }
    textmode(0x13);
    loadpcx("BITMAP.PCX", doublebuffer);
    loadpal("BITMAP.PCX", palette);
    setpal(palette);
    bload(&bitmap, 160, 100, 1, 1, 16, 16, doublebuffer, 1, 1, 320);
    loadpcx("BITBACK.PCX", doublebuffer);
    /* Lataus vasta kun bittikartta on otettu edellisest tiedostosta.
       Ei ladata palettia koska se on sama kuin edellisess PCX:ss. */

    while(!kbhit()) {
	bdraw(&bitmap);
	waitsync();
	flip(doublebuffer);
	bhide(&bitmap);
	bmove(&bitmap);
	if((bitmap.x+bitmap.xsize+bitmap.xspeed)>320 ||
	   bitmap.x+bitmap.xspeed<0)
	    bitmap.xspeed= -bitmap.xspeed;
	if((bitmap.y+bitmap.ysize+bitmap.yspeed)>200 ||
	   bitmap.y+bitmap.yspeed<0)
	    bitmap.yspeed= -bitmap.yspeed;
    }
    getch();
    fadetoblack(palette);
    textmode(0x3);

    return 0;
}

Varaa kunnolla aikaa ja tutki lhdekoodeja, mieti teoriaa ja kokeile kaikkea
kytnnss mit mieleen tulee. Kun luulet keksineesi idean niin palaa
takaisin dokumentin reen, ja siirrymme seuraavaan aiheesemme. Menehn
siit! Jos vielkin tuntui silt ettet tajunnut niin ota yhteytt ja
kysy mik ji mietityttmn, niin tarkennan sitten viel tt.


Animaatiot
----------

Tmnkertainen aiheemme on pieni parannus koodiin, joka on paljon ny-
tll ja jonka jlkeen on tmn tutoriaalin bittikarttarutiinit lhes k-
sitelty. Tulemme kyll hyvksikyttmn edellisen kappaleen koodia
tehdessmme fonttiengine, sek parantelemme koodia tehdessmme trmys-
tarkistuksen, mutta itse animointi- ja bittikarttateoria ksitelln
kokonaan tss ja edellisess kappaleessa.

Eli tnn tutustumme ensimmisen animaatiohin. Mit animaatiot sitten
ovat? No itseasiasas animaatio on vain sarja kuvia, joita vaihdellaan
ja saadaan kuva liikkeest. Animaatiota voidaan kytt lhes kaikkeen
peliss. Sill voidaan tehd pyriv alusanimaatio, jonka jokainen
kuva on yksi aluksen suunta. Jokaisella suunnalla voisi olla viel oma
animaationsa, joka saa vaikka rakettimoottorit hehkumaan ja laserit
aiheuttamaan vlhdyksi aluksen pinnassa. Pienell mielikuvituksella
ja taitavalla graafikolla pstn ihmeisiin. Tss kappaleessa esi-
telty kirjasto ei varmaankaan ky suoraan moneen tarkoitukseen tai ole
tarpeeksi nopea peliin, mutta enginen onkin vain tarkoitus nytt
pperiaatteita animoinnin ja muiden olennaisien asioiden takana.

Eli animaatio on kuvasarja, jotka nytetn tietyss jrjestyksess. Miten
sitten toteutamme tmn. Tss on tapa jolla min olen sen tehnyt. Meillhn
on tysin toimivat rutiinit yhden kuvan nyttmiseen. Tehkmme vain
animointikoodi, joka vaihtaa pointterin bitmap osoittamaan seuraavaan
kuvaa, eli frameen. Tt tytyy kutsua silloin kun sprite, joksi kutsumme
animoivaa bittikarttaamme tstlhin ei ole piirretty puskuriin. Jlleen
voit kokeilla siirt animointikoodin kutsun kohtaan jossa esine on piir-
rettyn, mutta se ei tule nyttmn hyvlt (jos objektin peittmn alueen
muoto muuttuu). Eli siis tarvitsemme uuden rakenteen, joka voi sil
useita kuvia, koodin joka vaihtaa bitmap-pointterin osoittamaan seuraavaan
kuvaan, laskurin joka kertoo monennessako kuvassa mennn ja toisen muuttu-
jan joka kertoo montako kuvaa meill on animaatiossa, sek lopulta uuden
latausfunktion, joka osaa ladata useita kuvia ksittvn animaation.

Thn kaikkeen voimme kopioida vanhaa koodiamme ja lisill sinne tar-
peellisia osia. Eli teemme nyt uuden rakenteen, jossa voi olla maksimis-
saan MAXFRAME mr frameja, eli kuvia (tm toteutuksen helpottamiseksi):

#define MAXFRAME 64

typedef struct {
    char *frame[MAXFRAME];
    int curfrm;
    int frames;

    char *bitmap;
    char *background;
    int x;
    int y;
    int xsize;
    int ysize;
    int xspeed;
    int yspeed;
} SPRITE;

Se olikin helppoa. Nm rutiinit tulevat kirjastoon SPRITE.H, josta lydt
mys joukon vanhoja tuttujamme uudelleennimettyn ja vhn
muunneltuina (sdraw, shide...). Seuraavaksi sitten animointirutiini:

void sanimate(SPRITE *s) {
    s->curfrm++;
    if(s->curfrm >= s->frames)
	s->curfrm=0;
    s->bitmap=s->frame[s->curfrm];
}

Radikaaleja muutoksia tarvinnee mys latausrutiinimme. Trkeimmt muutok-
set siin on, ett se lukee framet rivist. Katso SPRITE.PCX esimerkkin
tllaisesta animaatiosta. Jos ihmettelet outoja kertolaskuja joissain
kohdin se johtuu siit, ett jokaisen framen jlkeen hyptn 1 pikseli
yli, sill teemme rajat animaatioiden vliin selvennykseksi. Eli tss
olisi latauskoodimme, uusi parametri on animaatioiden mr:

void sload(SPRITE *s, int x, int y, int xspeed, int yspeed, int xsize,
    int ysize, char *bitmapbuffer, int bufferx, int buffery, 
    int bufferxs, int frames) {
    int yy, xx, current;

    ssetlocation(s, x, y);
    ssetspeed(s, xspeed, yspeed);
    s->xsize=xsize;
    s->ysize=ysize;
    s->curfrm=0;
    s->frames=frames;

    for(current=0; current<frames; current++) {
	s->frame[current]=(char *)malloc(xsize*ysize);
        if(s->frame[current]==NULL) {
            printf("Ei tarpeeksi muistia sprite-puskureille!\n");
            exit(1);
        }
    }
    s->background=(char *)malloc(xsize*ysize);
    s->bitmap=s->frame[s->curfrm];

    if(s->background==NULL) {
	printf("Ei tarpeeksi muistia sprite-puskureille!\n");
	exit(1);
    }
    
    /* Eli loopataan koko suorakulman kokoinen alue. bitmap-
       puskurissahan lasketaan sijainti seuraavasti:
       y * s->xsize + x. Uloimpana looppina on uutena framelooppi,
       joka on listty koska meidn pit ladata usea kuva. */
    for(current=0; current<frames; current++)
    for(yy=0; yy<ysize; yy++) { 
    for(xx=0; xx<xsize; xx++) {
	/* doublebuffer muuttuja osoittaa kaksoispuskuriin. Huomaa, ett
	   ylkulma on y*320+x, mutta koska haluamme viel piirt useita
	   rivej, lismme yy-looppimme y-arvoon, kutenn mys xx-looppi
	   x-arvoon. Jos et ymmrtnyt niin poista vliaikaisesti kohdat
	   ja net mit tapahtuu */
	s->frame[current][yy*xsize+xx]=
	    bitmapbuffer[ (buffery+yy) * bufferxs + (bufferx+xx) +
			  (xsize+1)*current ];
    }
    }
}

Kirjastoon SPRITE.H listn viel bdraw, bhide, bmove, bsetlocation ja
bsetspeed nimettyn nimill sdraw, shide, smove, ssetlocation ja ssetspeed
funktioiden erottamiseksi bitmap-rutiineista (jos vaikka halutaan kytt
molempia). Muitakin pikkumuutoksia on tehty. Huomaat ne helposti
kurkkaamalla kirjaston sisn. Nyt meill onkin animaatiot taitava engine,
jota meidn tytyy tietenkin heti kokeilla. Tss on esimerkkiohjelmamme
SPRITE1.C, joka havainnoi funktioiden kytt:

#include <go32.h>
#include <sys/movedata.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>

char *doublebuffer;

#include "palette.h"
#include "pcx.h"
#include "sprite.h"

#define flip(c) _dosmemputl(c, 64000/4, 0xA0000)

int main() {
    char palette[768];
    SPRITE sprite;

    doublebuffer=(char *)malloc(64000);
    if(doublebuffer==NULL) {
	printf("Ei tarpeeksi muistia kaksoipuskurin varaukseen!\n");
	return 1;
    }
    textmode(0x13);
    loadpcx("SPRITE.PCX", doublebuffer);
    loadpal("SPRITE.PCX", palette);
    setpal(palette);
    sload(&sprite, 160, 100, 1, 1, 16, 16, doublebuffer, 1, 1, 320, 8);
    loadpcx("BITBACK.PCX", doublebuffer);
    /* Lataus vasta kun bittikartta on otettu edellisest tiedostosta.
       Ei ladata palettia koska se on sama kuin edellisess PCX:ss. */

    while(!kbhit()) {
	sdraw(&sprite);
	waitsync();
	waitsync();
	flip(doublebuffer);
	shide(&sprite);
	smove(&sprite);
	sanimate(&sprite);
	if((sprite.x+sprite.xsize+sprite.xspeed)>320 ||
	   sprite.x+sprite.xspeed<0)
	    sprite.xspeed= -sprite.xspeed;
	if((sprite.y+sprite.ysize+sprite.yspeed)>200 ||
	   sprite.y+sprite.yspeed<0)
	    sprite.yspeed= -sprite.yspeed;
    }
    getch();
    fadetoblack(palette);
    textmode(0x3);

    return 0;
}

Luultavasti huomaat nykimist, sill tysin optimoimaton sprite-enginemme
ei aivan pysty 70 frameen sekunnissa. Siksi laitoin ohjelmamme odottamaan
kahta vertical retracea, jotta nykiminen ei olisi niin hiritsev
(P75:llni kahdella waitilla meno nytt paljon tasaisemmalta, eik yhden
framen hyppy ny lheskn niin selvsti). Jos kuitenkin sinulla on hidas
kone niin poista toinen tai kummatkin odotuksista, se nopeuttaa koodia
paljon, mutta voit joutua laittamaan delay-komennolla viivett stksesi
pyrimist tasaisemmaksi. Pienell optimoinnilla olisimme toki saaneet
moninkertaisesti lis nopeutta, mutta koodi olisi menettnyt luettavuut-
taan, joka esimerkkiohjelmien tarkoitus on. Tietenkin kun alat tekemn
omaa pelisi teet uudet ja paremmin tarkoitukseesi sopivat rutiinit ke-
rmiesi tietojen pohjalta.

Nyt onkin tmn kappaleen aika loppua ja sinun on aika paneutua uuden
asian pariin. Seuraavassa luvussamme ksitellnkin sitten viimeist
kysymyst spritejen parissa, monen spriten kytt, niiden trmyksi
ja ylitseliukumisia. Mutta nyt jtn sinut rauhaan. Nemme seuraavassa
luvussa!


Pitk spriten trmt? Ent coca-colan?
-----------------------------------------

Nyt psemmekin vihoviimeiseen vaiheeseen teoriassamme ja ryyditmme sit
pienin, tai ehk niinkn pienin muutoksin SPRITE.H-kirjastoomme. Nimit-
tin jokainen vhnkn vakavasti pelintekoa harkinnut tarvitsee useampia
kuin yhden spriten. Mutta mit tapahtuu kun ne ovat menossa pllekin?
Jos teet vain loopin, joka piirt spriten ja toisen, joka pyyhkii ne
samassa jrjestyksess olet varmaan huomannut, ett se ei aiheuta toivot-
tuja tuloksia. Muutos mit tarvitaan on pieni ja yksinkertainen, mutta
ajatellaanpas esimerkkimme.

Ajatellaan, ett sinulla on kolme pikseli. Punainen, sininen ja keltainen.
Haluat laittaa ne samaan kohtaan ruudulle. Laitat ne edell olevassa
jrjestyksess mustalle ruudulle ja laitat lapulle muistiin punaisen koh-
dalle, ett sen alla oli musta, sinisen kohdalle, ett sen alla oli
punainen ja keltaisen kohdalle, ett sen alla oli sininen.

Nyt haluat poistaa ne. Ottaisitko ne nyt samassa jrjestyksess, eli ensin
punainen, sitten sininen ja lopuksi keltainen? Et, sill jos ottaisit lopuksi
keltaisen, katsoisit lapustasi sen alla olleen sinisen vrin ja ruutu
muuttuisikin siniseksi. Tss meidn tytyykin menn knteisesti, eli
keltainen, sininen ja sitten vasta punainen, jonka tilalle laitat lopulta
mustan ja kaikki on hyvin.

Eli jos sinulla olisi 10 bittikarttaa taulukossa SPRITE s[10], niin niiden
piirto ja pyyhkiminen tapahtuisi seuraavasti:

for(c=0; c<10; c++) sdraw(s[c]);
flip(doublebuffer);
for(c=10; c>0; c--) shide(s[c-1]);

Ja ei en toimimattomia koodinptki, vaan hienosti toistensa ylitse
liukuvat spritet.

Mutta aina ei haluta kaikkien vain liukuvan toistensa ylitse. Milt
nyttisi matopeli, jossa madot kiltisti liukuvat toistensa ylitse?
Ei kovin oikealta, sanoisin. Meidn tytyy siis tehd rutiini, joka
tarkistaa trmyksen kahden spriten vlill. Olkoon sen kutsutapa
seuraava: scollision(SPRITE *a, SPRITE *b) ja se palauttaa arvon
1 jos trmys on tapahtunut, muuten se palauttaa nollan. Jos siis
haluat tehd trmyksen tultua jotakin, niin koodi menisi suurinpiirtein
nin:

if(scollision(sprite[0], sprite[1]))
    tee_jotain_kun_tulee_pamahdus();

Mutta, miten toimii tm salaperinen funktiomme? Itseasiassa min en
saanut siit mitn selv luettuani sen aikoinani Mikrobitin grafiikka-
ohjelmointikurssin toisesta osasta, mutta luulisin nyt pystyvni teke-
mn samanlaisen, ja jos onnistumme pystynen selittmnkin toimintaperi-
aatteen.

int scollision(SPRITE *a, SPRITE *b) {
    /* Lasketaan spritejen ylkulmien vliset etisyydet. Huomaa, ett tss
       lasketaan mukaan nopeudet, eli palautusarvo 1 kertoo spritejen
       trmvn ENSI vuorolla. Nin ehditn pllekkin meneminen est
       ajoissa. */
    int xdistance= (a->x+a->xspeed) - (b->x+b->xspeed);
    int ydistance= (a->y+a->yspeed) - (b->y+b->yspeed);
    int xx, yy;
    /* Jos x- tai y-etisyys on suurempi kuin suuremman leveys eivt
       spritet voi mitenkn olla toistensa pll. */
    if(xdistance>a->xsize && xdistance>b->xsize) return 0;
    if(ydistance>a->ysize && ydistance>b->ysize) return 0;

    for(xx=0; xx< a->xsize; xx++)
    for(yy=0; yy< a->ysize; yy++)
	if(xx+xdistance < b->xsize && xx+xdistance>=0 &&
	   yy+ydistance < b->ysize && yy+ydistance>=0)
	if(a->bitmap[ yy * a->xsize + xx ] &&
	   b->bitmap[ (yy+ydistance) * b->xsize + (xx+xdistance) ])
	    return 1;
    return 0;
}

Loopissa ideana on se, ett laskuilla saadaan b-spriten vastaava koordinaatti
selville ja jos se on siis positiivinen ja spriten b rajoissa (pienempi
kuin leveys tai y-koordinaatin ollessa kyseess korkeus). Tarkemmin en
ala selittmn. Jos vlttmtt haluat saada selville miten ptk toimii
niin piirr pari tilannetta paperilla ja katso miten niiden kanssa tapah-
tuu. Nyt meill onkin ksiteltyn kaikki trkein spriteist ja voimme
menn viimeiseen pelkstn spritej kyttvn ohjelmaamme. Tm ohjelma
on pienimuotoinen peli, jossa liikutaan edellisen esimerkin palikoilla. Pe-
laajia on 2 ja tarkoitus on leikki hippaa. Eli toinen yritt pakoon ja
toinen yritt ottaa kiinni. Peli loppuu kun pelaajat trmvt. Kontrol-
lit ovat pelaajalla 1 wsad ja pelaajalla 2 ujhk. Tm on vain pieni esi-
merkki siit mit nill taidoilla voisi tehd. Lisksi nappeina on
+ ja - nopeuden stn (nyt ei odoteta waitsyncill) sek ESC lopetuk-
seen kesken. Eli SPRITE2.C:

#include <go32.h>
#include <sys/movedata.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>

char *doublebuffer;

#include "palette.h"
#include "pcx.h"
#include "sprite.h"

#define flip(c) _dosmemputl(c, 64000/4, 0xA0000)

int main() {
    char palette[768];
    SPRITE pl1, pl2;
    int quit=0, waittime=0;

    doublebuffer=(char *)malloc(64000);
    if(doublebuffer==NULL) {
	printf("Ei tarpeeksi muistia kaksoipuskurin varaukseen!\n");
	return 1;
    }
    textmode(0x13);
    loadpcx("SPRITE.PCX", doublebuffer);
    loadpal("SPRITE.PCX", palette);
    setpal(palette);
    sload(&pl1, 100, 100, 0, 0, 16, 16, doublebuffer, 1, 1, 320, 8);
    sload(&pl2, 220, 100, 0, 0, 16, 16, doublebuffer, 1, 1, 320, 8);
    loadpcx("BITBACK.PCX", doublebuffer);

    while(!quit) {
	sdraw(&pl1);
	sdraw(&pl2);
	flip(doublebuffer);
	shide(&pl1);
	shide(&pl2);
	smove(&pl2);
	smove(&pl1);
	sanimate(&pl1);
	sanimate(&pl2);
	if((pl1.x+pl1.xsize+pl1.xspeed)>320 ||
	   pl1.x+pl1.xspeed<0)
	    pl1.xspeed= -pl1.xspeed;
	if((pl1.y+pl1.ysize+pl1.yspeed)>200 ||
	   pl1.y+pl1.yspeed<0)
	    pl1.yspeed= -pl1.yspeed;

	if((pl2.x+pl2.xsize+pl2.xspeed)>320 ||
	   pl2.x+pl2.xspeed<0)
	    pl2.xspeed= -pl2.xspeed;
	if((pl2.y+pl2.ysize+pl2.yspeed)>200 ||
	   pl2.y+pl2.yspeed<0)
	    pl2.yspeed= -pl2.yspeed;

	if(scollision(&pl1, &pl2))
	    quit=2; /* 2 tarkoittaa, ett toinen saatiin kiinni */

	while(kbhit()) { /* tyhjennetn nppispuskuri */
	    switch(getch()) {
		case 'w': pl1.yspeed=-1; pl1.xspeed=0; break;
		case 's': pl1.yspeed=1; pl1.xspeed=0; break;
		case 'a': pl1.xspeed=-1; pl1.yspeed=0; break;
		case 'd': pl1.xspeed=1; pl1.yspeed=0; break;

		case 'u': pl2.yspeed=-1; pl2.xspeed=0; break;
		case 'j': pl2.yspeed=1; pl2.xspeed=0; break;
		case 'h': pl2.xspeed=-1; pl2.yspeed=0; break;
		case 'k': pl2.xspeed=1; pl2.yspeed=0; break;

		case '+': if(waittime) waittime--; break;
		case '-': waittime++; break;

		case  27: quit=1; break;
	    }
	}
	delay(waittime);
    }
    if(quit==2) { /* jos kiinni, niin feidataan ensin valkoiseen (rjhdys) */
	fadetowhite(palette);
	for(waittime=0; waittime<256*3; waittime++)
	    palette[waittime]=63;
    }
    fadetoblack(palette);
    textmode(0x3);

    return 0;
}

Tss oli sitten sellainen lhdekoodi, jota kukaan vhnkn omanarvontuntoa
omaava peliohjelmoija, taikka muukaan ohjelmoija EI TEE. Jos pelist to-
della halutaan selv ja helposti laajennettava ei tehd jokaiselle pelaa-
jalle eri sprite eri nimell, vaan kaikki pelaajaspritet ovat
taulukossa. Ja muutenkin esimerkkikoodi ainoastaan demonstroi mahdolli-
suuksia oppimiemme asioiden kyttmiseen, ei suinkaan minklainen pelin
runko pitisi olla. Siihen me palaamme myhemmin. Mutta menepps pelaamaan
ja nyt kavereillesi minklaisia pelej osaisit jo tehd. =) lk
palaa takaisin ennenkuin tmn kappaleen asiat ovat hallussa. Sill niiden
osaamista luultavasti tullaan vaatimaan seuraavissakin luvuissa. Mutta jos
olet malttamaton, niin on tietenkin mahdollista palata takaisin opettelemaan,
mutta turhauttavaa se on.

Jlkikteen kaiken sprite, animaatio ja bittikarttanprilyn jlkeen totean,
ett kaikissa kohdissahan ei kytetty tsmlleen oikeita termej. Bittikart-
tahan on kytnnss vain kuvadata ja mahdollisesti hieman listietoa, ani-
maatio on yleens perkkisi bittikarttoja osaksi yhteisell datalla, 
olio on yleens sitten se mik osaa pyyhki itsens ja joka tiet mitk
bittikartat ja muut vastaavat sille kuuluvat, joka voi pyyhki itsens ja
tehd monia muitakin kivoja asioita. Sprite on sitten jotain siell jossain
vlill tai pss, en tied kovin tarkasti mutta kytin nyt tt nimityst
tysin toimivasta oliosta joka kykenee itsens ksittelyyn.

Nppimistn ksittely - ja nyt meill on hauskaa
-------------------------------------------------

Jos pelasit ahkerasti esimerkkipelimme, niin ehk huomasit, ett painaessasi
useita nappia ilmenee mys useita ongelmia. Nihin voivat kuulua nppimis-
tn jumiutuminen, nappien huomiotta jttminen jne. Tarvitsemme siis ru-
tiinin joka pstisi meidt plkhst. Tarvitsemme nppishandlerin!
Tm perustuu siihen, ett joka kerta kun nappia painetaan kutsutaan
keskeytyst 9, joka lukee merkin nppimistlt portista 60h (0x60) ja
muuntaa sen ASCII:ksi ja laittaa nppimistpuskuriin. Mutta meps ohi-
tammekin tmn ja teemme oman handlerin, joka ei muutakaan mitn miksi-
kn ASCII:ksi, vaan laittaa nppimisttaulukon vastaavan kohdan arvoon
1, josta peli voi sitten sen tarkistaa. Ja kun nappi pstetn tulee
mys keskeytys, tll kertaa tulee napin arvo + 128, joten vhennmme
luetusta arvosta 128 ja nollaamme vastaavan kohdan taulukosta. Ja millainen
on tm taulukko?

Taulukossa on 128 alkiota, yksi jokaiselle SCAN KOODILLE, jollaisia nppi-
mist syyt. Olen tehnyt nist numeroista kirjaston, jossa esimerkiksi
ESC-nppimen scan koodi on nimell SxESC ja sen arvo on 1. Jos siis haluat
pelisssi tiet onko ESC painettuna, osoitat nppimistpuskuriin:

if(keybuffer[SxESC]==1) printf("ESC painettu!\n");

Kirjasto on nimell D_SCAN.H. Ja sitten tarvitsemme siis koodia, joka lukee
tavun portista 60h ja jos se on alle 128 se laittaa vastaavan kohdan
taulukosta ykkseksi ja jos se on yli tai yhtsuuri kuin 128, niin laitamme
alkion tavu-128 nollaksi. Lopuksi lhetmme signaalin PIC:ille, ett kes-
keytyksemme on valmis, eli outtaamme tavun 20h porttiin 20h. Tllainen on
siis handlerimme (KEYBOARD.H):

void keyhandler() {
    register unsigned char tavu=inportb(0x60);

    if(tavu<128) keybuffer[tavu]=1;
    else keybuffer[tavu-128]=0;

    outportb(0x20, 0x20);
}

Tm onkin oikeastaan helpoin osa tehtvmme. Vaikeampi (joskin esimerkki-
koodin takia helppo) on koukuttaa tarvitsemamme nppimistkeskeytys ja
palauttaa se kun tarvitaan nppimistrutiineja (gets, getch...) tai pois-
tutaan ohjelmasta. Lisksi tarvitsemme joukon apumuuttujia, jotka ovat
tss:

volatile unsigned char keybuffer[128], installed;
_go32_dpmi_seginfo info, original;

Keybuffer sil nppinten tilat, installed kertoo onko tm handleri a-
sennettuna ja est samalla uudelleenasentamisen. Kaksi viimeist muuttujaa
info ja original ovat koukuttamiseen ja koukutuksen (hooking) poistamiseen
tarvittavia rakenteita, joista infoa kytetn oman asentamiseen ja origi-
naliin siltn alkup. handlerin osoite ja muut tarpeelliset tiedot.

Tss on koukutukseen ja palautukseen tarvittava koodi, johon emme perehdy
kovinkaan tarkasti, lisinfoa asiasta saat vaikka DJGPP:n FAQ:sta hakusanalla
handler:

void setkeyhandler() {
    int c;
    
    for(c=0; c<0x80; c++)
	napit[c]=0; /* nollataan napit */
    if(!installed) {
	_go32_dpmi_get_protected_mode_interrupt_vector(0x0009, &original);
	info.pm_offset=(unsigned long int)keyhandler;
	info.pm_selector=_my_cs();
	_go32_dpmi_allocate_iret_wrapper(&info);
	_go32_dpmi_set_protected_mode_interrupt_vector(0x0009, &info);
	installed=1;
	return 1;
    } else return 0;
}

int resetkeyhandler() {
    if(installed) {
	_go32_dpmi_set_protected_mode_interrupt_vector(0x0009, &original);
	installed=0;
	return 1;
    } else return 0;
}

Lismme kaikki kolme funktiota ja globaalit muuttujamme tiedostoon
KEYBOARD.H. Nyt meill on tarpeen vaatiessa tydellisen toimiva nppimis-
thandleri (jota ehk myhemmin tulemme kyttmn).


Fixed point matematiikka
------------------------

Alamme pikkuhiljaa lhesty kurssimme loppua (tai ken tiet, todellista
alkua?), joten ksittelen tss hieman pelin optimointiin vaikuttavia
tekijit ja parannuksia aiemmin esittelemiimme kirjastoihin (omaan peliin
kun kannattaa kuitenkin tehd osa kirjastoista uusiksi). Selitn fixed-
pointin, lookupin idean ja pari muuta nopeuttavaa temppua sek mainitsen
pullonkauloja joita nopeuttamalla saadaan aikaan dramaattisia muutoksia.

Siis fixed point, mit se on? Kuten tiedt, C:n int-tyyppi on kokonaisluku,
eli sill ei voi ilmoittaa desimaalilukuja. Monesti desimaaliluvu olisivat
tarpeellisia, esimerkiksi sprite-enginess, jos halutaan ett eri spritet
liikkuvat eri nopeuksilla. Nytt nimittin todella typerlt jos ohjus 
pomppii kymmenen pikseli eteenpin, koska se on 10 kertaa nopeampi kuin 
pelin hitain sprite. Tarvitsemme siis nopeudeksi desimaaliluvun, jolloin 
ohjuksen nopeus voisi olla 1 ja kilpikonnan 0.1 (jolloin se liikkuisi yhden
pikselin joka 10. frame). Valitettavasti float-tyyppisten muuttujien k-
sittely on moninkertaisesti hitaampaa (tosin pentium-optimoitu peli voi 
niit kytt, ainakin assemblerilla voidaan pentiumin matematiikkapro-
sessoria kytt tysipainoisesti ja peli nopeuttaa). Niinp meidn ty-
tyisi pysty esittmn kokonaisluvuilla desimaalilukuja. Onko tm mahdol-
listakaan? 

Kyll se on, katsokaamme hieman toisella tavalla normaaleja lukujamme.

Meidn luvuissamme on kokonaislukuosa ja desimaaliosa sek vliss piste. 
Kokonaislukuosalla voidaan ilmaista 10^<numeroja> lukua, eli jos 
kokonaislukuosassa on 3 numeroa niin voimme ilmaista sill 10^3=1000
erilaista lukua, vlill 0-999. Pisteen toisella puolella on kaikki muuten
samalla tavalla, mutta meidn tytyy ajatella knteisesti. Voimme ilmaista
desimaaliosalla desimaalin, joka on yksi 10^<numeroja>:sosa. Tm nytt
sekavalta, mutta oletetaan ett meill on 2-numeroinen desimaaliosa, niin
pienin desimaali on 1/10^2, eli yksi SADASOSA. Seuraava kaavio varmaan sel-
vent asiaa:

1234.123  =  1234 + 123/10^3  =  1234 + 123/1000  =  1234.123

Nyt menemme vhn pidemmlle. Oletetaan, ett meill olisi luvussa pilkku
AINA samalla kohdalla ja desimaalia esittvi lukuja 3. Takaisin voisimme
sen palauttaa vain jakamalla kokonaisluku tuhannella (kolme desimaalinumeroa,
eli siis 10^3=1000):

1234123 = 1234123/1000  =  1234.123

Kuten huomaat pilkku voidaan ajatella sinne nelosen ja ykksen vliin.
Nyt kysyt ehk ett mit hyty tst on. Siit on seuraava hyty: Meill
on kaksi lukua, 0.1 ja 5.4, jotka haluamme laskea yhteen. Muunnetaanpa ne
oikeaan muotoon: 0.1*1000=100 ja 5.4*1000=5400. Haluamme laskea ne yhteen:
100+5400 = 5500. Nyt muuntakaamme takaisin:

5500/1000 = 5.5 = 5.5 (5.4 + 0.1 = 5.5).

Eli meill on sama tulos! Vhennyslasku toimii ihan yht hyvin. Voimme las-
kea desimaalilukuja kokonaisluvuilla. Mutta tarvitsemme viel kaksi laskua,
kerto- ja jakolaskun. Koska lukumme ovat kummatkin 1000-kertaisia todelli-
suuteen nhden niin ne kertomalla saamme 1000000-kertaisen tuloksen, joten
lopuksi meidn tytyy jakaa tulos tuhannella. Eli:

5400*100 = 540000  =>  540000/1000 = 540  =>  540/1000 = 0.54
(5.4 * 0.1 = 0.54)

Ja tadaa! Meill onkin oikea tulos. Viel jakolasku, siinhn jaamme vain
numerot toisillamme, mutta tss hvi meilt desimaaliosa, eli meidn pi-
tisi kertoa tulos lopuksi tuhannella. Tarkemman tuloksen saamme kun 
kerromme ensin jaettavan tuhannella ja sitten vasta jaamme:

(5400*1000) / 100 = 54000  =>  54000/1000 = 54  (5.4 / 0.1 = 54).

Nyt meidn tytyy sitten syventy siihen miten toteutamme nopeasti edelliset 
asiat tietokoneen binrijrjestelmll. Se on erittin helppoa. Teemme 
vaikka 32-bittisen kokonaisluvun (unsigned int), josta 16 alinta bitti on 
varattu desimaaliosalle. Koska binrijrjestelm on 2-kantainen, niin 
meidn tytyy vain muuttaa pikku laskumme kahden potensseilla leikkimisiksi. 
Tllaisella luvulla voimme siis esitt 16-bittisen kokonaislukuosan, 
maksimissaan 2^16=65536 ja 16-bittisen desimaaliosan, joten pienin desimaali 
n 1/2^16 = 1/65536 = n. 0.000015228.

Entiset laskumme toimivat ihan hyvin, muunnamme vain luvut kertomalla ne
65536:ll ja palautamme jakamalla 65536:ll. Nopeuttamisessa apuna ovat 
viel bittisiirrot, joiden avulla voimme kertoa nopeasti 65536:lla 
siirtmll bittej 16 vasemmalle ja jakaa siirtmll niit oikealle. 
Tss on pieni esimerkkiohjelma, joka demonstroi fixedin kytt:

#include <stdio.h>

void main() {
    unsigned int a, b, tulos;

    a=(unsigned int)(5.4 * 65536.0);
    b=(unsigned int)(0.1 * 65536.0);

    tulos=a+b;
    printf("A+B=%f\n", tulos/65536.0);
    
    tulos=a-b;
    printf("A-B=%f\n", tulos/65536.0);
    
    tulos=(a*b)/65536;
    printf("A*B=%f\n", tulos/65536.0);
    
    tulos=(a/b)*65536;
    printf("A/B=%f\n", tulos/65536.0);
}                                  

Mieti nyt kaikkea ihan rauhassa. Jos luulet ymmrtneesi edes jotain niin
hyv, jos et ymmrtnyt mitn niin lue uudelleen ja uudelleen ja kokeile
paperilla. Jos et siltikn ymmrtnyt niin lue jostain toisesta dokumentis-
ta! Fixed-pointissa on huomattava pari asiaa:

1) Luvut voivat menn yli ja tulee ihmeellisi tuloksia. Jakolaskuesimerkis-
   sni en voinut kertoa a:ta ensin 65536:lla, sill muuten olisi luku men-
   nyt ympri. Kannattaa aina varmistaa ettei luku voi menn ympri.

2) Kyt bittioperaatioita aina kuin mahdollista. 32-bittisest
   16.16-fixedist (tarkoittaa, 16 bitti kokonais- ja 16 bitti desimaali-
   osalle) saat desimaaliosan halutessasi AND-funktiolla maskin 0xFFFF 
   kanssa. Voit kytt kaikkia nerokkaita optimointikikkoja jos vain kek-
   sit niit. Mys pyrhdyst voi kytt hyvksi (jotenkin).

3) Signed luvut toimivat samoin, mutta ylin bitti merkkaakin etumerkki,
   eli 16.16-luku int-tyyppin onkin oikeasti 15.16.

4) Valitse itse pilkun paikka. Mit enemmn bittej desimaaleille sit tar-
   kempia lukuja. Mit enemmn bittej kokonaisluvuille sit suurempia ja
   eptarkempia lukuja.


Lookup-tablet ja muita optimointivinkkej
-----------------------------------------

Lookup-tableissa, eli lookupeissa ei ole oikeastaan muuta selittmist, kuin
ett niiss toistuvia, vain yht (tai joskus kahtakin) muuttujaa kyttvis-
s monimutkaisissa laskutoimituksissa (tai muuten vain hidastavissa) 
lasketaan tulokset etukteen taulukkoon kytten indeksin sit lukua joka
oli muuttuvana laskutoimituksessa. Thn ky esimerkkin sinin laskeminen
taulukkoon. Sin-funktio on hidas laskea ja siin pit aina suorittaa pitk
konversio asteista radiaaneiksi (3.14*2*aste/256, 256:n ollessa suurin 
kulma + 1, 360-asteisella ympyrll luku olisi 360 ja suurin kulma 359) ja 
lopuksi viel ottaa siit sini. Nyt laskemmekin kaikki 256 arvoa taulukkoon 
(fixed-point-sellaiseen, muoto 1.14, 16-bittinen signed, muuntoluku 16384):

for(c=0; c<256; c++) 
    sin_table[c] = (short)(sin(3.141592654*2*c/256.0)*16384);

Nyt jos haluamme kulman 15 sinin, niin osoitamme vain sin_table[15], emmek
(short)(sin(3.141592654*2* 15 /256.0)*16384).

Sitten sekalaisia optimointivinkkej:

1) Suuria mri dataa ksittelevt loopit assemblerilla. Lis tietoa as-
   semblerin kytst DJGPP:ll tiedostosta DJTUT2_4.ZIP, vaikka MBnetist.

2) Kaikki muuttumattomat vertailulausekkeet loopin ulkopuolelle:
   for(c=0; c<1000000; c++) if(a==b) puskuri[c]=0;     onkin:
   if(a==b) for(c=0; c<1000000; c++) puskuri[c]=0;
   Vhennmme nin 1000000 vertailua.

3) l tuhlaa aikaasi optimoimalla suuria mri logiikkaa, ellei siit
   todella ole hyty.

4) Kyt fixedi floatin tilalla aina kuin mahdollista.

5) Laske kaikki toistuva konemainen laskenta taulukkoihin.

6) Kyt DJGPP:n knnsvalitsinta -O2, tai jopa -O3 (joka kyll suurentaa
   ohjelmaasi reilusti).

Yleenskin kannattaa uhrata paljon aikaa grafiikkakirjastojen ja nikirjas-
tojen optimointiin ja pit itse runko selken C-kielisen kutsujen joukko-
na. Tm ei paljoa hidasta ja selvent uskomattomasti koodia ja nopeuttaa
kehityst.


Vliaikatulokset ja fontteja
----------------------------

Tss vaiheessa osaat nyt kaikki trkeimmt niksit mit peliohjelmointiin
tarvitaan. Tst luvusta lhtien alan tietoisesti vhentmn, ellen
jopa joissain kohdissa poistamaan esimerkkiohjelmia. Mit tst lhtien
tarvitset on maalaisjrke ja kyky osata soveltaa oppimiasi asioita.

Eli tnn meill on siis jotain, mit kutsutaan nimell fontit? Idea fon-
tienginen teossa on tehd tavallaan karsittu bittikarttaengine. Fontti-
enginen voit tehd esimerkiksi poistamalla sprite-koodistamme pyyhkimisen
(halutessasi voit mys poistaa lpinkyvyyden tai jtt pyyhkimisen jos
tarvitset sit, sinun pit siin tapauksessa vain tehd erikoisjrjeste-
lyj) ja kytt animaationa kuvasarjaa jossa on piirrettyn merkit a-z,
A-Z, 0-9 ja sitten joitakin mahdollisesti tarvittavia vlimerkkej, kuten
.!?,;:'" ja muut vastaavat. Sitten vain teet funktion, joka vaihtaa framek-
si oikean kuvan ja piirt sen, jonka jlkeen se korottaa x-arvoa merkin
leveydell (plus jonkin verran vli seuraavan merkin ja viimeisen vlille)
ja ottaa ksittelyyn seuraavan merkkijonon merkin.

Koodi voisi nytt vaikka tlt:

void printString(char *string, int x, int y) {
    int c;

    for(c=0; c<strlen(string); c++ {
        if(string[c]>'a' && string[c]<'z') {
            setframe(string[c]-'a'); /* a olisi frame 0 */
            drawchar(x+c*9, y); /* merkin leveys 8 + 1 pikseli erottamaan */
        } else if(string[c]>'A' && string[c]<'Z') {
            setframe(string[c]-'A' + 'z'-'a' + 1);
            /* eli suomeksi A-kirjaimella olisi paikka heti viimeisen pienen
               kirjaimen jlkeen, joka on 'z'-'a' */
            drawchar(x+c*9, y);
        } else if(string[c]>'0' && string[c]<'9') {
            setframe(string[c]-'0' + 'z'-'a' + 1 + 'Z'-'A' + 1);
            /* tm taas tulee pienien JA isojen kirjaimien jlkeen */
            drawchar(x+c*9, y);
        } else if(c == '.') { // jos c on erikoismerkki
            setframe('9'-'0' + 1 + 'z'-'a' + 1 + 'Z'-'A' + 1);
            drawchar(x+c*9, y);
            /* ideana siis, ett piste tulee kaikkien kirjainten ja
               numeroiden jlkeen */
        }
        ...
            
    }
}

Kuten ehk huomasit tuli koodista aivan kammottavaa sekasotkua ja on ihme
jos sait siit jotain selv. Lisksi koodi ei ole erityisen nopeaakaan,
saati sitten ett se edes vlttmtt toimii. Mutta miten voisimme nopeuttaa
tt? Vastaus on lookup-tablet. Sill mehn tiedmme, ett C:ll kirjain on
vain numero vlill 0-255. Niinp teemme taulukon jonka jokainen alkio
osoittaa indeksin mukaisen ASCII-kirjaimen framenumeroon. Jos et ymmrtnyt
niin tss on esimerkki taulukon kytst:

frame = asciitaulukko['a'];

Asciitaulukon alkio 'a' (numerona 97) olisi 0, joten framenumeroksi tulisi
ninollen tm luku. Sitten vain framenvaihto: "setframe(frame)".
Tietenkin tuo kannattaisi kytt nin: "setframe(asciitaulukko['a'])"...

Mutta miten sitten taulukko alustetaan? Tapoja on monia, jotkin ovat seka-
vampia ja jotkin vhn selvempi, mutta annan sinun itsesi ptt mik on
paras. Mahdollisuutena olisi ensin tytt taulukko nollalla (joka olisi
tyhj frame) ja sitten loopata aakkoset a-z tytten taulukon kohdat 'a'-'z'
oikeilla framearvoilla (1...26), sitten loopataan 'A'-'Z' tytten ne alkiol-
la 27...52 jne. Mys lataaminen kannattaa automatisoida.

Muista lisksi huomioonottaa erikoismerkit enginesssi. Tarpeellisia voivat
olla vlilynti (32), rivinvaihto (\n), tabulaattori (\t) jne. Ja lisksi
saat aivan vapaasti ptt onko fontin vri mahdollista vaihtaa vai kytt-
k aina samanlaisia fontteja, joka mahdollistaa vhn hienommat, vaikka moni-
vriset fontit.


Maskatut spritet
----------------

Vhn aikaa sitten kerroin PC-Ohjelmointi -alueella tmn kurssin sisllst
ja eiks vain joku mennyt kysymn minulta selittik tutoriaali maskatut
vai maskaamattomat spritet. Minhn en ollut edes kuullut moisesta asiasta
ja utelin ideaa sen takana. Sainkin kuulla se ja tein sen pohjalta assemb-
lerilla nopean rutiinin. Pienell nopeuskokeella se osoittautui 11 kertaa
nopeammaksi kuin muutama luku sitten tekemmme rutiini. Aion nyt selitt
idean tmn tekniikan takana, joten kiinnittk turvavynne ja valmistau-
tukaa!

Maskatuiden spritejen ideana on se, ett niiden piirrossa ei tarvita pikse-
likohtaisia vertailulauseita lainkaan, jolloin voidaan kytt assembleril-
la neljn tavun kanssa operoivia funktioita. Mutta miten sitten kierrmme
vertailulausekkeet silytten silti lpinkyvyyden nollavrin kanssa?
Idea perustuu bittioperaattoreihin.

Jokaiselle spriten framelle tehdn etukteen maski, joka on nolla kohdissa
joissa on pikseli ja 255 lpinkyviss kohdissa. Nyt sitten vain suoritamme
kaksoispuskurin pikselille loogisen AND-operaation:

Maski spritelle  FF 00 FF FF
Nytt           4F 3C 93 5A
----------------------------
Tulos            4F 00 93 5A

Kuten huomaatte, jvt lpinkyvt kohdat (FF) jljelle. Sitten vain
kytmme OR-operaattoria sytyttmn spriten pikselit, sill ne kohdat
ovat juuri sken nollautuneet, joten looginen OR asettaa juuri oikeat
bitit:

Sprite           00 46 00 00
Maskattu nytt  4F 00 93 5A
----------------------------
Tulos            4F 46 93 5A

Lopun saat toteuttaa aivan itse. Huomattavaa tss on se, ett jos haluat
kytt tehokkaita 4 tavun (dword) operaatioita on bittikartan leveyden
oltava jaollinen neljll. Huipputehoon tarvitset assembleria, sill C:ll
on vaikea kontrolloida edell mainittuja asioita. Jos et viel osaa assemb-
leria, varsinkaan DJGPP:n AT&T syntaksia, suosittelen seuraavia tiedostoja:

ASSYT.ZIP     Assemblerin alkeet suomeksi.
PCGPE10.ZIP   PCGPE sislt kaiken muun lisksi assemblytutoriaalin.
DJTUT2_4.ZIP  Jos osaat Intel-syntaksin, muttet AT&T-syntaksia
              (movw %eax, %ebx). Sislt mys muuta kiinnostavaa
              materiaalia, jota tsskin tutoriaalissa on sivuttu.
NASM91.ZIP    Tll voit tehd Intel-syntaksin assemblerilla DJGPP:n
              COFF-muotoisia objektitiedostoja. Tiivistettyn TASM joka
              osaa myskin DJGPP:n objektiformaatin.

Lisksi voisi olla hyv idea lainata kirjastosta kirja 486-ohjelmointi,
joka on suomenkielinen assembler-ohjelmointia ksittelev kirja ja kaiken
lisksi hyv sellainen!

Loppulisyksen jlleen kiva vinkki Pekka Nurmiselta. Kaksoispuskuri 
kannattaa tarvittaessa tehd sen verran levemmksi, ett jos spitea
ei saada katki juuri neljn tavun kohdalta ei tuo tule toisesta reunasta
vastaan. Eli jtt sinne nelj tavua ruudun reunoihin, jota ei vain 
sitten kopioida nytlle. Nin kaksoispuskurin kooksi tulisi 328x200.


Hiirulainen, jokanrtin oma lemmikki
------------------------------------

Tnn, tytt ja pojat, set puhuu hieman kotielimist. Ne ovat sellaisia
pieni valkoisia tkit, joilla on hnt ja jotka viipottavat matolla.
Sen lisksi niit voi mys painella. Ei, nyt ei ole kyse mistn karvaisesta,
vaan ihan aidosta tietokoneen lislaitteesta, jota hiireksikin kutsutaan.

Tll karvattomalla ystvllmme on sdyttmn monia haaroja sukupuussaan.
Lytyy Logitechia, Microsoftia, Targaa ja ties mit vimputinta ja kaiken
kukkuraksi rautatasolla kskyttminenkin on suorastaan sdyttmn
epstandardia. Onneksi htiin rient kymmenisen vuotta vanha apu nimel-
tn _hiirikeskeytys_, kiinnostavemmin ilmaistuna keskeytys 33h. Tt
keskeytyst kytten saadaan kaikken hiireen tungettujen vimpainten, kuten
nappien ja pohjassa (yleens) pyrivn pallukan tila. Nm tiedot ovat helpon
saatavuuden lisksi mys naurettavan helppokyttisi, kunhan vain tiet
miten niit kytt.

Jos et viel tied miten keskeytyksi kytetn tulee tss tiivistettyn
niiden kytt DJGPP:ll. Keskeytykselle annetaan parametrit rekistereiss
ja ne saadaan rekistereiss. Jos DJGPP oli yht huoleton kuin Borland
Turbo-kntjineen olisi meillkin rekisteri ax nimell _AX jne. Mutta koska
kaikki on tehty rakkaalla kntjllmme hipun vaikeammaksi teemme sen
standardilla tavalla. Alhaalla net tarvittavat askeleen keskeytyksen kut-
sumiseksi ja rekisterien nplykseksi. Esimerkki kytt yht kymmenist kes-
keytyksen aiheuttavista funktiosta int86(...) kirjastosta dos.h:

1) Tarvitset rekisterit muuttujinaan sisltvn structin, int86:n tapauksessa
   structi on nimeltn REGS ja sen sisll on pari muuta structia joihin
   tutustut vaikka selaamalla ko. kirjastoa. En ala perehtymn syvemmin
   nihin x, d ja w-rakenteisiin. Tss kuitenkin kytmme viimeist, joka
   on 16-bittiset rekisterit.

   struct REGS rekisterit;

2) Tunge kaikki parametrit uuteen muuttujaasi.

   rekisterit.w.ax=jotain;
   rekisterit.w.di=muuta;
   rekisterit.w.cs=kivaa;

3) Kutsu funktiota int86(vektori, inputti rekisterit, outputti rekisterit)

   int86( keskeytys, &rekisterit, &rekisterit );

4) Kaivele esiin muuttuneet rekisterisi ja tallenna ne muuttujiin.

   ihan=rekisterit.w.bx;
   helppo=rekisterit.w.ds;
   homma!=rekisterit.w.cx;

Tehdesssi hiiriohjattua ohjelmaa sinun pit tietysti hiiren koordinaattien
ja nappien ksittelyn lisksi piirt kursori ruudulle, ellet sitten halua
kytt (amatrimisen nkist) kursoria, jonka ajuri piirtelee ruudullesi.
Grafiikkatilassa tm onnistuu vaikka tekemll hiirest yksi spriteist ja
liikuttelemalla sit. Antaa paljon paremman kuvan ohjelman tekijstkin!
Tekstitilassa vaihdat vaikka ko. kohdan vri. Thn ihmeelliseen tilaan
tutustumme kohtapuolin, eli jatka lukemistasi jos haluat tehd tekstitila-
ohjelman, joka kytt kursoria...

Tss nyt olisivat nm kaikkein kytnnllisimmt ja alkuun auttavat funk-
tiot. Lis lydt vaikkapas Ralph Brownin interruptilistasta tai kenties
jopa HelpPC:st. RP:n lista on MBnetiss nimell INTERxxy.ZIP, jossa xx on
versionumero (kai 48 tarkoittaen 4.8:aa) ja y paketin numero, itse listassa
A-E tjsp. ja muitakin kirjaimia on sislten muunmuassa selailuohjelman,
konvertoinnin Windowsin help-muotoon jne.. Mutta, kuten lupasin:

Funktio 0 - Hiiren alustus
  Parametrit: AX=0
  Palauttaa:  AX=0 jos ajuria ei ole installoitu, FFFFh jos on installoitu.

Funktio 1 - Nyt kursori (se kauhea siis)
  Parametrit: AX=1
  Palauttaa:  -

Funktio 2 - Piilota kursori (se kauhea siis)
  Parametrit: AX=2
  Palauttaa:  -

Funktio 3 - Anna koordinaatit ja nappien tila
  Parametrit: AX=3
  Palauttaa:  CX=x-koordinaatti (0...639)
              DX=y-koordinaatti (0...199)
              BX=nappien tila (bitti 0 vasen nappi, bitti 1 oikea ja
                               bitti 2 keskimminen nappi)

Funktio 4 - Aseta kursorin koordinaatit
  Parametrit: AX=4, CX=x-koordinaatti, DX=y-koordinaatti
  Palauttaa:  -

Funktio 5 - Nappien painallukset
  Parametrit: AX=5,
              BX=mik nappi (0 vasen, 1 oikea ja 2 keskimminen)
  Palauttaa:  Muuten kuten funktio 3, mutta koordinaatit kertovat kursorin
              sijainnin viime painalluksella ja BX kertoo ko. napin painal-
              luksien mrn sitten viime kutsun.

Funktio 6 - Nappien vapautukset
  Parametrit: AX=6,
              BX=mik nappi (0 vasen, 1 oikea ja 2 keskimminen)
  Palauttaa:  Muuten kuten funktio 5, mutta vapautuksen tiedot.

Funktio 7 - Vaakarajoitukset
  Parametrit: AX=7,
              CX=pienin sallittu X-sijainti,
              DX=suurin sallittu X-sijainti
  Palauttaa:  -

Funktio 8 - Pystyrajoitukset
  Parametrit: AX=8,
              CX=pienin sallittu Y-sijainti,
              DX=suurin sallittu Y-sijainti
  Palauttaa:  -

Funktio B - Liikemr
  Parametrit: AX=B
  Palauttaa:  CX=vaakamikkien mr
              DX=pystymikkien mr

Funktio F - Mikkej pikseli kohden
  Parametrit: AX=F
              CX=vaakamikkien mr
              DX=pystymikkien mr
  Palauttaa:  -

Lisksi on viel ainakin funktio C, joka asettaa oman ksittelijn, mutta
koska se ei luultavasti kiinnosta kovin monta (rm-osoitetta odottava ksit-
telij ei ehk oikein toimi PM:ss kunnolla jne...) jtn sen tss vliin.
Sitten vain tekemn kaiken maailman testiohjelmia. Esimerkkej ei tule
tss lainkaan, sill oletan jokaisen pystyvn edellisten ohjeiden perusteel-
la kyhmn itsen tyydyttvn ohjelman.

Jos homma ei kuitenkaan ota luonnistuakseen tai tss kappaleessa oli muita
epselvyyksi niin otahan yhteytt niin kaivelen lis tietoa aiheesta.
Erityiskiitos tmn kappaleen teon auttamisesta kuuluu nyt kyll MB:n numerol-
le 4/96 josta katsoin nopeasti tiivistelmn hiirifunktioista.

Ja ensi kappaleessa onkin uudet kujeet, nyttisi olevan tekstitilan hallinta
seuraavana edess...


Tekstitilan ksittely suoraan
-----------------------------

Tst kappaleesta tulee tulemaan rimmisen lyhyt. Ainoa meit kiinnostava
seikkahan on tekstimuistin osoite (tila 3, 80x25, mys muut voivat toimia)
ja rakenne. Osoite on perusmuistin segmentti B800h, eli lineearinen osoite
selektorin _dos_ds osoittamassa muistissa olisi C:ll 0xB8000. Rakenne
on mys naurettavan yksinkertainen. Erona VGA:han (ks. kappale
"Grafiikkaa - mit se on?" jos et muista) on vain se, ett yksi alkio
koostuu kahdesta tavusta (joista ensimminen on merkin ASCII ja toinen
merkin vri) ja ruudun leveys on 80 merkki. Jos ei mennyt phn niin
tutustu viel kerran VGA:ta ksittelevn kappaleeseen ja tutkaile seuraavia
makroja:

#define putchar(x, y, c) _farpokeb(_dos_ds, 0xB8000+(y*80+x)*2, c);
#define putcolor(x, y, c) _farpokeb(_dos_ds, 0xB8000+(y*80+x)*2+1, c);

Viel jos olit kiinnostunut hiiren kursorin tekemisest tekstitilaan voisi
seuraava funktio olla sinulle omiaan:

void inline addcolor(int x, int y, char c) {
    int originalc=_farpeekb(_dos_ds, 0xB8000+(y*80+x)*2+1);
    putcolor(x, y, originalc+c);
}

Sitten vain "piirrt" kursorin lismll vriarvoon - sanotaan vaikka 17
ja pyyhit kursorin lismll siihen saman arvon vastaluvun (-17), eli
toisinsanoen vhennt siit 17:

#define CShow(x, y, c) addcolor(x, y, c)
#define CHide(x, y, c) addcolor(x, y, -c)

Makrojen kytt sitten komennoilla "CShow(17)" ja "CHide(17)"...

Lopuksi viel sananen merkin vrin muodosta. Se on XYYYZZZZ, jossa jokainen
kirjain edustaa yht bitti vritavussa. X ilmaisee vilkkuuko merkki (1).
YYY ilmaisee taustan vrin (0-7) ja ZZZZ ilmaisee tekstin vrin (0-15).
Tss viel pikkuruinen makro, joka voi osoittautua hydylliseksi:

#define BuildC(blink, fore, back) ( (blink<<7) + (back<<4) + (fore) )

Sitten vain vaikka komento "putcolor(x, y, BuildC(0,15,1))", joka aiheuttaisi
vlkkymttmn valkoisen tekstin sinisell pohjalla (31).

Sellaista tll kertaa. Nyt painun suihkuun ja katsomaan X-Filesia. Jatketaan
taas vaikka huomenna!


Projektien hallinta - useat tiedostot
-------------------------------------

Nyt seuraakin sitten jakso lukuja (tai yksi luku, katsotaan nyt),
joissa ksitelln kaikkea trke mit pelej ohjelmoidessa pit
osata sen hardwaren tuntemuksen lisksi. Tarkoituksena on kyd lpi
useiden c-tiedostojen kytt, headerien teko, Rhiden projektit,
makefileet, ulkoisen assyn ja assyn yleenskin kytt, engineiden
teko, kirjastojen luonti. Kaikki suhteellisen kevytt kamaa kun ne
vain kerran opettelee, joten aloitamme.

Thn asti olen opettanut teille huonoja tapoja joita itsellni oli
tapana kytt viel puolitoista vuotta sitten (ja vasta viime aikoina
olen pssyt lopullesesti niist eroon). Olen nimittin laittanut
koodia noihin .h-tiedoistoihin ja tehnyt niist kirjastoja, joiden
rutiineja on sitten helppo kytt. Laajempien projektien ja miksei
hieman suppeampienkin kanssa alkaa kuitenkin ennenpitk esiinty
suorastaan rsyttvn hidasta kntmist. Ajattele seuraavaa
tapausta:

Peliprojektissa on niengine sound.h (yksinkertainen, vain vhn alle
3000 rivi), sprite-engine sprite.hh (minimaalinen toiminta, hieman
inline-assy, 800 rivi), sekalaisia hardware-rutiineja
(kellokeskeytys, nppishandleri jne. 1000 rivi) sek itse pelin
koodia 2000 rivi. Nin joka kerta knnmme vhn alle 7000 rivi
C-koodia. Mutta miksi knt kaikki joka kerta kun vain yksi muuttuu
yleens kerrallaan? Muuttakaamme hieman lhestymistapaa lytksemme
parempi keino.

Keinoa kutsutaan projekteiksi, usean C-tiedoston kytksi ja ties
miksi. Ideana on, ett jokainen looginen kokonaisuus on jaettu omaan
.c-tiedostoonsa ja .h-tiedostoonsa. Tllaisia voisivat olla
nppishandleri, timerhandleri, sprite-rutiinit, modien lataus,
nienginen ohjelmointirajapinta, sb-osa koodista, gus-osa koodista
jne.. Jokaiselle tiedostolle olisi sitten oma .h-tiedostonsa, jossa
mritelln kaikki c-tiedoston funktiot ja globaalit muuttujat (jos
niit tarvitaan). Sitten toiset c-tiedostot jotka tarvitsevat tuon
tiedoston funktiota tai muuttujia ottaisivat vain includella
h-tiedoston mukaan ja kntjn linkkeri huolehtisi siit, ett
ohjelmakutsut menevt oikeisiin osoitteisiinsa.

Katsotaanpas pient esimerkki h-tiedostoa ja c-tiedostoa. En vit
tmn olevan ainoa oikea tapa, tm on vain yksi tapa hoitaa homma:

ESIM.H:

#ifndef __ESIM_H
#define __ESIM_H

#include <stdio.h>

#define ESIMTEKSTI "Moikka, olen esimerkki!"

void Esimteksti();

extern int Kutsukertoja;

#endif


ESIM.C:

#include "esim.h"

int Kutsukertoja=0;

int Oma=666;

void Esimteksti() {
    puts(ESIMTEKSTI);
    Kutsukertoja++;
}


Lhdetnps askeltamaan ESIM.H-tiedostoamme lvitse. Ensimmisen
rivi #ifndef __ESIM_H, joka ilmoittaa C-koodin esiksittelijlle, ett
jos __ESIM_H ei ole mritelty (IF Not DEFined, IFNDEF) niin osio
#ifndef:in ja #endif:in vliss tulee ottaa mukaan. Sen jlkeen
mritelln tuo kyseinen muuttuja, jotta H-tiedostoa ei pureta
kahteen kertaan (voi sattua kaikkea hassua jos vaikka h-tiedostot
kutsuvat toisiaan). Sitten tulee tmn C-tiedoston tarvitsemien
funktioiden kirjastot ja #definet (kirjastot voitaisiin sijoittaa mys
C-tiedostoon, mutta joskus tst tulee ongelmia, jos kytetn makroja
tai muuta vastaavaa). 

Sitten tulevat muuttujat ja funktiot. Muuttujien eteen TULEE laittaa
extern-mre, joka kertoo ett ne on oikeasti mritelty jossain
muualla, jottei kntj varaa muistia nille joka H-tiedoston
includettamisen kohdalla, jolloin linkatessa useissa C-tiedostoissa on
varattu muistia samannimiselle globaalille muuttujalle -> ongelmia.
Funktioiden edess extern ei ole pakollinen ja sen voikin jtt pois
ja list extern-mreen jos ko. funktio on ulkoisessa
assembler-tiedostossa.

Funktion parametrien nimet voi halutessa jtt mrittelyist pois,
mutta se ei ole suositeltavaa. Muista mys, ett globaalit muuttujat
esitelln ja alustetaan VAIN ja AINOASTAAN C-tiedostossa, ei
H-tiedossa!

C-tiedosto sislt vastaavat H-tiedostossa "luvatut" funktiot ja
muuttujat. Jos haluat tehd globaaleja muuttujia jotka eivt ny
muihin C-tiedostoihin, niin jtt sen esittelyn H-tiedostosta pois,
jolloin headerin sisllyttvt muut C-tiedostot eivt tied mitn
ko. muuttujan olemassaolosta eik vahingossa tule virheit. Tllainen
on esimerkki C-tiedoston muuttuja Oma.

Useita C-tiedostoja kyttesssi teet siis jokaisesta loogisesta
kokonaisuudesta oman "paketin", joka sislt C-tiedoston, joka on
toimiva kokonaisuutensa ja H-tiedoston, joka tarjoaa muille
C-tiedostoille mahdollisuuden kytt tmn paketin rutiineja.
Muista, ett kyttesssi includea tuollaisen tiedoston kohdalla
kytetn heittomerkkej normaalin <>-parin sijasta, jottei kntj
lhde hakemaan ESIM.H:ta omasta include-hakemistostaan, vaan jotta se
hakisi tiedoston senhetkisest tyskentelyhakemistosta.

Mieti nyt nm asiat selviksi, jotta ymmrrt miten tehdn useita
tiedostoja ja kytetn ilman ongelmia, niin voit sen jlkeen jatkaa
seuraavaan lukuun, jossa kerrotaan miten niist muodostetaan ajettavia
ohjelmia, kirjastoja ja objektitiedostoja.


Useiden tiedostojen projektit - kntminen ja hallinta
-------------------------------------------------------

No niin, osaat nyt tehd C-tiedostoja ja H-tiedostoja, mutta sill ei
varmaankaan pitklle ptkit. Lhdemme nyt tutkimaan hieman
kntjmme, GCC:n sielunelm ja tutustumme muutamaan elintrken
tietoon joita ilman ei voi edes el. Nimittin janoamme tietoa
formaateista.

Tiedostot joiden kanssa pyrimme DJGPP:n kanssa voidaan jakaa helposti
pelkisten neljn (4) kategoriaan. Tss ne ovat:

1. Lhdekooditiedostot (c, cc, s, asm). Kntj muuttaa koodin
   konekieleksi ja tekee muut tarvittavat tehtvt tuottaen
   objektitiedoston.

2. Objektitiedosto (O). Sislt koodin ja symboleja (eli funktioiden
   ja muuttujien nimi) ja kaikkea muuta kivaa infoa jotka liittyvt
   olennaisesti rutiinien kskyihin ja dataan. Linkkeri linkkaa kaikki
   objektitiedostot yhteen ja lis tarvittavaa kynnistyskoodia sun
   muuta luodakseen ajettavan tiedoston. Nm ovat ernlaisia
   rakennuspalikoita, joissa kaikki on jo binrimuodossa.

3. Archive (A). Tt voidaan halutessa kytt useiden objektien
   silmiseen, eli paketoidaan monta objektitiedostoa yhteen kasaan
   jotka voidaan liitt sitten yhten pakettina
   kntjlle. Objekteista siis kootaan nippu jota voidaan ksitell
   yhten kokonaisuutena.

4. Ajettava tiedosto. Sislt objektitiedostoista tehdyn EXE:n, jossa
   on lisksi tarvittava koodi ohjelman kynnistmiseen.

GCC:n toimintaperiaate EXE:n knnss on seuraava: Lhdetn
kntmll lhdekooditiedostot objektitiedostoiksi. Tss vaiheessa
siis laajennetaan makrot, includet ja esiksittelijn komennot (kaikki
#ifndef-rakenteet sun muut). Sitten knnetn koodi konekielelle ja
tehdn objektitiedostot. 

Seuraavaksi kutsutaan linkkeri joka liitt objektitiedostot yhteen ja
lis tarvittavat kirjastot (LIBC.A tulee EXE:en aina mukaan ja
lisksi muut -l<nimi> parametreill annetut kirjastot) sek
aloituskoodin, joka kutsuu main-funktiota, jonka oletetaan lytyvn
jostain O-tiedostosta.

Itseasiassa tuo ei mene aivan noin yksinkertaisesti, mutta trkeint
on ymmrt, ett lhdekoodista tehdn rakennuspalikoita,
objektitiedostoja joista voidaan myhemmin koota ajettavia tiedostoja.

Jos meill siis olisi C-tiedostot main.c ja apu.c (mahdollisesti
vastaavine H-tiedostoineen), joista main.c sisltisi main-funktion ja
pkoodin ja apu.c kaikkia tarpeellisia rutiineja, niin voisimme
knt ne objektitiedostoiksi ja aina kun jompaakumpaa muunnetaan,
niin kntisimme tmn lhdekooditiedoston uudelleen. EXE
muodostettaisiin erikseen toisella komennolla jolloin muutos toisessa
tiedostossa vhentisi knnettvn koodin mr (tosin linkkausty
pysyisi ennallaan).

Miten sitten nit erilaisia tiedostoja tehdn? Hyv kysymys. Alla
nette kaikkein komentoja objektitiedostojen, EXE:jen ja archivejen
luontiin, lhdekoodit osaatte varmaan jo. =)

Objektitiedosto GCC:ll:
  gcc -c koodi.c -o objekti.o (halutessa lhdetiedostoja voi olla useampia)

Archive-tiedosto objektitiedostoista:
  ar rs archive.a objekti1.o ... (kaikki halutut objektit vain pern)

Ajettava tiedosto archive-, objekti- ja lhdekooditiedostoista (GCC
osaa ksitell ne ptteiden mukaan):
  gcc <tiedostot> -o tulos.exe <parametrit>

Lis infoa sit haluaville lytyy englanninkielisen komennolla
INFO. Sit lytyy aika paljon enk todellakaan halua tst
tutoriaalista mitn DJGPP:n komentoriviparametrien selityst. =)

Eli kerrataan viel vaiheet joita kyttte "oikeaoppisen" projektin
tekoon:

1. Luo C- ja H-tiedostot ja muu tarvittava lhdekoodi
2. Knn ne O-tiedostoiksi (tyyliin gcc -c koodi.c -o objekti.o)
3. Jos haluat tehd kirjastoja, niin tee objektitiedostoista ar:ll
   niit. Esimerkiksi grafiikkaenginen objektitiedostot voisi liitt
   yhteen ja nimet libgraf.a:ksi ja siirt DJGPP:n LIB-hakemistoon.
   Myhemmin nuo enginen objektit olisi helppo list EXE:een pelkll
   -lgraf -parametrilla.
4. Knn ajettava ohjelma objektitiedostoista ja archive-tiedostoista
   (gcc <tiedostot> -o tulos.exe <parametrit>). Archive-tiedoston
   nimen voi antaa joko tiedostojen mukana tai parametrin -l<nimi>
   JOS archive on DJGPP:n LIB-hakemmistossa nimell lib<nimi>.a.

Grafiikkaenginekin voi olla projekti, jolloin jttte EXE:ksi
kntmisen kokonaan pois, ja teette vain archive-tiedoston. Tai jos
tarvit vain yhden .o -tiedoston, niin miks siin, valinta on vapaa.

Nyt sinun pitisi osata tehd objektitiedostoja lhdekoodista,
kirjastotiedostoja objekteista ja ajettava ohjelma objekteista (ja
mahdollisesti mys kirjastoista). Kun hallitset nm asiat jatkamme
jlleen taivaltamme.


Hieman automaatiota - tapaus Rhide
----------------------------------

No tll hetkell me osaamme kaikki tarvittavat taidot komentorivilt,
mutta uusien tiedostojen nimien muistaminen ei aina ole kivaa ja
komentorivill vntminen sopii vain perusteiden harjoitteluun. Rhide
on tapa pst koko roskasta helpolla ilman perusteita edes
objektitiedostoista, mutta koska teill tulee olemaan niin paljon
helpompaa kun ne osaatte niin olen katsonut tarpeelliseksi ne mys
neuvoa. (sill Rhidenkin kanssa kunnon projekteilla tarvitaan tuota
osaamista).

Ainahan psee helpolla, mutta valitettava tosiasia on, ett se joka
hyppsi edelliset kappaleet ylitse onkin sormi suussa kun tulee
ongelma eteen. Mikn ei korvaa tietoa ja kokemusta, ei edes hyv
ohjelmointivline.

Eli tmn kappaleen tarjoama informaatio ksittelee Rhide ja sen
projekteja projektien hallinnassa. Jos teit ei Rhide kiinnosta niin
voitte hypt yli, lupaan ett seuraava kappale kiinnostaa teit,
sill makefilejen kytt on vaihtoehtoinen (ja gurumpi, elegantimpi ja
yleisempikin) tapa automatisoida projektien kntminen. Mutta te
joita kiinnostaa yksi tmn hetken parhaimmista DOS-ympristn
IDE-ohjelmista pysyk kappaleessa, tosin asia voi olla joillekin jo
vanhaa leip.

Eli Rhiden sislt makefileiden kaltaisen jrjestelmn projektien
hallintaan, mutta toisin kuin make se sislt tekoly, joka osaa
projektille valitusta kohteesta ptell millainen tulos halutaan ja
projektin tiedostojen ptteist minktyyppinen tiedosto on kyseess
ja miten se pit knt. Koska Rhide on aika yksinkertainen
jrjestelm ksittelen vain lyhyesti sen perusasiat, eli projektien
teon, availun, ksittelyn, Rhiden kustomoinnin ja kohteiden
mrmisen.

Eli aloittakaamme tekemll oletusprojekti Rhidelle. Ensimminen
tehtvsi lienee installoida Rhide, joka yleens koostuu purkamisesta
DJGPP-hakemistoon ja ohjelman kynnistmisest kokeeksi. Dokumenttien
lukeminenkaan ei ole pahasta, mutta kyll ilmankin voi prjt, tosin
vaikeuksien sattuessa ne ovat usein korvaamattomia. Rhiden jotkin
versiot ovat olleet enemmn tai vhemmn bugisia, mutta ainakin
versiot 1.1 (bugikorjattuna!), 1.2 ja 1.3 ovat toimineet minulla hyvin,
joten joko Altavistaan hakusanalla Rhide, MBnettiin tai MB:n
H&H-rompulle.

Sitten kun Rhide toimii niin menette DJGPP:n BIN-hakemistoon ja
kirjoitatte "rhide rhide". Tm tarkoitus on luoda/muuttaa
BIN-hakemistossa olevaa rhide-nimist projektia, jonka asetukset
ladataan AINA kun rhide kynnistetn ilman projektia ja jotka
toimivat uusien projektien oletusasetuksina. Muuttele rhide-projektia
niin paljon kuin haluat/uskallat/viitsit ja lopeta sen jlkeen
rhide. Voit kokeilla viel asetusten toimivuutta menemll jonnekin
hakemistoon miss on jokin muu mr kuin yksi projekteja (jos niit
on vain yksi niin se ladataan automaattisesti) ja kynnistmll
Rhiden.

Nyt pitisi kaiken olla valmista uuden projektin teolle. Ota
Project-valikosta Open project ja kirjoita avautuvan ikkunan
Name-sarakkeeseen haluamasi projektin nimi. Ruudun alalaitaan avautuu
ikkuna joka kertoo projektin tiedostot. Aktivoimalla tmn ikkunan ja
painamalla insert-nappia (tai Project-valikosta Add item) saat
listty uusia tiedostoja. Kun olet valmis paina Cancel-nappia.
Tll tavalla list haluamasi tiedostot (lhdekooditiedostot, tosin
jos ehdottomasti haluat voit laittaa jonkin valmiiksi knnetynkin O-
tai A-tiedoston mukaan) projektiin.

Mukaan listtvi kirjastoja voit mritt Options-valikon
Libraries-kohdasta. Muista, ett tm hakee kirjastoja VAIN DJGPP:n
LIB-hakemistosta, ja ett kirjaston nimeen listn aina kntjn
toimesta eteen LIB ja loppuun .A, eli l kirjoita koko kirjaston
nime tyyliin LIBJOKIN.A, vaan JOKIN. Sellainen erikoisuus kyll
kntjst lytyy, ett ylipitkt (yli 5 merkki) kirjaston nimet
katkaistaan, joten IOSTREAM antaa tiedoston LIBIOSTR.A, eik
virheellist LIBIOSTREAM.A:ta (joka olisi siis liian pitk).

Kun olet tyytyvinen kaikkeen muuhun niin ota viel Project-valikosta
main targetname ja mrit kohteen nimi. Jos olet tekemss
niengine, niin sinulla on nienginen C-tiedostot projektissasi ja
kohteena (esim.) LIBSND.A. Jos taas teet C++ EXE:, niin sinulla on
C-tiedostot joita kytetn, kohteena (esim.) PLUSPLUS.EXE ja
mahdollisesti kirjastossa IOSTR ja jotain muuta. .A-ptteest Rhide
osaa automaattisesti knt archive-muotoisen tiedoston ja
.EXE-ptteest ajettavan. Muutkin voivat toimia (O ainakin), mutten
ole kokeillut koskaan, sill siihen ei yleens ole tarvetta.

Projektin kntminen onnistuu napilla F9, jolloin Rhide osaa
automaattisesti katsoa tiedoston pivyksist mitk tiedostot ovat
muuttuneita (lhteen pivmr uudempi kuin kohteen) ja knt nin
vain tarpeellisen. Aikaa sstyy ja hermoja samoin. Kntmisen
jlkeen hakemistostasi lytyy luultavasti kasa objektitiedostoja,
joita voidaan kytt myhemmin linkkauksessa (jos vastaava
lhdekooditiedosto ei ole muuttunut).

Sellaista tll kertaa. Aika perusasiaa ja itsekin pteltviss,
mutta joskus vain ky siten ettei jotain perusasiaa itse hoksaa, tai
ainakin sst aikaa kun ei tarvitse kaikkea kokeilla. Nyt hallussa
pitisi olla projektien teko Rhidell ja niiden toimimaan saaminen, ei
sen kummempaa tll kertaa. Voit jatkaa halutessasi seuraavaan jos
tuntuu ett osaat tmnkin kappaleen materiaalin.


Todellista guruutta - salaperinen make
---------------------------------------

Make on kuin suoraan Unix-maailmasta tullut. Jos pelkk vilkaisu sen
info-sivuille (INFO MAKE) saa aloittelijan vapisemaan horkassa. Mutta
ei ht, min kvin siell ja selvisin elossa - tosin en ole en
ollut sama itseni sen jlkeen. Olen nimittin huomattavasti gurumpi
jlleen sill voin knnell projektini halutessani hienosti
komentorivilt automatisoituna. Ja se onnistuu maken
makefileill. Tss luvussa kerron miten niit tehdn, tosin en
mitn monimutkaisempaa valota kun mitn ihmekonsteja harvemmin
normaalissa perustyskentelyss tarvitsee.

Eli ensimmisen tehtvn on jlleen kaivaa make jostain, paikat ja
keinot ovat samat kuin Rhiden kohdalla, mutta toisin kuin Rhide maken
pitisi toimia ilman manuaaliin vilkaisua (koska se on huomattavasti
yksinkertaisempi systeemi). Ideana on tehd projektille ns. makefile,
jonka make osaa tulkita ja tehd sen mukaan tiedostossa ksketyt
asiat.

Mutta tehdksemme oikeanlaisia makefilej meidn tytyy ensin hieman
ymmrt filosofiaa maken takana.

Normaali makefile koostuu yleens alussa olevasta kasasta
muuttujamrittelyj, joita myhemmin kytetn kntmisess. Sen
jlkeen on kasa ohjeita, jotka koostuvat muutamasta
komponentista. Tss on ohjeen muoto ja esimerkki yhdest:

kohde: riippuvuudet
	komento kohteen tekoon

esim.

ohjelma.exe: ohjelma.o
	gcc ohjelma.o -o ohjelma.exe -s -Wall -v -O2

Eli ensimmisen on kohde joka kertoo makelle, ett tss on ohje
miten teet tmn. Sitten on riippuvuudet, joka kertoo, ett niden
pit olla kunnossa ennenkuin tt ohjetta aletaan
toteuttamaan. Seuraavalla rivill on yksi TAB:in painallus ja komento
jolla kohde tehdn (komentoja voi olla useampiakin, jokainen omalla
rivilln alkaen TAB:illa). Huomaa, ett tarvitsemme EHDOTTOMASTI
oikean TAB:in, emme mits MSDOS EDIT:in lelutabbeja, jotka eivt
itseasiassa ole kuin mrtty mr vlilyntej. Eli pit olla
jonkinlainen editori, joka osaa kytt aitoja TAB-merkkej.

En taida alkaa miettimn syvllisemmin maken toimintaa, mutta ideana
on, ett esittelet ensin pkohteen ja sen riippuvuudet ja sen jlkeen
esittelet nm uudet riippuvuudet ja niiden riippuvuudet jatkaen
pohjalle asti kunnes lopulta sinulla on kohteena objektitiedosto ja
lhteen lhdekooditiedosto ja alla komento tmn kntmiseksi,
jolloin make katsoo pivmrn mukaan tarvitseeko tm kohde
pivittmist. Jos lhde on uudempi kuin kohde niin ksky suoritetaan
mutta jos kohde on uudempi niin se on tydytty knt lhteen
muuttamisen jlkeen eik knt tarvita. Tll tavalla vain
muuttuneiden tiedostojen aiheuttamat knnstarpeet hoidetaan eik
ylimrist tyt tehd.

Yleens makefiless on ensin kohde all, jossa riippuvuuksina on kaikki
mit makefilen tulee saada tuloksena valmiiksi (EXE:t, kirjastot),
sitten on niden tuloksien ohjeet riippuvuuksina objekti- ja
archive-tiedostot, sitten archive-tiedostot riippuvuuksina
objektitiedostot ja lopuksi objektitiedostot riippuvuuksina
lhdekooditiedostot. Tss on esimerkki joka varmaan valaisee aika
sekavaa selitystni. =) Huomaa mys makrot, jotka mritelln alussa
ja joita muuttamalla on helppo vaihtaa knnksess tarvittavia
parametrej ja kntjien nimi:

CC=gcc
CFLAGS=-s -Wall

AR=ar
ARFLAGS=rs

all: esim.exe libx.a

esim.exe: esim.o libx.a
	$(CC) $(CFLAGS) esim.o libx.a -o esim.exe

libx.a: x1.o x2.o
	$(AR) $(ARFLAGS) libx.a x1.o x2.o

esim.o: esim.c
	$(CC) $(CFLAGS) -c esim.c -o esim.o

x1.o: x1.c
	$(CC) $(CFLAGS) -c x1.c -o x1.o

x2.o: x2.c
	$(CC) $(CFLAGS) -c x2.c -o x2.o

Kun tmn tiedoston tallentaa nimelle makefile tarvitsee sinun vain
antaa komento make niin ohjelma osaa automaattisesti knt kaikki
makefiless mritellyt tiedostot. Kyttksesi muita makefilen nimi
pit maken komentoriville antaa parametri -f<makefile>.

Esimerkki oli hyvin yksinkertaistettu ja vltin kyttmst paria
hauskaa kikkaa jotka tekevt makefilest paljon lyhyemmn (ja
sotkuisemman nkisen). Jos kuitenkin toiminta on epvarmaa, niin
selostetaan se tss viel kertaalleen:

1. Make aloittaa lausekkeesta all (komentorivill voit halutessasi
   mrt mik ohje tulee tehd, esim make libx.a ei koskisi esim.*
   -tiedostoihin) ja etenee tekemn esim.exe:.

2. Esim.exe:n teko tarvitsee ensin esim.o:n, siirrytn siihen.

3. Esim.o tarvitsee esim.c:n, mutta sille ei lydy ohjetta, joten
   suoritetaan ensimminen knns. Makrot CC ja CFLAGS puretaan
   komentoriville ja se suoritetaan ja kaiutetaan nytlle. Jatketaan
   esim.exe:n riippuvuuksien tutkimista.

4. Esim.exe:n teko tyss kun siihenkin pit tehd libx.a, joten
   siirrytn tekemn sit.

3. Libx.a:han pit olla x1.o ja x2.o, joten siirrytn niihin.

4. Riippuvuudelle x1.c ei ole ohjetta, joten suoritetaan x1.o:n
   komento (niss kohtaa olisi pivmrtarkistus, mutta koska
   noita objektitiedostoja ei viel ole olemassa niin...) ja palataan
   takaisin.

5. x2.o tehdn samaan tapaan kuin edellinen ja palataan libx.a:n
   pariin

6. Riippuvuudet kunnossa, tehdn kirjasto libx.a, palataan esim.exe:n
   kimppuun.

7. Esim.exe:n riippuvuudetkin ovat hanskassa, joten tehdn se ja
   palataan kohtaan all.

8. Libx:kin on tehty juuri, joten kaikki on valmista, poistutaan.

No niin, kyll toiminta varmaankin selvisi, ja jos ei niin paljon
pidemmt ja selvemmt tekstit lyt englanniksi komennolla info make
(no selvemmist en itseasiassa tied :).

Mutta make ei viel ole ohitse, en uskalla pst teit kappaleesta
ennenkuin osaatte tehd ohjeita jotka tekevt vaikka 30
objektitiedostoa kerralla, ne kun ovat kovin mukavia systeemej
verrattuna siihen ett joutuisit kirjoittamaan jokaista varten oman
ohjeen.

Ideana tss on ernlainen nimentydennys. Make osaa poistaa ptteen
nimest ja korvata sen toisella, jota ominaisuutta kytetn juuri
thn useiden samankaltaisten tiedostojen tekoon kerralla. Jos siis
sinulla on 10 objektitiedostoa ja jokainen knnetn
vastaavannimisest lhdekooditiedostosta (o1.o ja o1.c, o2.o ja o2.c
jne.), niin niiden knt onnistuu seuraavalla tyylill (aika maken
infoista pllitty ja suoraan knnetty tavaraa mutta who cares?-):

KOHTEET: KOHDE-PATTERN: RIIPPUVUUS-PATTERN ...

OBJECTS=object0.o object1.o object2.o object3.o object4.o object5.o 
	object6.o object7.o object8.o object9.o

$(OBJECTS): %.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

Eli ensimmisen tulee lista (OBJECTS) tehtvist kohteista, sitten
tulee %-merkki, joka esiintyy kohde-patternissa vain kerran, ja
maken infosivut kyttvt siit nime "stem". Tm vastaa mit tahansa
kohtaa yhden kohteen nimest, kaikki muut kohteen nimess (.o tss
tapauksessa) tytyy vastata tysin.

Jos siis kohteena olisi foo.o ja kohde-pattern olisi %.o, niin "stem"
(anteeksi minulla ei ole sanakirjaa ksill ;) saisi arvon foo. Jos
riippuvuus-pattern olisi %.c niin riippuvuus tlle tiedostolle olisi
foo.c. Ei mitn sen vaikeampaa, % on kuin DOS-maailman * ja
ensimmisen tulee lista tiedostoista (kuten hakemistolistaus), sitten
stemill varustettu patterni ja lopuksi riippuvuudet jotka
tydennetn sill mit stem vastaa.

Lisksi tytyy kiinnitt huomio merkkisarjoihin $< ja $@, joista
ensimminen korvataan riippuvuudella (tai riippuviiksilla jos niit on
useampia) ja toinen kohteen nimell. Mys muita vastaanvankaltaisia
lytyy, mutta ne eivt ole lheskn niin hydyllisi kuin nm kaksi.

Nill evill ainakin pitisi onnistua makefileiden teko aika
pitklle. Hyvi esimerkkej lytyy lukemattomista DJGPP-paketeista,
joissa kntminen hoidetaan makefileill. Makefilet ovat muutenkin
yleisin tapa levitt lhdekoodin kanssa softaa, harvemmin olen nhnyt
kirjaston knnst automatisoitavan Rhiden projekteilla. :)


Ammattimaista meininki - enginen teko
--------------------------------------

Tm luku kertoo hieman niist vhisist kokemuksista mit minulla on
ollut projektien kanssa, tai oikeammin kertoo mit kannattaisi ottaa
huomioon enginen teossa, jotta se toimisi mys huomenna ja jotta siit
jlkeenpin saisi jotain selvkin.

Nppr tapa pohjelman yksinkertaistamiseksi on tehd tietyn
tehtvn suorittavista tiedostoista yksi paketti, kirjasto jonka
headerin koodiin sisllyttmll voi kyseisen tehtvn hoitaa
kirjaston tarjoamilla rutiineilla. 

Sen lisksi ett tapa yksinkertaistaa koodia se mys parantaa sen
yllpidettvyytt huomattavasti ja myskin muunneltavuus on aivan eri
luokkaa kuin "kaikki-yhdess-kasassa" -ohjelmilla. Lisksi kun engine
on kerran valmis voi sit kytt uudelleen ja uudelleen - yleens
pienill muutoksilla tai parhaimmillaan muuttamattomanakin.

Mutta tllaisenkin teossa kannattaa huomioida joitakin asioita, jottei
jlkeenpin paljastuisi ett olet tehnyt turhaa tyt koko
ajan. Nimittin ensin on tarkoin otettava selv mit enginelt
vaaditaan ennenkuin sellaista alkaa tekemn. Hyv tapa on mietti
millaista peli on tekemss ja millaisia ominaisuuksia enginelt
vaaditaan. Matopelin teossa ei vlttmtt tarvita kovin kummoisia
jrjestelmi, sill ne eivt useastikaan vaadi kovinkaan monimutkaista
toimintaa hyvn jljen aikaansaamiseksi. Toisin on vaikka sivultapin
kuvatussa ammuskelupeliss, jossa spritejen piirron pit olla
rimmisen nopeaa ja turhaa piirtely tulee vltt. Skrollaus vaatii
mys tllaisissa peleiss tehoja ja muuttujia spriteihin tulee
huomattavasti enemmn kuin matopeliss.

Mikn ei voita kunnon suunnittelua kun koodausta sitten aletaan
tekemn. Hyvll onnella koko enginen teko on suoraviivaista koodin
kirjoittamista jos trkeimpi algoritmej on jo hahmoteltu paperilla
ja mieless on kunkin funktion toiminta ja tarvittavat muuttujat
kuhunkin tehtvn.

Kun tarpeet ovat vihdoin paperilla ja koodin kirjoitus edess voi olla
hyv viel etukteen nimet enginen lohkot ja nimet ne. Nppr tapa
jolla psee suoraan toimeen on kynnist vaikka Rhide ja lhte
lisilemn uuteen projektiin tiedostojen nimi. Tiedostoja ei
tarvitse edes olla olemassa vaan riitt ett hahmotat mit
jrjestelmn pit tehd ja minklaisiin osiin se pitisi
jakaa. Kaikkein kevyimmt enginet eivt edes paljoa tiedostoja tarvi,
nppishandleri ja timerhandleri, hiirirutiinit ja yksinkertaisemmat
grafiikkaenginet menevt ainakin thn kastiin. nienginet,
playerit ja 3D-enginet sek raskaammat grafiikkaenginet taas voivat
hyvinkin vied toistakymmentkin tiedostoa.

Hyvi jakotapoja on monia ja jrki varmaan sanoo, ett hyv jakotapa
ei ole aakkosjrjestys taikka pituusjrjestys. Hyv jakotapa voi olla
vaikka nienginen teossa ptiedosto sislten kynnistys- ja
lopetusfunktiot ja jonka .h-tiedostosta lytyvt keskeiset
datarakenteet, latausrutiinit sisltv tiedosto, universaali
efektinsoittorajapinta ja eri tiedostot jokaiselle nikortille,
modien lataus, modien soittorutiinit sisltv tiedosto jne.. Aivoja
saa, pit ja kannattaa kytt.

Trkeit suunnittelun kohteita on mys se miten ohjelma sil datansa
sek muistissa ett kovalevyll. Jo alussa fiksusti ja
laajennettavasti tehty rakenne on monta kertaa kyttkelpoisempi kuin
senhetkiseen tarpeeseen vstty kyhelm. Mys tallennus- ja
latausrutiinit kannattaa tehd erikseen eik pyrki tekemn mitn
purkkaviritelmi jotka kaatuvat vhintnkin kun haluat list uuden
ominaisuuden.

Hyv idea on mys tehd universaalit rutiinit virheist
ilmoittamiseen, muistin varaukseen ja vaikka tiedostojenkin
lukuun. Yleenskin enginen suurin osa tulisi sijoittaa keskivlille
muutaman kriittisten low-level -rutiinien jdess alapuolelle ja
ylpuolelle tuleva rajapinta ohjelmalle mahdollistaa enginen
muuttumisen radikaalistikin ilman muutoksia pohjelmaan. Low-level
-rutiinien siirto toisille nimille jo pelkill #define-lausekkeilla
(tyyliin "#define OmaFopen(a,b) fopen(a,b)") auttaa sen verran, ett
kun haluatkin muuttaa kaikki tiedostorutiinit pakattuja datatiedostoja
kyttviksi ei tarvitse muuttaa kuin pari kohtaa kaiken muun jdess
samanlaiseksi.

Kommentointi on elintrke engine tehdess, sill hyv engine voi
olla kytss pitknkin aikaa ja sitten kun se lopulta j ahtaaksi
voi huonosti kommentoineen kooderin peri hukka muuntelun
osoittautuessa mahdottomaksi yksinkertaisesti siit syyst ettei edes
tekijll ole en mitn aavistusta mit hnen koodinsa tekee. Hyv
ohjelmoija tekee sen verran lyhyit funktioita, ett niist saa selv
vhn tutkailemalla ja nime muuttujat ja funktiot kuvainnollisesti
sstelemtt turhaan nimen pituudessa (jrkevll tasolla kuitenkin,
mutta saa se nyt enemmn olla kuin Jdrwsprt()). Kun epselvemmt
kohdat viel kommentoi koodista pitisikin saada huomattavasti
paremmin selv.

Yksi hydyllinen asia voisi olla tiedostoja editoidessa kirjoittaa
tietty headeri jokaisen tiedoston alkuun. Hyvi voisi olla
copyright-ilmoitukset (joilla ei kyll omassa kytss tee mitn),
luontipivmr, viimeisen muutoksen pivmr ja muutoshistoria,
jonne kirjataan muutokset koodiin. Jlkeenpin ja bugeja etsiess
tuollaisesta on kummasti hyty, kun miettii mit onkaan tullut
lhiaikoina muunneltua.

Viimeinen asia mik koodissa pit viel huomioida on ne funktiot,
jotka tarjoavat rajapinnan, "kyttliittymn" engineen. Nm funktiot
ovat siis ne jotka tarjotaan engine kyttvlle ohjelmalle enginen
kyttn. Niden tulee olla tarpeelliksi kattavat jotta kaikkia
enginen ominaisuuksia voidaan halutessa kytt hyvksi. Hydyllist
on tehd Init- ja Deinit-funktiot, joita kutsutaan pohjelmasta
ohjelman kynnistyess ja siit poistuttaessa.

Mys funktioiden nimeminen erottamiseksi muista mahdollisista
samankaltaisista funktioista voi olla hydyllist. Kirjaston
funktioille ja globaaleille muuttujille voisi antaa jonkin etuliitteen
erottamaan ne muista ja huolehtimaan siit ettei kahdella funktiolla
ole samaa nime. Omassa grafiikkakirjastossani kytn JG-etuliitett,
jolloin funktioiden nimet ovat tyyliin JG_Draw, JG_Hide jne.. Mys
mahdollinen versionumero kirjastolle on ktev jos sit aikoo todella
kehitt kunnolla.

Sitten vain huolehtimaan siit ett enginest ei lydy
pullonkauloja. Helpointa lienee tehd enginen eniten tehoa vaativat
osat mahdollisimman nopeiksi, jolloin pohjelma on helppo tehd
korkean tason koodilla. Assembler-optimointikin voisi olla ihan kiva,
joten seuraavassa luvussa luulen ett selitn hieman sen lisilyst
DJGPP:n koodiin.

Tm luku ei nyt varsinaisesti opettanut mitn, mutta ainakin jotain
evst pitisi nyt lyty ensimmisen enginen tekoon. Katsotaan mits
thn nyt keksisikn seuraavaksi. =)


Vauhtia peliin - ulkoisen assyn kytt
--------------------------------------

No niin, assembler, tuo kielist jaloin nytt olevan tmnkertaisen
kiinnostukseemme kohteena. Vaan mik on tuo salaperinen kieli ja
miten sit kytetn. Se j ihan sinun itsesi selvitettvksi, mutta
voin kuitenkin antaa jonkinlaisia ohjeita jotta lytisit tiedon
lhteille. Ensihtn kannattaa hakea koneelleen ainakin seuraavat
opukset vaikkapa MBnetin ohjelmointialueen kautta:

ASSYT.ZIP: 
    Cyberdune (tjsp.) magazinen assykurssit kaikki samassa kasassa,
    suomeksi opettaa assemblerin perusasiat.

HELPPC21.ZIP + HPC21_P5.ZIP:
    HelpPC referenssiteos ja Pentium-update sislten mm. kaikki
    x86-prosessorikskyt, matikkaprossukskyt ja Pentiumin omat kskyt
    (kuten CMPXCHG8B tai jotain).

PCGPE10.ZIP:
    Assytutoriaali lytyy tltkin, tosin englanniksi.

Lisksi todella hyv kirja assyn opetteluun (ja ainoita suomeksi) on
kirja nimeltn 486-ohjelmointi. Tuota kaikki aina suosittelevat enk
itsekn voi kirjaa haukkua. Kirjastosta tuon saa viel kaiken lisksi
ilmaiseksi, vhintn kaukolainauksella.

Jos sinua ei assembler kiinnosta yhtn niin voit tietenkin hypt
tmn kappaleen yli, mutta varoituksen sana sit ennen: Jos aiot tehd
joskus nopean toimintapelin (lhiaikoina ainakin), niin tulet hyvin
luultavasti kaipaamaan assembler-osaamista. No tietenkin jos odottaa
tarpeeksi niin voi tehd kaiken vaikka Visual Basicin kasiversiolla,
mutta en minkn takaa ett pysyn myhemmin tutoriaalissa pelkss
C:ss. <grin>

Mutta sen jlkeen kun osaat assyn, niin alahan lukemaan pidemmlle,
sill ksittelen hieman C-kielisest ohjelmasta kutsuttavien
funktioiden tekoa assyll. En aio selitt sinulle mik on pino, sill
assyoppaista lytyy tuokin tieto. Muistiasi virkistkseni mainitsen
kuitenkin, ett tulee muistaa pinon kasvavan alaspin, eli jos haluat
varata pinosta 16 tavua niin sinun tulee vhent esp:st (extended
stack pointer) 16 tavua, ei list! Palautus taas hoituu lismll.

Eli hieman tietoa siit miten C-kielinen ohjelma kutsuu funktiota ja
mit se tekee sinun palattuasi. Eli kutsuessaan funktiota C-kielinen
ohjelma ensin pushaa parametrit pinoon lhtien parametrilistan
oikeasta laidasta ptyen lopulta ensimmiseen parametriin ja sitten
se heitt ebp:ns pinoon, kopioi ebp:n esp:hen ja lis siihen itse
kyttmns muistin mrn (eli itseasiassa vain varmistaa ett esp
osoittaa pinon plle) ja kutsuu funktiota kytten call-komentoa,
joka viel kaiken huippuna heitt senhetkisen eip:n (extended
instruction pointer) pinoon.

Huomaamme, ett kun suoritus alkaa omasta funktiostamme on asioiden
laita seuraava:

Pino sislt indeksiss 0 pinon huipun, eli tll hetkell kutsuneen
ohjelman eip:n. Sen jlkeen on ensimminen parametri, sitten toinen
parametri jne.. Mutta koska meidn tytyy aluksi tallentaa ebp pinon
plle pushaamalla se huipulle, jolloin tiedmme, ett parametrit ovat
kahden kaksoissanan (ebp ja eip), eli 8 tavun pss. Tss funktion
tarvitsema alustuskoodi:

push ebp
mov  ebp, esp

Lisksi on mahdollista varata pinosta muistia haluttu mr
vhentmll esp:t,jolloin siihen j aukko jonka alussa ebp
on. Muista kuitenkin vapauttaa muisti korottamalla esp:t. Muista
lisksi, ett koska pino menee alaspin, niin varattu muisti sijaitsee
mys esp:st alaspin, eli negatiivisiss offseteissa.

Sen jlkeen vain osoitellaan parametrej. Ensimminen parametri on
siis nyt kohdassa ebp+8 (koska kopioimme ebp:hen esp:n, jossa pino
oli), seuraava kohdassa ebp+8+ensimmisen_parametrin_koko
jne.. Ensimmisen parametrin koon ollessa sana (2 tavua) olisi toinen
tietystikin osoitteessa ebp+10 ja kolmas parametri osoitteessa
ebp+10+toisen_parametrin_koko. Tajusit varmaan pointin.

Koko roska on itseasiassa hemmetin vaikea ymmrt ja olen tunnin ajan
loikkinut ympri kovalevyni etsimss tarkennuksia pinon toimintaan
ja miten C-funktiota itse asiassa kutsutaan, sill en ole koskaan
ottanut viimeisen plle selv kntjn sielunelmst.

Piirrn nyt pikkaisen kaavion siit mit tietkseni muistista lytyy
sen jlkeen, kun funktiota void func(short,long) on kutsuttu, ebp on
pushattu ja esp siirretty siihen ja pinosta varattu muistia 2 tavua:

          C     B           A                              
 ----------------------------------------------------------
 |RR|RR|RR|MM|MM|BP|BP|BP|BP|IP|IP|IP|IP|11|11|22|22|22|22|
 ----------------------------------------------------------

A) Ohjelmaan tullessa ESP osoittaa thn
B) Kun EBP on pushattu niin ESP osoittaa thn, samoin EBP kun ESP on
   ensin siirretty mys EBP:hen. Huomaa EBP:n ja EIP:n sijainti
   kohdasta B nhden ja parametrin 1 sijainti offsetissa 8, sek
   parametrin 2 sijainti offsetissa 10 (parametrin 1 koko on short,
   eli 2 tavua!)
C) Kun ESP:t vhennetn kahdella jotta pinosta saadaan ohjelmalle 2
   tavua muistia on meill nyt kaksi tavua muistia kytss alkaen
   offsetista EBP-2. ESP osoittaa tmn muistin alkuun, mutta
   pushailun sattuessa se lhtee vaeltelemaan yh kauemmas vasemmalle.

Palautuksessa poppaillaan kaikki, jolloin ESP on taas kohdassa C. Sen
jlkeen vapautetaan pino vhentmll ESP:t kahdella, jolloin ESP ja
EBP ovat jlleen samoja, eli kohdassa B molemmat. Nyt viel popataan
EBP, jolloin EBP on alkuperisess tilassaan, samoin kuin ESP, joka
osoittaa EIP:n kohdalle. Nyt vain ret, joka ottaa EIP:n pinosta ja
palaa thn osoitteeseen.

JES! TEIN SEN! (anteeksi tunteenpurkaus mutten uskonut saavani tt
itsekn selville ilman kenenkn apua ;)

Huomaa, ett on aina kutsuvan ohjelman vastuulla pit rekistereistn
huolta ja puhdistaa parametrit pinosta, jotka sinne on pitnyt
pushailla ennen ohjelman kutsua (niit ohjelma ei palauta).

Tss nyt tm lopullinen assyosuus, joka pit olla alussa ja
lopussa:

push	ebp
mov	ebp, esp
dec	esp, <pinon koko>

<koodia>

add	esp, <pinon koko>
pop	ebp
ret

Toisen C-funktion kutsu taas onnistuu seuraavasti, otetaan esimerkkin
vaikka foo(int,short,char,int):

push	<int>
push	<char>
push	<short>
push	<int>
call	_foo
add	esp, 11

Nuo <int>-hommat siis tarkoittavat oikeankokoisia rekisterej tai
muistialueita. Huomaa mys lopussa esp:n palautus korottamalla sit
parametrien yhteenlasketun koon verran. Huomaa mys, ett C lis
assykoodiin aina yhden alaviivan lis, eli omien rutiiniesi
funktionimien edess pit ASM-tiedostossa olla aina yksi alaviiva
enemmn kuin mit C-kielisess. Mys C-kirjaston funktioita kutsuessa
pit muistaa, eli _printf, _puts jne.. Funktioille joiden nimiss on
C:llkin yksi tai useampia alaviivoja suoritetaan vain yhden alaviivan
eteenlisys.

No niin, nyt menee kaikki muu funktioissa, mutta viel palautus ja
structit sek reaaliluvut. No tss kaikki vh mit min siit
tiedn:

Pointtereiden ja dword (4 tavua siis) kokoisten kokonaislukujen
palautus EAX:ss. Sanojen (2 tavua, word) palautus AX:ss ja tavujen
palautus AL:ss. Reaaliluvut matikkarekisteriss ST[0]. Structeista
minulla ei ole aavistusta, sill olen kyttnyt helpompaa ja yleens
hydyllisemp tapaa vlitt ne vain structin osoitteina.

Reaaliluvut annetaan parametrein tietkseni ihan samoin kuin muutkin
parametrit.

No mutta. Kaikki tietvt nyt miten varata muistia, kutsua funktioita,
palauttaa tietoja, kytt parametreja. Mutta trkein puuttuu, sill
kukaan ei osaa tehd tiedostoja jotka voisi linkata DJGPP-ohjelman
mukaan. Siisp tihin!

Jotta objektitiedoston voisi linkata mukaan DJGPP-ohjelmaan tyty sen
olla oikeaa formaattia. DJGPP:n hyvksym formaatti tunnetaan nimell
COFF (ei kaljaa!), eli common object file format. Ainoat
kyttmistni assembler-kntjost jotka tuota tukevat ovat as ja
NASM. As on GNU assembler ja sislt TODELLA kryptisen nkist AT&T
assembleria kntvn yksikn. Mutta kerron jo etukteen, ett
AT&T-formaatti, jota DJGPP kytt itse sen Unix-taustan takia on
aivan toisen nkist kuin Intel-syntaksin assy, joten suosittelen,
ett ette kyt sit (halukkaat imuroivat tiedoston DJTUT255.ZIP)!

Paljon parempi kntj on nimeltn Netwide Assembler, lyhyesti NASM,
jonka lyt ainakin MBnetist ja tietenkin Internetist. Nimi on
NASM094B.ZIP, mutta voi kyll olla ett uudempiakin on
ilmestnyt. Jokatapauksessa kntj on aivan loistava ja sen kyttkin
on suhteellisen yksinkertaista. Kaikkein parhaiten sen kytn oppii
lukemalla NASM.DOC lpi ja tutkailemalla esimerkkikoodeja (etenkin
AOUTTEST.ASM!) hakemistosta TEST. Mutta niille jotka eivt mielelln
lue englantia on ihan pikkuinen esimerkkisorsa, jolla psee nyt
ainakin alkuun siihen asti, ett kunnon sanakirja tai tulkkaava kaveri
lytyy:

ASMEXP.ASM:

BITS 32

EXTERN _cfunktio
EXTERN _cmuuttuja
GLOBAL _asmmuuttuja
GLOBAL _asmfunktio

SECTION .text

; int asmfunktio(int)

_asmfunktio:
	push	ebp
	mov	ebp, esp
	
	mov	eax, [ebp+8]
	add	[_asmmuuttuja], eax

        push	eax
	call	_cfunktio
	add	esp, 4

        mov	eax, [_asmmuuttuja]
	pop	ebp
	ret

SECTION .data

_asmmuuttuja	DD 0


ASMEXP.H

extern int asmfunktio(int);

void cfunktio(int);

int cmuuttuja;


ASMEXP.C

#include <stdio.h>

void cfunktio(int luku) {
    puts("kutsuttiin C-funktiota parametrilla %d\n", luku);
}

void main() {
    printf("ASM-funktio palautti arvon %d\n", asmfunktio(10));
    printf("ASM-funktio palautti arvon %d\n", asmfunktio(20));
}


H-tiedoston ja C-tiedoston varmaan ymmrrtte, mutta selvennyksen
viel assosuudesta, ett ensin asetetaan NASM 32-bittiseen
koodinknttilaan, sitten mritelln ulkoiset muuttujat _cmuuttuja
(kaksoisasna) ja _cfunktio (kaksoissana sislten rutiinin
osoitteen). Sitten koodisegmentiss (.text) on _asmfunktio, joka tekee
kuten aiemmin neuvottiin, eli tallettaa ebp:n ja kopioi esp:n
ebp:hen. Sen jlkeen se korottaa _asmmuuttuja -muuttujaa parametrill
ja kutsuu viel _cfunktio -funktiota parametrill palauttaen lopuksi
_asmmuttuja:n arvon. Datasegmentiss on varattu _asmmuuttuja
-muuttujalle tilaa kaksoissanan verran ja alustettu se nollaksi.

Sitten vain tutkimaan antaako ohjelma oikean tulosteen. En minkn
tied mutta menen katsomaan. =) Toimi ainakin minulla. Jaa ett se
kntminen NASM:illa?-) No se on tietenkin komennolla:

nasm -o jokin.o -f coff jokin.asm

No niin, nyt sinun pitisi hallita assemblerin kytt C:n kanssa
jotakuinkin vltten ja nasmilla kntelykin pitisi onnistua, sek
nasm-tiedostojen tekokin ainakin rajoitetusti. Pahoittelen ett
tarkempia ohjeita ei annettu, sill ne olisivat olleet niin pitkt,
ett katsoin oppimisen onnistuvan ilman tarkempia ohjeita. Mutta jos
kuitenkin tuntuu, ett tmn kappaleen taso leijui kilometritolkulla
tajuntasi ylpuolella niin pyydn ottamaan yhteytt, sill en
ihmettele vaikka tm olisikin vaikein osa thn asti ja kaikki apu
sen suhteen miten tt pitisi parantaa on tarpeen.

Mutta toisaalta jos et assy muuten osaa etk ole kaikkea
dokumentaatiota kaivanut esiin mit lydt voi olla ett asia on
paljon selkempi jo muutaman pivn pst. Jos ei kuitenkaan helpota
niin heit viesti tnnekin pin. Mutta nyt jatkan taas kohti uutta
tuntematonta.

Phew, tmhn ky tyst kun koko pivn kirjoittaa!


PIT - aikaa ja purkkaa
----------------------

Hiphei taipaleemme jatkuu edelleen, vaikka kello osoitteleekin
kirjoitushetkell melkein kahtatoista. Mys ihmeellisest tekstist
voinee sen ptell etten ole vlttmtt aivan parhaimmillani ja
tervillimmillni (villimmillni?) thn aikaan pivst. No, tehn
siit vain krsitte, en min, joten jatkakaamme! ;)

Eli ihmeellinen lyhenne PIT? Mist se tulee? No tietenkin sanoista
Programmable Interrupt Timer, eli ohjelmoitava keskeytysajastin. Tm
on tllainen hauska piiri PC:ll, joka kykenee generoimaan ties mill
tavalla keskeytyksi. Kiinnostavaa ja tarkkaa tietoa lytyy PCGPE:st
(PCGPE10.ZIP) tiedostosta PIT.TXT, mutta me keskitymme vain
olennaiseen, nimittin systeemin omaan kelloon, keskeytykseen
8. Kerron kuitenkin hieman mill tavalla piiri laskee milloin pit
generoida keskeytys 8, ennenkuin psemme hauskaan tavaraan (eli
esimerkkikoodiin ;).

Eli PIT tikitt 1193181Hz:n taajuudella, eli suomeksi 1193181 kertaa
sekunnissa. Joka kerta se esim. vhent kanavan 0 laskuria yhdell ja
jos se on 0 niin se generoi keskeytyksen ja asettaa uudelleen laskurin
haluttuun arvoon ja lhtee laskemaan alaspin. Laskuri on kahden
tavun, eli yhden sanan mittainen ja kykenee ninollen vastaanottamaan
luvun vlilt 0-65335. Mutta erikoisuutena on se, ett jos laskurin
alustusarvo 0 ei tarkoitakaan ett keskeytyst kutsutaan jatkuvalla
sytll, vaan ett sit kutsutaan 65536:n "tikahduksen" (ei nin
myhn oikein sanat muistu mieleen) jlkeen. Normaali systeemikello
on asetettu thn kutsuntatiheyteen, eli sit kutsutaan
1193181/65536=n. 18.2 kertaa sekunnissa.

Jos siis koukutamme tmn keskeytyksen kuten olemme aiemmin tehneet
nppiskeskeytyksellekin tulee alkuperist kutsua thn tahtiin, sill
toisin kuin nppiskeskeytys, kellokeskeytys on huomattavasti
trkemmss asemassa eik sit voi hypt noin vain yli (ainakin
DOS:in kello pyshtyy koko ajaksi =). Jos me siis koukutamme
keskeytyksen tulee sen olla tmntyylinen:

funktio kellokeskeytys

    <tee jotain>

    laskuri = laskuri + tikkej_per_kutsu;

    jos (laskuri on suurempi tai yhtsuuri kuin 65536)

        laskuri = laskuri - 65536

        kutsu_vanhaa();

    muuten

        kuittaa_keskeytys();

    end jos

end funktio

Tikkej_per_kutsu on siis uusi mr tarvittavia tikkej jokaisen
keskeytyksen vliss. Jos vaikka haluaisimme ett omaa kelloamme
kutsutaan 100 kertaa sekunnissa, niin meidn pitisi asettaa PIT:ille
laskurin alustusluvuksi 1193181 / 100 = n. 11931. Sitten vain joka
kutsulla listn laskuria sen mukaan montako tikki on kulunut
edellisest vanhan kellon kutsusta ja jos se on alkuperinen 65536 tai
suurempi, niin vhennetn siit tm luku ja kutsutaan vanhaa
keskeytyst. Jos se on viel alle 65536, niin lhetetn tuttuun
tapaan tavu 0x20 porttiin 0x20.

Kellokeskeytyksen <tee jotain> -kohdan voi ja kannattaakin yleens
korvata laskurilla, jota korotetaan jatkuvasti. Tt voi kytt
vaikka ajanottoon tai muuhun hydylliseen, kuten nemme myhemmin.
Kaikki tuntuisi olevan toteutusta vailla - MUTTA.

Ongelmaksi muodostuu vanhan kutsuminen. Kun keskeytys generoidaan niin
senhetkinen koodisegmentti ja -osoitin (eli CS+EIP) kipataan pinoon,
samoin kuin liput ja kutsutaan ksittelij. Vastaavasti iret
keskeytysksittelijn lopussa ne otetaan sielt pois ja niiden avulla
palataan jatkamaan keskeytynytt ohjelman suoritusta samasta tilasta.

Mutta kun kutsumme vanhaakin ksittelij vliss, niin pinosta pois
otto tapahtuu kahdesti, mik eteen? Selv on, ett ohjelma kaatuu jos
ei tt ongelmaa korjata. Mutta htiin saapuu Kaj Bjrklund uljaalla
inline assembler-ratsullaan pelastaen meidt pulasta! Meidn tarvitsee
vain kellokeskeytyst asetettaessa ottaa talteen alkup. handlerin
koodiselektori ja offsetti sek tallentaa ne 64-bittiseen muuttujaan
(long long). Sitten vain kytetn seuraavanlaista inline-ptk:

__asm__ __volatile(
"pushfl
 lcall %0
" 
: 
: "g" (oldhandler));

Edellinen koodinptk tekee samat temput ennen funktion kutsumista
kuin mit sanoin normaalisti tehtvn, eli heitt liput pinoon ja
lcall pist sinne CS:n ja EIP:nkin, joten iret vanhassa
timer-rutiinissa palaakin omaan koodiimme ja kaikki toimii hienosti,
kun if...else huolehtii siit ettei outata kahdesti porttiin 0x20!
Hienoa! Nyt meill onkin oikeastaan kaikki tarvittava tieto handlerin
tekoon:

#include <dos.h>
#include <dpmi.h>
#include <go32.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/nearptr.h>

_go32_dpmi_seginfo info;
_go32_dpmi_seginfo original;

volatile long long OldTimerHandler;
volatile int TicksPerCall, OriginalTicks, Counter;

static volatile void TimerStart() {}

void TimerHandler() {
    Counter++;
    OriginalTicks+=TicksPerCall;
    if(OriginalTicks>=65536) {
        OriginalTicks-=65536;
        __asm__ __volatile__ ("
            pushfl
            lcall %0
        "
        :
        : "g" (OldTimerHandler));
    } else {
        outportb(0x20, 0x20);
    }
}

static volatile void TimerEnd() {}

void SetTimerRate(unsigned short ticks) {
  outportb(0x43, 0x34);
  outportb(0x40, ( ticks & 0x00FF ) );
  outportb(0x40, ( ( ticks >> 8 ) & 0x00FF ) );
}

void InitTimer(int tickspersecond) {
    __dpmi_meminfo lock;

    lock.address = __djgpp_base_address + (unsigned) &TimerStart;
    lock.size = ((unsigned)&TimerEnd - (unsigned)&TimerStart);
    __dpmi_lock_linear_region(&lock);

    Counter=0;
    OriginalTicks=0;
    TicksPerCall=1193181/((unsigned short)tickspersecond);
  
    disable();

    _go32_dpmi_get_protected_mode_interrupt_vector(0x0008, &original);

    OldTimerHandler=((unsigned long long)original.pm_offset) +
                    (((unsigned long long)original.pm_selector)<<32);

    info.pm_offset=(unsigned long int)TimerHandler;
    info.pm_selector=_my_cs();
    _go32_dpmi_allocate_iret_wrapper(&info);

    SetTimerRate(TicksPerCall);
    _go32_dpmi_set_protected_mode_interrupt_vector(0x0008, &info);
    enable();
}

void DeinitTimer() {
    disable();
    SetTimerRate(0);
    _go32_dpmi_set_protected_mode_interrupt_vector(0x0008, &original);
    enable();  
}

Muu mennee ihan hyvin tajunnan perlle asti, mutta InitTimer-rutiinin
alku voi hyvinkin tuottaa ihmettely, samoin kuin kaksi tyhj
funktiota kummallakin puolella TimerHandler-rutiinia. No minps
kerron mist on kyse. Kyse on muistin lukitsemisesta, kuten ehk
komentojen nimist voi ptell. Normaalisti DPMI-palvelin (jos se
siihen kykenee) voi swapata levylle koodia ja dataa jos silt tuntuu,
mutta kun muistialue lukitaan niin sit ei swappaillakaan
minnekn. l huoli jos epilet ettet olisi osannut noita tehd itse,
sill minkin varas- kytin apunani libc:n lhdekoodeista lytyv
koodinptk ja Bjrklundin esimerkin koodia.

No nyt vain sitten esimerkkiohjelma, joka nytt hieman mihin
timer-rutiini pystyy:

#include <stdio.h>
#include <conio.h>

extern void InitTimer(int);
extern void DeinitTimer();
extern volatile int Counter;

void main() {
    InitTimer(100);
    while(!kbhit()) {
        printf("Counter=%d\r", Counter);
        fflush(stdout);
    }
    getch();
    DeinitTimer();
}

Nin. Seuraavassa luvussa esittelen ennen nukkumaanmenoani (ellei joku
tule ajamaan minua unten maille ennen kuin ehdin kirjoittaa seuraavan
luvun =) kiinnostavaa kyttkin tlle, joten pysyk kanavalla!


Miten peli toimii yht nopeasti kaikilla koneilla
-------------------------------------------------

No thn on useita tapoja, mutta lhes kaikissa tarvitaan ajanottoa ja
ninollen edellisen luvun ajastinrutiini pohjusti varsin mukavasti
tmn luvun aihetta (josta tulee luultavasti todella lyhyt). Idea on
siis se, ett jokaisella koneella peli pyrisi yht nopeasti. No
helpommin sanottu kuin tehty.

Varmastikin kytetyin ja toimivin on menetelm, jota kutsutaan
hienosti termill "frameskip", eli kuvien yli hyppiminen. Ilkka
Pelkonen kytti siit brutaalia termi harppominen, mutta koska
minulle tulee siit mieleen vain pitkjalkaiset laihat
kumisaapasjalkaiset miehet niin kytn englanninkielist termi
(Ilkka, kyll min kyttisin edes "loikkimista", siit tulee edes
kengurut mieleen ;).

Eli idea on, ett kaikki muu tehdn joka framelle, mutta piirtminen
jtetn vliin jos ollaan "aikataulusta jljess". Niinp kun meill
on nyt ajastinrutiini voimme kytt tllaista systeemi:

plooppi

    ksitteleframe

    vhenn timerlaskuria

    jos timerlaskuri = 0

        piirr

    tai jos timerlaskuri < 0
      
        odota kunnes timerlaskuri >= 0

end plooppi


Eli itseasiassa edellisen luvun laskuria vhennetn itse peliss koko
ajan pyrkien pitmn se nollassa, mutta jos piirron aikana on ehtinyt
menn useampi frame sivu suun niin ksitelln framea ja vhennetn
timerlaskuria niin kauan ett ollaan taas saatu "kiinni" oikea tahti
ja voidaan pivitt seuraava ruutu. Mys toinen mahdollisuus, eli
"ylinopea" kone tytyy huomioida odottelemalla jos pyyhkistn jo
aikataulusta ohitse.

Valitettavasti tll alle 18.2:n framen nopeudet eivt toimi, joten
sellaisiin tapauksiin pit kehitell erikoisratkaisuja (fixed-point
-laskuri esimerkiksi, joka korottuu vain puolella joka vuoro tms.).

On mys muita mahdollisuuksia toteuttaa frameskip, kuten siirtmll
ksitteleframe -funktio suoraan timeriin, joka ei tosin mielestni ole
hyv ratkaisu, mutta joka toisaalta on tietyll tavalla selv
laskurien jdess pois. Mutta mit teetkn kun kesken ruudulle
piirron pivitetn aluksien paikkaa? Ei ole en kenestkn,
(varsinkaan pelaajasta) kivaa siin vaiheessa.

Toinen paljon toimivampi vaihtoehto on kytt kulunutta aikaa
iknkuin kertoimena tehtviss. Eli jos vaikka joka vuorolla pit
siirt sprite 1 eteenpin, niin siirretn joka framella sprite
1*kulunut aika verran eteenpin. Tm valitettavasti vaatii paljon
tihemmn ajastimen kutsun kuin sellaiset 70 kertaa sekunnissa
toimiakseen hyvin ja lisksi fixed-point -matikka on yleens aika
vlttmtn tmnkaltaisessa toteutuksessa.

Mutta, aihe ei ole vaikea ja varmasti osaat ptt minklaisen
toteutuksen teet itse peliisi. Min hivyn nukkumaan ja jtn sinut
oman onnesi nojaan. it!


Saatteeksi
----------

Nyt et ole en laama, vaan hatarasti pasiat osaava, toivottavasti innokas
peliohjelmoijan alku. Tmn dokumentin tarkoituksena on ollut saattaa sinut
vain alkuun. Ensimminen neuvoni aloittelevalle peliohjelmoijalle, eli sinul-
le on, ett l jt lukemistasi thn. Mikn ei korvaa tuhansia tunteja
tutoriaalien ja kokiksen ress vietettyj tunteja. Samaa asiaa voi opetella
useasta eri lhteest, jolloin tajuaa asiat paremmin, selkemmin ja syvemmin
kuin yhden tutoriaalin luvulla.

Toisena on se, ett englannin kieli on pakko opetella. Sen oppii parhaiten
sanakirjan kanssa tutoriaaleja kahlailemalla. Jos aiot prjt hyvin peli-
ohjelmoinnissa tytyy englantia osata. Jos et viel sit osaa, niin tys-
kentele oppiaksesi.

Kolmanneksi kaikkein trkeimpn ovat oman jrjen kytt, rautainen tahto
ja sammumaton tiedonhalu, sek ahkeruus. Maailma on tynn laamoja, jotka
eivt osaa mitn siksi koska eivt ole tosissaan yrittneet. Min aloi-
tin C-ohjelmoinnin vhn yli vuosi sitten ja pelkll kovalla yrittmisel-
l ja innostuneisuudellani opettelin koodaamaan. Olen lukenut tuhansia ja
tuhansia rivej ohjelmointiasiaa, enk ole viel sit joutunut katumaan.

MBnetist lytyy valtava mr lhdekoodia ja tutoriaaleja lhes kaikkiin
ohjelmoinnin haaroihin. Ennenkuin menett toivosi tai menet kysymn mis-
tn kahlaile sielt kaikki tarpeelliset alueet ("C/C++" ja "muut") lpi.
Melkein kaikkeen pitisi vastaus noista dokumenteista lyty (mist muual-
ta henkilt joilta kysytn olisivat ne saaneet selville kuin dokumenteista).

Tss muutama erityismaininnat ansaitseva tutoriaali / dokumentti / kirjasto,
jotka kannattanee imuroida pahan pivn varalle:

PCGPE10.ZIP  Jokaisen ohjelmoijan pakkoimurointi. Sekalainen kokoelma valit-
	     tuja paloja. Sislt 10 ensimmist Aphyxian traineria!
FMODDOC2.ZIP Kaikille nikorteista ja MOD-playereist kiinnostuineille 
	     hieman vaikea (nikortin ohjelmointi ei nimittin aina ole 
	     helppoa) tutoriaali sislten kaiken tarvittavan tiedon. Ly-
	     tyy jokaisen itsen kunnioittavan TosiKooderin kovalevylt.
HELPPC21.ZIP Mainio asioiden tarkistamiseen soveltuva lhdeteos.
HPC21_P5.ZIP Pivitys edelliseen sislten Pentium-kskyt.
TUT*.ZIP     Asphyxian VGA-trainerit. Etsi hakusanalla Asphyxia.
3DICA*.ZIP   3D ohjelmointitutoriaali by Ilkka Pelkonen. Suomeksi vielp!
DJTUT2_4.ZIP Selitt DJGPP:n AT&T-syntaksin ja inline-asseblerin 
	     englanniksi. Korvaamaton jos haluaa kytt assembleria DJGPP-
	     ohjelmissaan!

Lisksi lytyy kymmeni ohjelmointikirjastoja, joissa tulee lhdekoodi
mukana. Ja muista, ett itsekin voi tehd ptelmi ja kokeiluja. Kyll
aina jostain tarvittu tieto lytyy!
