Skip to content

Instantly share code, notes, and snippets.

@seece

seece/LAAMATUT.TXT

Created May 26, 2016
Embed
What would you like to do?
Laaman tie DJGPP-peliohjelmointiin versio 2.10. By Jokke / BAD KARMA
▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄
█▓▒░ ░█ █▓▒░ ░▒▓▒█ █▒░ ░▒▓▒░ █ █░ ░▒▓▒░█ █▓▒░ ░▒▓█ █ ░▒▓▒░ ░▒█
█▒░ ░▒█ █▒░ ░▒▓▒░█ █░ ░▒▓▒░ ░█ █ ░▒▓▒░ █ █▒░ ░▒▓▒█ █░▒▓▒░ ░▒▓█
█░ ░▒▓█ █▒░ ░▒▓▒░ ░█ █░ ░▒▓▒░ ░▒▓█ █░▒▓▒░ ░▒█▒░ ░▒▓▒░█ █▒▓▒░ ░▒▓▒█
█ ░▒▓▒█ █░ ░▒▓▒░ ░▒█ █ ░▒▓▒░ ░▒▓▒█ █▒▓▒░ ░▒▓▒░ ░▒▓▒░ █ █▒▓▒░ ░▒▓▒░ █
█░▒▓▒░█ █ ░▒▓██ ░▒▓█ █░▒▓▒░██▒▓▒░█ █▓▒░ ░▒▓▒░ ░▒▓▒░ ░█ █▓▒░ ░██▒░ ░█
█▒▓▒░ █ █░▒▓▒██░▒▓▒█ █▒▓▒░ ██▓▒░ ░██▒░ ░▒█▒░ ░▒█▒░ ░▒█ █▒░ ░▒██░ ░▒█
█▓▒░ ░█ █░▒▓▒░██▒▓▒░ █▒▓▒░ ░██▒░ ░▒██░ ░▒▓█░ ░▒▓█░ ░▒▓█ █░ ░▒▓██ ░▒▓█
█▒░ ░▒█▄▄▄█▒▓▒░ ░▒▓▒░ ░█▓▒░ ░▒▓▒░ ░▒▓██ ░▒▓▒█ ░▒▓▒█ ░▒▓▒██░ ░▒▓▒░ ░▒▓▒░█
█░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░██▒▓▒██░▒▓▒░██ ░▒▓▒░ ░▒▓▒░ █
█ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ██▓▒░██▒▓▒░ ██░▒▓▒░ ░▒▓▒░ ░█
█░▒▓▒░ ░▒▓▒░ ░▒▓██ ░▒▓▒░ ░▒▓▒░██▒▓▒░ ░▒▓▒░ ░██▒░ ██▓▒░ ░██▒▓▒░ ░██▒░ ░▒█
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀
Laaman tie DJGPP-peliohjelmointiin versio 2.10. By Jokke / BAD KARMA
Copyright (C) Joonas Pihlajamaa 1997. All rights reserved.
Sisällysluettelo:
1. Esittely
1.1 Disclaimer
1.2 Mistä uusin versio?
1.3 Huomattavaa lukijalle
1.4 Kenelle tämä on tarkoitettu?
1.5 Kreditsit
1.6 Versiohistoria
1.7 Yhteystiedot
1.8 Esimerkkien kääntäminen
2. Alkeet
2.1 DJGPP - vaikea, suuri, monimutkainen, omituinen, hidas?
2.2 Grafiikkaa - mitä se on?
2.3 Paletti - hörhelöhameita ja tanssia?
3. Peruskikkoja
3.1 Kaksoispuskuri - luonnonoikku, horoskooppi?
3.2 PCX-kuvien lataus - vain vähän oikaisemalla
4. Bittikartat ja animaatiot
4.1 Bitmapit - eikai vain suunnistusta?
4.2 Animaatiot
4.3 Pitääkö spriten törmätä? Entä coca-colan?
4.4 Maskatut spritet
5. Hieman kehittyneempää yleistavaraa
5.1 Näppäimistön käsittely - ja nyt meillä on hauskaa
5.2 Fixed point matematiikka
5.3 Lookup-tablet ja muita optimointivinkkejä
5.4 Väliaikatulokset ja fontteja
5.5 Hiirulainen, jokanörtin oma lemmikki
5.6 Tekstitilan käsittely suoraan
6. Projektinhallinta
6.1 Projektien hallinta - useat tiedostot
6.2 Useiden tiedostojen projektit - kääntäminen ja hallinta
6.3 Hieman automaatiota - tapaus Rhide
6.4 Todellista guruutta - salaperäinen make
6.5 Ammattimaista meininkiä - enginen teko
7. Kehittyneemmät yksityiskohdat
7.1 Vauhtia peliin - ulkoisen assyn käyttö
7.2 PIT - aikaa ja purkkaa
7.3 Miten peli toimii yhtä nopeasti kaikilla koneilla
7.4 Yleistä asiaa pelin levityksestä
7.5 Interpolointi ja viivoja
7.6 Vapaa skrollaus
7.7 Sinit ja kosinit sekä plasmaa
7.8 Paletin kvantisointi ja rekursio - Median cut
7.9 Lisää paletin kvantisointia - Local K Mean
7.10 VESA 2.0, rakenteet
7.11 VESA 2.0, ensimmäiset keskeytykset
7.12 Miten se todella pitäisi tehdä
8. Asioiden taustaa
8.1 Datatiedostot - miten?
8.2 Läpinäkyvyys ja sen vaihtoehto - shadebobit
8.3 Motion blur - sumeeta menoa
8.4 Vektorit pelimaailmassa
8.5 Musiikkijärjestelmistä
8.6 Plasma tekee comebackin - wobblerit
8.7 Prekalkattuja pintoja - ja tunneli
8.8 Lisää kivaa - zoomaus
8.9 Polygoneista ja niiden fillauksesta
8.10 Pari kivaa feikkimoodia
9. Liitteet, jatkeet ja muu roina
9.1 Saatteeksi
9.2 Hieman koodereiden jargonia
9.3 Lähteet
1.1 Disclaimer
--------------
Tämän dokumentin ja kaikkien muiden paketin tiedostojen tekijänoikeudet
kuuluvat Joonas Pihlajamaalle, ellei tiedostossa ole toisin ilmoitettu
ja nämä ehdot pätevät kaikkiin paketin tiedostoihin jotka eivät
sisällä erillisiä ehtoja tai joista ei ole näissä ehdoissa erikseen
mainittu. Paketin sisältämän materiaalin käyttö on sallittu vain
allaolevien ehtojen rajoissa. Jos käyttäjä ei hyväksy ehtoja tulee
hänen poistaa tämä paketti ja sen tiedostot. Paketin sisältämän
materiaalin käyttö tarkoittaa käyttäjän hyväksyneen levitysehdot.
Dokumentin levitys, monistus ja muu jakelu on sallittu vain
alkuperäisessä, muuttamattomassa muodossa, lukuunottamatta file_id.diz
-tiedostoa, joka voidaan halutessa uudelleennimetä .old- tai
.org-päätteiseksi ja lisätä uusi .diz-tiedosto, jotta kuvaus sopisi
levitettävän BBS-järjestelmän käyttämään formaattiin.
Minkäänlaista maksua ei saa periä lukuunottamatta kopiointi- ja
levityskustannuksia, niin kauan kuin niiden yhteenlaskettu summa ei
ylitä 20 suomen markkaa. Lähdekoodin käyttö on sallittu omissa
ohjelmissa, mutta ohjelman dokumentaatiossa täytyy mainita lähdekoodin
lähde. Tutoriaalin kautta opittu tieto on täysin vapaasti
sovellettavissa.
Tekijä ei ota minkäänlaista vastuuta paketin tiedostojen toiminnasta tai
tietojen oikeellisuudesta. Minkäänlaista takuuta tutoriaalin sisältämän
informaation käytännöllisyydestä ja virheettömyydestä ei anneta.
Jos paketti aiotaan sisällyttää jonkin suuren tiedostopalvelimen,
CD-ROM levyn tai muun vastaavan massalevitykseen tarkoitetun median
jonka oletetaan leviävän suuria määriä olisi tekijälle hyvä ilmoittaa
sähköpostitse tapahtumasta.
Tutoriaaliin liittyy myös rajoitettu tyytyväisyystakuu. Jos et jostain
syystä pidä tuotteesta voit poistaa sen määräämättömän ajan jälkeen
ohjelman asennuksesta. Vapautat kiintolevytilaasi ja saat ilman
erillistä maksua kokea tutoriaalin poistamisesta aiheutuvan henkisen
tyydytyksen.
Epäselvyydet, puutteet ja huomautukset disclaimerista pyydetään
lähettämään tekijälle.
1.2 Mistä uusin versio?
-----------------------
Tutoriaalin teko alkoi alunperin MBnetin FAQ-jahkailusta, kun veikkailtiin
tehtäisiinkö PC-Ohjelmointi -alueen kysymyksistä FAQ vai eikö. Minä päätin
sitten tehdä ainakin jotain ja niinpä uusin versio pitäisi olla aina
saatavilla MBnetistä PC-Ohjelmointi -alueelta. Alue tullaan jakamaan
jossain vaiheessa, mutta Apajalta se löytyy ainakin.
Lisäksi Laamatutin virallinen kotisivu löytyy osoitteen www.mbnet.fi/~jokke
alta. Tästä osoitteesta pitäisi myöskin löytyä Laamatutin uusin versio
nopeasti ja helposti (jopa nopeammin kuin mitä se tulee MBnettiin).
Tiedostonimi on aina LAAMAxyy.ZIP, jossa x on suurempi (major) versionumero
ja yy pienempi (minor). Pitäkäähän silmä tarkkana!
1.3 Huomattavaa lukijalle
-------------------------
Dokumentin koko on alkuperäisestä jo viisinkertaistunut ja public
betasta ei voine enää puhua. Silti kommentteja täydellisyyksistä,
virheistä ja puutteista tarvitaan ehkä jopa enemmän kuin beta-aikoina,
kun alue on liian laaja yksin tarkistettavaksi. Olen myös kiinnostunut
mahdollisista lisäjuttujen tekijöistä, jolloin luonnollisesti minun ei
tarvitse kirjoittaa kaikkea. Korvauksena pääset sitten kreditseihin ja
dokumenttisi julkaistaan tämän mukana.
Teemu Keinonen on jo osallistunut Laamatutin tekoon ja on näinollen
ansainnut erityiskiitokseni samat kiitokset kuuluvat myös 3D-starfield
-selostuksen tehneelle Erik Seesjärvelle. Heidän teoksensa löytyvät
myöskin tästä päähakemistosta nimillä LUVUT.TXT ja STARFLD.TXT. Herra
Seesjärvi koodaa nykyään kunniallisena ihmisenä 3D-engineä ja
pyynnöistä huolimatta starfield säilyy kunniakkaana osana
tutoriaalia. Lisäksi kiitoksen jo tässä ansaitsee Pekka Nurminen
lukuista tarkennuksista ja lisäehdotuksista joidenkin asioiden
suhteen, sekä Tero Kontkanen maanmainion "Laama"-logon teosta.
Eli kun törmäät johonkin epäselvyyteen, päällekkäisyyteen,
epäloogisuuteen, toistoon, virheeseen tai puutteeseen niin
ilmoittelehan heti minulle. Osoite tuolta tiedoston lopusta. Vastaan
postiin mahdollisuuksieni mukaan (vastaan siis jokaiseen ellen sitten
huku postiin). Jokainen kommentti tekee minut iloiseksi, sillä on aina
mukavaa nähdä jos joku on tutoriaalista hyötynyt.
Minulle saa lähettää viestimuotoisen kannustuksen lisäksi myös rahaa
ja 20 markkaa olisi oikein hauska yllätys joskus löytää postiluukusta,
tosin vähemmän ja enemmänkin voi halutessaan lähettää. =) Myös pelkkä
postikortti tai e-maili on mukavaa. Rahan takia en tätä tee, saldo
taitaa tähän mennessä olla yksi lahjoitus Erikiltä. :)
Jatkossa tulen julkaisemaan uusia versioita sitä mukaa kun asiaa tulee
lisää. Eli pidä silmä tarkkana ja mieli valppaana tutkiessasi
käyttämiesi purkkien tiedostoalueita. Uusimman version löytäminen on
selostettu tarkemmin luvussa 1.2. Muista, että Laamatutin levittäminen
on suorastaan toivottua muuttamattomassa muodossa, joten älä epäröi
lähettää sitä suosikkipurkkeihisi!
1.4 Kenelle tämä on tarkoitettu?
--------------------------------
Aloitin dokumentin kirjoittamisen Ilkka Pelkosen mainion suomenkielisen
3d-tutoriaalin innoittamana ja toivon, että tästä on hyötyä monille
aloitteleville peli/demokoodereille DJGPP:llä. Tutoriaali kattaa
DJGPP:n asennuksen ja monia grafiikkaohjelmoinnin perusniksejä, joskus
lähdekoodinkin kanssa. Myöhempi osa alkaa menemään pikkuhiljaa yhä
teoreettisemmalle tasolle eikä tahattomasti, sillä kunnon
ohjelmointiin kuuluu paljon muutakin kuin hardware-tuntemus. Pyrin
myös valaisemaan asioita jotka kuuluvat vähemmänkin peliohjelmointiin,
mutta joista ei kunnollista juttua mistään muualta ole saatavilla.
Ehdotuksia saa aina lähettää.
Lähtövaatimuksena tämän lukijalle on siedettävä matematiikan taito
(kertolaskut pitää olla hallussa, kuten myös jotkin muut
peruskäsitteet, kuten kokonaisluvut, desimaaliluvut jne.) Sekä
C-kielen taitaminen. Assemblerikin voi olla hyödyllinen. Tätä
kirjoittaessani en vielä tiedä millainen tutoriaalista tulee, joten
katsotaan nyt... Teemu Keinonen on kirjoittanut tähän tutoriaaliin
mainion pikku dokumentin lukujärjestelmistä ja bittioperaatioista, joten
jos et niitä vielä hallitse niin lue ensin tiedosto LUVUT.TXT!
Tutoriaalin esimerkit EIVÄT MISSÄÄN NIMESSÄ ole tarkoitettu
käytettäviksi peleissä suoraan. Niitä kyllä saa käyttää, mutta ne ovat
hitaita ja ne ovat esimerkkiohjelmia, eivät juuri yhdenlaiseen
pelityyppiin sopivia räätälöityjä rutiineja. Sitäpaitsi mikään ei
voita kokemusta ja kirjoittaessasi omat rutiinisi opit asian paremmin
kuin mitenkään muuten. Jos minä olisin käyttänyt muiden rutiineja niin
en olisi nyt tässä selittämässä ideaa niiden takana, vaan tekisin
alkeellisia pelejä, koska en osaisi muunnella muiden koodeja peleihini
sopiviksi. Eli tämä dokumentti ei kirjoita sinulle valmiiksi parhaita
ja sopivimpia rutiineja, vaan ainoastaan demonstroi mahdollisia
toteutustapoja, joka tulisi pitää mielessä dokumenttia lukiessa.
Huomaa myös, että tämän on kirjoittanut OikeaIhminen(tm), jolla on
myös Sähköpostiosoite, jolla voit ottaa häneen yhteyttä. Mikään ei ole
minulle mieluisampaa kuin nähdä, että edes joku on tyytyväinen tai
tyytymätön tähän tutoriaaliin.
Ja tietenkin koska olen oikea ihminen voit kysyä minulta epäselväksi
jääneitä kohtia ja katson voinko selventää tätä ja kenties lisään
vastauksen myös seuraavaan versioon tutoriaalista ja autat siten muita
aloittelijoita. Voit jopa saada nimesi jonnekin, ken tietää? Eli kun
tulee jotain mieleen niin mene dokumentin loppuun ja lue
yhteystietoni. Myös kirjoitusvirheistä, huonosta / hyvästä tekstistä
tai selvästä tekstistä kannattaa ilmoittaa, en nimittäin ole ainakaan
vielä lukenut tätä kokonaan lävitse (lukuunottamatta kun kirjoitin
tämän). Ja kaikki enemmän osaavat voivat ilmoittaa tarkennuksia ja
oikaisuja tutoriaalin tekstiin. =)
Koulutus Kokkolan Yhteislyseon lukiossa (eli lyhyemmin Länsipuistossa)
on nyt sitten viimein alkanut, jonka jälkeen edessä on jokin
teknillinen korkeakoulu ja DI:n arvo, jos luoja suo. :)
1.5 Kreditsit
-------------
Ennenkuin aloitamme, haluaisin tervehtiä joukkoa tuntemiani
henkilöitä. Tiedoksi kaikki MBnetin ohjelmointi-alueen lukijoille,
että ainakin yritin muistaa niin monta kuin vain mahdollista, jos
siis nimeäsi ei ole listassa ja tunnet sinne kuuluvasi niin ilmoittele!
Teemu Keinonen: Erityiskiitokset lukujärjestelmät -jutustasi!
Erik Seesjärvi: Kiitoksia starfieldistä ja onnea 3D-enginelle. =)
Pekka Nurminen: Kiitos mainiosta palautteesta ja avusta monessa asiassa.
Tero Kontkanen: Mahtava logo! Muistinpas vihdoin lisätä senkin.
Sami Kuronen: Alias pysyy, I hope. Jatka vain lukemista! ;)
Jyri Pieniniemi: Tällä dokumentilla voi olla laksatiivisia vaikutuksia!
Ilkka Pelkonen: Sinun takiasi jouduin tällaista kirjoittamaan... Tsemppiä!
Tommi Kemppainen: Koodaus, skene ja elämä. Pitääkö muuta sanoa?-)
Johan Brandt: Täytyyhän meidän nörttien pitää yhtä!
Asko Soukka: Onnea sen C++:ssan opettelun kanssa, toivottavasti onnistut!
Jari Karppanen: Filekamu, vain 2 vuotta myöhässä?-) Muistin nyt sinutkin!
Tero Karras: Jos joinaat Doomsdayhin niin katso, että Bad Karmaa greetataan!
Jere Sanisalo: Terveisiä vain sinnekin, toivottavasti Kaboomia on rekattu! ;)
Kaj Björklund: Toivon RC:n imevän monta sielua ja seuraavan version! :)
Aleksi Kallio: Näpit irti siitä Watcomista! DJGPP ja herneet 4ever!
Juhana Venäläinen: Hmm, kai tagisaarto ES:ää vastaan on vielä voimassa?-)
Marko Åkerberg: Menikö nimi oikein?-) BLAST 'EM RULEZ, JEE!!!
Jarmo Muukka: Miten ikinä JAKSAT kirjoittaa yli sadan rivinohjelmaesimerkkejä?
Jukka Vuokko: Huomentapäivää. Aiotko tehdä Emacsiin sprite-enginen?-)
Petteri Järvinen: Tsemiä autopeliin! Toivottavasti kirje saapui perille. :)
Ilja Bräysy: No toivottavasti sait jotain tolkkua jostakin =)
Henri Pyyny: Toivottavasti ette huku lumeen siellä Lapissa!
Lasse Laurila: Kyllä minä vielä saan sinut kirjoitetuissa messuissa kiinni!
Santeri Saarimaa: Yhä NNY?
Äiti&Isi: Mitä te tätä luette?!?
Tomi Jutila: Olet sinäkin siis päättänyt alkaa kooderiksi?-)
Timo Jutila: Quakee?!?!
Teemu Kellosalo: Älä vain väitä että aiot lukea tämän?
Kalle Liukkonen: Muistin sitten sinutkin. =) Shefun oikat hanskassa?-)
Juho Östman: No laitoinpas sinutkin tänne. Yllätyitkö?-)
The Pihlajamaa: Hemmetti, etunimi pääsi unohtumaan, tsemiä!
Viznut / PwP: Onko sinulla jokin oikea nimikin?-) No mitä tuosta...
Erityiskiitoksen ansaitsevat vielä koko MBnetin ylläpito, sillä ilman ko.
purkkia ei minulle olisi koskaan ollut mahdollista oppia niin paljon
ohjelmoinnista, että voisin kirjoittaa tämän. Näistäkin ylläpitäjistä
mainitsen vielä erikseen Jere Käpyahon, Tarmo Toikkasen ja Rasmus Wickholmin,
jotka ovat ahkerasti olleet mukana PC-Ohjelmointialueella. Kiitos!
1.6 Versiohistoria
------------------
Kehitystä on jälleen tapahtunut ja mikäs sen mukavampi paikka
nauttia niistä etukäteen kuin tämä luku. Uusi termikin on ilmaantunut,
"uusi tausta" tarkoittaa selostusta toiminnasta Asioiden taustaa
-osaan.
Versio 2.1:
+ Jälleen korjauksia, pitäisi alkaa olla jo aika virheetöntä
tavaraa, poistin //-kommentit ja kaikki mainit nyt tyyppiä int
+ Uusi luku VESA 2.0-rakenteista
+ Uusi luku VESA 2.0-keskeytyksistä
+ Uusi luku grafiikkaenginen teosta
+ Asioiden taustaa -osa, jossa kerron vain mikä on homman nimi,
koodia ei enää tipu
+ Uusi tausta datatiedostoista
+ Uusi tausta läpinäkyvyydestä ja shadebobeista
+ Uusi tausta motion blurrista
+ Uusi tausta vektoreista pelimaailmassa
+ Uusi tausta musiikkijärjestelmistä
+ Uusi tausta wobblereista
+ Uusi tausta tunneli-efektistä
+ Uusi tausta zoomauksesta
+ Uusi tausta polygoneista ja niiden fillauksest
+ Uusi tausta feikkimoodeista
Versio 2.01:
+ Joukko korjauksia enemmän tai vähemmän kriittisiin asioihin
+ Ei julkisessa levityksessä
Versio 2.0: The Joulu Edition Enhanced
+ Ei enää READJUST.NOW -tiedostoa
+ Vaikeaselkoisempi disclaimer-teksti
+ Pikku korjauksia materiaaliin ja joitakin tarkennuksia
+ Mahtava, tuore versionumero
+ Uusi, hieno ja selkeä lukujako ja joitain järjestelyjä
+ Uusi, laaja (?) slangisanasto
+ Lisää kiinnostavia ja selkeitä ohjelmaesimerkkejä
+ Uusi luku interpoloinnista ja viivanpiirrosta
+ Uusi luku skrollauksesta
+ Uusi luku sineistä, kosineista ja plasmasta
+ Uusi luku kvantisoinnista median cut -algoritmilla
+ Uusi luku kvantisoinnista local K mean -algoritmilla
Versio 1.3: Assembly-mix, jotain purtavaa myös demokoodereille
+ Tarkennuksia ja parannuksia VGA:n muistista kertovaan osaan
+ Lisää koodia pseudona bitmap-osuuteen ja muutenkin enemmän
selvennystä ko. kohtaan. Kiitoksia selvennyspyynnöistä.
+ Uusi luku useiden C-tiedostojen käytöstä
+ Uusi luku objekti- ja archive-tiedostojen teosta
+ Uusi luku Rhiden konffauksesta ja projektinhallinnasta
+ Uusi luku makefileiden käytöstä
+ Uusi luku enginen teosta
+ Uusi luku ulkoisen assyn käytöstä
+ Uusi luku timerin koukutuksesta C:llä
+ Uusi luku frameskipistä
+ EJS:n starfield-esimerkki ja -selostus.
Versio 1.2: Kesä-release, toinen julkisesti levitetty versio
+ Hiiren käsittely
+ Tekstitilan käsittely
+ Lisää korjauksia, kiitos ahkeran palautteen
Versio 1.1: Bugikorjaus-release, ei yleisesti levityksessä
+ Lukuisia korjauksia enemmän tai vähemmän vialliseen
tietoon siellä sun täällä tutoriaalissa
Versio 1.0: Ensimmäinen julkaistu versio
+ DJGPP:n asenuns
+ Grafiikka
+ Paletti
+ Kaksoispuskuri
+ PCX-kuvat
+ Bittikartat
+ Animaatiot
+ Spritet
+ Näppäimistö
+ Fixed point
+ Lookup-tablet
+ Fontit
+ Maskatut spritet
1.7 Yhteystiedot
----------------
Hyvä, olet siis päättänyt ottaa yhteyttä minuun. Yhteyden minuun saat
useallakin tavalla, mutta tässä ovat ne joita luultavimmin tarvitset:
www.mbnet.fi/~jokke/ sisältää minun, Bad Karman ja sen tuotosten, sekä
Laamatutin viralliset kotisivut sekä joukon linkkejä maailmalle (ainakin
jossain vaiheessa ;).
joonas.pihlajamaa@mbnet.fi on sähköpostiosoite, josta minut pitäisi saada
kiinni.
Joonas Pihlajamaa on käyttäjätunnukseni MBnetissä, jolle voit kirjoittaa
yksityispostiin. Ainakin tällä hetkellä luen viestini keskimäärin 3 kertaa
viikossa, joten vastaus pitäisi tulla viikon sisällä (ellen ole
lomailemassa tai paastolla koneestani ;).
Joonas Pihlajamaa
Säveltäjäntie 40
67300 Kokkola
Tämä on se osoite, jossa asun. Jos et aivan käymään viitsi tulla niin mikset
lähettäisi postikortilla terveisiä? Vastauksista kirjeisiin en tiedä, mutta
katsotaan nyt, ei ole ainakaan vielä tullut ainoatakaan kirjettä...
Kuulun gruuppiin BAD KARMA, joka tekee tällä hetkellä peliä nimeltään
SLiDER: Roadkill, joka on autopeli ja sen on tarkoitus hakata Slicks 'n'
Slide sekä muut vastaavat pelit mennen tullen. Kannattaa tutkia tarkasti
purkkien tiedostoalueita, jos vaikka ilmestyisi. Ilmestymisajankohta
on luultavasti (ensi?-) vuosituhannen loppupuolella.
1.8 Esimerkkien kääntäminen
---------------------------
Tutoriaalin mukana seuraa sankka joukko esimerkkiohjelmia ja ne
löytyvät hakemistosta EXAMPLE. Jos sinulla on 'make', niin kääntö
sujuu yksinkertaisesti menemällä esimerkkikoodit sisältävään
hakemistoon ja ajamalla komennon 'make' ja sen jälkeen 'make test.exe'
jos sinulla on NASM. 'make clean' / 'make realclean' vastaavasti
tyhjentävät objektitiedostot / objekti- ja exetiedostot.
Kiitoksia Tero Kontkaselle makefile-esimerkistä. Tein sen pohjalta nyt
uuden, koska esimerkkiohjelmia oli tullut jonkin verran lisää. Jos
sinulla ei ole 'make'-ohjelmaa onnistuu kääntäminen käsinkin. Lähes
kaikki tiedostot ovat itsenäisiä eivätkä tarvitse muita
objektitiedostoja tai kirjastoja toimiakseen. Poikkeuksina
timertst.exe joka tarvitsee sekä timer.c:n ja timertst.c:n käännettynä
ja test.exe, joka tarvitsee test.asm:n ja test.c:n käännettynä.
Hauskaa kokeilua, minä menen nukkumaan!
2.1 DJGPP - vaikea, suuri, monimutkainen, omituinen, hidas?
-----------------------------------------------------------
Tutoriaali sivuaa koko ajan DJ Delorien ilmaista Gnu-kääntäjää
DOS:ille, eli DJGPP:tä, erityisesti sen kakkosversiota. Itse siirryin
puolessa välissä tätä tutoriaalia 2.0 -versiosta versioon 2.01 ja
luulisin, että esimerkit toimivat molemmilla näistä versioista ja
luultavasti uudemmillakin. Vanhemmat versiot eivät luultavastikaan
toimi näiden lähdekoodien kanssa.
Tämän mahtavan ilmaiskääntäjän löydät esimerkiksi internetistä
osoitteesta ftp://x2ftp.oulu.fi jostain
pub/msdos/programming-hakemiston alihakemistosta. Sen saa myös
MBnetistä, tarvittavat tiedostot ovat alueella PC-Ohjelmointi (area
8), tiedostoja on useita, ja ne löytyvät ko. alueelta löytyvästä
MBNETDJ2.TXT:stä. Myös kaikille Mikrobitin tilaajille tullut Huvi &
Hyötyromppu sisältää tämän kääntäjän hakemistossa MIKROBIT\DJGPP201\,
tosin sieltä puuttuu LGP2721B.ZIP (tarvitaan C++ koodin kääntämisessä),
jonka Käpyaho 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
jälkeen lisäät 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 tekemällä pienen C-ohjelman (vaikka
koe.c) ja kirjoittamalla:
GCC koe.c -o koe.exe
Lisää infoa GCC:n käännösoptioista ja kääntäjästä saat kirjoittamalla:
INFO GCC
Suosittelisin että lueskelet DJGPP:n dokumentaatiota ja teet tässä
vaiheessa paljon testiohjelmia ja opettelet käyttämään
info-lukijaa. Hyödyllinen hankinta on myös Rhide, joka on IDE
DJGPP:lle. Ohjelma löytyy MBnetistä alueelta 8 (ETSI RHIDE) sekä
H&H-Rompulta. Kun tunnet osaavasi käyttää vaivattomasti kääntäjää
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
selittämään kaikkein yksinkertaisimpia asioita esimerkkikoodeissa
taikka kommentoimaan liiemmälti koodia.
2.2 Grafiikkaa - mitä se on?
----------------------------
No olet siis päättänyt edetä seuraavaan aiheeseen, joka näyttäisi
olevan grafiikan ohjelmointi DJGPP:llä. Aloittakaamme siis! Tiedoksi
nyt etukäteen, että muistiosoitteet ovat heksoina, vaikkei sitä
ilmoitetakaan.
Esimerkkinä käytän VGA:n perusmoodia, 13h (heksaluku, desimaalina
19), joka on erittäin helppokäyttöinen. Kun tarvitset muita moodeja
sinulla on varmasti jo tarpeeksi taitoa hankkia itse informaatiota,
mutta tämän neuvon ihan alusta alkaen.
Eli olipa kerran PC, jossa oli 16-bittinen muistiväylä, 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 muistiväylä 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 lisäämällä siihen
OFFSET. (B800:0000 = B800*16+0000 = B8000) Ja kun kummatkin olivat
16-bittisiä lukuja saatiin näin 20-bittinen siirrososoite. Ja koska 20
bitillä voi ilmoittaa täsmäälleen kaksi potensiin 20 eri arvoa oli
maksimimäärä mitä voidaan osoittaa 1 megatavu. Kymmenen ensimmäistä
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 kehittyivät ja esiteltiin suojattu tila, eli PROTECTED
MODE (PM), joka käsitteli 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 käsiteltiin. DJGPP, joka on
suojatun tilan kääntäjä esim. antaa ohjelmalle alussa 2 selektoria, toinen
osoittaa dataan ja toinen koodiin. Tästä pidemmälle 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. Meidän täytyy ensin löytää oikea
selektori, jonka osoittama looginen muistialue vastaisi PC:n
perusmuistia. Ja tällainen löytyykin nimellä _dos_ds. Tämän selektorin
osoittaman muistialueen 0. tavu on perusmuistin 0. tavu, 1. tavu on
perusmuistin 1. tavu ja niin jatkuu edelleen, kunnes tavu numero A0000
on ensimmäinen 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 vähintään 5:llä
eri tavalla, mutta perehdymme helpompaan. Kirjaston sys/farptr.h
funktioon _farpokeb(selektori, siirrososoite, tavu), jolla pääsemme
käsiksi tuonne. Normaalin pointterin tekohan ei onnistu, vaan meillä
pitää olla funktio, joka kykenee osoittamaan toisen selektorin
alueelle.
Näinollen esimerkkiohjelma, joka asettaa VGA-muistin 235. tavun arvoon
100 on tämän näköinen (PIXEL1.C):
#include <go32.h> /* muistathan, _dos_ds on määritelty täällä! */
#include <sys/farptr.h> /* täältä löytyy _farpokeb */
int main() {
int selektori=_dos_ds,
siirros=0xA0000 + 235,
arvo=100;
_farpokeb( selektori, siirros, arvo );
return 0;
}
Arvaan, että ehkä menit ja kokeilit tuota ja petyit, kun mitään ei
tapahtunutkaan. Ei se mitään, niin pitääkin tapahtua, sillä olimme
tekstitilassa. Jotta jotain tapahtuisi meidän pitää olla oikeassa
tilassa, joka oli siis 0x13 (heksanumero 13 C:ssä, desimaalimuodossa
19). Tämän tilan rakenne onkin seuraava mihin perehdymme. Ole huoleti,
valitsin tämän tilan, sillä se on KAIKKEIN yksinkertaisin tila
PC-yhteensopivalla tietokoneella. Resoluutio on 320 riviä vaakatasossa
ja 200 pystytasossa. Jokaista pikseliä merkitään yhdellä tavulla, eli
sillä voi olla 256 erilaista arvoa. Näyttö alkaa aivan ruudun
vasemmasta yläkulmasta (miksi? sitä ei kukaan oikein tiedä, menee
filosofiaksi) ja jatkuu tavu tavulta (pikseli pikseliltä) päättyen
lopulta oikeaan alakulmaan. Eli ensimmäiset 320 tavua ovat ensimmäisen
rivin kaikki vaakatasossa olevat pikselit, sitten seuraavat 320 ovat
toisen rivin pikselit, kunnes lopulta ollaan ruudun alakulmassa.
Ja kun muistamme, että ensimmäinen tavu on kohdassa A0000 (heksa siis
tämäkin), eli 0 tavua alusta eteenpäin, niin me voimmekin tehdä hienon
kaavion:
Pikselit: Sijainti:
..........................
0...319 1. rivi
320...639 2. rivi
...
63680...63999 200. rivi
Näin 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 yhdistämme tietomme: VGA:n muisti sijaitsee selektorissa _dos_ds,
alkaen osoitteesta A0000 (heksa, C:ssä 0xA0000) ja siitä lähtee 64000
tavua, joka on näyttömuisti. Pikselin osoite tässä 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 väriä ja resoluutio 320 x 200.
Mutta miten pääsemme sinne? Vastaus on helppo: conio.h:n funktiolla
textmode(moodi)! Ja kun vielä yhdistämme tähän funktion getch(), joka
odottaa napinpainallusta (löytyy myöskin 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() */
int 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);
return 0;
}
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 tästä pikselinsytytyksestä makron
#define-komennolla. Tämä ei hidasta ohjelmaa yhtään, mutta varmasti
selventää koodia. Se määrittelee makron putpixel(x, y, c), jonka
kääntäjä muuttaa käännösvaiheeksa _farpokeb-funktioksi. x tarkoittaa
saraketta väliltä 0-319 ja y riviä väliltä 0-199, sekä c väriä väliltä
0-255. Muista, että vaikka teetkin makron sinun pitää silti
sisällyttää mukaan kirjastot sys/farptr.h ja go32.h! Sulut makron
farpokeb-funktion muuttujien x ja y ympärillä selittyvät 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 tietenkään ole haluttu tulos, sillä
40+a pitää käsitellä ennen sijoitusta, eli sulut vain ympärille! Tässä
se siis on:
#define putpixel(x, y, c) _farpokeb( _dos_ds, 0xA0000+(y)*320+(x), c)
Kun haluat käyttää sitä, niin teet vaikka seuraavanlaisen
koodinpätkän (PIXEL3.C):
#include <sys/farptr.h>
#include <go32.h>
#include <conio.h> /* textmode(moodi) ja getch() löytyvät täältä! */
#define putpixel(x, y, c) _farpokeb( _dos_ds, 0xA0000+y*320+x, c)
int main() {
textmode(0x13);
putpixel(319, 199, 150);
getch();
textmode(0x3);
return 0;
}
Ohjelma sytyttää pikselin aivan ruudun alareunaan. Jos et enää muista,
miten ohjelma käännettiin DJGPP:llä, on tämän kokeilemiseksi
tarvittava komento: "GCC PIXEL3.C -o PIXEL3.EXE" ja sitten kokeilu
komennolla "PIXEL3".
Painu nyt kokeilemaan ohjelmaa ja muuntelemaan sitä! Laita se tekemään
ruksi, pystyviiva, vaakaviiva, tai vaikka ympyrä jos osaat, tai
yhdistä se randomin kanssa ja tee näytönsäästäjä! Kokeilemalla tulet
parhaiten sinuiksi uuden asian kanssa. Ja kun olet valmis, siirrymme
seuraavaan aiheeseen, palettiin.
2.3 Paletti - hörhelöhameita ja tanssia?
----------------------------------------
Kuten edellisessä luvussa opimme, voi tilassa 13h olla 256 erilaista
väriä. Teit ehkä jo ohjelman, joka piirtää pikselin jokaisella värillä
viivaa ja huomasit, että käytössä olevat värit ovat huonoja,
puuttelisia, kirkkaita, tummia tai muuten vain inhottavia. Mutta ei
hätää - niitä voi muuttaa! Ja vaikka paletissa ei mielestäsi olisikaan
mitään vikaa haluat ehkä tehdä sellaisia efektejä kuten häivytys,
plasma, "crossfade" (toinen kuva ilmestyy toisen alta pikkuhiljaa)...
Näissä kaikissa tarvitaan enemmän tai vähemmän itse tehtyä palettia ja
siksi meidän pitääkin opetella nämä asiat ennenkuin menemme pidemmälle.
Kaiken ytimenä on VGA ja sen paletti, etenkin sen asettaminen, mutta
ehkä myös sen lukeminen. Tässä luvussa teemme funktiot, yhden tai
useamman värin, asettamiseen ja lukemiseen, sekä tutustumme
paletinpyöritykseen (palette rotation).
Ensin taas vähän teoriaa efektien ja paletin takana. Kuten ehkä
tiedätkin, valo voidaan koostaa komponenteista. Tietokoneella
jokaisella värillä on yleensä kolme komponenttia: punainen, vihreä ja
sininen (red, green, blue). Tätä kutsutaan nimellä RGB. Itseasiassa
jokainen moodin 13h väri on vain osoite taulukkoon, jonka jokainen
alkio sisältää värin punaisen, virheän ja sinisen komponentin määrän,
eli vahvuuden.
Jos meillä olisi puhtaan punainen väri, 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
täysi määrä punaista ja sinistä). Jos taas haluaisimme tumman punaisen
värin, olisivat sen väriarvot vaikka 30, 0, 0. Koska 30 on vähemmän
kuin puolet kirkkaan punaisen puna-arvosta, on tämä väri siis yli
puolet tummempi! Helppoa! Ja miksi maksimimäärä on vain 63? Siksi,
koska VGA:n rekistereissä värille on varattuna vain 6 bittiä, jolla
voidaan esittää numerot välillä 0...63. Tämä joudutaan huomioimaan
esim. PCX:n paletin latauksessa, sillä siinä värit ovat välillä
0...255. Tässä joudutaan jakamaan väriarvot neljällä, jotta saadaan
toimiva luku.
Eli ymmärrämme nyt, että jokaisella värillä 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) väriä helpolla joukolla komentoja. Meidän tarvitsee
vain kirjoittaa asetettavan värin 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. Tämän jälkeen VGA korottaa väri-indeksiä automaattisesti
yhdellä, eli jos ensin syötämme porttiin 3C8h värinumeron 5 ja sitten
punaisen, virheän ja sinisen porttiin 3C9h korottuu VGA:n sisäinen
laskuri yhdellä, ja voimme halutessamme tunkea heti seuraavan värin
RGB arvot porttiin 3C9.
Nyt olemme jauhaneet teoriaa tarpeeksi. Menkäämme pikkuiseen
esimerkkiin. Esittelemme tietorakenteen RGB, joka sisältää värin
RGB-arvot ja sitten funktion, jolle annetaan parametrinä osoitin
tällaiseen rakenteeseen ja värin numero jolle nämä väriarvot
asetetaan. Myöhemmin yhdistämme tämän 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 ensimmäisenä portin numero ja sitten sinne syötettävä
tavu. Funktion käyttämiseksi sisällytät mukaan kirjaston dos.h.
Ehkä sinua kiinnostaisi myös tämän käyttö? No olkoon, tehkäämme
esimerkkiohjelma kokonaisuudessaan. Kun edellinen pikku koodinpätkä on
nimellä PALETTE.H, voimme helposti sisällyttää sen seuraavaan
esimerkkiohjelmaamme kuten ihan tavallisen kirjaston. Muista vain,
että kirjaston täytyy olla samassa hakemistossa ohjelman kanssa,
muuten ei esimerkki käänny. Eli tässä sitten itse koodiosa, joka
tuikkaa keskelle ruutua värin 50. Sitten se odottaa napinpainallusta
ja muuttaa funktiollamme värin punaiseksi. Huomaa, että vain alussa
kajotaan näyttömuistiin. Toinen kohta hoidetaan värinvaihdolla!
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)
int 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);
return 0;
}
Seuraavana huomionkohteenamme onkin sitten väriarvojen luku, joka on
yhtä suoraviivaista kuin edellinenkin (tosin tarpeellisuus on
kyseenalaista, tätä ei tarvitse jos on itse asettanut paletin).
Erotuksena on, että väriarvo kirjoitetaankin porttiin 3C7h ja portista
3C9h _luetaan_ värin arvo. Jälleen tripletin (kolme alkiota, RGB) luvun
jälkeen indeksi kohoaa, joten voisimme lukea seuraavat värit. Luku
portista tapahtuu funktiolla inportb(portti). Muuta tietoa emme
tarvitsekaan.
Lisätkäämme nyt kirjastoomme (PALETTE.H) kolme uutta funktiota.
getcolor(int index, RGB *color) lukee värin <index> väriarvot ja
asettaa ne RGB-rakenteeseen <color>. setpal(char *palette) asettaa
koko paletin kerralla hyväksikäyttäen automaattista indeksin korotusta
(indeksi nollataan aluksi ja syötetään koko data perään, indeksi
korottuu jokaisen rgb-arvon jälkeen). getpal(char *palette) taas lukee
vastaavasti koko paletin. Niiden käytöstä 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. Tämä siksi, että koko paletti on huomattavasti
helpompi käsitellä näin. Jos olet sitä mieltä, että RGB oli parempi
tai haluat muuttaa loputkin pointtereiksi, en sitä
estä. Char-pointteriversiossa on aina kolme tavua peräkkäin
ilmoittamassa RGB-triplettiä. Toisen värin r alkaa siis 4. tavusta,
eli indeksistä 3. Jos haluat jonkin värin r-arvon, niin lasket:
"palette[number*3+0]". Vihreällä korotat tuota yhdellä (number*3+1) ja
sinisen kanssa kahdella. Helppoa tämäkin.
Nyt on kaikki tärkein katettu VGA:n paletista, joten kysytkin ehkä
(aina sinä sitten olet kysymässä ;) mihin näitä nyt sitten voi
käyttää. Itseasiassa paletilla on loputtomasti
käyttömahdollisuuksia. Ensimmäinen on 256-väristen kuvien paletin
asettaminen, sillä väärällä paletilla kuvat yleensä näyttävät enemmän
tai vähemmän sotkulta. Toisena on häivytysefekti, sekä feidaus
valkoiseen. Palettiliutuksesta käytetään usein termiä feidaus, joka
tarkoittaa, että palettia liutetaan sävy sävyltä toiseen väriin,
jolloin saadaan vaikka hieno ruudun tummeneminen. Kokeilemmekin sitä
ihan kohta, kunhan selitän vielä yhden efektin, palettirotaation.
Palettirotaatiossa on paletti, jonka väriarvoja pyöritetään
ympäri. Eli käytännössä väri, joka ennen oli numerolla 5 onkin
rotaation jälkeen värinumerossa 6. Tätä jatketaan koko ajan, ja väri
matkaa koko paletin lävitse, ja kun se on lopussa niin se siirretään
paletin alkuun. Yleensä väriä 0 ei kuitenkaan siirretä, sillä se on
taustaväri ja yleensä musta. Usein käytetään myös palettia, jossa on
useampia värejä kuin 256, jolloin erona on vain se, että ainoastaan
osa väreistä näkyy ruudulla.
"JA MIHIN TÄTÄ", kuulen sinun kysyvän. Olet kenties nähnyt plasman,
jonka värit vaihtuvat koko ajan (kunnon plasmassa on kyllä lisäksi
mukana muutakin kuin pyörivä paletti, mutta pyörityksellä saadaan
kummasti lisäeloa muuten liikkuvaan plasmaan). Tai tunnelin, jossa
värit siirtyvät kauemmaksi tai lähemmäksi. Tällaisia efektejä voidaan
helposti toteuttaa palettirotaatiolla. Ennenkuin ymmärrät voit ehkä
tarvita pienen demonstraation. Kohta teemmekin esimerkin, joka piirtää
vaakatasossa viivoja, jokainen eri värillä alkaen yhdestä päättyen
255:teen. Sitten teemme hienon liukupaletin ja alamme pyörittämään
sitä. Eli tehkäämme vielä funktio (lisätään kirjastoon PALETTE.H):
void rotatepal(int startcolor, int endcolor, char *pal) {
char r, g, b;
int c;
r=pal[startcolor*3+0]; /* tallennamme ensimmäiset värit ja siirrämme */
g=pal[startcolor*3+1]; /* ne lopuksi loppuun. Tämä paletti pyörii siten, */
b=pal[startcolor*3+2]; /* että viimeinen väri kulkeutuu kohti alkua */
for(c=startcolor*3; c<endcolor*3; c++)
pal[c]=pal[c+3]; /* muista, että uusi väri on kolmen välein,
sillä välissähän on aina kolme tavua, r,
g ja b, joita ei saa sekoittaa, muuten
saisimme aikaan vaikkapa sinisen paloauton!
(kiinnostava tavoite sinänsä) */
pal[endcolor*3+0]=r;
pal[endcolor*3+1]=g;
pal[endcolor*3+2]=b;
}
Vielä ennen esimerkkiä tarvitsemme yhden rutiinin, joka tekee
efektistämme edes jotenkin siedettävän. Palettia pitää nimittäin
vaihtaa ennen kuin ruudulle aletaan piirtää, tai muuten voi edessä
olla aika huonolaatuinen efekti (normaalipaletissa ei ole mitään
väriliukuja). Varsinkin näin yksinkertaisessa ohjelmassa voi nopealla
näytönohjaimella/koneella nopeus olla liiankin suuri, joten hidastamme
vähän rutiinia odottamalla signaalia, jonka VGA antaa päästessään
ruudun loppuun ja lähtiessään palaamaan yläkulmaan aloittaakseen taas
piirron. Tähän teemme funktion, joka odottaa kunnes piirto on valmis
ja kuvaruudulle voi kopioida pelkäämättä kesken piirron muutoksia
tehdessä aiheutuvia ongelmia. Lisätkäämme 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
pyöritti palettia. Huomaa funktio genpal(char *palette), joka asettaa
paletin liukuväreillä tehdyksi, sekä waitsync()-funktion käyttö
(kokeile vaikka ilman waitsync():iä, niin näet eron)! Eli tässä 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++) { /* LILA (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--;
}
}
int 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);
return 0;
}
Huomasit varmaan, että ruudun onnettoman geometrian takia kaikki värit
EIVÄT mahtuneet ruudulle. No niin. Ja mitäs kivaa seuraavaksi?
Seuraavaksi tutustumme viimeiseen palettikikkaan, jonka periaatteen
olet jo voinut keksiäkin, eli feidauksen.
Genpal-funktio olisi voinut käyttää myös erillistä rutiinia jolle
annetaan parametreina monenko värin matkalla liu'utaan väristä toiseen.
Kuitenkin koska tuo oli yksinkertaisemman näköinen tein sen tuolla
tapaa.
Teemme minimaalisia lisäyksiä PALETTE.H:hon, sekä pikkuisen
esimerkkiohjelman, joka demonstroi efektiä käytännössä. Ideahan on
erittäin yksinkertainen. Meillä on paletti, jossa on sekailaisia
värejä ja haluamme häivyttää sen. Miten? No tietenkin muuttamalla
ruudun mustaksi. Miten se tapahtuu? Nollaamme jokaisen värin, mutta
emme kerralla, vaan vähennämme joka kierroksella ja asetamme uuden
paletin. Tästä funktiosta voit tehdä helposti muitakin efektejä,
kuten feidauksen valkoiseen (korotetaan jokaista väriä joka
kierroksella kunnes ollaan värissä 63) tai vaikka paletista toiseen
(jos kohdepaletin vastaava komponentti on suurempi niin korotetaan
arvoa, jos pienempi niin vähennetään). Esittelen tässä vain
häivytyksen, mutta löydät kirjastosta PALETTE.H toteutettuna myös
valkoiseen ja toiseen palettiin feidauksen. Voit myös itse tehdä
hauskoja efektejä, kuten feidata valkoiseen, tehdä valkoisen paletin
ja feidata sen mustaan. Kokeile! Mutta, tässä 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 yhdistämme efektin lopuksi edelliseen esimerkkiohjelmaamme
lisäämällä sen juuri ennen tekstitilaan vaihtoa:
fadetoblack(palette);
Kokonaisuudessaan ja toimivana, vanhat osat mukana on esimerkkimme
tiedostossa PAL3.C. Siihen on tehty myös pari muuta muutosta, kuten
se, että aluksi paletti feidataan valkoiseen, asetetaan oikeasti val-
koiseksi (muuten feidatessa mustaan paletti välähtää hetken normaaliväri-
senä, tätäkin SAA kokeilla).
No niin. Pahin tiedonnälkäsi lienee tältä erältä tyydytetty! Viihdy
esimerkkien parissa ja tee mitä vain mieleen juolahtaa niillä. Muista,
että palettifunktiot toimivat myös tekstitilassa. Tämän voit kokeilla
vaikka käyttämällä fadetoblack-funktiota. Muista kuitenkin laittaa
loppuun textmode(0x3), vaikket moodia olisi vaihtanutkaan, sillä et
välttämättä pidä DOS-kehotteestasi jokainen väri mustana...
3.1 Kaksoispuskuri - luonnonoikku, horoskooppi?
-----------------------------------------------
No niin, olet näemmä sulattanut jo kaiken edellisen tiedon. Mainiota!
Tänään pääsemme (tai miten nyt haluamme asian ilmaista) yhteen
peliohjelmoinnin perustempuista, kaksoispuskuriin. Periaate tämän
takana on aivan naurettavan yksinkertainen, ja itseasiassa minä opin
tämän erään lehden lähdekoodia vilkaisemalla (Mikrobitin
grafiikkaohjelmointikurssi, numero 11/95). Eli tähän asti olemme
tunkeneet grafiikkaamme suoraan näyttöpuskuriin tavu
kerrallaan. Valitettavasti tässä on haittoja. Ensimmäisenä on se, että
meillä on kiire. Nimittäin käytössä on vain lyhyt aika kun näyttöä ei
piirretä monitorille ja jos siinä ajassa ei ehdä piirtää näyttöä niin
näyttö alkaa välkkymään, ilmestyy lumisadetta (varsinkin paletinvaihdon
kanssa!) ja muitakin ei-toivottavia ilmiöitä esiintyy.
Lisäksi on todettava valitettava tosiasia: Näyttömuisti on
HIDASTA. Jos haluamme tehdä sen kaikkein tehokkaimmin niin kopioimme
kaiken tavaran kerralla näytölle. Eli sen sijaan, että läiskisimme
pikseleitä sinne, toisia tänne kopioimme tavaran näytölle näytön
alusta loppuun neljän tavun (kaksoissana) kokoisina palasina. Mutta
miten saamme ruudulle pikseleitä sinne tänne, kun kaikki pitäisi
kopioida kerralla? Vastaus on, että käytämme kaksoispuskuria!
Kaksoispuskuri, englanniksi doublebuffer on saman kokoinen kuin
näyttömuisti, mutta sille on varattu tilaa keskusmuistista, joten se
on nopeampaa kuin hidas, kortilla sijaitseva näyttömuisti (näin vain
on, uskokaa pois). Sinne pikselinpiirto tapahtuu huomattavasti
sutjakammin, ja kaiken lisäksi meillä ei ole mitään kiirettä. Vaikka
piirrämme uuden pikselin, ei se näy näytöllä ennenkuin kaksoispuskuri
on kopioitu, eli flipattu näyttömuistiin.
DJGPP:llä näyttömuisti varataan vaikka malloc-käskyllä ja vapautetaan
suorituksen loppuessa free-käskyllä. Kokoa pitää puskurilla olla
tilassa 13h 64000 tavua. Eroja oikeaan näyttömuistiin
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
näyttökortin ja VGA:n rakenteen vika.)
- Se ei näy ruudulla ennenkuin käsketään.
- Se ei vilku.
- Se säilyy muistissa vaikka käytäisiin tekstitilassa.
- Paljon muuta kivaa.
Voit käyttää myös dynaamisen muistinvarauksen (malloc tai C++:ssalla
new-operaattori) tilalla taulukkoa, kuten joissakin esimerkeissä on
tehty, tällöin käytät muotoa char dblbuf[64000] (tai unsigned
char...). Mallocin käyttö on kuitenkin suositeltavampaa kuin tällainen
valtavien taulukoiden ottaminen pinosta.
Muttamutta, tarvitsisimme esimerkin. Mistä saamme sellaisen? No tässä
pieni esimerkki. Mukana on makro flip(char *buffer), joka kopioi 64000
tavua puskuria näyttömuistiin DJGPP:n _dosmemputl-komennolla, joka
löytyy kirjastosta sys/movedata.h ja tarvitsee myös _dos_ds:ää ja
siten kirjastoa go32.h. Eli tässä tällaista (DOUBLE1.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;
flip(dblbuf);
}
getch();
textmode(0x3);
return 0;
}
Kokeile myös ohjelmaa DOUBLE2.C, joka on toteutettu ilman
kaksoispuskurointia, jos eroa ei vielä huomaa, tulee se
joka tapauksessa vielä esiin, ja on muitakin hyödyllisiä asioita missä
kaksoispuskuri, tai kolmoispuskurikin on tarpeen. Mutta, kokeile tämän
käyttöä ja palaa tämän dokumentin pariin VASTA kun osaat täydellisesti
kaksoispuskurin käytön (oikeammin ymmärrät miten se toimii, miten sitä
käytetään, mihin se perustuu ja miten siihen piirretään
pisteitä). Sitten syöksymmekin uuteen tuntemattomaan. Katsotaan nyt
mihin...
3.2 PCX-kuvien lataus - vain vähän oikaisemalla
-----------------------------------------------
Noniin, kaikki wannabe gamekooderit. Nyt on aika mennä vaikeimpaan
aiheeseemme, johon monen kooderin taidot ovat viimein tyssänneet ja
jota minäkään en vielä täysin ymmärrä, enkä tiedä osaanko sitä
selittää.
Se on hyväuskoisuus, sillä PCX:n sisältä löytyy looginen ja helposti
ymmärrettävä rakenne. Ja vaikkei sitäkään täysin ymmärrä, voi
aina vain käyttää samaa rutiinia (kuten minä) PCX:n lataamiseen.
Esittelenkin tässä kappaleessa lyhyesti tämän yhden yleisimmistä
kuvaformaateista olevan tiedostotyypin saloja. 256-värisen tyypillisen
PCX:n rakenne voidaan jakaa karkeasti neljään (4) osaan:
- 128 ensimmäistä tavua headeria sisältäen info kuvasta
- kuvadata RLE-pakattuna
- salaperäinen tavu 12
- palettidata, viimeiset 768 tavua
Ensimmäisenä ja kaikkein vaikeimpana on headeri, jonka loikimme lähes
kokonaan yli, sillä tosipelikooderi tietää lataavansa oikeaa
PCX-kuvaa, joka on oikeaa formaattia oikeankokoiseen puskuriin ja
jättää selittämättömät kaatumiset muiden harteille! Tai itseasiassa en
sitä selitä kun en siihen ole perehtynyt syvemmin. Kiinnostuneille
PCGPE:ssä on tämäkin formaatti selitettynä lahjakkaan kryptisesti
englannin kamalalla mongerruksella. Kaikki sitä haluavat hankkivat sitten
tiedoston PCGPE10.ZIP, joka sisältää kaikkea hyödyllistä
peliohjelmointiasiaa, englanniksi siis.
Headerista tahdomme tietää vain sen, että PCX-kuvan koko lasketaan
seuraavasti:
- Mennään offsettiin 8 (fseek(handle, 8, SEEK_SET)).
- Luetaan kaksi tavua ja tehdään niistä sana (unsigned short int,
katsomme latauskoodia kohta) ja meillä on koko vaakatasossa.
- Luetaan toiset kaksi tavua ja tehdään 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 peräkkäin 10 pikseliä väriä 15 emme kirjoitakaan PCX:ään
kymmentä kertaa numeroa 15, vaan kirjoitamme sinne tavun 192+10=202 ja
sen perään tavun 15. Nyt kun PCX-lukija lukee ensimmäisen tavun se
katsoo, että ahaa, nyt tulee toistoa ja toistaa seuraavaa tavua
puskuriin tavu-192 kertaa (202-192=10). Näin 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.
Näin helppoa, nyt vielä paletti. Sekin on helppoa, kunhan muistamme
kaksi seikkaa:
1) Etsimme paletin tiedoston LOPUSTA päin (fseek(handle, -768, SEEK_END))
2) Jaamme värikomponentit neljällä, sillä PCX:ssä väriarvot ovat
väliltä 0-255, VGA:ssa 0-63 (255/4=63).
Nyt yhdistämme 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 etukäteen. Voit itse
tehdä muutokset ohjelmaan jos haluat. Yleensä kuitenkin etukäteen on
tiedossa kuvan koko, kun PCX:iä käytetään
peleissä. Kuvankatseluohjelmaa tehdessä pitää kuitenkin koko ottaa
selville jo viimeistään sen vuoksi, että kuva näytetään oikein, vaikka
puskurissa olisikin tilaa.
Eli tässä meillä on valmiiksi pureskeltu PCX-lataajan runko, teemme
sille oikein oman kirjaston PCX.H. Kirjasto tarvitsee stdio.h:n
tiedostonkäsittelyfunktioita 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 löydy!\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 löydy!\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ä. Tämä siksi, että joskus on huomattavasti kätevämpää ladata
vain kuva, jos palettia ei mihinkään tarvita. Seuraavaksi seuraa
kappaleen esimerkkiohjelma, joka käyttää hyväkseen tutoriaalin
varrella esiteltyjä rutiineja ja muodostaa pienen esityksen. Ohjelma
lataa PCX-kuvan PICTURE.PCX ja paletin siitä. Sitten se läiskäisee sen
ruudulle. Lopuksi kuva himmenee tyhjyyteen ja palataan
tekstitilaan. Esimerkki olettaa kuvan olevan kokoa 320x200,
256-värinen ja paletin sisältävä PCX-kuva RLE-pakattuna. Voit korvata
kuvan millä haluat joko muuttamalla lähdekoodia tai kopioimalla oman
kuvasi PICTURE.PCX:n päälle.
Huomaa, että ohjelmassa luodaan kaksoispuskuri, johon kuva
ladataan. Näyttömuistin vänkääminen parametriksi aiheuttaa 100%
varmasti kaatumisen, tai jos jotenkin säästyt siltä niin ainakaan
mitään ei ilmesty näytölle. 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)
int 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);
return 0;
}
Toivottavasti ymmärsit tästä luvusta ainakin käyttöperiaatteen. Eli
loadpcx(nimi, puskuri) lataa kuvan puskuriin ja flip(puskuri) laittaa
sen näytölle (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ää väliin getch(), jotta ehdit kat-
sella rauhassa muutosta. Tällaista tässä kappaleessa. Mene nyt kokeilemaan
PCX-kuvien latausta. Seuraavassa kappaleessa tutustummekin sitten johonkin
peliohjelmoijaa lähellä olevaan asiaan...
4.1 Bitmapit - eikai vain suunnistusta?
---------------------------------------
Tänään siis teemme pienen bitmap-enginen C:llä. Itse olen aiemmin tehnyt
kaikki sprite- ja bitmap -rutiinini C++:ssalla, mutta tällä kertaa
käytämme C:tä, sillä haluan näiden esimerkkien toimivan ilman plussiakin.
Eli mitä on bitmap?
Bitmap, eli bittikartta on määrätyn kokoinen suorakulmion muotoinen esine,
jolla on puskuri muistissa sisältäen sen värit, kuten näyttöpuskurinkin
kanssa on. Hyödylliseksi bitmapin tekee se, että laitamme siihen pyyhkimis-
ja piirtotoiminnot, sekä liikutustoiminnot, joilla voimme siirrellä bitmap-
piamme ympäri ruutua. Lisäksi teemme siihen värin, joka tarkoittaa ettei
sitä kohtaa bitmapista tarvitse kopioida ruudulle. Näin saamme tehtyä bit-
mappiimme reikiä, eli teemme sen osittain läpinäkyväksi. Mutta miten tämä
kaikki sitten tehdään? Koko asia on, kuten kaikki asiat ohjelmoinnissa lo-
pulta ovat - naurettavan helppo.
Eli, menkäämme takaisin kaksoispuskurin aikoihin. Siinä meillä on
puskuri, jonka koko on 320x200 pikseliä ja se kopioidaan kokonaan näytön
päälle. 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 peittämä tausta tallennetaan ja palautetaan kun bittikartta
pyyhitään pois, mikä mahdollistaa liikuttelemisen.
- Siinä on läpinäkyvä väri, meillä 0, jota ei piirretä ruudulle. Jos siis
koko bittikartta olisi väriä 0, emme näkisi ruudulla mitään!
Eli itseasiassa bittikartta on pari puskuria, joille on varattu tilaa
siten, että jokainen bittikartan väri voidaan säilöä
puskuriin. Puskureita on perusbittikartassa kaksi, eli itse kuvan
sisältävä kartta, joka on järjestelty aivan samoin kuin
esim. kaksoispuskuri, mutta koko on bittikartan mukainen. Toinen on
taustapuskuri, joka on muuten sama, mutta sinne vain säilötään
piirrettäessä alle jääneet pikselit, jotta ne voidaan bittikarttaa
ruudulta pyyhkiessä palauttaa sieltä.
Eli tällainen voisi olla 3x3 kokoinen bittikartta:
Bittikartta: Taustapuskuri (mitä bittikartan alle on
piirrettäessä jäänyt):
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 jäi on täynnä mustaa, eli
väriä 0. Bittikartta on kaikkein helpointa määritellä omaan
datarakenteeseensa, joka sisältää tarvittavat tiedot kartan piirtelyyn
ja pyyhkimiseeen, nimetään se vaikka structiksi BITMAP.
Koordinaattien määrittely 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. Piirrettäessä
loopataan X:ää ja Y:tä siten, että luemme yksi kerrallaan pikselin
bittikartasta, ja jos se on jokin muu kuin väri 0 (yleensä musta, tämä
oli siis läpinäkyväksi sovittu väri), otamme ensin sen alle jäävän
pikselin talteen taustapuskuriin ja laitamme sitten vasta bittikartan
värin ruudulle oikeaan kohtaan (bittikartan värit sisältävästä
puskurista).
Eli tarvittavat tiedot bittikarttarakenteeseen ovat:
- bittikartan värit (char * -pointteri)
- taustan värit (char * -pointteri)
- x-sijainti ruudulla (int)
- y-sijainti ruudulla (int)
- koko x-suunnassa (int)
- koko y-suunnassa (int)
Lisäksi meillä on xspeed ja yspeed, joita käytetään esimerkeissä
säilömään bittikartan liikenopeutta x- ja y-suunnassa. Näillä
tempuilla meillä on nyt teoria liikuteltavan bitmapin tekemiseksi.
Ensin määrittelemme rakenteen, joka sisältää 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 tehtävänämme on tehdä "interface", eli käyttöliittymä
bitmap-engineemme. Siihen sisällytämme 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 JÄLKEEN ON TULTAVA TYHJENNYS
ja että BITTIKARTTAA EI LIIKUTETA SEN OLLESSA RUUDULLA (todellisuudessa
tietenkin kaksoispuskurissa, joka kopioidaan ruudulle kun kaikki bitti-
kartat ovat näkyvissä, sanoinhan, että hyödymme 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 ylös 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ä lähtien kertoo
latauspuskurista, jona tulemme käyttämään 320x200 kokoista PCX, kuvaa,
sisältäen kaikki bitmapit mitä pitää ladata. Jos kuvan x-koko ja y-koko,
sekä aloituskoordinaatit kuvassa on ilmoitettu oikein, onnistuu lataus
suorakulmion muotoiselta alueelta täysin onnistuneesti, eikä lataus-
rutiinin käyttö vaadi kovin paljoa miettimistä. Lisää käytöstä ajal-
laan tulevassa esimerkissä.
No niin. Lähtekäämme tekemään kirjastoamme BITMAP.H yksi funktio kerrallaan.
Rakenne BITMAP on jo esitelty, joten alkakaamme keräämään sen perään
käsittelyfunktioita. Ensimmäisenähän oli vuorossa bdraw(), joka onkin
helpoimpia ja tärkeimpiä 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 värillä 0 tämä vertailu alla ei ole tosi, joten värillä
0 merkittyjä kohtia EI piirretä! */
if(b->bitmap[yy*b->xsize+xx]) {
/* doublebuffer muuttuja osoittaa kaksoispuskuriin. Huomaa, että
yläkulma on y*320+x, mutta koska haluamme vielä piirtää useita
rivejä, lisäämme yy-looppimme y-arvoon, kutenn myös xx-looppi
x-arvoon. Jos et ymmärtänyt niin poista väliaikaisesti kohdat
ja näet 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ää hämärän
peittoon, niin esittelen tässä saman pseudona, jos se olisi hieman
selvempää:
funktio bdraw
kokonaisluvun kokoiset kierroslaskurit a ja b
looppaa a välillä 0 - <y-koko>
looppaa b välillä 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 lähdet korvaamaan a:n muuttujalla yy ja b:n muuttujalla xx ja
korvaat bittikartan sisäiset 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 ensimmäisessä,
alkuperäisessä sorsassa. Jos yhtään selventää niin voit poistaa
kommentit alkuperäisestä sorsasta kokonaan ja siirtää sijainnin laskut
sieltä []-sulkeiden sisästä juuri tuollaisiin
bittikarttasijainti-tyylisiin apumuuttujiin, jolloin koodi selvenee
hieman. Olkoot, tässä 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 tärkeää, että ymmärrät periaat-
teen. Tietenkin saat lisäselvyyttä kokeilemalla muuttaa noita kohtia, jol-
loin näet muutoksen kääntämällä uudelleen esimerkkiohjelman, jonka
myöhemmin esittelemme ja ajamalla muunnellun version. Seuraavana onkin
huomattavasti nopeammin tehty pyyhintäfunktio, joka eroaa vain siten, että
sen sijaan, että säilöisimme taustan ja korvaisimme ruudun pikselin
bitmap-puskurin arvolla laitammekin background-puskuriin tallennetun pikse-
lin takaisin kaksoispuskuriin, joka on piilotusfunktion jälkeen 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 värillä 0 tämä vertailu alla ei ole tosi, joten värillä
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 sehän eroaa
edellisestä vain tuon sijoituksen osalta, eli ensimmäinen sijoitus
draw-funktiosta käännetään vain toisinpäin, niin alkup. tausta
palautuu.
Seuraavaksi kolme helponta funktiota heti rivissä, sillä niiden toteuttami-
nen on helppoa ja ymmärtäminen vielä helpompaa, muista, että X-ja Y-koor-
dinaatteja vähennetään 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 lisään koodia saadakseni siitä vähän
selvemmäksi. Idea siis on, että otamme pikselin tuplapuskuriin ladatus-
ta ja laitamme sen bitmap-puskuriin. Eli oikeastaan käänteisesti näyt-
töfunktioon nähden. 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ä
yläkulma on y*320+x, mutta koska haluamme vielä piirtää useita
rivejä, lisäämme yy-looppimme y-arvoon, kutenn myös xx-looppi
x-arvoon. Jos et ymmärtänyt niin poista väliaikaisesti kohdat
ja näet mitä tapahtuu */
b->bitmap[yy*xsize+xx]=
bitmapbuffer[ (buffery+yy) * bufferxs + (bufferx+xx) ];
}
}
}
bload on itseasassa täysin sama kuin ensimmäinenkin 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 tässä tapauksessa, jottei tarvi oikeaa
kaksoispuskuria välttämättä käyttää) pikselit. Ei se loppujenlopuksi
ole sen vaikeampi.
Nyt kun lisäämme kaikki yhteen kirjastoomme BITMAP.H ja teemme lopuksi
vielä pienen esimerkkiohjelman, joka liikuttelee palloa
ruudulla. Koska kirjastomme ei kykene estämään ruudun yli menemisiä,
niin meidän pitää kääntää 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 täyttyy niin käännetään
pallon suuntaa ja saadaan pallo "pomppimaan" reunoista.
Mutta, olemme taas puhuneet ihan tarpeeksi. Menkäämme nyt esimerkkiohjel-
mamme pariin (BITMAP1.C). Siinä lataamme bittikartan tiedostosta BITMAP.PCX
ja tausta tiedostosta BITBACK.PCX. Näin näemme läpinäkyvyyden toiminnassa
(muutenhan pallo olisi neliönmuotoinen). Lisäksi tietenkin käytämme 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 lähdekoodeja, mieti teoriaa ja kokeile kaikkea
käytännössä mitä mieleen tulee. Kun luulet keksineesi idean niin palaa
takaisin dokumentin ääreen, ja siirrymme seuraavaan aiheesemme. Menehän
siitä! Jos vieläkin tuntui siltä ettet tajunnut niin ota yhteyttä ja
kysy mikä jäi mietityttämään, niin tarkennan sitten vielä tätä.
4.2 Animaatiot
--------------
Tämänkertainen aiheemme on pieni parannus koodiin, joka on paljon näy-
töllä ja jonka jälkeen on tämän tutoriaalin bittikarttarutiinit lähes kä-
sitelty. Tulemme kyllä hyväksikäyttämään edellisen kappaleen koodia
tehdessämme fonttiengineä, sekä parantelemme koodia tehdessämme törmäys-
tarkistuksen, mutta itse animointi- ja bittikarttateoria käsitellään
kokonaan tässä ja edellisessä kappaleessa.
Eli tänään tutustumme ensimmäisenä animaatiohin. Mitä animaatiot sitten
ovat? No itseasiasas animaatio on vain sarja kuvia, joita vaihdellaan
ja saadaan kuva liikkeestä. Animaatiota voidaan käyttä lähes kaikkeen
pelissä. Sillä voidaan tehdä pyörivä alusanimaatio, jonka jokainen
kuva on yksi aluksen suunta. Jokaisella suunnalla voisi olla vielä oma
animaationsa, joka saa vaikka rakettimoottorit hehkumaan ja laserit
aiheuttamaan välähdyksiä aluksen pinnassa. Pienellä mielikuvituksella
ja taitavalla graafikolla päästään ihmeisiin. Tässä kappaleessa esi-
telty kirjasto ei varmaankaan käy suoraan moneen tarkoitukseen tai ole
tarpeeksi nopea peliin, mutta enginen onkin vain tarkoitus näyttää
pääperiaatteita animoinnin ja muiden olennaisien asioiden takana.
Eli animaatio on kuvasarja, jotka näytetään tietyssä järjestyksessä. Miten
sitten toteutamme tämän. Tässä on tapa jolla minä olen sen tehnyt. Meillähän
on täysin toimivat rutiinit yhden kuvan näyttämiseen. Tehkäämme vain
animointikoodi, joka vaihtaa pointterin bitmap osoittamaan seuraavaan
kuvaa, eli frameen. Tätä täytyy kutsua silloin kun spriteä, joksi kutsumme
animoivaa bittikarttaamme tästälähin ei ole piirretty puskuriin. Jälleen
voit kokeilla siirtää animointikoodin kutsun kohtaan jossa esine on piir-
rettynä, mutta se ei tule näyttämään hyvältä (jos objektin peittämän alueen
muoto muuttuu). Eli siis tarvitsemme uuden rakenteen, joka voi säilöä
useita kuvia, koodin joka vaihtaa bitmap-pointterin osoittamaan seuraavaan
kuvaan, laskurin joka kertoo monennessako kuvassa mennään ja toisen muuttu-
jan joka kertoo montako kuvaa meillä on animaatiossa, sekä lopulta uuden
latausfunktion, joka osaa ladata useita kuvia käsittävän animaation.
Tähän kaikkeen voimme kopioida vanhaa koodiamme ja lisäillä sinne tar-
peellisia osia. Eli teemme nyt uuden rakenteen, jossa voi olla maksimis-
saan MAXFRAME määrä frameja, eli kuvia (tämä 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. Nämä rutiinit tulevat kirjastoon SPRITE.H, josta löydät
myös joukon vanhoja tuttujamme uudelleennimettynä ja vähän
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 myös latausrutiinimme. Tärkeimmät muutok-
set siinä on, että se lukee framet rivistä. Katso SPRITE.PCX esimerkkinä
tällaisesta animaatiosta. Jos ihmettelet outoja kertolaskuja joissain
kohdin se johtuu siitä, että jokaisen framen jälkeen hypätään 1 pikseli
yli, sillä teemme rajat animaatioiden väliin selvennykseksi. Eli tässä
olisi latauskoodimme, uusi parametri on animaatioiden määrä:
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 lisätty koska meidän 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ä
yläkulma on y*320+x, mutta koska haluamme vielä piirtää useita
rivejä, lisäämme yy-looppimme y-arvoon, kutenn myös xx-looppi
x-arvoon. Jos et ymmärtänyt niin poista väliaikaisesti kohdat
ja näet mitä tapahtuu */
s->frame[current][yy*xsize+xx]=
bitmapbuffer[ (buffery+yy) * bufferxs + (bufferx+xx) +
(xsize+1)*current ];
}
}
}
Kirjastoon SPRITE.H lisätään vielä bdraw, bhide, bmove, bsetlocation ja
bsetspeed nimettynä nimillä sdraw, shide, smove, ssetlocation ja ssetspeed
funktioiden erottamiseksi bitmap-rutiineista (jos vaikka halutaan käyttää
molempia). Muitakin pikkumuutoksia on tehty. Huomaat ne helposti
kurkkaamalla kirjaston sisään. Nyt meillä onkin animaatiot taitava engine,
jota meidän täytyy tietenkin heti kokeilla. Tässä on esimerkkiohjelmamme
SPRITE1.C, joka havainnoi funktioiden käyttöä:
#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ä täysin optimoimaton sprite-enginemme
ei aivan pysty 70 frameen sekunnissa. Siksi laitoin ohjelmamme odottamaan
kahta vertical retracea, jotta nykiminen ei olisi niin häiritsevää
(P75:lläni kahdella waitilla meno näyttää paljon tasaisemmalta, eikä yhden
framen hyppy näy läheskään niin selvästi). Jos kuitenkin sinulla on hidas
kone niin poista toinen tai kummatkin odotuksista, se nopeuttaa koodia
paljon, mutta voit joutua laittamaan delay-komennolla viivettä säätääksesi
pyörimistä tasaisemmaksi. Pienellä optimoinnilla olisimme toki saaneet
moninkertaisesti lisää nopeutta, mutta koodi olisi menettänyt luettavuut-
taan, joka esimerkkiohjelmien tarkoitus on. Tietenkin kun alat tekemään
omaa peliäsi teet uudet ja paremmin tarkoitukseesi sopivat rutiinit ke-
räämiesi tietojen pohjalta.
Nyt onkin tämän kappaleen aika loppua ja sinun on aika paneutua uuden
asian pariin. Seuraavassa luvussamme käsitelläänkin sitten viimeistä
kysymystä spritejen parissa, monen spriten käyttöä, niiden törmäyksiä
ja ylitseliukumisia. Mutta nyt jätän sinut rauhaan. Näemme seuraavassa
luvussa!
4.3 Pitääkö spriten törmätä? Entä coca-colan?
---------------------------------------------
Nyt pääsemmekin vihoviimeiseen vaiheeseen teoriassamme ja ryyditämme sitä
pienin, tai ehkä niinkään pienin muutoksin SPRITE.H-kirjastoomme. Nimit-
täin jokainen vähänkään vakavasti pelintekoa harkinnut tarvitsee useampia
kuin yhden spriten. Mutta mitä tapahtuu kun ne ovat menossa päällekäin?
Jos teet vain loopin, joka piirtää spriten ja toisen, joka pyyhkii ne
samassa järjestyksessä olet varmaan huomannut, että se ei aiheuta toivot-
tuja tuloksia. Muutos mitä tarvitaan on pieni ja yksinkertainen, mutta
ajatellaanpas esimerkkiämme.
Ajatellaan, että sinulla on kolme pikseliä. Punainen, sininen ja keltainen.
Haluat laittaa ne samaan kohtaan ruudulle. Laitat ne edellä olevassa
järjestyksessä 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 järjestyksessä, eli ensin
punainen, sitten sininen ja lopuksi keltainen? Et, sillä jos ottaisit lopuksi
keltaisen, katsoisit lapustasi sen alla olleen sinisen värin ja ruutu
muuttuisikin siniseksi. Tässä meidän täytyykin mennä käänteisesti, 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]);
Ja ei enää toimimattomia koodinpätkiä, vaan hienosti toistensa ylitse
liukuvat spritet.
Mutta aina ei haluta kaikkien vain liukuvan toistensa ylitse. Miltä
näyttäisi matopeli, jossa madot kiltisti liukuvat toistensa ylitse?
Ei kovin oikealta, sanoisin. Meidän täytyy siis tehdä rutiini, joka
tarkistaa törmäyksen kahden spriten välillä. Olkoon sen kutsutapa
seuraava: scollision(SPRITE *a, SPRITE *b) ja se palauttaa arvon
1 jos törmäys on tapahtunut, muuten se palauttaa nollan. Jos siis
haluat tehdä törmäyksen tultua jotakin, niin koodi menisi suurinpiirtein
näin:
if(scollision(sprite[0], sprite[1]))
tee_jotain_kun_tulee_pamahdus();
Mutta, miten toimii tämä salaperäinen funktiomme? Itseasiassa minä en
saanut siitä mitään selvää luettuani sen aikoinani Mikrobitin grafiikka-
ohjelmointikurssin toisesta osasta, mutta luulisin nyt pystyväni teke-
mään samanlaisen, ja jos onnistumme pystynen selittämäänkin toimintaperi-
aatteen.
int scollision(SPRITE *a, SPRITE *b) {
/* Lasketaan spritejen yläkulmien väliset etäisyydet. Huomaa, että tässä
lasketaan mukaan nopeudet, eli palautusarvo 1 kertoo spritejen
törmäävän ENSI vuorolla. Näin ehditään päällekkäin 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-etäisyys on suurempi kuin suuremman leveys eivät
spritet voi mitenkään olla toistensa päällä. */
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 selittämään. Jos välttämättä haluat saada selville miten pätkä toimii
niin piirrä pari tilannetta paperilla ja katso miten niiden kanssa tapah-
tuu. Nyt meillä onkin käsiteltynä kaikki tärkein spriteistä ja voimme
mennä viimeiseen pelkästään spritejä käyttävään ohjelmaamme. Tämä 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 törmäävät. Kontrol-
lit ovat pelaajalla 1 wsad ja pelaajalla 2 ujhk. Tämä on vain pieni esi-
merkki siitä mitä näillä taidoilla voisi tehdä. Lisäksi nappeina on
+ ja - nopeuden säätöön (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()) { /* tyhjennetään näppispuskuri */
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 (räjähdys) */
fadetowhite(palette);
for(waittime=0; waittime<256*3; waittime++)
palette[waittime]=63;
}
fadetoblack(palette);
textmode(0x3);
return 0;
}
Tässä oli sitten sellainen lähdekoodi, jota kukaan vähänkään 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 käyttämiseen, ei suinkaan minkälainen pelin
runko pitäisi olla. Siihen me palaamme myöhemmin. Mutta meneppäs pelaamaan
ja näytä kavereillesi minkälaisia pelejä osaisit jo tehdä. =) Äläkä
palaa takaisin ennenkuin tämän 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.
Jälkikäteen kaiken sprite, animaatio ja bittikarttanäpräilyn jälkeen totean,
että kaikissa kohdissahan ei käytetty täsmälleen oikeita termejä. Bittikart-
tahan on käytännössä vain kuvadata ja mahdollisesti hieman lisätietoa, ani-
maatio on yleensä peräkkäisiä 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
välillä tai päässä, en tiedä kovin tarkasti mutta käytin nyt tätä nimitystä
täysin toimivasta oliosta joka kykenee itsensä käsittelyyn.
4.4 Maskatut spritet
--------------------
Vähän aikaa sitten kerroin PC-Ohjelmointi -alueella tämän kurssin sisällöstä
ja eikös vain joku mennyt kysymään minulta selittikö tutoriaali maskatut
vai maskaamattomat spritet. Minähän 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 tekemämme rutiini. Aion nyt selittää
idean tämän tekniikan takana, joten kiinnittäkää turvavyönne ja valmistau-
tukaa!
Maskatuiden spritejen ideana on se, että niiden piirrossa ei tarvita pikse-
likohtaisia vertailulauseita lainkaan, jolloin voidaan käyttää assembleril-
la neljän tavun kanssa operoivia funktioita. Mutta miten sitten kierrämme
vertailulausekkeet säilyttäen silti läpinäkyvyyden nollavärin kanssa?
Idea perustuu bittioperaattoreihin.
Jokaiselle spriten framelle tehdään etukäteen maski, joka on nolla kohdissa
joissa on pikseli ja 255 läpinäkyvissä kohdissa. Nyt sitten vain suoritamme
kaksoispuskurin pikselille loogisen AND-operaation:
Maski spritelle FF 00 FF FF
Näyttö 4F 3C 93 5A
----------------------------
Tulos 4F 00 93 5A
Kuten huomaatte, jäävät läpinäkyvät kohdat (FF) jäljelle. Sitten vain
käytämme OR-operaattoria sytyttämään spriten pikselit, sillä ne kohdat
ovat juuri äsken nollautuneet, joten looginen OR asettaa juuri oikeat
bitit:
Sprite 00 46 00 00
Maskattu näyttö 4F 00 93 5A
----------------------------
Tulos 4F 46 93 5A
Lopun saat toteuttaa aivan itse. Huomattavaa tässä on se, että jos haluat
käyttää tehokkaita 4 tavun (dword) operaatioita on bittikartan leveyden
oltava jaollinen neljällä. 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 sisältää kaiken muun lisäksi assemblytutoriaalin.
DJTUT2_4.ZIP Jos osaat Intel-syntaksin, muttet AT&T-syntaksia
(movd %eax, %ebx). Sisältää myös muuta kiinnostavaa
materiaalia, jota tässäkin tutoriaalissa on sivuttu.
NASM095B.ZIP Tällä voit tehdä Intel-syntaksin assemblerilla DJGPP:n
COFF-muotoisia objektitiedostoja. Tiivistettynä TASM joka
osaa myöskin DJGPP:n objektiformaatin. Huomaa, että uusin
versio voi olla muutakin kuin 0.95 (NASM095B.ZIP).
Lisäksi voisi olla hyvä idea lainata kirjastosta kirja 486-ohjelmointi,
joka on suomenkielinen assembler-ohjelmointia käsittelevä kirja ja kaiken
lisäksi hyvä sellainen!
Loppulisäyksenä jälleen kiva vinkki Pekka Nurmiselta. Kaksoispuskuri
kannattaa tarvittaessa tehdä sen verran leveämmäksi, että jos spitea
ei saada katki juuri neljän tavun kohdalta ei tuo tule toisesta reunasta
vastaan. Eli jättää sinne neljä tavua ruudun reunoihin, jota ei vain
sitten kopioida näytälle. Näin kaksoispuskurin kooksi tulisi 328x200.
5.1 Näppäimistön käsittely - ja nyt meillä on hauskaa
-----------------------------------------------------
Jos pelasit ahkerasti esimerkkipeliämme, niin ehkä huomasit, että painaessasi
useita nappia ilmenee myös useita ongelmia. Näihin voivat kuulua näppäimis-
tön jumiutuminen, nappien huomiotta jättäminen jne. Tarvitsemme siis ru-
tiinin joka päästäisi meidät pälkähästä. Tarvitsemme näppishandlerin!
Tämä perustuu siihen, että joka kerta kun nappia painetaan kutsutaan
keskeytystä 9, joka lukee merkin näppäimistöltä portista 60h (0x60) ja
muuntaa sen ASCII:ksi ja laittaa näppäimistöpuskuriin. Mutta mepäs ohi-
tammekin tämän ja teemme oman handlerin, joka ei muutakaan mitään miksi-
kään ASCII:ksi, vaan laittaa näppäimistötaulukon vastaavan kohdan arvoon
1, josta peli voi sitten sen tarkistaa. Ja kun nappi päästetään tulee
myös keskeytys, tällä kertaa tulee napin arvo + 128, joten vähennämme
luetusta arvosta 128 ja nollaamme vastaavan kohdan taulukosta. Ja millainen
on tämä taulukko?
Taulukossa on 128 alkiota, yksi jokaiselle SCAN KOODILLE, jollaisia näppäi-
mistö syytää. Olen tehnyt näistä numeroista kirjaston, jossa esimerkiksi
ESC-näppäimen scan koodi on nimellä SxESC ja sen arvo on 1. Jos siis haluat
pelissäsi tietää onko ESC painettuna, osoitat näppäimistöpuskuriin:
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 ykköseksi ja jos se on yli tai yhtäsuuri kuin 128, niin laitamme
alkion tavu-128 nollaksi. Lopuksi lähetämme signaalin PIC:ille, että kes-
keytyksemme on valmis, eli outtaamme tavun 20h porttiin 20h. Tällainen 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);
}
Tämä onkin oikeastaan helpoin osa tehtäväämme. Vaikeampi (joskin esimerkki-
koodin takia helppo) on koukuttaa tarvitsemamme näppäimistökeskeytys ja
palauttaa se kun tarvitaan näppäimistörutiineja (gets, getch...) tai pois-
tutaan ohjelmasta. Lisäksi tarvitsemme joukon apumuuttujia, jotka ovat
tässä:
volatile unsigned char keybuffer[128], installed;
_go32_dpmi_seginfo info, original;
Keybuffer säilöö näppäinten tilat, installed kertoo onko tämä handleri a-
sennettuna ja estää samalla uudelleenasentamisen. Kaksi viimeistä muuttujaa
info ja original ovat koukuttamiseen ja koukutuksen (hooking) poistamiseen
tarvittavia rakenteita, joista infoa käytetään oman asentamiseen ja origi-
naliin säilötään alkup. handlerin osoite ja muut tarpeelliset tiedot.
Tässä on koukutukseen ja palautukseen tarvittava koodi, johon emme perehdy
kovinkaan tarkasti, lisäinfoa asiasta saat vaikka DJGPP:n FAQ:sta hakusanalla
handler:
int setkeyhandler() {
int c;
for(c=0; c<0x80; c++)
keybuffer[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;
}
Lisäämme kaikki kolme funktiota ja globaalit muuttujamme tiedostoon
KEYBOARD.H. Nyt meillä on tarpeen vaatiessa täydellisen toimiva näppäimis-
töhandleri (jota ehkä myöhemmin tulemme käyttämään).
5.2 Fixed point matematiikka
----------------------------
Alamme pikkuhiljaa lähestyä kurssimme loppua (tai ken tietää, todellista
alkua?), joten käsittelen tässä hieman pelin optimointiin vaikuttavia
tekijöitä ja parannuksia aiemmin esittelemiimme kirjastoihin (omaan peliin
kun kannattaa kuitenkin tehdä osa kirjastoista uusiksi). Selitän 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 tiedät, 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. Näyttää nimittäin todella typerältä jos ohjus
pomppii kymmenen pikseliä eteenpäin, 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ä käyttää, ainakin assemblerilla voidaan pentiumin matematiikkapro-
sessoria käyttää täysipainoisesti ja peliä nopeuttaa). Niinpä meidän täy-
tyisi pystyä esittämään kokonaisluvuilla desimaalilukuja. Onko tämä mahdol-
listakaan?
Kyllä se on, katsokaamme hieman toisella tavalla normaaleja lukujamme.
Meidän luvuissamme on kokonaislukuosa ja desimaaliosa sekä välissä piste.
Kokonaislukuosalla voidaan ilmaista 10^<numeroja> lukua, eli jos
kokonaislukuosassa on 3 numeroa niin voimme ilmaista sillä 10^3=1000
erilaista lukua, välillä 0-999. Pisteen toisella puolella on kaikki muuten
samalla tavalla, mutta meidän täytyy ajatella käänteisesti. Voimme ilmaista
desimaaliosalla desimaalin, joka on yksi 10^<numeroja>:sosa. Tämä näyttää
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 vähän pidemmälle. Oletetaan, että meillä olisi luvussa pilkku
AINA samalla kohdalla ja desimaalia esittäviä 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 ykkösen väliin.
Nyt kysyt ehkä että mitä hyötyä tästä on. Siitä on seuraava hyöty: 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! Vähennyslasku 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 nähden niin ne kertomalla saamme 1000000-kertaisen tuloksen, joten
lopuksi meidän täytyy 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, siinähän jaamme vain
numerot toisillamme, mutta tässä häviää meiltä desimaaliosa, eli meidän pi-
täisi 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 meidän täytyy sitten syventyä siihen miten toteutamme nopeasti edelliset
asiat tietokoneen binäärijärjestelmällä. Se on erittäin helppoa. Teemme
vaikka 32-bittisen luonnollisen (unsigned int), josta 16 alinta bittiä on
varattu desimaaliosalle. Koska binäärijärjestelmä on 2-kantainen, niin
meidän täytyy vain muuttaa pikku laskumme kahden potensseilla leikkimisiksi.
Tällaisella 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
siirtämällä bittejä 16 vasemmalle ja jakaa siirtämällä niitä oikealle.
Tässä on pieni esimerkkiohjelma, joka demonstroi fixedin käyttöä:
#include <stdio.h>
int 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);
return 0;
}
Mieti nyt kaikkea ihan rauhassa. Jos luulet ymmärtäneesi edes jotain niin
hyvä, jos et ymmärtänyt mitään niin lue uudelleen ja uudelleen ja kokeile
paperilla. Jos et siltikään ymmärtänyt niin lue jostain toisesta dokumentis-
ta! Fixed-pointissa on huomattava pari asiaa:
1) Luvut voivat mennä yli ja tulee ihmeellisiä tuloksia. Jakolaskuesimerkis-
säni en voinut kertoa a:ta ensin 65536:lla, sillä muuten olisi luku men-
nyt ympäri. Kannattaa aina varmistaa ettei luku voi mennä ympäri.
2) Käytä 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 käyttää kaikkia nerokkaita optimointikikkoja jos vain kek-
sit niitä. Myös pyörähdystä voi käyttää hyväksi (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ä enemmän bittejä desimaaleille sitä tar-
kempia lukuja. Mitä enemmän bittejä kokonaisluvuille sitä suurempia ja
epätarkempia lukuja.
5.3 Lookup-tablet ja muita optimointivinkkejä
---------------------------------------------
Lookup-tableissa, eli lookupeissa ei ole oikeastaan muuta selittämistä, kuin
että niissä toistuvia, vain yhtä (tai joskus kahtakin) muuttujaa käyttävis-
sä monimutkaisissa laskutoimituksissa (tai muuten vain hidastavissa)
lasketaan tulokset etukäteen taulukkoon käyttäen indeksinä sitä lukua joka
oli muuttuvana laskutoimituksessa. Tähän käy 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 ympyrällä 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 määriä dataa käsittelevät loopit assemblerilla. Lisää tietoa
inline-assemblerin käytöstä DJGPP:llä tiedostosta DJTUT*.ZIP,
vaikka MBnetistä, tai tämän tutoriaalin Nasmia käsittelevästä
luvusta.
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;
Vähennämme näin 1000000 vertailua.
3) Älä tuhlaa aikaasi optimoimalla suuria määriä logiikkaa, ellei siitä
todella ole hyötyä. Esimerkkinä vaikka kaksoispuskurin tyhjennyksen
tekeminen inlinenä memsetin sijaan säästää kyllä aikaa, mutta kun
ajansäästö funktiokutsun jäämisessä pois on jotain 1/10000 siitä
mitä aikaa memsetissä menee joka tapauksessa, on hyödyttömyys
varsin ilmeistä.
4) Käytä fixediä floatin tilalla aina kuin mahdollista.
5) Laske kaikki toistuva konemainen laskenta taulukkoihin.
6) Käytä DJGPP:n käännösvalitsinta -O2, tai jopa -O3 (joka kyllä suurentaa
ohjelmaasi reilusti).
Yleensäkin kannattaa uhrata paljon aikaa grafiikkakirjastojen ja äänikirjas-
tojen optimointiin ja pitää itse runko selkeänä C-kielisenä kutsujen joukko-
na. Tämä ei paljoa hidasta ja selventää uskomattomasti koodia ja nopeuttaa
kehitystä.
5.4 Väliaikatulokset ja fontteja
--------------------------------
Tässä vaiheessa osaat nyt kaikki tärkeimmät niksit mitä peliohjelmointiin
tarvitaan. Tästä luvusta lähtien alan tietoisesti vähentämään, ellen
jopa joissain kohdissa poistamaan esimerkkiohjelmia. Mitä tästä lähtien
tarvitset on maalaisjärkeä ja kykyä osata soveltaa oppimiasi asioita.
Eli tänään 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 myös poistaa läpinäkyvyyden tai jättää pyyhkimisen jos
tarvitset sitä, sinun pitää siinä tapauksessa vain tehdä erikoisjärjeste-
lyjä) ja käyttää animaationa kuvasarjaa jossa on piirrettynä merkit a-z,
A-Z, 0-9 ja sitten joitakin mahdollisesti tarvittavia välimerkkejä, kuten
.!?,;:'" ja muut vastaavat. Sitten vain teet funktion, joka vaihtaa framek-
si oikean kuvan ja piirtää sen, jonka jälkeen se korottaa x-arvoa merkin
leveydellä (plus jonkin verran väliä seuraavan merkin ja viimeisen välille)
ja ottaa käsittelyyn seuraavan merkkijonon merkin.
Koodi voisi näyttää vaikka tältä:
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 jälkeen, 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);
/* tämä taas tulee pienien JA isojen kirjaimien jälkeen */
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 jälkeen */
}
...
}
}
Kuten ehkä huomasit tuli koodista aivan kammottavaa sekasotkua ja on ihme
jos sait siitä jotain selvää. Lisäksi koodi ei ole erityisen nopeaakaan,
saati sitten että se edes välttämättä toimii. Mutta miten voisimme nopeuttaa
tätä? Vastaus on lookup-tablet. Sillä mehän tiedämme, että C:llä kirjain on
vain numero välillä 0-255. Niinpä teemme taulukon jonka jokainen alkio
osoittaa indeksin mukaisen ASCII-kirjaimen framenumeroon. Jos et ymmärtänyt
niin tässä on esimerkki taulukon käytöstä:
frame = asciitaulukko['a'];
Asciitaulukon alkio 'a' (numerona 97) olisi 0, joten framenumeroksi tulisi
näinollen tämä luku. Sitten vain framenvaihto: "setframe(frame)".
Tietenkin tuo kannattaisi käyttää näin: "setframe(asciitaulukko['a'])"...
Mutta miten sitten taulukko alustetaan? Tapoja on monia, jotkin ovat seka-
vampia ja jotkin vähän selvempiä, mutta annan sinun itsesi päättää mikä on
paras. Mahdollisuutena olisi ensin täyttää taulukko nollalla (joka olisi
tyhjä frame) ja sitten loopata aakkoset a-z täyttäen taulukon kohdat 'a'-'z'
oikeilla framearvoilla (1...26), sitten loopataan 'A'-'Z' täyttäen ne alkiol-
la 27...52 jne. Myös lataaminen kannattaa automatisoida.
Muista lisäksi huomioonottaa erikoismerkit enginessäsi. Tarpeellisia voivat
olla välilyönti (32), rivinvaihto (\n), tabulaattori (\t) jne. Ja lisäksi
saat aivan vapaasti päättää onko fontin väri mahdollista vaihtaa vai käytät-
kö aina samanlaisia fontteja, joka mahdollistaa vähän hienommat, vaikka moni-
väriset fontit.
5.5 Hiirulainen, jokanörtin oma lemmikki
----------------------------------------
Tänään, tytöt ja pojat, setä puhuu hieman kotieläimistä. Ne ovat sellaisia
pieniä valkoisia ötököitä, joilla on häntä ja jotka viipottavat matolla.
Sen lisäksi niitä voi myös painella. Ei, nyt ei ole kyse mistään karvaisesta,
vaan ihan aidosta tietokoneen lisälaitteesta, jota hiireksikin kutsutaan.
Tällä karvattomalla ystävällämme on säädyttömän monia haaroja sukupuussaan.
Löytyy Logitechia, Microsoftia, Targaa ja ties mitä vimputinta ja kaiken
kukkuraksi rautatasolla käskyttäminenkin on suorastaan säädyttömän
epästandardia. Onneksi hätiin rientää kymmenisen vuotta vanha apu nimel-
tään _hiirikeskeytys_, kiinnostavemmin ilmaistuna keskeytys 33h. Tätä
keskeytystä käyttäen saadaan kaikkien hiireen tungettujen vimpainten, kuten
nappien ja pohjassa (yleensä) pyörivän pallukan tila. Nämä tiedot ovat helpon
saatavuuden lisäksi myös naurettavan helppokäyttöisiä, kunhan vain tietää
miten niitä käyttää.
Jos et vielä tiedä miten keskeytyksiä käytetään tulee tässä tiivistettynä
niiden käyttö DJGPP:llä. Keskeytykselle annetaan parametrit rekistereissä
ja ne saadaan rekistereissä. Jos DJGPP oli yhtä huoleton kuin Borland
Turbo-kääntäjineen olisi meilläkin rekisteri ax nimellä _AX jne. Mutta koska
kaikki on tehty rakkaalla kääntäjällämme hipun vaikeammaksi teemme sen
standardilla tavalla. Alhaalla näet tarvittavat askeleen keskeytyksen kut-
sumiseksi ja rekisterien näpläykseksi. Esimerkki käyttää yhtä kymmenistä kes-
keytyksen aiheuttavista funktiosta int86(...) kirjastosta dos.h:
1) Tarvitset rekisterit muuttujinaan sisältävän unionin, int86:n tapauksessa
unioni on nimeltään REGS ja sen sisällä on pari structia joihin
tutustut vaikka selaamalla ko. kirjastoa. En ala perehtymään syvemmin
näihin x, d ja w-rakenteisiin. Tässä kuitenkin käytämme viimeistä, joka
on 16-bittiset rekisterit.
union 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;
Tehdessäsi hiiriohjattua ohjelmaa sinun pitää tietysti hiiren koordinaattien
ja nappien käsittelyn lisäksi piirtää kursori ruudulle, ellet sitten halua
käyttää (amatöörimäisen näköistä) kursoria, jonka ajuri piirtelee ruudullesi.
Grafiikkatilassa tämä onnistuu vaikka tekemällä hiirestä yksi spriteistä ja
liikuttelemalla sitä. Antaa paljon paremman kuvan ohjelman tekijästäkin!
Tekstitilassa vaihdat vaikka ko. kohdan väriä. Tähän ihmeelliseen tilaan
tutustumme kohtapuolin, eli jatka lukemistasi jos haluat tehdä tekstitila-
ohjelman, joka käyttää kursoria...
Tässä nyt olisivat nämä kaikkein käytännöllisimmät ja alkuun auttavat funk-
tiot. Lisää löydät vaikkapas Ralph Brownin interruptilistasta tai kenties
jopa HelpPC:stä. RB: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 sisältäen 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 - Näytä 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 keskimmäinen 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 keskimmäinen)
Palauttaa: Muuten kuten funktio 3, mutta koordinaatit kertovat kursorin
sijainnin viime painalluksella ja BX kertoo ko. napin painal-
luksien määrän sitten viime kutsun.
Funktio 6 - Nappien vapautukset
Parametrit: AX=6,
BX=mikä nappi (0 vasen, 1 oikea ja 2 keskimmäinen)
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 - Liikemäärä
Parametrit: AX=B
Palauttaa: CX=vaakamikkien määrä
DX=pystymikkien määrä
Funktio F - Mikkejä pikseliä kohden
Parametrit: AX=F
CX=vaakamikkien määrä
DX=pystymikkien määrä
Palauttaa: -
Lisäksi on vielä ainakin funktio C, joka asettaa oman käsittelijän, mutta
koska se ei luultavasti kiinnosta kovin monta (rm-osoitetta odottava käsit-
telijä ei ehkä oikein toimi PM:ssä kunnolla jne...) jätän sen tässä väliin.
Sitten vain tekemään kaiken maailman testiohjelmia. Esimerkkejä ei tule
tässä lainkaan, sillä oletan jokaisen pystyvän edellisten ohjeiden perusteel-
la kyhäämään itseään tyydyttävän ohjelman.
Jos homma ei kuitenkaan ota luonnistuakseen tai tässä kappaleessa oli muita
epäselvyyksiä niin otahan yhteyttä niin kaivelen lisää tietoa aiheesta.
Erityiskiitos tämän kappaleen teon auttamisesta kuuluu nyt kyllä MB:n numerol-
le 4/96 josta katsoin nopeasti tiivistelmän hiirifunktioista.
Ja ensi kappaleessa onkin uudet kujeet, näyttäisi olevan tekstitilan hallinta
seuraavana edessä...
5.6 Tekstitilan käsittely suoraan
---------------------------------
Tästä kappaleesta tulee tulemaan äärimmäisen lyhyt. Ainoa meitä kiinnostava
seikkahan on tekstimuistin osoite (tila 3, 80x25, myös muut voivat toimia)
ja rakenne. Osoite on perusmuistin segmentti B800h, eli lineearinen osoite
selektorin _dos_ds osoittamassa muistissa olisi C:llä 0xB8000. Rakenne
on myös naurettavan yksinkertainen. Erona VGA:han (ks. kappale
"Grafiikkaa - mitä se on?" jos et muista) on vain se, että yksi alkio
koostuu kahdesta tavusta (joista ensimmäinen on merkin ASCII ja toinen
merkin väri) ja ruudun leveys on 80 merkkiä. Jos ei mennyt päähän niin
tutustu vielä kerran VGA:ta käsittelevään 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 "piirrät" kursorin lisäämällä väriarvoon - sanotaan vaikka 17
ja pyyhit kursorin lisäämällä siihen saman arvon vastaluvun (-17), eli
toisinsanoen vähennät siitä 17:
#define CShow(x, y, c) addcolor(x, y, c)
#define CHide(x, y, c) addcolor(x, y, -c)
Makrojen käyttö sitten komennoilla "CShow(17)" ja "CHide(17)"...
Lopuksi vielä sananen merkin värin muodosta. Se on XYYYZZZZ, jossa jokainen
kirjain edustaa yhtä bittiä väritavussa. X ilmaisee vilkkuuko merkki (1).
YYY ilmaisee taustan värin (0-7) ja ZZZZ ilmaisee tekstin värin (0-15).
Tässä vielä pikkuruinen makro, joka voi osoittautua hyödylliseksi:
#define BuildC(blink, fore, back) ( (blink<<7) + (back<<4) + (fore) )
Sitten vain vaikka komento "putcolor(x, y, BuildC(0,15,1))", joka aiheuttaisi
välkkymättömän valkoisen tekstin sinisellä pohjalla (31).
Sellaista tällä kertaa. Nyt painun suihkuun ja katsomaan X-Filesia. Jatketaan
taas vaikka huomenna!
6.1 Projektien hallinta - useat tiedostot
-----------------------------------------
Nyt seuraakin sitten jakso lukuja (tai yksi luku, katsotaan nyt),
joissa käsitellään kaikkea tärkeää mitä pelejä ohjelmoidessa pitää
osata sen hardwaren tuntemuksen lisäksi. Tarkoituksena on käydä läpi
useiden c-tiedostojen käyttö, headerien teko, Rhiden projektit,
makefileet, ulkoisen assyn ja assyn yleensäkin käyttö, engineiden
teko, kirjastojen luonti. Kaikki suhteellisen kevyttä kamaa kun ne
vain kerran opettelee, joten aloitamme.
Tähän asti olen opettanut teille huonoja tapoja joita itselläni oli
tapana käyttää vielä puolitoista vuotta sitten (ja vasta viime aikoina
olen päässyt lopullesesti niistä eroon). Olen nimittäin laittanut
koodia noihin .h-tiedoistoihin ja tehnyt niistä kirjastoja, joiden
rutiineja on sitten helppo käyttää. Laajempien projektien ja miksei
hieman suppeampienkin kanssa alkaa kuitenkin ennenpitkää esiintyä
suorastaan ärsyttävän hidasta kääntämistä. Ajattele seuraavaa
tapausta:
Peliprojektissa on ääniengine sound.h (yksinkertainen, vain vähän alle
3000 riviä), sprite-engine sprite.hh (minimaalinen toiminta, hieman
inline-assyä, 800 riviä), sekalaisia hardware-rutiineja
(kellokeskeytys, näppishandleri jne. 1000 riviä) sekä itse pelin
koodia 2000 riviä. Näin joka kerta käännämme vähän alle 7000 riviä
C-koodia. Mutta miksi kääntää kaikki joka kerta kun vain yksi muuttuu
yleensä kerrallaan? Muuttakaamme hieman lähestymistapaa löytääksemme
parempi keino.
Keinoa kutsutaan projekteiksi, usean C-tiedoston käytöksi ja ties
miksi. Ideana on, että jokainen looginen kokonaisuus on jaettu omaan
.c-tiedostoonsa ja .h-tiedostoonsa. Tällaisia voisivat olla
näppishandleri, timerhandleri, sprite-rutiinit, modien lataus,
äänienginen ohjelmointirajapinta, sb-osa koodista, gus-osa koodista
jne.. Jokaiselle tiedostolle olisi sitten oma .h-tiedostonsa, jossa
määritellään 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 kääntäjän linkkeri huolehtisi siitä, että
ohjelmakutsut menevät oikeisiin osoitteisiinsa.
Katsotaanpas pientä esimerkki h-tiedostoa ja c-tiedostoa. En väitä
tämän olevan ainoa oikea tapa, tämä 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++;
}
Lähdetäänpäs askeltamaan ESIM.H-tiedostoamme lävitse. Ensimmäisenä
rivi #ifndef __ESIM_H, joka ilmoittaa C-koodin esikäsittelijälle, että
jos __ESIM_H ei ole määritelty (IF Not DEFined, IFNDEF) niin osio
#ifndef:in ja #endif:in välissä tulee ottaa mukaan. Sen jälkeen
määritellään tuo kyseinen muuttuja, jotta H-tiedostoa ei pureta
kahteen kertaan (voi sattua kaikkea hassua jos vaikka h-tiedostot
kutsuvat toisiaan). Sitten tulee tämän C-tiedoston tarvitsemien
funktioiden kirjastot ja #definet (kirjastot voitaisiin sijoittaa myös
C-tiedostoon, mutta joskus tästä tulee ongelmia, jos käytetään makroja
tai muuta vastaavaa).
Sitten tulevat muuttujat ja funktiot. Muuttujien eteen TULEE laittaa
extern-määre, joka kertoo että ne on oikeasti määritelty jossain
muualla, jottei kääntäjä varaa muistia näille 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 jättää pois
ja lisätä extern-määreen jos ko. funktio on ulkoisessa
assembler-tiedostossa.
Funktion parametrien nimet voi halutessa jättää määrittelyistä pois,
mutta se ei ole suositeltavaa. Muista myös, että globaalit muuttujat
esitellään ja alustetaan VAIN ja AINOASTAAN C-tiedostossa, ei
H-tiedossa!
C-tiedosto sisältää vastaavat H-tiedostossa "luvatut" funktiot ja
muuttujat. Jos haluat tehdä globaaleja muuttujia jotka eivät näy
muihin C-tiedostoihin, niin jätät sen esittelyn H-tiedostosta pois,
jolloin headerin sisällyttävät muut C-tiedostot eivät tiedä mitään
ko. muuttujan olemassaolosta eikä vahingossa tule virheitä. Tällainen
on esimerkki C-tiedoston muuttuja Oma.
Useita C-tiedostoja käyttäessäsi teet siis jokaisesta loogisesta
kokonaisuudesta oman "paketin", joka sisältää C-tiedoston, joka on
toimiva kokonaisuutensa ja H-tiedoston, joka tarjoaa muille
C-tiedostoille mahdollisuuden käyttää tämän paketin rutiineja.
Muista, että käyttäessäsi includea tuollaisen tiedoston kohdalla
käytetään heittomerkkejä normaalin <>-parin sijasta, jottei kääntäjä
lähde hakemaan ESIM.H:ta omasta include-hakemistostaan, vaan jotta se
hakisi tiedoston senhetkisestä työskentelyhakemistosta.
Mieti nyt nämä asiat selviksi, jotta ymmärrät miten tehdään useita
tiedostoja ja käytetään ilman ongelmia, niin voit sen jälkeen jatkaa
seuraavaan lukuun, jossa kerrotaan miten niistä muodostetaan ajettavia
ohjelmia, kirjastoja ja objektitiedostoja.
6.2 Useiden tiedostojen projektit - kääntäminen ja hallinta
-----------------------------------------------------------
No niin, osaat nyt tehdä C-tiedostoja ja H-tiedostoja, mutta sillä ei
varmaankaan pitkälle pötkitä. Lähdemme nyt tutkimaan hieman
kääntäjämme, GCC:n sielunelämää ja tutustumme muutamaan elintärkeään
tietoon joita ilman ei voi edes elää. Nimittäin janoamme tietoa
formaateista.
Tiedostot joiden kanssa pyörimme DJGPP:n kanssa voidaan jakaa helposti
pelkistäen neljään (4) kategoriaan. Tässä ne ovat:
1. Lähdekooditiedostot (c, cc, s, asm). Kääntäjä muuttaa koodin
konekieleksi ja tekee muut tarvittavat tehtävät tuottaen
objektitiedoston.
2. Objektitiedosto (O). Sisältää koodin ja symboleja (eli funktioiden
ja muuttujien nimiä) ja kaikkea muuta kivaa infoa jotka liittyvät
olennaisesti rutiinien käskyihin ja dataan. Linkkeri linkkaa kaikki
objektitiedostot yhteen ja lisää tarvittavaa käynnistyskoodia sun
muuta luodakseen ajettavan tiedoston. Nämä ovat eräänlaisia
rakennuspalikoita, joissa kaikki on jo binäärimuodossa.
3. Archive (A). Tätä voidaan halutessa käyttää useiden objektien
säilömiseen, eli paketoidaan monta objektitiedostoa yhteen kasaan
jotka voidaan liittää sitten yhtenä pakettina
kääntäjälle. Objekteista siis kootaan nippu jota voidaan käsitellä
yhtenä kokonaisuutena.
4. Ajettava tiedosto. Sisältää objektitiedostoista tehdyn EXE:n, jossa
on lisäksi tarvittava koodi ohjelman käynnistämiseen.
GCC:n toimintaperiaate EXE:n käännössä on seuraava: Lähdetään
kääntämällä lähdekooditiedostot objektitiedostoiksi. Tässä vaiheessa
siis laajennetaan makrot, includet ja esikäsittelijän komennot (kaikki
#ifndef-rakenteet sun muut). Sitten käännetään koodi konekielelle ja
tehdään objektitiedostot.
Seuraavaksi kutsutaan linkkeri joka liittää objektitiedostot yhteen ja
lisää tarvittavat kirjastot (LIBC.A tulee EXE:en aina mukaan ja
lisäksi muut -l<nimi> parametreillä annetut kirjastot) sekä
aloituskoodin, joka kutsuu main-funktiota, jonka oletetaan löytyvän
jostain O-tiedostosta.
Itseasiassa tuo ei mene aivan noin yksinkertaisesti, mutta tärkeintä
on ymmärtää, että lähdekoodista tehdään rakennuspalikoita,
objektitiedostoja joista voidaan myöhemmin koota ajettavia tiedostoja.
Jos meillä siis olisi C-tiedostot main.c ja apu.c (mahdollisesti
vastaavine H-tiedostoineen), joista main.c sisältäisi main-funktion ja
pääkoodin ja apu.c kaikkia tarpeellisia rutiineja, niin voisimme
kääntää ne objektitiedostoiksi ja aina kun jompaakumpaa muunnetaan,
niin kääntäisimme tämän lähdekooditiedoston uudelleen. EXE
muodostettaisiin erikseen toisella komennolla jolloin muutos toisessa
tiedostossa vähentäisi käännettävän koodin määrää (tosin linkkaustyö
pysyisi ennallaan).
Miten sitten näitä erilaisia tiedostoja tehdään? Hyvä kysymys. Alla
näette kaikkein komentoja objektitiedostojen, EXE:jen ja archivejen
luontiin, lähdekoodit osaatte varmaan jo. =)
Objektitiedosto GCC:llä:
gcc -c koodi.c -o objekti.o (halutessa lähdetiedostoja voi olla useampia)
Archive-tiedosto objektitiedostoista:
ar rs archive.a objekti1.o ... (kaikki halutut objektit vain perään)
Ajettava tiedosto archive-, objekti- ja lähdekooditiedostoista (GCC
osaa käsitellä ne päätteiden mukaan):
gcc <tiedostot> -o tulos.exe <parametrit>
Lisää infoa sitä haluaville löytyy englanninkielisenä komennolla
INFO. Sitä löytyy aika paljon enkä todellakaan halua tästä
tutoriaalista mitään DJGPP:n komentoriviparametrien selitystä. =)
Eli kerrataan vielä vaiheet joita käytätte "oikeaoppisen" projektin
tekoon:
1. Luo C- ja H-tiedostot ja muu tarvittava lähdekoodi
2. Käännä 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.
Myöhemmin nuo enginen objektit olisi helppo lisätä EXE:een pelkällä
-lgraf -parametrilla.
4. Käännä 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 jätätte EXE:ksi
kääntämisen kokonaan pois, ja teette vain archive-tiedoston. Tai jos
tarvit vain yhden .o -tiedoston, niin mikäs siinä, valinta on vapaa.
Nyt sinun pitäisi osata tehdä objektitiedostoja lähdekoodista,
kirjastotiedostoja objekteista ja ajettava ohjelma objekteista (ja
mahdollisesti myös kirjastoista). Kun hallitset nämä asiat jatkamme
jälleen taivaltamme.
6.3 Hieman automaatiota - tapaus Rhide
--------------------------------------
No tällä hetkellä me osaamme kaikki tarvittavat taidot komentoriviltä,
mutta uusien tiedostojen nimien muistaminen ei aina ole kivaa ja
komentorivillä vääntäminen sopii vain perusteiden harjoitteluun. Rhide
on tapa päästä koko roskasta helpolla ilman perusteita edes
objektitiedostoista, mutta koska teillä tulee olemaan niin paljon
helpompaa kun ne osaatte niin olen katsonut tarpeelliseksi ne myös
neuvoa. (sillä Rhidenkin kanssa kunnon projekteilla tarvitaan tuota
osaamista).
Ainahan pääsee helpolla, mutta valitettava tosiasia on, että se joka
hyppäsi edelliset kappaleet ylitse onkin sormi suussa kun tulee
ongelma eteen. Mikään ei korvaa tietoa ja kokemusta, ei edes hyvä
ohjelmointiväline.
Eli tämän kappaleen tarjoama informaatio käsittelee Rhideä ja sen
projekteja projektien hallinnassa. Jos teitä ei Rhide kiinnosta niin
voitte hypätä yli, lupaan että seuraava kappale kiinnostaa teitä,
sillä makefilejen käyttö on vaihtoehtoinen (ja gurumpi, elegantimpi ja
yleisempikin) tapa automatisoida projektien kääntäminen. Mutta te
joita kiinnostaa yksi tämän hetken parhaimmista DOS-ympäristön
IDE-ohjelmista pysykää kappaleessa, tosin asia voi olla joillekin jo
vanhaa leipää.
Eli Rhiden sisältää makefileiden kaltaisen järjestelmän projektien
hallintaan, mutta toisin kuin make se sisältää tekoälyä, joka osaa
projektille valitusta kohteesta päätellä millainen tulos halutaan ja
projektin tiedostojen päätteistä minkätyyppinen tiedosto on kyseessä
ja miten se pitää kääntää. Koska Rhide on aika yksinkertainen
järjestelmä käsittelen vain lyhyesti sen perusasiat, eli projektien
teon, availun, käsittelyn, Rhiden kustomoinnin ja kohteiden
määräämisen.
Eli aloittakaamme tekemällä oletusprojekti Rhidelle. Ensimmäinen
tehtäväsi lienee installoida Rhide, joka yleensä koostuu purkamisesta
DJGPP-hakemistoon ja ohjelman käynnistämisestä kokeeksi. Dokumenttien
lukeminenkaan ei ole pahasta, mutta kyllä ilmankin voi pärjätä, tosin
vaikeuksien sattuessa ne ovat usein korvaamattomia. Rhiden jotkin
versiot ovat olleet enemmän tai vähemmän 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". Tämä tarkoitus on luoda/muuttaa
BIN-hakemistossa olevaa rhide-nimistä projektia, jonka asetukset
ladataan AINA kun rhide käynnistetään ilman projektia ja jotka
toimivat uusien projektien oletusasetuksina. Muuttele rhide-projektia
niin paljon kuin haluat/uskallat/viitsit ja lopeta sen jälkeen
rhide. Voit kokeilla vielä asetusten toimivuutta menemällä jonnekin
hakemistoon missä on jokin muu määrä kuin yksi projekteja (jos niitä
on vain yksi niin se ladataan automaattisesti) ja käynnistämällä
Rhiden.
Nyt pitäisi 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 tämän ikkunan ja
painamalla insert-nappia (tai Project-valikosta Add item) saat
lisättyä uusia tiedostoja. Kun olet valmis paina Cancel-nappia.
Tällä tavalla lisäät haluamasi tiedostot (lähdekooditiedostot, tosin
jos ehdottomasti haluat voit laittaa jonkin valmiiksi käännetynkin O-
tai A-tiedoston mukaan) projektiin.
Mukaan lisättäviä kirjastoja voit määrittää Options-valikon
Libraries-kohdasta. Muista, että tämä hakee kirjastoja VAIN DJGPP:n
LIB-hakemistosta, ja että kirjaston nimeen lisätään aina kääntäjän
toimesta eteen LIB ja loppuun .A, eli älä kirjoita koko kirjaston
nimeä tyyliin LIBJOKIN.A, vaan JOKIN. Sellainen erikoisuus kyllä
kääntäjästä löytyy, että ylipitkät (yli 5 merkkiä) kirjaston nimet
katkaistaan, joten IOSTREAM antaa tiedoston LIBIOSTR.A, eikä
virheellistä LIBIOSTREAM.A:ta (joka olisi siis liian pitkä).
Kun olet tyytyväinen kaikkeen muuhun niin ota vielä Project-valikosta
main targetname ja määritä kohteen nimi. Jos olet tekemässä
ääniengineä, niin sinulla on äänienginen C-tiedostot projektissasi ja
kohteena (esim.) LIBSND.A. Jos taas teet C++ EXE:ä, niin sinulla on
C-tiedostot joita käytetään, kohteena (esim.) PLUSPLUS.EXE ja
mahdollisesti kirjastossa IOSTR ja jotain muuta. .A-päätteestä Rhide
osaa automaattisesti kääntää archive-muotoisen tiedoston ja
.EXE-päätteestä ajettavan. Muutkin voivat toimia (O ainakin), mutten
ole kokeillut koskaan, sillä siihen ei yleensä ole tarvetta.
Projektin kääntäminen onnistuu napilla F9, jolloin Rhide osaa
automaattisesti katsoa tiedoston päiväyksistä mitkä tiedostot ovat
muuttuneita (lähteen päivämäärä uudempi kuin kohteen) ja kääntää näin
vain tarpeellisen. Aikaa säästyy ja hermoja samoin. Kääntämisen
jälkeen hakemistostasi löytyy luultavasti kasa objektitiedostoja,
joita voidaan käyttää myöhemmin linkkauksessa (jos vastaava
lähdekooditiedosto ei ole muuttunut).
Sellaista tällä kertaa. Aika perusasiaa ja itsekin pääteltävissä,
mutta joskus vain käy siten ettei jotain perusasiaa itse hoksaa, tai
ainakin säästää aikaa kun ei tarvitse kaikkea kokeilla. Nyt hallussa
pitäisi olla projektien teko Rhidellä ja niiden toimimaan saaminen, ei
sen kummempaa tällä kertaa. Voit jatkaa halutessasi seuraavaan jos
tuntuu että osaat tämänkin kappaleen materiaalin.
6.4 Todellista guruutta - salaperäinen make
-------------------------------------------
Make on kuin suoraan Unix-maailmasta tullut. Jos pelkkä vilkaisu sen
info-sivuille (INFO MAKE) saa aloittelijan vapisemaan horkassa. Mutta
ei hätää, minä kävin siellä ja selvisin elossa - tosin en ole enää
ollut sama itseni sen jälkeen. Olen nimittäin huomattavasti gurumpi
jälleen sillä voin käännellä projektini halutessani hienosti
komentoriviltä automatisoituna. Ja se onnistuu maken
makefileillä. Tässä luvussa kerron miten niitä tehdään, tosin en
mitään monimutkaisempaa valota kun mitään ihmekonsteja harvemmin
normaalissa perustyöskentelyssä tarvitsee.
Eli ensimmäisenä tehtävänä on jälleen kaivaa make jostain, paikat ja
keinot ovat samat kuin Rhiden kohdalla, mutta toisin kuin Rhide maken
pitäisi 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 käsketyt
asiat.
Mutta tehdäksemme oikeanlaisia makefilejä meidän täytyy ensin hieman
ymmärtää filosofiaa maken takana.
Normaali makefile koostuu yleensä alussa olevasta kasasta
muuttujamäärittelyjä, joita myöhemmin käytetään kääntämisessä. Sen
jälkeen on kasa ohjeita, jotka koostuvat muutamasta
komponentista. Tässä 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 ensimmäisenä on kohde joka kertoo makelle, että tässä on ohje
miten teet tämän. Sitten on riippuvuudet, joka kertoo, että näiden
pitää olla kunnossa ennenkuin tätä ohjetta aletaan
toteuttamaan. Seuraavalla rivillä on yksi TAB:in painallus ja komento
jolla kohde tehdään (komentoja voi olla useampiakin, jokainen omalla
rivillään alkaen TAB:illa). Huomaa, että tarvitsemme EHDOTTOMASTI
oikean TAB:in, emme mitääs MSDOS EDIT:in lelutabbeja, jotka eivät
itseasiassa ole kuin määrätty määrä välilyöntejä. Eli pitää olla
jonkinlainen editori, joka osaa käyttää aitoja TAB-merkkejä.
En taida alkaa miettimään syvällisemmin maken toimintaa, mutta ideana
on, että esittelet ensin pääkohteen ja sen riippuvuudet ja sen jälkeen
esittelet nämä uudet riippuvuudet ja niiden riippuvuudet jatkaen
pohjalle asti kunnes lopulta sinulla on kohteena objektitiedosto ja
lähteenä lähdekooditiedosto ja alla komento tämän kääntämiseksi,
jolloin make katsoo päivämäärän mukaan tarvitseeko tämä kohde
päivittämistä. Jos lähde on uudempi kuin kohde niin käsky suoritetaan
mutta jos kohde on uudempi niin se on täydytty kääntää lähteen
muuttamisen jälkeen eikä kääntöä tarvita. Tällä tavalla vain
muuttuneiden tiedostojen aiheuttamat käännöstarpeet hoidetaan eikä
ylimääräistä työtä tehdä.
Yleensä makefilessä on ensin kohde all, jossa riippuvuuksina on kaikki
mitä makefilen tulee saada tuloksena valmiiksi (EXE:t, kirjastot),
sitten on näiden tuloksien ohjeet riippuvuuksina objekti- ja
archive-tiedostot, sitten archive-tiedostot riippuvuuksina
objektitiedostot ja lopuksi objektitiedostot riippuvuuksina
lähdekooditiedostot. Tässä on esimerkki joka varmaan valaisee aika
sekavaa selitystäni. =) Huomaa myös makrot, jotka määritellään alussa
ja joita muuttamalla on helppo vaihtaa käännöksessä tarvittavia
parametrejä ja kääntäjien 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 tämän tiedoston tallentaa nimelle makefile tarvitsee sinun vain
antaa komento make niin ohjelma osaa automaattisesti kääntää kaikki
makefilessä määritellyt tiedostot. Käyttääksesi muita makefilen nimiä
pitää maken komentoriville antaa parametri -f<makefile>.
Esimerkki oli hyvin yksinkertaistettu ja vältin käyttämästä paria
hauskaa kikkaa jotka tekevät makefilestä paljon lyhyemmän (ja
sotkuisemman näköisen). Jos kuitenkin toiminta on epävarmaa, niin
selostetaan se tässä vielä kertaalleen:
1. Make aloittaa lausekkeesta all (komentorivillä voit halutessasi
määrätä mikä ohje tulee tehdä, esim make libx.a ei koskisi esim.*
-tiedostoihin) ja etenee tekemään esim.exe:ä.
2. Esim.exe:n teko tarvitsee ensin esim.o:n, siirrytään siihen.
3. Esim.o tarvitsee esim.c:n, mutta sille ei löydy ohjetta, joten
suoritetaan ensimmäinen käännös. Makrot CC ja CFLAGS puretaan
komentoriville ja se suoritetaan ja kaiutetaan näytölle. Jatketaan
esim.exe:n riippuvuuksien tutkimista.
4. Esim.exe:n teko tyssää kun siihenkin pitää tehdä libx.a, joten
siirrytään tekemään sitä.
3. Libx.a:han pitää olla x1.o ja x2.o, joten siirrytään niihin.
4. Riippuvuudelle x1.c ei ole ohjetta, joten suoritetaan x1.o:n
komento (näissä kohtaa olisi päivämäärätarkistus, mutta koska
noita objektitiedostoja ei vielä ole olemassa niin...) ja palataan
takaisin.
5. x2.o tehdään samaan tapaan kuin edellinen ja palataan libx.a:n
pariin
6. Riippuvuudet kunnossa, tehdään kirjasto libx.a, palataan esim.exe:n
kimppuun.
7. Esim.exe:n riippuvuudetkin ovat hanskassa, joten tehdään 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
pidemmät ja selvemmät tekstit löytää englanniksi komennolla info make
(no selvemmistä en itseasiassa tiedä :).
Mutta make ei vielä ole ohitse, en uskalla päästää teitä kappaleesta
ennenkuin osaatte tehdä ohjeita jotka tekevät vaikka 30
objektitiedostoa kerralla, ne kun ovat kovin mukavia systeemejä
verrattuna siihen että joutuisit kirjoittamaan jokaista varten oman
ohjeen.
Ideana tässä on eräänlainen nimentäydennys. Make osaa poistaa päätteen
nimestä ja korvata sen toisella, jota ominaisuutta käytetään juuri
tähän useiden samankaltaisten tiedostojen tekoon kerralla. Jos siis
sinulla on 10 objektitiedostoa ja jokainen käännetään
vastaavannimisestä lähdekooditiedostosta (o1.o ja o1.c, o2.o ja o2.c
jne.), niin niiden kääntö onnistuu seuraavalla tyylillä (aika maken
infoista pöllittyä ja suoraan käännettyä 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 ensimmäisenä tulee lista (OBJECTS) tehtävistä kohteista, sitten
tulee %-merkki, joka esiintyy kohde-patternissa vain kerran, ja
maken infosivut käyttävät siitä nimeä "stem". Tämä vastaa mitä tahansa
kohtaa yhden kohteen nimestä, kaikki muut kohteen nimessä (.o tässä
tapauksessa) täytyy vastata täysin.
Jos siis kohteena olisi foo.o ja kohde-pattern olisi %.o, niin "stem"
(anteeksi minulla ei ole sanakirjaa käsillä ;) saisi arvon foo. Jos
riippuvuus-pattern olisi %.c niin riippuvuus tälle tiedostolle olisi
foo.c. Ei mitään sen vaikeampaa, % on kuin DOS-maailman * ja
ensimmäisenä tulee lista tiedostoista (kuten hakemistolistaus), sitten
stemillä varustettu patterni ja lopuksi riippuvuudet jotka
täydennetään sillä mitä stem vastaa.
Lisäksi täytyy kiinnittää huomio merkkisarjoihin $< ja $@, joista
ensimmäinen korvataan riippuvuudella (tai riippuviiksilla jos niitä on
useampia) ja toinen kohteen nimellä. Myös muita vastaanvankaltaisia
löytyy, mutta ne eivät ole läheskään niin hyödyllisiä kuin nämä kaksi.
Näillä eväillä ainakin pitäisi onnistua makefileiden teko aika
pitkälle. Hyviä esimerkkejä löytyy lukemattomista DJGPP-paketeista,
joissa kääntäminen hoidetaan makefileillä. Makefilet ovat muutenkin
yleisin tapa levittää lähdekoodin kanssa softaa, harvemmin olen nähnyt
kirjaston käännöstä automatisoitavan Rhiden projekteilla. :)
6.5 Ammattimaista meininkiä - enginen teko
------------------------------------------
Tämä luku kertoo hieman niistä vähäisistä kokemuksista mitä minulla on
ollut projektien kanssa, tai oikeammin kertoo mitä kannattaisi ottaa
huomioon enginen teossa, jotta se toimisi myös huomenna ja jotta siitä
jälkeenpäin saisi jotain selvääkin.
Näppärä tapa pääohjelman yksinkertaistamiseksi on tehdä tietyn
tehtävän suorittavista tiedostoista yksi paketti, kirjasto jonka
headerin koodiin sisällyttämällä voi kyseisen tehtävän hoitaa
kirjaston tarjoamilla rutiineilla.
Sen lisäksi että tapa yksinkertaistaa koodia se myös parantaa sen
ylläpidettävyyttä huomattavasti ja myöskin muunneltavuus on aivan eri
luokkaa kuin "kaikki-yhdessä-kasassa" -ohjelmilla. Lisäksi kun engine
on kerran valmis voi sitä käyttää uudelleen ja uudelleen - yleensä
pienillä muutoksilla tai parhaimmillaan muuttamattomanakin.
Mutta tällaisenkin teossa kannattaa huomioida joitakin asioita, jottei
jälkeenpäin paljastuisi että olet tehnyt turhaa työtä koko
ajan. Nimittäin ensin on tarkoin otettava selvää mitä engineltä
vaaditaan ennenkuin sellaista alkaa tekemään. Hyvä tapa on miettiä
millaista peliä on tekemässä ja millaisia ominaisuuksia engineltä
vaaditaan. Matopelin teossa ei välttämättä tarvita kovin kummoisia
järjestelmiä, sillä ne eivät useastikaan vaadi kovinkaan monimutkaista
toimintaa hyvän jäljen aikaansaamiseksi. Toisin on vaikka sivultapäin
kuvatussa ammuskelupelissä, jossa spritejen piirron pitää olla
äärimmäisen nopeaa ja turhaa piirtelyä tulee välttää. Skrollaus vaatii
myös tällaisissa peleissä tehoja ja muuttujia spriteihin tulee
huomattavasti enemmän kuin matopelissä.
Mikään ei voita kunnon suunnittelua kun koodausta sitten aletaan
tekemään. Hyvällä onnella koko enginen teko on suoraviivaista koodin
kirjoittamista jos tärkeimpiä algoritmejä on jo hahmoteltu paperilla
ja mielessä on kunkin funktion toiminta ja tarvittavat muuttujat
kuhunkin tehtävään.
Kun tarpeet ovat vihdoin paperilla ja koodin kirjoitus edessä voi olla
hyvä vielä etukäteen nimetä enginen lohkot ja nimetä ne. Näppärä tapa
jolla pääsee suoraan toimeen on käynnistää vaikka Rhide ja lähteä
lisäilemään uuteen projektiin tiedostojen nimiä. Tiedostoja ei
tarvitse edes olla olemassa vaan riittää että hahmotat mitä
järjestelmän pitää tehdä ja minkälaisiin osiin se pitäisi
jakaa. Kaikkein kevyimmät enginet eivät edes paljoa tiedostoja tarvi,
näppishandleri ja timerhandleri, hiirirutiinit ja yksinkertaisemmat
grafiikkaenginet menevät ainakin tähän kastiin. Äänienginet,
playerit ja 3D-enginet sekä raskaammat grafiikkaenginet taas voivat
hyvinkin viedä toistakymmentäkin tiedostoa.
Hyviä jakotapoja on monia ja järki varmaan sanoo, että hyvä jakotapa
ei ole aakkosjärjestys taikka pituusjärjestys. Hyvä jakotapa voi olla
vaikka äänienginen teossa päätiedosto sisältäen käynnistys- ja
lopetusfunktiot ja jonka .h-tiedostosta löytyvät keskeiset
datarakenteet, latausrutiinit sisältävä tiedosto, universaali
efektinsoittorajapinta ja eri tiedostot jokaiselle äänikortille,
modien lataus, modien soittorutiinit sisältävä tiedosto jne.. Aivoja
saa, pitää ja kannattaa käyttää.
Tärkeitä suunnittelun kohteita on myös se miten ohjelma säilöö datansa
sekä muistissa että kovalevyllä. Jo alussa fiksusti ja
laajennettavasti tehty rakenne on monta kertaa käyttökelpoisempi kuin
senhetkiseen tarpeeseen väsätty kyhäelmä. Myös tallennus- ja
latausrutiinit kannattaa tehdä erikseen eikä pyrkiä tekemään mitään
purkkaviritelmiä jotka kaatuvat vähintäänkin kun haluat lisätä uuden
ominaisuuden.
Hyvä idea on myös tehdä universaalit rutiinit virheistä
ilmoittamiseen, muistin varaukseen ja vaikka tiedostojenkin
lukuun. Yleensäkin enginen suurin osa tulisi sijoittaa keskivälille
muutaman kriittisten low-level -rutiinien jäädessä alapuolelle ja
yläpuolelle tuleva rajapinta ohjelmalle mahdollistaa enginen
muuttumisen radikaalistikin ilman muutoksia pääohjelmaan. 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
käyttäviksi ei tarvitse muuttaa kuin pari kohtaa kaiken muun jäädessä
samanlaiseksi.
Kommentointi on elintärkeää engineä tehdessä, sillä hyvä engine voi
olla käytössä pitkänkin aikaa ja sitten kun se lopulta jää ahtaaksi
voi huonosti kommentoineen kooderin periä hukka muuntelun
osoittautuessa mahdottomaksi yksinkertaisesti siitä syystä ettei edes
tekijällä ole enää mitään aavistusta mitä hänen koodinsa tekee. Hyvä
ohjelmoija tekee sen verran lyhyitä funktioita, että niistä saa selvää
vähän tutkailemalla ja nimeää muuttujat ja funktiot kuvainnollisesti
säästelemättä turhaan nimen pituudessa (järkevällä tasolla kuitenkin,
mutta saa se nyt enemmän olla kuin Jdrwsprt()). Kun epäselvemmät
kohdat vielä kommentoi koodista pitäisikin saada huomattavasti
paremmin selvää.
Yksi hyödyllinen asia voisi olla tiedostoja editoidessa kirjoittaa
tietty headeri jokaisen tiedoston alkuun. Hyviä voisi olla
copyright-ilmoitukset (joilla ei kyllä omassa käytössä tee mitään),
luontipäivämäärä, viimeisen muutoksen päivämäärä ja muutoshistoria,
jonne kirjataan muutokset koodiin. Jälkeenpäin ja bugeja etsiessä
tuollaisesta on kummasti hyötyä, kun miettii mitä onkaan tullut
lähiaikoina muunneltua.
Viimeinen asia mikä koodissa pitää vielä huomioida on ne funktiot,
jotka tarjoavat rajapinnan, "käyttöliittymän" engineen. Nämä funktiot
ovat siis ne jotka tarjotaan engineä käyttävälle ohjelmalle enginen
käyttöön. Näiden tulee olla tarpeelliksi kattavat jotta kaikkia
enginen ominaisuuksia voidaan halutessa käyttää hyväksi. Hyödyllistä
on tehdä Init- ja Deinit-funktiot, joita kutsutaan pääohjelmasta
ohjelman käynnistyessä ja siitä poistuttaessa.
Myös funktioiden nimeäminen erottamiseksi muista mahdollisista
samankaltaisista funktioista voi olla hyödyllistä. Kirjaston
funktioille ja globaaleille muuttujille voisi antaa jonkin etuliitteen
erottamaan ne muista ja huolehtimaan siitä ettei kahdella funktiolla
ole samaa nimeä. Omassa grafiikkakirjastossani käytän JG-etuliitettä,
jolloin funktioiden nimet ovat tyyliin JG_Draw, JG_Hide jne.. Myös
mahdollinen versionumero kirjastolle on kätevä jos sitä aikoo todella
kehittää kunnolla.
Sitten vain huolehtimaan siitä että enginestä ei löydy
pullonkauloja. Helpointa lienee tehdä enginen eniten tehoa vaativat
osat mahdollisimman nopeiksi, jolloin pääohjelma on helppo tehdä
korkean tason koodilla. Assembler-optimointikin voisi olla ihan kiva,
joten seuraavassa luvussa luulen että selitän hieman sen lisäilystä
DJGPP:n koodiin.
Tämä luku ei nyt varsinaisesti opettanut mitään, mutta ainakin jotain
evästä pitäisi nyt löytyä ensimmäisen enginen tekoon. Katsotaan mitäs
tähän nyt keksisikään seuraavaksi. =)
7.1 Vauhtia peliin - ulkoisen assyn käyttö
------------------------------------------
No niin, assembler, tuo kielistä jaloin näyttää olevan tämänkertaisen
kiinnostukseemme kohteena. Vaan mikä on tuo salaperäinen kieli ja
miten sitä käytetään. Se jää ihan sinun itsesi selvitettäväksi, mutta
voin kuitenkin antaa jonkinlaisia ohjeita jotta löytäisit tiedon
lähteille. Ensihätään 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 sisältäen mm. kaikki
x86-prosessorikäskyt, matikkaprossukäskyt ja Pentiumin omat käskyt
(kuten CMPXCHG8B tai jotain).
PCGPE10.ZIP:
Assytutoriaali löytyy täältäkin, tosin englanniksi.
3DICA*.ZIP:
Sisältää Henri Tuhkasen mainion assembler-optimointitutoriaalin.
Ehdoton ensihankinta optimoinnista kiinnostuneelle.
Lisäksi todella hyvä kirja assyn opetteluun (ja ainoita suomeksi) on
kirja nimeltään 486-ohjelmointi. Tuota kaikki aina suosittelevat enkä
itsekään voi kirjaa haukkua. Kirjastosta tuon saa vielä kaiken lisäksi
ilmaiseksi, vähintään kaukolainauksella.
Jos sinua ei assembler kiinnosta yhtään niin voit tietenkin hypätä
tämän kappaleen yli, mutta varoituksen sana sitä ennen: Jos aiot tehdä
joskus nopean toimintapelin (lähiaikoina ainakin), niin tulet hyvin
luultavasti kaipaamaan assembler-osaamista. No tietenkin jos odottaa
tarpeeksi niin voi tehdä kaiken vaikka Visual Basicin kasiversiolla,
mutta en minäkään takaa että pysyn myöhemmin tutoriaalissa pelkässä
C:ssä. <grin>
Mutta sen jälkeen kun osaat assyn, niin alahan lukemaan pidemmälle,
sillä käsittelen hieman C-kielisestä ohjelmasta kutsuttavien
funktioiden tekoa assyllä. En aio selittää sinulle mikä on pino, sillä
assyoppaista löytyy tuokin tieto. Muistiasi virkistääkseni mainitsen
kuitenkin, että tulee muistaa pinon kasvavan alaspäin, eli jos haluat
varata pinosta 16 tavua niin sinun tulee vähentää esp:stä (extended
stack pointer) 16 tavua, ei lisätä! Palautus taas hoituu lisäämällä.
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 lähtien parametrilistan
oikeasta laidasta päätyen lopulta ensimmäiseen parametriin ja sitten
se heittää ebp:nsä pinoon, kopioi ebp:n esp:hen ja lisää siihen itse
käyttämänsä muistin määrän (eli itseasiassa vain varmistaa että esp
osoittaa pinon päälle) ja kutsuu funktiota käyttäen 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 sisältää indeksissä 0 pinon huipun, eli tällä hetkellä kutsuneen
ohjelman eip:n. Sen jälkeen on ensimmäinen parametri, sitten toinen
parametri jne.. Mutta koska meidän täytyy aluksi tallentaa ebp pinon
päälle pushaamalla se huipulle, jolloin tiedämme, että parametrit ovat
kahden kaksoissanan (ebp ja eip), eli 8 tavun päässä. Tässä funktion
tarvitsema alustuskoodi:
push ebp
mov ebp, esp
Lisäksi on mahdollista varata pinosta muistia haluttu määrä
vähentämällä esp:tä,jolloin siihen jää aukko jonka alussa ebp
on. Muista kuitenkin vapauttaa muisti korottamalla esp:tä. Muista
lisäksi, että koska pino menee alaspäin, niin varattu muisti sijaitsee
myös esp:stä alaspäin, eli negatiivisissä offseteissa.
Sen jälkeen vain osoitellaan parametrejä. Ensimmäinen parametri on
siis nyt kohdassa ebp+8 (koska kopioimme ebp:hen esp:n, jossa pino
oli), ja parametrit seuraavat järjestyksessä 4 tavun välein
riippumatta parametrin koosta, DJGPP näet sijoittelee myös nuo
mahdollisimman hyvin, toisin kuin aiemmin luulin.
Koko roska on itseasiassa hemmetin vaikea ymmärtää ja olen tunnin ajan
loikkinut ympäri kovalevyäni etsimässä tarkennuksia pinon toimintaan
ja miten C-funktiota itse asiassa kutsutaan, sillä en ole koskaan
ottanut viimeisen päälle selvää kääntäjän sielunelämästä.
Piirrän nyt pikkaisen kaavion siitä mitä tietääkseni muistista löytyy
sen jälkeen, 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 tähän
B) Kun EBP on pushattu niin ESP osoittaa tähän, samoin EBP kun ESP on
ensin siirretty myös EBP:hen. Huomaa EBP:n ja EIP:n sijainti
kohdasta B nähden ja parametrin 1 sijainti offsetissa 8 (viivat
ovat käyttämättömiä palasia), sekä
parametrin 2 sijainti offsetissa 10 (parametrin 1 koko on short,
eli 2 tavua!)
C) Kun ESP:tä vähennetään kahdella jotta pinosta saadaan ohjelmalle 2
tavua muistia on meillä nyt kaksi tavua muistia käytössä alkaen
offsetista EBP-2. ESP osoittaa tämän muistin alkuun, mutta
pushailun sattuessa se lähtee vaeltelemaan yhä kauemmas vasemmalle.
Palautuksessa poppaillaan kaikki, jolloin ESP on taas kohdassa C. Sen
jälkeen vapautetaan pino vähentämällä ESP:tä kahdella, jolloin ESP ja
EBP ovat jälleen samoja, eli kohdassa B molemmat. Nyt vielä popataan
EBP, jolloin EBP on alkuperäisessä tilassaan, samoin kuin ESP, joka
osoittaa EIP:n kohdalle. Nyt vain ret, joka ottaa EIP:n pinosta ja
palaa tähän osoitteeseen.
JES! TEIN SEN! (anteeksi tunteenpurkaus mutten uskonut saavani tätä
itsekään selville ilman kenenkään apua ;)
Huomaa, että on aina kutsuvan ohjelman vastuulla pitää rekistereistään
huolta ja puhdistaa parametrit pinosta, jotka sinne on pitänyt
pushailla ennen ohjelman kutsua (niitä ohjelma ei palauta).
Tässä nyt tämä lopullinen assyosuus, joka pitää olla alussa ja
lopussa:
push ebp
mov ebp, esp
sub 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 myös lopussa esp:n palautus korottamalla sitä
parametrien yhteenlasketun koon verran. Huomaa myös, että C lisää
assykoodiin aina yhden alaviivan lisää, eli omien rutiiniesi
funktionimien edessä pitää ASM-tiedostossa olla aina yksi alaviiva
enemmän kuin mitä C-kielisessä. Myös C-kirjaston funktioita kutsuessa
pitää muistaa, eli _printf, _puts jne.. Funktioille joiden nimissä on
C:lläkin yksi tai useampia alaviivoja suoritetaan vain yhden alaviivan
eteenlisäys.
No niin, nyt menee kaikki muu funktioissa, mutta vielä palautus ja
structit sekä reaaliluvut. No tässä kaikki vähä mitä minä siitä
tiedän:
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 käyttänyt helpompaa ja yleensä
hyödyllisempää tapaa välittää ne vain structin osoitteina.
Reaaliluvut annetaan parametreinä tietääkseni ihan samoin kuin muutkin
parametrit.
No mutta. Kaikki tietävät nyt miten varata muistia, kutsua funktioita,
palauttaa tietoja, käyttää parametreja. Mutta tärkein puuttuu, sillä
kukaan ei osaa tehdä tiedostoja jotka voisi linkata DJGPP-ohjelman
mukaan. Siispä töihin!
Jotta objektitiedoston voisi linkata mukaan DJGPP-ohjelmaan täyty sen
olla oikeaa formaattia. DJGPP:n hyväksymä formaatti tunnetaan nimellä
COFF (ei kaljaa!), eli common object file format. Ainoat
käyttämistäni assembler-kääntäjostä jotka tuota tukevat ovat as ja
NASM. As on GNU assembler ja sisältää TODELLA kryptisen näköistä AT&T
assembleria kääntävän yksikön. Mutta kerron jo etukäteen, että
AT&T-formaatti, jota DJGPP käyttää itse sen Unix-taustan takia on
aivan toisen näköistä kuin Intel-syntaksin assy, joten suosittelen,
että ette käytä sitä (halukkaat imuroivat tiedoston DJTUT255.ZIP)!
Paljon parempi kääntäjä on nimeltään Netwide Assembler, lyhyesti NASM,
jonka löytää ainakin MBnetistä ja tietenkin Internetistä. Nimi on
NASM094B.ZIP, mutta voi kyllä olla että uudempiakin on
ilmestnyt. Jokatapauksessa kääntäjä on aivan loistava ja sen käyttökin
on suhteellisen yksinkertaista. Kaikkein parhaiten sen käytön oppii
lukemalla NASM.DOC läpi ja tutkailemalla esimerkkikoodeja (etenkin
AOUTTEST.ASM!) hakemistosta TEST. Mutta niille jotka eivät mielellään
lue englantia on ihan pikkuinen esimerkkisorsa, jolla pääsee nyt
ainakin alkuun siihen asti, että kunnon sanakirja tai tulkkaava kaveri
löytyy:
TEST.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
TEST.H
extern int asmfunktio(int);
void cfunktio(int);
int cmuuttuja;
TEST.C
#include <stdio.h>
void cfunktio(int luku) {
puts("kutsuttiin C-funktiota parametrilla %d\n", luku);
}
int main() {
printf("asmfunktio(10) palautti arvon %d\n", asmfunktio(10));
printf("asmfunktio(20) palautti arvon %d\n", asmfunktio(20));
printf("asmfunktio(5) palautti arvon %d\n", asmfunktio(5));
printf("asmfunktio(2) palautti arvon %d\n", asmfunktio(2));
return 0;
}
H-tiedoston ja C-tiedoston varmaan ymmärrätte, mutta selvennyksenä
vielä assysuudesta, että ensin asetetaan NASM 32-bittiseen
koodinkääntötilaan, sitten määritellään ulkoiset muuttujat _cmuuttuja
(kaksoisasna) ja _cfunktio (kaksoissana sisältäen rutiinin
osoitteen). Sitten koodisegmentissä (.text) on _asmfunktio, joka tekee
kuten aiemmin neuvottiin, eli tallettaa ebp:n ja kopioi esp:n
ebp:hen. Sen jälkeen 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 minäkään
tiedä mutta menen katsomaan. =) Toimi ainakin minulla. Jaa että se
kääntäminen NASM:illa?-) No se on tietenkin komennolla:
nasm -o jokin.o -f coff jokin.asm
No niin, nyt sinun pitäisi hallita assemblerin käyttö C:n kanssa
jotakuinkin välttäen ja nasmilla kääntelykin pitäisi onnistua, sekä
nasm-tiedostojen tekokin ainakin rajoitetusti. Pahoittelen että
tarkempia ohjeita ei annettu, sillä ne olisivat olleet niin pitkät,
että katsoin oppimisen onnistuvan ilman tarkempia ohjeita. Mutta jos
kuitenkin tuntuu, että tämän kappaleen taso leijui kilometritolkulla
tajuntasi yläpuolella niin pyydän ottamaan yhteyttä, sillä en
ihmettele vaikka tämä olisikin vaikein osa tähän asti ja kaikki apu
sen suhteen miten tätä pitäisi parantaa on tarpeen.
Mutta toisaalta jos et assyä muuten osaa etkä ole kaikkea
dokumentaatiota kaivanut esiin mitä löydät voi olla että asia on
paljon selkeämpi jo muutaman päivän päästä. Jos ei kuitenkaan helpota
niin heitä viestiä tännekin päin. Mutta nyt jatkan taas kohti uutta
tuntematonta.
Phew, tämähän käy työstä kun koko päivän kirjoittaa!
7.2 PIT - aikaa ja purkkaa
--------------------------
Hiphei taipaleemme jatkuu edelleen, vaikka kello osoitteleekin
kirjoitushetkellä melkein kahtatoista. Myös ihmeellisestä tekstistä
voinee sen päätellä etten ole välttämättä aivan parhaimmillani ja
terävillimmilläni (villimmilläni?) tähän aikaan päivästä. No, tehän
siitä vain kärsitte, en minä, joten jatkakaamme! ;)
Eli ihmeellinen lyhenne PIT? Mistä se tulee? No tietenkin sanoista
Programmable Interval Timer, eli ohjelmoitava keskeytysajastin. Tämä
on tällainen hauska piiri PC:llä, joka kykenee generoimaan ties millä
tavalla keskeytyksiä. Kiinnostavaa ja tarkkaa tietoa löytyy PCGPE:stä
(PCGPE10.ZIP) tiedostosta PIT.TXT, mutta me keskitymme vain
olennaiseen, nimittäin systeemin omaan kelloon, keskeytykseen
8. Kerron kuitenkin hieman millä tavalla piiri laskee milloin pitää
generoida keskeytys 8, ennenkuin pääsemme hauskaan tavaraan (eli
esimerkkikoodiin ;).
Eli PIT tikittää 1193181Hz:n taajuudella, eli suomeksi 1193181 kertaa
sekunnissa. Joka kerta se esim. vähentää kanavan 0 laskuria yhdellä ja
jos se on 0 niin se generoi keskeytyksen ja asettaa uudelleen laskurin
haluttuun arvoon ja lähtee laskemaan alaspäin. Laskuri on kahden
tavun, eli yhden sanan mittainen ja kykenee näinollen vastaanottamaan
luvun väliltä 0-65335. Mutta erikoisuutena on se, että jos laskurin
alustusarvo 0 ei tarkoitakaan että keskeytystä kutsutaan jatkuvalla
syötöllä, vaan että sitä kutsutaan 65536:n "tikahduksen" (ei näin
myöhään oikein sanat muistu mieleen) jälkeen. Normaali systeemikello
on asetettu tähän kutsuntatiheyteen, eli sitä kutsutaan
1193181/65536=n. 18.2 kertaa sekunnissa.
Jos siis koukutamme tämän keskeytyksen kuten olemme aiemmin tehneet
näppiskeskeytyksellekin tulee alkuperäistä kutsua tähän tahtiin, sillä
toisin kuin näppiskeskeytys, kellokeskeytys on huomattavasti
tärkeämmässä asemassa eikä sitä voi hypätä noin vain yli (ainakin
DOS:in kello pysähtyy koko ajaksi =). Jos me siis koukutamme
keskeytyksen tulee sen olla tämäntyylinen:
funktio kellokeskeytys
<tee jotain>
laskuri = laskuri + tikkejä_per_kutsu;
jos (laskuri on suurempi tai yhtäsuuri kuin 65536)
laskuri = laskuri - 65536
kutsu_vanhaa();
muuten
kuittaa_keskeytys();
end jos
end funktio
Tikkejä_per_kutsu on siis uusi määrä tarvittavia tikkejä jokaisen
keskeytyksen välissä. Jos vaikka haluaisimme että omaa kelloamme
kutsutaan 100 kertaa sekunnissa, niin meidän pitäisi asettaa PIT:ille
laskurin alustusluvuksi 1193181 / 100 = n. 11931. Sitten vain joka
kutsulla lisätään laskuria sen mukaan montako tikkiä on kulunut
edellisestä vanhan kellon kutsusta ja jos se on alkuperäinen 65536 tai
suurempi, niin vähennetään siitä tämä luku ja kutsutaan vanhaa
keskeytystä. Jos se on vielä alle 65536, niin lähetetään tuttuun
tapaan tavu 0x20 porttiin 0x20.
Kellokeskeytyksen <tee jotain> -kohdan voi ja kannattaakin yleensä
korvata laskurilla, jota korotetaan jatkuvasti. Tätä voi käyttää
vaikka ajanottoon tai muuhun hyödylliseen, kuten näemme myöhemmin.
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 käsittelijää. Vastaavasti iret
keskeytyskäsittelijän lopussa ne otetaan sieltä pois ja niiden avulla
palataan jatkamaan keskeytynyttä ohjelman suoritusta samasta tilasta.
Mutta kun kutsumme vanhaakin käsittelijää välissä, niin pinosta pois
otto tapahtuu kahdesti, mikä eteen? Selvää on, että ohjelma kaatuu jos
ei tätä ongelmaa korjata. Mutta hätiin saapuu Kaj Björklund uljaalla
inline assembler-ratsullaan pelastaen meidät pulasta! Meidän tarvitsee
vain kellokeskeytystä asetettaessa ottaa talteen alkup. handlerin
koodiselektori ja offsetti sekä tallentaa ne 64-bittiseen muuttujaan
(long long). Sitten vain käytetään seuraavanlaista inline-pätkää:
__asm__ __volatile(
"pushfl
lcall %0
"
:
: "g" (oldhandler));
Edellinen koodinpätkä tekee samat temput ennen funktion kutsumista
kuin mitä sanoin normaalisti tehtävän, 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 {