Skip to content

Instantly share code, notes, and snippets.

@pekkavaa
Created May 26, 2016 18:40
Show Gist options
  • Save pekkavaa/59d66f6f8ca38c2a72de84cd39c72561 to your computer and use it in GitHub Desktop.
Save pekkavaa/59d66f6f8ca38c2a72de84cd39c72561 to your computer and use it in GitHub Desktop.
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 ;).
[email protected] 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 {
outportb(0x20, 0x20);
}
}
static volatile void TimerEnd() {}
void SetTimerRate(unsigned short ticks) {
outportb(0x43, 0x34);
outportb(0x40, ( ticks & 0x00FF ) );
outportb(0x40, ( ( ticks >> 8 ) & 0x00FF ) );
}
void InitTimer(int tickspersecond) {
__dpmi_meminfo lock;
lock.address = __djgpp_base_address + (unsigned) &TimerStart;
lock.size = ((unsigned)&TimerEnd - (unsigned)&TimerStart);
__dpmi_lock_linear_region(&lock);
Counter=0;
OriginalTicks=0;
TicksPerCall=1193181/((unsigned short)tickspersecond);
disable();
_go32_dpmi_get_protected_mode_interrupt_vector(0x0008, &original);
OldTimerHandler=((unsigned long long)original.pm_offset) +
(((unsigned long long)original.pm_selector)<<32);
info.pm_offset=(unsigned long int)TimerHandler;
info.pm_selector=_my_cs();
_go32_dpmi_allocate_iret_wrapper(&info);
SetTimerRate(TicksPerCall);
_go32_dpmi_set_protected_mode_interrupt_vector(0x0008, &info);
enable();
}
void DeinitTimer() {
disable();
SetTimerRate(0);
_go32_dpmi_set_protected_mode_interrupt_vector(0x0008, &original);
enable();
}
Muu mennee ihan hyvin tajunnan perälle asti, mutta InitTimer-rutiinin
alku voi hyvinkin tuottaa ihmettelyä, samoin kuin kaksi tyhjää
funktiota kummallakin puolella TimerHandler-rutiinia. No minäpäs
kerron mistä on kyse. Kyse on muistin lukitsemisesta, kuten ehkä
komentojen nimistä voi päätellä. Normaalisti DPMI-palvelin (jos se
siihen kykenee) voi swapata levylle koodia ja dataa jos siltä tuntuu,
mutta kun muistialue lukitaan niin sitä ei swappaillakaan
minnekään. Älä huoli jos epäilet ettet olisi osannut noita tehdä itse,
sillä minäkin varas- käytin apunani libc:n lähdekoodeista löytyvää
koodinpätkää ja Kaj Björklundin esimerkkikoodia.
No nyt vain sitten esimerkkiohjelma, joka näyttää hieman mihin
timer-rutiini pystyy:
#include <stdio.h>
#include <conio.h>
extern void InitTimer(int);
extern void DeinitTimer();
extern volatile int Counter;
int main() {
InitTimer(100);
while(!kbhit()) {
printf("Counter=%d\r", Counter);
fflush(stdout);
}
getch();
DeinitTimer();
return 0;
}
Näin. Seuraavassa luvussa esittelen ennen nukkumaanmenoani (ellei joku
tule ajamaan minua unten maille ennen kuin ehdin kirjoittaa seuraavan
luvun =) kiinnostavaa käyttöäkin tälle, joten pysykää kanavalla!
7.3 Miten peli toimii yhtä nopeasti kaikilla koneilla
-----------------------------------------------------
No tähän on useita tapoja, mutta lähes kaikissa tarvitaan ajanottoa ja
näinollen edellisen luvun ajastinrutiini pohjusti varsin mukavasti
tämän luvun aihetta (josta tulee luultavasti todella lyhyt). Idea on
siis se, että jokaisella koneella peli pyörisi yhtä nopeasti. No
helpommin sanottu kuin tehty.
Varmastikin käytetyin ja toimivin on menetelmä, jota kutsutaan
hienosti termillä "frameskip", eli kuvien yli hyppiminen. Ilkka
Pelkonen käytti siitä brutaalia termiä harppominen, mutta koska
minulle tulee siitä mieleen vain pitkäjalkaiset laihat
kumisaapasjalkaiset miehet niin käytän englanninkielistä termiä
(Ilkka, kyllä minä käyttäisin edes "loikkimista", siitä tulee edes
kengurut mieleen ;).
Eli idea on, että kaikki muu tehdään joka framelle, mutta piirtäminen
jätetään väliin jos ollaan "aikataulusta jäljessä". Niinpä kun meillä
on nyt ajastinrutiini voimme käyttää tällaista systeemiä:
päälooppi
käsitteleframe
vähennä timerlaskuria
jos timerlaskuri = 0
piirrä
tai jos timerlaskuri < 0
odota kunnes timerlaskuri >= 0
end päälooppi
Eli itseasiassa edellisen luvun laskuria vähennetään itse pelissä koko
ajan pyrkien pitämään se nollassa, mutta jos piirron aikana on ehtinyt
mennä useampi frame sivu suun niin käsitellään framea ja vähennetään
timerlaskuria niin kauan että ollaan taas saatu "kiinni" oikea tahti
ja voidaan päivittää seuraava ruutu. Myös toinen mahdollisuus, eli
"ylinopea" kone täytyy huomioida odottelemalla jos pyyhkäistään jo
aikataulusta ohitse.
Valitettavasti tällä alle 18.2:n framen nopeudet eivät toimi, joten
sellaisiin tapauksiin pitää kehitellä erikoisratkaisuja (fixed-point
-laskuri esimerkiksi, joka korottuu vain puolella joka vuoro tms.).
On myös muita mahdollisuuksia toteuttaa frameskip, kuten siirtämällä
käsitteleframe -funktio suoraan timeriin, joka ei tosin mielestäni ole
hyvä ratkaisu, mutta joka toisaalta on tietyllä tavalla selvä
laskurien jäädessä pois. Mutta mitä teetkään kun kesken ruudulle
piirron päivitetään aluksien paikkaa? Ei ole enää kenestäkään,
(varsinkaan pelaajasta) kivaa siinä vaiheessa.
Toinen paljon toimivampi vaihtoehto on käyttää kulunutta aikaa
ikäänkuin kertoimena tehtävissä. Eli jos vaikka joka vuorolla pitää
siirtää spriteä 1 eteenpäin, niin siirretään joka framella spriteä
1*kulunut aika verran eteenpäin. Tämä valitettavasti vaatii paljon
tiheämmän ajastimen kutsun kuin sellaiset 70 kertaa sekunnissa
toimiakseen hyvin ja lisäksi fixed-point -matikka on yleensä aika
välttämätön tämänkaltaisessa toteutuksessa.
Mutta, aihe ei ole vaikea ja varmasti osaat päättää minkälaisen
toteutuksen teet itse peliisi. Minä häivyn nukkumaan ja jätän sinut
oman onnesi nojaan. Öitä!
7.4 Yleistä asiaa pelin levityksestä
------------------------------------
Tässä luvussa olisi tarkoitus hieman valaista pelinteon toista puolta,
eli sen joka ei sisällä ohjelmointia, vaan dokumentaation kirjoitusta,
grafiikan piirtoa, musiikkia, pelin levitystä ja ties mitä. Taidan
kyllä olla aika turha tästä kauheasti puhumaan, sillä valmiiksi emme
ole saaneet kuin vasta yhden pelin ja toinen on kovasti tekeillä, kunhan
laiska kooderimme (minä) löytäisi jostain aikaa kirjoittaa koodia.
Ensimmäinen homma pelin teossa olisi varmaan päättää minkä tyyppisen
pelin tekee. Parasta on valita sellainen pelityyppi, jonka uskoo
pystyvänsä toteuttamaan. Ensimmäisenä projektina kannattaa varmaan
tehdä jokin yksinkertainen kaksiulotteisen toimintapelin, vaikkapa
sitten sen iänikuisen matopelin. Sitten vasta pikkuhiljaa kun kokemusta
kertyy niin kannattaa jatkaa vaikeammilla projekteilla.
Nimi lienee toinen huolenaihe kun pelityyppi ja sen pääpiirteet ovat
tiedossa. Älä mielellään nimeä ohjelmaa samannimiseksi kuin jokin
olemassaoleva tuote. Esimerkiksi matopeli, jonka nimi on Windows voi
aiheuttaa lievää närää jos se leviää laajemmalle (tosin yleensä
ensimmäinen peliprojekti ei leviä kauhean laajalle, mutta mistä sitä
koskaan kuitenkaan tietää).
Sen jälkeen olisi varmaan parasta alkaa pelin teko. Sen lisäksi, että
pelin engine täytyy saada kuntoon olisi myös hyvä tehdä siihen
grafiikkaa. Musiikki ja ääniefektitkin olisivat varsin mukava idea,
jos kunnianhimoa löytyy tarpeeksi. Levityksessä on useita
äänikirjastoja, jotka tarjoavat enemmän tai vähemmän toimivan
ratkaisun ääniongelmiin. Enginen lisenssit kannattaa varmaan kuitenkin
tarkistaa hieman tavallista tarkemmin, kun joidenkin mukana tuppaa
olevan varsin kirjavia käyttöehtoja (suurin osa kieltää kaupallisen
käytön).
Jos grafiikka tai musiikki ei itseltä suju on tietenkin mahdollista
hankkia joku kaveri tai vaikka aivan tuntematonkin mukaan projektiin
tekemään grafiikkaa ja säveltämään musiikkia. Pelin ollessa sitten
muiden osien osalta kasassa alkaakin kannattaa miettimään levitystä
ja dokumentointia, jotka ovat muun kokonaisuuden kanssa myös tärkeitä.
Normaalisti käytettyjä levitystyyppejä on kolme, PD, FW ja SW (ja
täysin kaupallinen levitys, mutta tätä tutoriaalia ei kyllä sellaisen
tekijöille ole tarkoitettu). PD (Public Domain) tarkoittaa, että
luovut kaikista oikeuksistasi ohjelman suhteen, eli muut saavat
tehdä ohjelmallasi mitä ikinä keksivät, vaihtaa nimen ja levittää tai
myydä miten haluavat.
Hieman rajoitetumpi muoto on FW (FreeWare), jossa pidät
tekijänoikeutesi tuotokseesi ja saat itse sanella ehdot miten sitä
levitetään. FreeWare -tuotteista ei kuitenkaan saa periä mitään (sillä
se on termi jota käytetään ilmaisesta tuotteesta). SW (ShareWare) taas
on levitystyyppi, jossa käyttäjä saa kokeilla ohjelmaa tietyn ajan ja
sitten vasta päättää mitä tekee ohjelman kanssa.
Ensimmäinen tehtävä päättäessäsi minkätyyppinen ohjelmastasi tulee
on miettiä mihin ohjelmasi pystyy. Jos tuotos on ensimmäinen pelisi
ja harjoitustyö voi hyvinkin olla järkevää antaa koko ohjelma
lähdekoodeineen muiden levitykseen. Tällaiset julkistukset ovat aina
harvinaisia ja ohjelmasi ehkä leviää tällä tavoin paremmin. Jos
olet kuitenkin sitä mieltä, että et halua muiden käyttävän peliäsi
miten haluavat kannattanee levitysmuodoksi laittaa FW.
Sharewarena tuotetta kannattaa levittää vasta jos todella olet panos-
tanut siihen vain siinä mielessä, että saat siitä rahaa tai jos olet
sitä mieltä, että ohjelmasi on merkittävästi parempi kuin kilpailevat,
kaupalliset tai SW-tuotteet. Shareware-ohjelmaksi ei kuitenkaan
kannata laittaa sitä ensimmäistä matopeliä tai jotain bugista
viritelmää, jos haluaa säilyttää maineensa. =)
Sharewareakin on kolmea tyyppiä, nimittäin tiukka aikarajoitettu
shareware, aikarajoitettu shareware ja rajoittamaton shareware. Tiukka
aikarajoitettu SW on tyypillisesti kuten kaikki mahdolliset Windows-
viritellyt HTML-editorit, joissa 99% on jokin viritelmä, joka terminoi
ohjelman ennemmin tai myöhemmin (yleensä ennemmin). Löysemmästi
aikarajoitetut ohjelmat ovat siitä kiitollisia, että niiden toimivuus
säilyy aikarajan jälkeenkin. Rajoittamattomat ovat sitten tietenkin
ne kaikkein mukavimmat ja niiden toimintaperiaate ei enää yleensä
olekaan antaa käyttäjän kokeilla ohjelmaa, vaan rahaa pyydetään siitä,
että käyttäjä ottaa käyttöönsä kaikki ohjelman toiminnot.
Jos peli on aivan ehdottoman huippu niin voi yrittää levittää sitä
täysin kaupallisesti, mutta se vaatiikin sitten yleensä aika lailla
kokemusta ja tietenkin hieman hyvää tuuria.
Sama minkä tyypin levitykseen sitten päätyy, niin kannattaa varmaan
kirjoittaa hieman tekstiä, jossa kerrot miten haluat ohjelmaasi
levitettävän. PD-tyypillä et tarvitse ehkä kuin tekstitiedoston, jossa
ilmoitat luopuvasi kaikista oikeuksista ja kaikesta vastuusta ohjelman
suhteen. SW:n ja FW:n kanssa kannattaakin sitten panostaa
lainopilliseen puoleen hieman enemmän.
Tärkein on ilmoittaa selvästi pelissä, että tekijänoikeudet kuuluvat
sinulle tai useammalle henkilölle ja kertoa ehdot joiden rajoissa
ohjelmaa saa levittää. Tärkein rivi lienee tämänkin dokumentin alusta
löytyvä:
Copyright (C) Joonas Pihlajamaa 1997. All rights reserved.
Tekijänoikeuksien merkki, (C) ei toistu oikein tietokoneella, sillä
sen pitäisi itseasiassa olla ympyrän keskellä oleva C. Näinollen
Copyright-teksti alussa voi olla varsin hyödyllinen. Sen jälkeen tulee
tekijän nimi ja loppuun yleensä vuodet joiden aikana olet tuotteen
tekijänoikeuksia pitänyt hallussasi (eli käytännössä minä aikana olet
peliä tehnyt). Jos tekijöitä on useita kannattaa varmaan pelata varman
päälle ja selittää tarkemmin ketkä henkilöt ovat tehneet mitäkin.
Sitten vain perään kaikki ehdot, joita haluat peliäsi levitettäessä
noudatettavan. Suhteellisen kattava vastaava löytynee suomeksi
tämänkin dokumentin alusta (porsaanrei'istä saa kyllä vapaasti
ilmoittaa ;), sekä kotisivujeni levitysehdoista, joiden parissa vietin
runsaasti aikaa pyrkien saada siitä niin vaikean kuin mahdollista.
Jos noiden tekeminen tuntuu turhalta, niin kannattaa muistaa, että jos
joskus satut joutumaan kahnauksiin ohjelmasi väärinkäytösten tai sen
aiheuttamien ongelmien kanssa, niin tuo teksti saattaa olla ainoa
apusi. Ilman tekstiä on paljon hankalampaa sanoa oikeudessa, ettet ole
vastuussa ohjelman aiheuttamasta sydämentahdistimen pysähtymisestä,
toisin kuin jos olisit kirjoittanut ehtoihin, että et ole vastuussa
moisista vahingoista.
Mitä sinun tulisi ilmoituksessa mainita olisivat seuraavat:
0. Mihin kaikkeen ehdot ilmoituksessa ulottuvat
1. Miten ohjelmaa ja sen tiedostoja saa käyttää
2. Mistä olet vastuussa
3. Mitä tehdä jos ei suostu ehtoihin ja milloin katsotaan käyttäjän
suostuneen niihin
4. Miten ohjelmaa saa levittää
5. Missä ohjelmaa saa levittää
Kun lainopillinen puoli ja itse peli on kunnossa lienee jäljellä vain
levityspuoli. Se onkin suhteellisen helppoa. Lähetys pariin suosittuun
purkkiin (MBnettiin =) ja kenties Internetiinkin lisää varmasti
leviämistä aivan toisin kuin kavereille antaminen. Mainostustakin voi
harrastaa, mutta kannattanee pitää se kohtuullisissa rajoissa, ettei
ohjelmasi saa negatiivista julkisuutta häiritsevästä mainonnasta. :)
Jos ohjelmasi on SW-tuote, niin lienee vielä yksi kohta, nimittäin
rekisteröinnit ja päivitykset, sekä mahdollisien lisäominaisuuksien
"vapautus" (enabling, tätä se on kun lukee liikaa englanninkielistä
materiaalia). Rekisteröimätön versio kannattaa pitää niin paljon
ominaisuuksia sisältävänä, että siitä todella on jotain iloa, mutta
pitää niin paljon hyviä ominaisuuksia rekisteröidyssä versiossa, että
rekisteröimätön käyttäjä näkee saavansa rekisteröintirahoilleen
vastinetta rekatessaan pelin. Myös mahdolliset ilmaiset/alennetut
päivitykset tai muut vastaavat etuisuudet tulevaisuudessa voivat
jonkin verran avittaa, mutta muista, että suurin osa käyttäjistä etsii
välitöntä hyötyä, eikä paljoa välitä tulevien pelien
rekisteröintihintojen alentumisista.
Kannattaa myös harkita millä tavalla hoidat rekisteröinnit. Maksaminen
pitää tehdä helpoksi (ja mielellään halvaksi), sillä suurin osa
rekisteröijistä on kuitenkin laiskaa porukkaa ja mahdollisuus
rekisteröityä tuoliltaan nousematta voi olla hyvinkin suuri
etu. Maksutapoina kannattaa ainakin huomioida suoran käteisen lisäksi
pankkisiirrot, jotka ovat viitteiden kanssa varsin näppärä tapa
rekisteröidä. Myös postiennakko on hyvä tapa, vaikka sillä on
suhteellisen korkeat kustannukset se on kuitenkin näppärä keino
varsinkin vähän tyyriimmille ohjelmille (20 markan
rekisteröintihintaan saman verran lisää voi pelottaa ostajia).
Rekisteröidyn version lähetykseenkin on useita mahdollisuuksia. Itse
olen miettinyt näitä ja tässä on muutama, mistä valita, osa helppoja
toteuttaa ja osa vaikeita:
1. Rekisteröintiavain
+ Pieni, näppärä lähettää vaikka sähköpostilla
- Todella helppo kopioida
- Helppo murtaa
2. Rekisteröity EXE
+ Suhteellisen pieni, mennee suurempiin sähköpostilaatikkoihin
+ Varma, vaikea murtaa
- Lähes yhtä helppo kopioida
3. Rekisteröity versio
+ Helppo toteuttaa
+ Ihan pikkuisen vaikeampi kopioida
+ Varma, vaikea murtaa
4. Rekisteröity versio ja avain
+ Varmin menetelmistä, vaikea kopioida, helppo toteuttaa
Toisaalta kannattaa muistaa, että jos sinä jaksat laittaa sen
disketeillä postissa ei se ole kenellekään ongelma laittaa
viidellekymmenelle koneelle ja vaikka valmistaa diskcopyllä rekatusta
versioista piraattiversioita jatkolevitykseen, eli kopiointi on aina
aika helppoa. Toisaalta kopiointia haittaa ainakin hieman erillisenä
annettava avain tai installointiohjelmassa rekisterijöijän nimen ja
tunnuksen pyytäminen jne.
Myös voi pitää mielessä, että mitä enemmän turvatoimia sitä hankalampi
se on rekisteröijälle. Kohtuus kaikessa niin pysyvät rekkaajatkin
tyytyväisinä.
Siinä lienevät ne tärkeimmät asiat, joita kannattaa pitää mielessä
peliä tehdessä. Lisäksi tietenkin löytyy kokonaisia kirjoja pelinteon
taiteesta ja niiden suunnittelusta, mutta tämän luvun päätarkoitus on
ollut valaista pelinteon käytännöllisempiä puolia. Nyt tämä
tutorialisti lähtee lukemaan ruotsin kokeisiin!
7.5 Interpolointi ja viivoja
----------------------------
Ilja Bräysy taisi tässä kuukausi sitten patistaa minua neuvomaan miten
DJGPP:llä piirretään viivoja. No, pääsin pälkähästä lupaamalla
kirjoittaa siitä jutun sitten Laamatuttiin. No minkä taakseen jättää
sen edestään löytää, eikä tämäkään kerta näytä olevan mikään poikkeus.
Kuitenkin tällä kertaa selitän muutakin kuin sen viivanpiirron, nimittäin
selitän mitä tarkoittaa interpolointi, sekä miten ja mihin sitä voi
tietokoneella käyttää.
Eli termimme on interpolointi. Inter voisi latinassa tai jossain muussa
kielessä hyvinkin tarkoittaa välissä, ainakin interpolointi tarkoittaa
jotain tähän hyvin liittyvää. Interpolointi on nimittäin sitä, että kun
tiedossamme on kaksi pistettä, niin voimme "arvata" sinne keskelle ääret-
tömästi uusia pisteitä, jotka kaikki kuuluvat samalle suoralle. Tämä
on ns. lineaarista interpolointia, eli interpoloidaan pisteitä samalle
suoralle. Tällaisesta toimenpiteestä hyvä esimerkki voisi hyvinkin
olla viivanpiirto, sillä siinähän meillä on kaksi pistettä, ja meidän
täytyy saada niiden välillä tarpeellinen määrä pisteitä viivan
esittämiseksi.
No niin, tiedämme siis mitä on interpolointi. Se on siis pisteiden
lisäämistä kahden tunnetun pisteen välille. Vaan miten noiden pisteiden
sijainti sitten pitäisi laskea? No, miettikäämme tilannetta, jossa meillä
on kaksi pistettä, a ja b, joiden koordinaatit ovat vastaavasti (ax,ay)
ja (bx,by). Nyt me laskemme näiden välillä yhden pisteen. Ensimmäinen
tehtävä lienee laskea, kuinka pitkästi meillä on matkaa x- ja y-suunnassa.
Näitä lukuja nimitetään yleisesti delta-arvoiksi. Ne lasketaan
seuraavasti:
delta_x = | bx - ax |
delta_y = | by - ay |
Missä merkit "|" tarkoittavat itseisarvoa, siis "| a |" luetaan
"a:n itseisarvo". C:llä funktio on abs, tai fabs, jos käytämme
floatteja.
No niin, tiedämme kuinka kaukana pisteet ovat toisistaan, mutta mitä
ihmettä sitten oikein teemme tällä uudella, kiinnostavalla tiedolla?
No jatketaanpas hieman viivanpiirron kehittelyä. Jos haluamme
katkeamattoman viivan, niin meillä pitää olla yhtä monta pikseliä kuin
viivan pidemmän akselin pituus on. Eli jos delta_x on suurempi kuin
delta_y, niin tarvitsemme delta_x:n verran pikseleitä. Tilaanteen
ollessa päinvastainen on tarvittavien pikselien määrä tietenkin
vastaavasti delta_y.
Sitten pidemmälle toteutukseen. Kun nyt tiedämme montako pikseliä
tarvitsemme ja kummassa suunnassa, niin voimmekin suunnitella
seuraavanlaisen piirtorakenteen:
jos delta_x >= delta_y niin
y = ay
y_korotus = delta_y / delta_x
looppaa x välillä ax...bx
piste ( x, y, väri )
y = y + y_korotus
end looppi
muutoin
x = ax
x_korotus = delta_x / delta_y
looppaa y välillä ay...by
piste ( x, y, väri )
x = x + x_korotus
end looppi
end jos
Nyt te tietenkin kysytte: "Mitä tuo tekee?" No, olen ilkeä ja kerron
teille. Koska meidän täytyy piirtää pidemmän akselin verran
pikseleitä, niin se tarkoittaa, että piirtosilmukan täytyy korottaa
pidemmän akselin koordinaattia yhdellä ja lyhemmän jollain pienemmällä
kuin yhdellä. Jos alkaisimme piirtelemään lyhyemmän akselin mukaan,
niin viivan toinen akseli harppoisi yli 1 pikselin askelia ja viivaan
jäisi reikiä.
Eli jos...muutoin -rakenne valitsee pidemmän akselin. Sitten
alustetaan lyhyemmän akselin aloituskoordinaatti ja korotus jo
valmiiksi. Koska tiedämme, että looppi korottuessaan yhdellä tulee
toistamaan sen sisällä olevan koodin yhtä monta kertaa kuin pidemmälle
akselille tulee pikseleitä (jos delta_x on pidempi akseli, niin
delta_x kertaa) niin voimme helposti laskea paljonko lyhyemmällä
akselilla täytyy liikkua yhden kierroksen aikana. Tämä korotus
saadaan siis jakamalla lyhyen akselin pituus pidemmän akselin
pituudella.
Ette varmaan ymmärtäneet mitään, joten parasta ottaa esimerkki. Meillä
on viiva pisteestä (10, 10) (eli siis ax=10 ja ay=10) pisteeseen (30,
20) (eli taas bx=30 ja by=20).
delta_x = | bx - ax | = | 30 - 10 | = | 20 | = 20
delta_y = | by - ay | = | 20 - 10 | = | 10 | = 10
Huomaamme, että delta_x on pidempi ja meidän täytyy piirtää delta_x
kappaletta pikseleitä saadaksemme yhtenäisen viivan. Valitsemme siis
pseudo-koodistamme jos-osaa seuraavan pätkän, sillä lause
'delta_x >= delta_y' on tosi.
y = ay = 10
y_korotus = delta_y / delta_x = 10 / 20 = 0.5
Nyt kun siis looppaamme x:n välillä 20...30, niin joka x:n korotusta
yhdellä seuraa y:n korotus 0.5:llä. Näin siis x ja y menevät:
x | y
---------
10 | 10
11 | 10.5
12 | 11
.. | ..
29 | 19.5
30 | 20
Huomaa, että koska piirrossa pitää käyttää kokonaislukuja, niin nuo
desimaaliosan sisältävät y-koordinaatit pyöristyvät aina alaspäin,
jolloin piirtokoordinaatit ovat:
x | y
--------- -- (10, 10)
10 | 10 --
11 | 10 --
12 | 11 --
13 | 11 --
14 | 12 --
15 | 12 --
.. | .. --
27 | 18 - (30, 20)
28 | 19
29 | 19
30 | 20
Vasemmalla siis taulukko loopissa kiertäessä x- ja y-arvoista ja
sitten oikealla viiva, jonka näköinen tuosta suurinpiistein tulee.
Mutta, hienoa muuten, mutta pari ongelmaa on
ratkaisematta. Selvitettyämme pidemmän akselin ja laskettuamme
lyhyemmälle akselille tarvittavan koordinaatin korotuksen voimme kyllä
piirtää viivan noin, mutta ongelmia seuraa heti, jos ensimmäinen piste
on toisen pisteen oikealla- tai alapuolella. Sillä korotus on aina
positiivinen, kun sekä jaettava että jakaja ovat
positiivisia. Ongelmia aiheuttaa myös se, että jos pidemmän akselin
ensimmäinen koordinaatti on suurempi kuin jälkimmäinen, niin
korotuksen tilallahan pitäisi olla vähennys!
No, hieman lisälogiikkaa ja hyvin menee. Teemme nimittäin sillä
tavalla, että järjestämme pidemmän akselin ensimmäisen koordinaatin
aina pienemmäksi kuin toisen. Eli jos ensimmäinen piste onkin toisen
oikealla-/alapuolella, niin funktiomme vaihtaa pisteiden
paikkoja. Sama viiva se on silti, mutta loopissa ei tarvitse miettiä
onko se ensimmäinen pienempi tai suurempi, sillä se on aina pienempi.
Ja kun vielä poistamme pyöristykset alusta, niin jos lyhyemmän akselin
pituus on negatiivinen, niin sen jako pidemmän akselin pituudella
tuottaa negatiivisen korotuksen (y_korotus ja x_korotus). Ja jo
ala-asteellahan on opetettu, että negatiivisen luvun lisäys on sama
kuin vastaluvun vähennys. (eli suomeksi: 10 + (-10) = 10 - 10)
Eli upea pseudorutiinimme kokonaisuudessaan:
funktio viiva( int ax, int ay, int bx, int by, char väri )
float x, y, x_korotus, y_korotus, delta_x, delta_y
delta_x = bx-ax
delta_y = by-ay
jos |delta_x| >= |delta_y| niin
jos delta_x < 0 niin
vaihda( ax, bx )
vaihda( ay, by )
end jos
y = ay
jos delta_y == 0 niin
y_korotus = 0
muutoin
y_korotus = delta_y / delta_x
end jos
looppaa x välillä ax...bx
piste( (int)x, (int)y, väri )
y = y + y_korotus
end looppaa
muutoin
jos delta_y < 0 niin
vaihda( ax, bx )
vaihda( ay, by )
end jos
x = ax
jos delta_x == 0 niin
x_korotus = 0
muutoin
x_korotus = delta_x / delta_y
end jos
looppaa y välillä ay...by
piste( (int)x, (int)y, väri )
x = x + x_korotus
end looppaa
end jos
end funktio
Tuo tarkistus nollasta pidemmän akselin kohdalla ('jos delta_x == 0'
sekä 'jos delta_y == 0') siksi, että pystyviivan kanssa pitää korotus
olla 0, eikä jako nollalla tule kysymykseen muutenkaan, sillä se
kaataa ohjelman. Itseisarvot vertailussa 'jos |delta_x|>=|delta_y|'
pitää olla siksi, että emme käyttäneet niitä aiemmin lainkaan.
No juu, voin kyllä lyödä vetoa, ettei se toimi, mutta kirjoitetaanpas
silti kauniilla C-kielellä puhtaaksi:
void vaihda( int *a, int *b ) {
int temp;
temp=*a;
*a=*b;
*b=temp;
}
void viiva( int ax, int ay, int bx, int by, char vari ) {
float x, y, x_korotus, y_korotus, delta_x, delta_y;
delta_x = bx-ax;
delta_y = by-ay;
if( fabs(delta_x) >= fabs(delta_y) ) {
if( delta_x < 0 ) {
vaihda( &ax, &bx );
vaihda( &ay, &by );
}
y = ay;
if( delta_y == 0 ) {
y_korotus = 0;
} else {
y_korotus = delta_y / delta_x;
}
for( x=ax; x<=bx; x++ ) {
putpixel( (int)x, (int)y, vari );
y = y + y_korotus;
}
} else {
if( delta_y < 0 ) {
vaihda( &ax, &bx );
vaihda( &ay, &by );
}
x = ax;
if( delta_x == 0 ) {
x_korotus = 0;
} else {
x_korotus = delta_x / delta_y;
}
for( y=ay; y<=by; y++ ) {
putpixel( (int)x, (int)y, vari );
x = x + x_korotus;
}
}
}
Tuon testaamiseksi paras on omat silmät ja niinpä yhdistin
viivanpiirtorutiinin ja hiiriesimerkin pyynnön yhteen
tiedostoon. Hiirellä pyörivä viivanpiirtäjä löytyy EXAMPLE-hakemiston
alta tiedostosta LINE.C. Nyt varmaan olisi paras, että selvität
itsellesi miten interpolointi viivanpiirrossa toimii ja miten rutiini
yleensäkin toimii. Interpolointi on siis yksinkertaisesti pisteiden
laskemista kahden pisteen välille ja tietokoneella se tehdään yleensä
siten, että otetaan koordinaatit ja jaetaan niiden välillä oleva tila
n kappaleeseen jakolaskulla ja sitten vain loopataan n kertaa
korottaen koordinaatteja tällä luvulla. Viivanpiirrossa, kuten myös
monessa muussa hommassa järjestetään asiat siten, että toinen
korotettavista on 1 ja toinen sitten mitä tarve vaatii.
Kannattaa myös muistaa se, että esitelty viivanpiirtorutiini on, ellei
hitain, niin kuitenkin todella takkuinen. Ensimmäisenä voisi aloittaa
muuttamalla float-muuttujat fixed-pointiksi. Sitten myös omat rutiinit
pysty- ja vaakasuuntaisille sekä diagonaalisille (45 asteen kulma)
viivoille. Myös "pitkille" ja "leveille" rutiineille voisi tehdä
jotain optimointia. Erilliset rutiinit molemmille tai jokin kikka
millä yhdistää logiikkaa voisi hyvinkin nopeuttaa. Sitten todellisille
nopeuskiihkoilijoille assyoptimointi tai ehkä mieluummin Bresenhamin
viivanpiirron opettelu (löytyy PCGPE:stä) voisi olla
tarpeen. Bresenhamia en ala opettamaan, kun en itsekään rutiinin
toimintaa ymmärrä. Nopea se on joka tapauksessa.
Myös tutkiminen paperilla voi auttaa. Jos kuitenkin tuntuu, että jokin
jäi epäselväksi, niin sitten vain postia. En nimittäin tiedä kuinka
epätäydellinen selityksestä tuli, kun itse olen asian kanssa takunnut
niin kauan, että sen osaa etu- ja takaperin. Interpolointi on parasta
olla hanskassa, sillä sitä tarvitaan myös esim. kaikkeen polygonien
piirtoon liittyvässä. Mutta minä jatkan seuraavaan aiheeseen, nähdään
siellä!
7.6 Vapaa skrollaus
-------------------
Tänään teemme sitä ylös ja alas, sivulle ja toiselle sekä useampia
yhtä aikaa. Se ei ole mitä luulet, vaan se on vapaasuuntaista
skrollausta, tarvittaessa vaikka osiin jaetulla ruudulla jokaisessa
omaan suuntaansa.
Aihe on helppo. Niin helppo, että minä tein sellaisen ilman mitään
ongelmia. Ja se on sitten aika helppoa. Mutta jotta ne jotka eivät
osaa/jaksa itse paneutua ongelmaan paria minuuttia enempää omien
aivojen voimin saavat tässä tyhjentävän selityksen. Skrollaushan on
pienen palasen näyttämistä suuremmasta kokonaisuudesta. Ruutu on
ikäänkuin ikkuna suurempaan ruutuun. Kuten alhaalla näkyy:
+---------------------------+ Kuvan pisteet vain hahmottavat
| . . . . . . . . . | näytettävää aluetta. Ne eivät
| (x,y) . . KOKO. . . .| merkitse mitään. :)
|. o-----+ . . . . . |
| . | |. NÄYTETTÄVÄ . |
| .|RUUTU| . . . . . .|
|. | | .ALUE . . . |
| . +-----+. . . . . . |
| . . . . . . . . .|
+---------------------------+
Ruutu on yleensä koko näyttöruudun kokoinen, tai sitten jos
näyttoruutu on jaettu useaan osaan niin sen osan kokoinen, johon
ikkuna piirretään. Kyse on siis vähän samantapaisesta toiminnasta,
kuin bittikarttojen kanssa. Bittikartoissa vain piirretään ruutua
pienempi kuva ruudulle, kun skrollauksessa luetaan ruutua suuremman
kuvan osa ruudulle. Piirrossa aloitamme näytettävän alueen kohdasta
(x,y) (merkitty kuvassa kirjaimella 'o' ruudun yläkulmaan). Sitten
vain kopioimme ruudun leveyden verran pikseleitä näytettävästä
alueesta ja siirrymme taas seuraavalle riville. Yksinkertaista, mutta
helppoa! Jatkamme tätä kunnes olemme saaneet ruudun täyteen. Helppoa!
Skrollaavassa pelissä täytyy ottaa nyt huomioon, että spritejä ja
muita ei enää piirretä kaksoispuskuriin, vaan tähän näytettävän alueen
puskuriin. Sen koko voi sitten olla mitä vain maan ja taivaan välillä
- ainakin lukualueen rajoissa, kuitenkin. Skrollauksessa näyttö onkin
nyt vain ikkuna liikkuvaan ja elävään pelimaailmaan. Fiksu kooderi
tietenkin piirtää vain näkyvissä olevat asiat, mutta sellaiset
hienoudet jäävät ohjelmoijan päätöksen varaan.
Aika heittää editorisi ruudulle hieman pseudoa:
char alue[640][400]
char ruutu[320][200]
funktio päivitä( int ylä_x, int ylä_y )
int rivi, sarake
looppaa rivi välillä 0...199
looppaa sarake välillä 0...319
ruutu[ rivi ][ sarake ] = alue[ ylä_y + rivi ][ ylä_x + sarake ]
end looppaa
end looppaa
end funktio
Näyttääkö vaikealta? Ei pitäisi, ei ainakaan minusta näytä. :)
Mutta pistetään vähän vaikeammaksi. C-toteutuksessa kun meillä
kuitenkin on vain yksiuloitteinen taulukko, niin sijoituksessa pitää
osoite laskea käsin:
ruutu[ rivi * 320 + sarake ] =
alue [ (ylä_y + rivi) * 640 + ylä_x + sarake ];
Toisena tuo on toivottoman hidasta. Kannattanee säästä sisempi looppi
ja kopioida memcpy:llä koko rivi kerralla:
memcpy( &ruutu[ rivi * 320 ],
&alue[ (ylä_y + rivi) * 640 + ylä_x ],
320 );
Näin saamme seuraavanlaisen C-kielisen kyhäelmän:
char alue[640*400];
char ruutu[320*200];
void paivita( int yla_x, int yla_y ) {
int rivi;
for(rivi=0; rivi<200; rivi++)
memcpy( &ruutu[ rivi * 320 ],
&alue[ (yla_y + rivi) * 640 + yla_x ],
320 );
}
Eri kokoisten näyttöalueiden/virtuaaliruutujen (tai miksi niitä nyt
haluatkin sitten kutsua) toteuttaminen ei paljoa vaadi. Puskurin koko
vain muokkaukseen ja offsetin ((yla_y + rivi) * 640 + yla_x) laskuun
pikku muutos ja se onkin siinä. Sitten vielä pitää hoitaa niin, että
piirrettävän alueen alakulma ei mene virtuaaliruudun ulkopuolelle, eli
tarkoitan tätä:
+-----------+
| |
| VIRTUAALI |
| RUUTU +--+--+
| | | |
+--------+--+ |
|RUUTU|
+-----+
Jos yla_x tai yla_y kasvaa niin suureksi, että yla_x+320 tai yla_y+200
menisi ruudun yli, niin silloin kaivetaan tavuja varatun muistialueen
ulkopuolelta aiheuttaen joko ihmeellistä käyttäytymistä tai koneen
kaatumisen. Joten pidetäänpäs koordinaatit kurissa!
Mitä tuo onkaan mitä kuulen? (olemattomia?) Totta, taisin luvata
muutakin kuin koko ruudun skrollausta. No, se ei ole vaikeaa. Kun
ylemmässä esimerkissä me piirsimme koko ruudulle, niin olisimme
tietenkin sen sijaan voineet aloittaa ruudultakin jostain muualta kuin
oikeasta yläkulmasta ja ikkunan koko olisi voinut olla vaikka 100x100.
Kun ikkunan koko on vähemmän kuin koko näyttöruudun koko se tarkoittaa
myös sitä, että ikkunoita mahtuu ruudulle tarvettaessa
useampia. Tällainen onnistuu funktiolla, joka ottaa parametreinään
virtuaaliruudun aloituspisteen lisäksi myös aloituspisteen
näyttöruudulla ja ikkunan korkeuden ja leveyden.
Tosi kooderi osaa tietenkin toteuttaa tuollaisen pienellä
miettimisellä. Ja koska minäkin olen sellainen, niin olen tehnyt
yhdistetyn näppäinesimerkin ja skrollausesimerkin joka löytyy myös
EXAMPLE-hakemiston alta tiedostosta, tällä kertaa nimen SCROLL.C alta.
Tässä vaiheessa täytyy vielä varoittaa siitä, että memcpy on syntisen
hidas tapa kopioida muistia. Optimointi assyllä tai jopa C:llä voi
nopeuttaa toimintaa, jos muistia heitellään 4 tavun palasissa. Mittaa
kuitenkin mahdollinen nopeushyöty, ettet vahingossa laita hitaampaa
korvaavaa rutiinia! Sitten vain sisäistämään luvun asiaa, olikos se
nyt niin vaikeaa? Viiden sekunnin sormienvenyttelytaun jälkeen onkin
sitten vuorossa sinit ja kosinitit, sekä plasmaa.
7.7 Sinit ja kosinit sekä plasmaa
---------------------------------
No nyt hieman kertausta yhdeksännen luokan matematiikasta. Sinit ja
kosinit. Mitä ne sitten ovat ja mitä niillä tehdään? Ennenkuin
vastaan, niin tutustukaamme Suorakulmaiseen Kolmioon:
o a = kateetti
|\
| \ b = kateetti
a | \ c
| \ c = hypotenuusa
| \
| \
| /\ * = tässä nurkassa on kulma alpha
o-------*
b
No kaikki varmaan osaavat jo pythagoraan lauseen c^2 = a^2 + b^2.
Mutta sinit ja kosinit ovatkin jotain ihan uutta, ainakin
8-luokkalaisille ja ysinsä aloittaneille, kenties:
sin alpha = a/c
cos alpha = b/c
tan alpha = a/b
Vain kaksi ensimmäistä oikeastaan kiinnostavat meitä, sillä niitä
yleensä käytetään. Nyt tiedämme siis, että sini jostain kulmasta on
yhtä kuin kateetin a ja hypotenuusan osamäärä ja kosini vastaavasti
kateetin b ja hypotenuusan. Vaan mitä h**vettiä oikein teemme tällä
tiedolla? No KÄYTÄMME HYVÄKSEMME!
Nimittäin kiinnostavaa on, että jos tiedämme c:n, eli hypotenuusan
pituuden ja kulman, niin voimme laskea vastaavan suorakulmaisen
kolmion molempien kateettien pituudet. Pyörittelemällä hieman tavaraa
puolelta toiselle (kai yhtälön ratkaisu oli jo seiskalla?):
a = sin alpha * c ; sini
b = cos alpha * c ; kosini
Kiinnostavaa on myös, että jos kuviossa tähdellä merkitty kärki on
ympyrän keskipiste ja c ympyrän säde, niin 'sin alpha * c' antaa
ympyrältä kulman alpha kohdalla olevan pisteen y-koordinaatin ja
kosini sitten vastaavasti x-koordinaatin, kas näin:
--^-- Kuten kaaviosta näkee, niin ympyrän säteestä
-- | -- muodostuu hypotenuusa ja kun tiedämme kulman
- | X alpha voimme selvittää kateettien pituudet,
- | /|- jotka samalla ovat pisteen X koordinaatit
- | / |- kuviossa. Eli
- | / | -
- |/\ | - X = (cos alpha * radius, sin alpha * radius)
<------o----+->
- | - Viivanpiirron kehittely tästä ei olisi vaikeaa,
- | - tarvitaan vain looppaus vaikka 360 astetta
- | - ja joka kerralla lasketaan pisteen koordinaatit
- | - laskurin osoittamalle kulmalle (0...359), jolloin
- | - ruudulle piirtyy kaunis ympyrä säteeltään
-- | -- <radius>.
--v--
Olet myös varmaan pelannut autopeliä nimeltään Slicks, tai jotakin
luolalentelyä (esim. Auts, V-Wing, Kops, Rocket Chase, Kaboom,
Spruits, Wings, PP, Turboraketti, A-Wing, ...). Tällaisissa peleissä,
joissa täytyy pystyä liikkumaan muuallekin kuin ylös, alas, oikealle
ja vasemmalle täytyy myös pystyä liikkumaan muihin ilmansuuntiin.
Jos ajattelemme tietokoneen koordinaatistioa, niin aste 0 osoittaa
oikealle, 90 alas, 180 vasemmalle ja 270 ylös. Nyt jos haluamme tietää
paljonko pitää alusta siirtää x-suunnassa ja paljonko y-suunnassa
nopeuden ollessa vaikkapa 5 vasemmalle alaviistoon (siis 90+45=135
astetta) saamme seuraavan lausekkeen:
x_nopeus = (cos 135)*5
y_nopeus = (sin 135)*5
Kaikki näyttää helpolta. Osaamme piirtää ympyrän, laskea tarvittavan
x- ja y-nopeuden tiettyyn suuntaan kohdistuvalle liikkeelle ja vaikka
tehdä pyörivän tähden viivanpiirtorutiinien avulla. Vaan vielä hieman
pitää pinnistää päästäksemme tavoitteeseemme C:llä. Alla muutamia
totuuksia sinistä ja kosinista:
1) Ne palauttavat 99.99% tilanteista arvon joka on yli -1 ja alle 1,
joten jos leikitään kokonaisluvuilla saadaan tulokseksi 0
2) Koska arvoalue on niin pieni, täytyy aina käyttää joko liukulukuja
(float) taikka fixed point -lukuja. Fixedeinäkin sietää käyttää
monta bittiä desimaaliosalle, jottei tule karkeita muotoja.
3) Kuten muitakin matikkatavaroita käyttävät funktiot, myös sinit ja
kosinit vaativat math.h:n ja libm.a:n (käännösoptio -lm) mukaansa
toimiakseen.
4) Parametrina funktioille sin() ja cos() annetaan luku RADIAANEINA,
muunto tapahtuu seuraavasti:
radiaanit = 3.1415 * 2 * kulma / MAX_KULMA
MAX_KULMA on sama kuin suurin_mahdollinen_kulma+1. Eli
normaalistihan se on 360, mutta tietokoneella käytetään usein
256-, 512- ja jopa 1024-asteisia kulmia, sillä ne ovat huomattavan
helppoja laskea. Etenkin 256-asteinen on näppärä, sillä kun
suurinta kulman arvoa 255:ttä korotetaan ja laskuri on tyyppiä
unsigned char, niin se pyörähtää automaattisesti ympäri, takaisin
nollaan. Huomaa myös, että 360-asteisillakin ympyröillä maksimi
kulma on 359!
Nyt kun tiedät kaiken tärkeän, niin olet valmis käyttämään taitojasi
käytännössä. Mutta vielä yksi asia: Taulukointi. Sini ja kosini ovat
molemmat luvattoman hitaita suorittaa, joten parasta on tehdä
lookup-taulukko niille. Eli teemme 256-alkioiset taulukot sekä sinille
ja kosinille ja laskemme niihin molempiin valmiiksi arvot sinistä ja
kosinista kulmille 0...255 (käytämme siis 256-asteista ympyrää):
int loop;
float sintab[256], costab[256];
for(loop=0; loop<256; loop++) {
sintab[loop] = sin(3.1415*2*(float)loop/256.0);
costab[loop] = cos(3.1415*2*(float)loop/256.0);
}
Nyt ei sitten tarvita turhia vääntelehtimisiä minkään
radiaanikonversion kanssa tai muutakaan yhtä epämiellyttävää, vaan
toiminta on suorasukaista. Laittakaamme aluksemme kohti kaakkoa:
x_suunta = costab[45];
y_suunta = sintab[45];
Jotain oli vielä... aijuu, se plasma! No jotkut ovat ehkä tämän
tehneet jo ja toiset ovat tehneet ainakin
paletinpyöritysplasman. Mutteivät vielä sitä aitoa ja oikeaa. Vaan nyt
tulee asiaan muutos. Liikkuva plasma on oikein mukava olla olemassa ja
tässä tulee idea lyhykäisesti:
1) Tehdään 6 kappaletta eri "korkuisia" sinikäyriä (siis hypotenuusan
pituus / ympyrän säde / sinin kerroin) ja jotka mielellään alkavat
eri kohdista ja joissa aallon pituus on eri (eli toinen käyrä on
kuin 256-asteista ympyrää varten tehty ja toinen taas kuin
128-asteista jne.). Myös kosinia kannattaa käyttää.
Idea on kuitenkin se, että jokainen käyrä tallennetaan omaan
taulukkoonsa ja että jokaista käyrää voidaan "monistaa" peräkkäin,
eli jos samaa käyrää piirretään kaksi peräkkäin ei käyrä katkea
kesken, siis:
VÄÄRIN:
\ \ \
\ \ \
\ / \ / \ /
\---/ \---/ \---/
OIKEIN:
\ /--- \ /--- \ /---
\ / \ / \ /
\---/ \---/ \---/
Eli koska käyrää joudutaan toistamaan peräkkäin niin jos se ei
palaa lähtöpaikkaansa loppuun mennessä tulee sahalaitaa. Plasman
tapauksessa tuloksena on epämiellyttävän näköisiä loikkauksia
muuten pehmeissä väriliu'uissa.
Alla esimerkki kuuden erilaisen käyrän alustuksesta ja
generoinnista:
float wave1[256], wave2[256], wave3[256],
wave4[256], wave5[256], wave6[256];
int loop;
for(loop=0; loop<256; loop++) {
wave1[loop]=cos(3.1415*2* (float) loop /256) * 25.0 + 25.0;
wave2[loop]=cos(3.1415*2* (float) (loop%128) /128) * 15.0 + 15.0;
wave3[loop]=cos(3.1415*2* (float) (255-loop) /256) * 17.5 + 17.5;
wave4[loop]=sin(3.1415*2* (float) (loop%64) /64) * 22.5 + 22.5;
wave5[loop]=cos(3.1415*2* (float) ((128-loop)%128) /128) * 20.0 + 20.0;
wave6[loop]=sin(3.1415*2* (float) loop /256) * 25.0 + 25.0;
}
Koska sin ja cos palauttavat myös negatiivisia arvoja, täytyy
niihin lisätä sama luku kuin kerroinkin, jotta ne olisivat aina
positiivisia. Näinollen kaikkien aaltojen summa on pahimmillaan
kerrointen summa * 2, suomeksi maksimissaan 250.
2) Kun aallot ovat tallessa aletaan liikuttamaan niitä eri
suuntiin. Käytännössä tämä hoituu käyttämällä yhtä laskuria
jokaista aaltoa kohti, joka kertoo senhetkisen aallon alun
sijainnin.
Sitten vain aletaan piirtämään. Kolme ensimmäistä aaltoa ovat
vaaka-aaltoja ja kolme viimeistä pystyaaltoa. Tämä tarkoittaa
sitä, että kolmen ensimmäisen aallon alkion numero riippuu
väritettävän pikselin x-koordinaatista ja kolmen viimeisen indeksi
on riippuvainen y:stä. Kun indeksi vielä typistetään välille
0...255 and-funktion maskilla 0xFF niin voimme laskea jokaiselle
aallolle oikein indeksin:
wave1[ ( ind1 + x ) & 0xFF ]
wave2[ ( ind2 + x ) & 0xFF ]
wave3[ ( ind3 + x ) & 0xFF ]
wave4[ ( ind4 + y ) & 0xFF ]
wave5[ ( ind5 + y ) & 0xFF ]
wave6[ ( ind6 + y ) & 0xFF ]
Sitten vain ynnätään jokaiselle pikselille aallon senhetkiset
alkiot yhteen ja saatu luku tungetaan ruudulle pikselin
väriarvona. Koska x- ja y-koordinaatit liikuttavat tasaisesti
aaltojen indeksiä eteenpäin syntyy ynnäämällä tasainen kumpuileva
värimaasto. Eli piirtolooppi:
for(x=0; x<320; x++) for(y=0; y<200; y++) {
dblbuf[y*320+x] =
wave1[ ( ind1 + x ) & 0xFF ]+
wave2[ ( ind2 + x ) & 0xFF ]+
wave3[ ( ind3 + x ) & 0xFF ]+
wave4[ ( ind4 + y ) & 0xFF ]+
wave5[ ( ind5 + y ) & 0xFF ]+
wave6[ ( ind6 + y ) & 0xFF ];
}
Jonka ulkopuolella korotetaan ja vähennetään aaltojen
aloituskohtia (ind1-ind6) ja tätä toimintaa toistetaan niin kauan
kunnes painetaan nappia. Täydet sorsat EXAMPLE-hakemistosta
tiedostosta PLASMA.C.
Siinä olikin tämän kappaleen asia. Nyt taidan katsoa läksyni ja tehdä
esimerkit loppuun. Vielä pitäisi paletin kvantisointi saada tehtyä
ennen joulua, katsotaan ehdinkö ajoissa, pakko kai kyllä varmaan on. :)
No niin, nyt vain piirtelemään ihme käyriä ja kuvioita paperille ja
ruudulle, jota sinin ja kosinin syvin olemus selviää täydellisesti.
7.8 Paletin kvantisointi - Median cut
-------------------------------------
Nyt ollaankin sitten kyynärpäitä myöden mudassa. Paletin kvantisointi
lienee sellainen temppu, jota eivät kaikki kokeneemmatkaan osaa, ja
kaiken lisäksi se on suhteellisen vaikea homma. Joten kiinnittäkää
turvavyönne ja valmistautukaa yritykseeni selventää hieman asiaa
tuntemistani kvantisointitavoista helpomman, tai ainakin nopeamman,
osalta.
Eli mistä nyt sitten puhumme? Paletin kvantisointi on sitä, että kun
sinulla on vaikkapa 6 erilaista PCX-kuvaa, joissa on yhteensä 1321
erilaista väriä, niin esittääksesi nämä 1321 erilaista väriä ruudulla
täytyy sinun KVANTISOIDA palettisi. Kvantisointi on siis värimäärän
tiputusta. Ja nyt seuraa se, miten sen teemme.
Ensin hieman funktioiden ideasta. Kuvittelemme värit kuutiona, jossa
x-y-z -akseliston tilalla onkin r, g ja b. Meillä on siis
ns. rgb-avaruus, jossa värin sijainnin kuutiossa kertoo punaisen,
vihreän ja sinisen komponentin määrä. Jos jokainen koordinaatti on
välillä 0...63 (kuten normaaleissa PCX-kuvissa), niin meillä on
64*64*64 pikseliä sisältävä kuutio, jonka särmän pituus siis on 64
pituusyksikköä.
Tämän monimutkaisen ajatusrakennelman pohjalle perustuu
algoritmimme. Kun teemme taulukon, joka sisältää kaikki erilaiset
värit on taulukon jokainen alkio (rgb-tripletti) piste
kuutiossa. Funktiomme etsii sen akselin (siis r, g tai b), joka on
"pisin", eli suomeksi katsotaan jokaisen värikomponentin pienin ja
suurin esiintyvä arvo. Sitten funktio jakaa värikuution kahtia
täsmälleen siten, että puolet pisteistä/väreistä jää toiselle ja
puolet toiselle puolelle. Ei siis suurimman ja pienimmän väriarvon
puolesta välistä!
Kun koko kuutio on jaettu, niin kutsumme vain samaa jakofunktiota
kummallekin pienemmälle kuutiolle, jossa molemmissa on nyt siis yhtä
monta väriä. Nämä funktiot etsivät oman palasensa pisimmän
värikomponenttien välin ja jakavat kuution kahtia kutsuen itseään
molemmille kuutioille.
Funktio, joka kutsuu itseään on ns. rekursiivinen funktio ja se on
näppärä monessa asiassa. Jos piirrät paperille yhden laatikon ja siitä
lähtemään kaksi laatikkoa, joista kummastakin lähtee kaksi laatikkoa,
joista jokaisesta lähtee kaksi laatikkoa jne., niin saat huomaat, että
joka rekursiotasolla funktioiden "määrä" kaksinkertaistuu. Jos siis
asetamme rekursiorajaksi 3, niin värit jakautuvat 2^3:n, eli
kahdeksaan pienempään kuutioon. Kvantisointi 256:een väriin vaatii
siis kahdeksan rekursiotasoa, jotta saataisiin kuutio 256:een osaan.
Nyt te, jotka saitte ahaa-elämyksen (jokaisen pitäisi saada ;) menette
tekemään oman funktionne. Tyhmemmille ja kenties niille, jotka
haluavat saada vielä hieman varmennusta tulee kuitenkin vielä
teknisempi selostus, jonka teossa apuna on käytetty skenelehti
Imphobian osan 10 sisältämää informaatiota. Kiitoksia Fakerille.
Ensimmäiseksi teemme iiison taulukon, jonka koko on kaikkien
mahdollisten värien yhteenlaskettu määrä. Jos valitsemme 64 sävyä
jokaiselle värikomponentille saamme siis kooltaan 64x64x64 kokoisen
värikuution. Varaamme tälle muistia:
unsigned char *PaletteArray=(unsigned char *)calloc(64*64*64, 1);
(huomaa kaikkien alkioiden nollaus alussa) Kun haluamme lisätä värin
kvantisoitavien joukkoon, merkkaamme yksinkertaisesti tämän kuution
vastaavan pikselin ykköseksi. Näin meillä on kvantisoinnin alkaessa
kuutiossa ykköstä käytettyjen värien kohdalla ja voimme koostaa niistä
näppärästi värilistan sisältäen kaikki kvantisoitavat
värit. Koordinaatin kuutiossahan voimme laskea vaikka kaavasta:
r*64*64 + g*64 + b
No niin, sitten itse kvantisointirutiiniin. Funktion tehtävä on siis
ottaa kaikki pikselit tietyltä värikuution osakuutiolta ja katsoa mikä
värikomponentti vaihtelee eniten (eli tummimman ja vaaleimman värisävyn
ero on suurin). Osakuution kuvailemiseksi tarvitsemme tietenkin rajat
kuutiolle, eli tummimman ja vaaleimman mukaan otettavan sävyn kustakin
värikomponentista, eli:
int RedSubspaceBottom;
int GreenSubspaceBottom;
int BlueSubspaceBottom;
int RedSubspaceTop;
int GreenSubspaceTop;
int BlueSubspaceTop;
Nämä funktiot ovat siis punaisen, vihreän ja sinisen alimmat sallitut
pitoisuudet ja vastaavasti kolme viimeistä korkeimmat sallitut. Hyvä
idea on laittaa tällaisen kuution tiedot yhteen rakenteeseen. Sekaan
laitamme vielä tilaa funktion laskemille kunkin värisävyn
optimiarvoille, joka siis lasketaan sitten, että jos kuutio
halkaistaan tämän sävyn kohdalta, jää molemmille kuution puolikkaille
yhtä monta väriä:
int OptimalRed;
int OptimalGreen;
int OptimalBlue;
Nämä kaikki on esimerkissä laitettu structiin BORDERS. Nyt kun meillä
on pätevä rakenne kuution määrittelemiseksi, niin voimmekin alkaa
pohtimaan käytännön toimia, mitä rekursiivisen kuutionjakajamme tulee
toteuttaa. Idea on seuraava:
1) Tyhjennetään punaisten, vihreiden ja sinisten värikomponenttien
laskurit (RedCount[64], GreenCount[64] ja BlueCount[64]).
2) Lasketaan kuution rajojen sisällä jokaisen värikomponentin sävyn
määrä looppaamalla kaikki kvantisoitavat värit läpi ja katsomalla,
ovatko värin rgb-arvot parametrina annetun Borders (tyyppiä
BORDERS) sisällä ja jos ovat, niin korotetaan vastaavia punaisen,
vihreän ja sinisen laskureita:
RedCount[red]++;
BlueCount[blue]++;
GreenCount[green]++;
Lisäksi täytyy pitää yllä tietoa pienimmästä ja suurimmasta mukaan
otetusta värikomponentin sävystä, eli tyyliin:
jos red < PieninPunainen
PieninPunainen = red
tai jos red > SuurinPunainen
SuurinPunainen = red
3) Nyt kun sävyt on laskettu, seuraakin jännittävä vaihe. Muutamme
kunkin värisävyn määrät sisältävän taulukon juoksevaksi laskuriksi,
eli tässä näette muutoksen:
Indeksi 0 1 2 3 4 5 6 7 8
Aluksi 0 0 3 1 0 2 2 0 1
Nyt 0 0 3 4 4 6 8 8 9
Tämäntyyppinen rutiini toimii:
for(loop=1; loop<64; loop++)
RedCount[loop]+=RedCount[loop-1];
Nyt värisävytaulukossa on siis tietyn indeksin kohdalla, ei
suinkaan sen sävyn määrä, vaan siihen värisävyyn 'mennessä'
olleiden värien määrä. Nyt vielä etsitään se 'optimaalinen'
katkaisukohta kulkemalla kohti taulukon loppua, kunnes olemme
ohittaneet (noin) puolet pikseleistä, eli kun laskuri on suurempi
kuin RedCount[63]/2 (joka on siis kaikkien mukana olevien värien
määrä jaettuna kahdella). Onnistuu esim. seuraavasti:
for(loop=0; loop<63; loop++) {
if(RedCount[loop+1]>(RedCount[63]/2)) {
Borders.OptimalRed=loop;
break;
}
}
Älkää ihmeessä kysykö miksi se on tuollainen. Minulla oli aiemmin
jotain ongelmia toisenlaisen lähestymistavan kanssa ja tein
tuollaisen idioottivarman systeemin.
Tämä toistetaan tietenkin kaikille värisävyille.
4) Nyt vasta kivaa tuleekin. Funktiolle parametrina annettu
rekursiotason laskuri tarkistetaan ja toimitaan sen mukaan. Jos
taso on 0, niin olemme siinä pisteessä, että kuutioita ei enää
jaeta. Voimmekin kirjoittaa Borders-rakenteen optimaaliset
värisävyt (OptimalRed, OptimalGreen, OptimalBlue) lopullista
paletinmuodostusta odottamaan.
Esimerkissä funktio saa parametrinaan osoittimen
BORDERS-taulukkoon, sekä laskurin, joka kertoo montako ollaan jo
täytetty. Niinpä tallennus onnistuu varsin vaivattomasti:
memcpy(&BorderTable[TablesUsed[0]], &Borders, sizeof(BORDERS));
TablesUsed[0]++;
Jos on kuitenkin niin onnettomasti, ettei vielä olla lopussa niin
tehtävämme on silti helppo. Etsimme pisimmän akselin vähentämällä
alussa keräämämme suurimman ja pienimmän väriarvon sisältävät
muuttujat toisistaan:
red=SuurinPunainen-PieninPunainen;
green=SuurinVihreä-PieninVihreä;
blue=SuurinSininen-PieninSininen;
Esimerkissä nämä muuttujat tottelevat lyhyempiä nimi sr, br, sg,
bg, sb ja bb.
Sitten vain katsotaan mikä on pisin akseli ja tehdään uudet
pikkukuutiot näppärästi kahteen pienempään ja jaetaan kuutioiden
väriavaruudet siten, että toisen ylärajaksi tulee optimiväri-1 ja
toisen alarajaksi optimiväri. Tämä ylä- ja alarajojen muuttaminen
siis _vain_ pisimmän väriakselin arvojen kohdalta. Esimerkistä
löydät koodin miten tämä on toteutettu. Sitten vain kutsumme
itseämme molemmille pienemmille kuutioille, yhtä matalemmalla
rekursiotasolla ja annamme logiikan hoita loput.
Tästä puuttuu vielä tarkistus, josko kuutioon kuuluu enää vain 1 väri,
jolloin tehdään siitä suoraan paletin väri ja palataan rekursiossa
ylöspäin (ks. esimerkkiohjelma).
Kun itse rekursiivinen funktio on valmis, täytyy vielä hieman laittaa
lihaa ympärille. Tarvitsemme ohjelman, joka muuttaa alussa neuvotulla
tavalla varatun värikuution värivaraukset (eli ykköset värin kohdalla)
normaaliksi rgb-triplettitaulukoksi, varaa muistia
BORDERS-rakenteille, joihin optimaaliset värit tallennetaan, laskee
tarvittavan rekursiotason ja lopuksi hoitaa alussa ykköstä ja nollaa
sisältäneen värikuution sisältämään vastaavan sijainnin kvantisoidun
värin.
Viimeksimainittuun voisimmekin perehtyä hieman tarkemmin. Kun oikein
kutsuttu rekursiivinen funktio loppuu ja palaamme takaisin, on meillä
siististi koko kuutiomme jaettu 256:een (yleensä) pienempään
värikuutioon. Emme kuitenkaan vielä tiedä mikä väri tarkoittaa
milläkin välillä olevia sävyjä, joten teemme vielä yhden
homman.
Looppaamme jokaisen BORDERS-rakenteen läpi ja laitamme looppimuuttujan
mukaisen arvon alussa varattuun PaletteArray-muuttujaan kaikkiin
rakenteen ilmoittamiin pikseleihin. Eli piirrämme kuution sisään
SubspaceBottom ja SubspaceTop -muuttujien rajoittamalle alueelle
pienemmän kuution värillä, jonka rakenteen indeksi taulukossa
ilmoittaa ja talletamme rakenteen Optimal-tripletin palettiin indeksin
kohdalle.
Kuten aiemmin mainittiin, joissakin tapauksissa paletti menee siten,
että ennen rekursiotason 0 saavuttamista on jäljellä vain 1
väri. Tässä tapauksessa BORDERS-rakenteisiin ei talletetakaan täyttä
256:tta väriä optimisävyineen, joka taas täytyy ottaa huomioon
palettia tehtäessä. Eli ei mitään looppia välillä 0..256, vaan välillä
0..N, jossa N on se laskuri, jota korotetaan aina kun rekursiivinen
funktio täyttää yhden BORDERS-rakenteen. Esimerkkiohjelmassa
'TablesUsed'.
Pseudona se menisi jotenkin näin:
looppaa loop välillä 0...TablesUsed
looppaa r välillä border[loop].RedSubspaceBottom ..
border[loop].RedSubspaceTop
looppaa g välillä border[loop].GreenSubspaceBottom ..
border[loop].GreenSubspaceTop
looppaa b välillä border[loop].BlueSubspaceBottom ..
border[loop].BlueSubspaceTop
PaletteArray[r*64*64+g*64+b]=loop;
end looppaa
end looppaa
end looppaa
paletti[index].red=border[loop].OptimalRed;
paletti[index].green=border[loop].OptimalGreen;
paletti[index].blue=border[loop].OptimalBlue;
end looppaa
Sitten vain palauttamaan syntynyt paletti. Alustusfunktiomme on
muuttanut paletinvaraustaulukon taulukoksi, josta voidaan rgb-arvojen
avulla hakea oikea väri (colortorgb = PaletteArray[r*64*64+g*64+b]) ja
palauttanut tarvittavan paletin, jotta väri myös näyttää joltakin.
Paljon mainostettu Esimerkkiohjelma löytyy EXAMPLE-hakemistosta
nimellä QUANTIZ.C. Koodi on kieltämättä vähintään viisi kertaa
vaikeampaa kuin aiemmat esimerkit, mutta kyllä täytyy myöntää, että
kvantisointi asianakaan ei ole läheskään niin helppoa kuin
viivanpiirto.
Kvantisoinnin hyödyistä voidaan olla monta mieltä, mutta yksi asia on
varma. Jos ei kunnollista värimäärää omaavaa näyttötilaa ole
saatavilla, niin kyllä kvantisoitu paletti aina päihittää kotikutoisen
2-3-2 -järjestelmän (2 bittiä punaiselle ja siniselle ja 3 vihreälle).
Lisäksi kvantisoinnin tuloksena syntyvän kuution avulla voi tehdä
monta kivaa asiaa, kuten esimerkiksi motion blurin (väri on uuden ja
vanhan pikselin rgb-arvojen sekoitus) tai jotain muuta yhtä
hyödyllistä.
Yritä sisäistää asia. Jos ei mene kaaliin sitten millään (= mieti
kauemmin kuin 15 minuuttia), niin ilmoittele hämäristä kohdista. Asia
ON vaikea, mutta mielestäni selitin sen melkein ymmärrettävästi. Ja
ne, jotka ymmärsivät idean ja tekivät oman rutiinin (vain hullut
käyttävät esimerkkiohjelman koodia ;) saavat vain kiristää niitä
turvavöitään, sillä ensi luvussa hieman vaikeampaa kvantisointia!
Silti jo tämä tapa, etenkin nopeutensa ja suhteellisen hyvän
tuloksensa ansiosta on varsin hyvä.
7.9 Lisää paletin kvantisointia - Local K Mean
----------------------------------------------
Niille, jotka nauroivat itsensä ulos edellisen luvun
esimerkkiohjelmasta lyödään nyt luu kerralla kurkkuun. Tätä lähemmäksi
täydellisyyttä ette pääse - ainakaan tässä luvussa. Tämä algoritmi on
niin hidas, että edellinen versio on tähän verrattuna kuin rasvaamaton
salama. Myös 3Dicassa on selostettu pääpiirteittäin tämä tekniikka ja
kumarrankin kohti Sampsa Lehtosta, sillä muokkailen hänen selostustaan
hieman.
Perusidea tämän takana, toisin kuin kuutioihin jakavassa
rekursiivisessa versiossa, on pallomainen
ajattelutapa. Värikuutiossamme onkin nyt Palloja, joiden sijainti on,
kuten edellisessäkin, värin rgb-arvo. Koko taasen määräytyy sen
mukaan, kuinka monta tämän väristä pikseliä löytyy kvantisoitavasta
kuvasta. Jos et käytä kuvia tai et jostain syystä halua laskea mukaan
pallojen vetovoimaa, johon niiden koko vaikuttaa, niin värin määrä
kuvassa on aina 1, jolloin asialla ei kaavoissa ole merkitystä.
Näiden väripallojen seassa liikkuu sitten paletin verran
palettipalloja, eli yleensä 256 kappaletta. Näillä palloilla ei ole
kokoa. Väripallot vetävät puoleensa näitä kelluvia palloja sen mukaan,
kuinka suuria ne ovat ja nämä paletin värejä esittävät pallukat
liikkuvat sitten näiden mukana.
Vitsinä on se, että värit ovat kuin palloja vetäviä kappaleita ja
palettipallot pyrkivät sijoittumaan optimaaliseen paikkaan
väripallojen väliin. Koska jokaisella kerralla pallot liikkuvat vain
hieman, tulee kertoja luultavasti aika useita, ennenkuin palettipallot
ovat saavuttaneet optimaalisen sijaintinsa, joista tulee sitten
kvantisoidun paletin rgb-arvot. Pallojen yhteenlaskettua liikettä
käytetäänkin laskemaan sitä, milloin pallot ovat tarpeeksi lähellä
parhaita sijaintipaikkojaan (=liike edelliseen pientä). Mitä pienempi
liikkeen pitää vuorolla olla loppumisen tapahtumiseksi, sitä kauemmin
homma kestää ja sitä parempi tulos tulee.
Koska väripallot vetävät vain lähintä palettipalloa, niin jokin pallo
voi jäädä ilman vetovoimaa. Tässä tapauksessa pallo heitetään jonkin
värin lähelle tai kohdalle, jotta tämäkin väärälle tielle eksynyt väri
saadaan käyttöön.
Ja kuten Ilkan editoima selostuskin tekee, menemme sitten teknisempään
puoleen. Niin tein minäkin tätä opetellessani, joten älkää hävetkö
lukea tätä ennenkuin yritätte tehdä oman versionne rutiinista.
Kvantisoinnin aluksi teemme histogrammin, eli käyrän, joka ilmoittaa
kunkin värin määrän kuvassa. Esimerkissä käytämme sanan kokoista
laskuria 15-bittisille pikseleille (5 bittiä jokaiselle
värikomponentille), jolloin taulukon koko on 2^15 * 2 = 65536 tavua.
Nollaamme sen aluksi ja sitten korotamme jokaista tietyn värin
esiintymää kohti histogrammin tätä kohtaa yhdellä. Tietyn
rgb-tripletin sijaintihan on taas r*32*32 + g*32 + b.
Seuraavana sitten teemme taulukon niistä väreistä, joita todella
kuvassa on. Tallentaa täytyy rgb-tripletin lisäksi jokaisen värin
määrän, jonka saamme nyt histogrammista, joka taasen on 0 jos ei
tiettyä väriä ole lainkaan. Lehtonen suosittelee seuraavanlaista
rakennetta:
typedef struct {
unsigned char R; /* väriarvo */
unsigned char G; /* väriarvo */
unsigned char B; /* väriarvo */
unsigned long count; /* Värimäärä kuvassa */
} colorListStruct;
colorListStruct colorList[32768];
Muistia säästää tietenkin myös jos laskee värit ja varaa sitten
staattisesti muistia systeemille:
colorListStruct colorList=
(colorListStruct *)malloc(sizeof(colorListStruct)*colors);
Lisäksi täytyy vielä tallettaa kaikkien eri värien määrä kuvassa,
vaikka muuttujaan colorListCount. Sitten seuraavana peruspaletti:
unsigned long palette[256][3]; /* 3 = R,G & B */
Ja muuttujien lisääminen vain lisääntyy... Teemme vielä
värilaskuritaulukon, johon summaamme palettipalloa kutsuneiden värien
rgb-arvot kerrottuna värin määrällä. Tarvitsemme siis suht' suuren
lukualueen. Ja sitten vielä laskuri värien yhteismäärälle.
unsigned long colorSum[256][3]; /* 256 väriä, 3 = R,G & B */
unsigned long colorCount[256]; /* Voidaan yhdistää kyllä
colorSummiinkin */
Ja lopuksi vielä pisteenä i:n päälle läiskäisemme laskurin, joka
laskee paletin muutoksen edelliseen.
unsigned long variance;
Sitten vain kvantisoimaan. Jälleen rankasti kopioituna Sampsalta
tarvittavat askeleet. Mitäs teki niin hyvän jutun tästä. :) Eli itse
kvantisointirutiini:
1) colorSum ja colorCount -laskurien nollaus ja paletin täytto
colorList:in ensimmäisillä (256:lla) värillä.
2) Läpikäydään colorList:in värit. Värien määrähän löytyi
muuttujasta colorListCount, kuten aiemmin kerrottiin.
Loopataan c välillä 0 .. colorListCount-1
a) Otetaan colorList:istä väri c
b) Etsitään lähin väri palette-muuttujasta. Tuloksena numero
välillä 0..256. Etäisyys avaruudessahan on r- g- ja
b-etäisyyksien neliöiden summan neliöjuuri. Eli
delta_r = abs( r2-r1 )
delta_g = abs( g2-g1 )
delta_b = abs( b2-b1 )
sqrt( delta_r^2 + delta_g^2 + delta_b^2 )
Meidän täytyy loopata joka väri ja laskea tämä etäisyys ja
verrata sitä siihen mennessä löytyneeseen lyhimpään
etäisyyteen ja jos uusi väri on lähempänä tallennamme tämän
numeron ja etäisyyden ja jatkamme.
Optimointikikkoina se, että koska toinen potenssi on aina
positiivinen, putoaa itseisarvo (abs) pois. Ja koska
a^2 < b^2 <=> a < b
Niin neliöjuuriakaan ei tarvita. Esimerkkiohjelman
Dist-funktion ydin on seuraava:
for(loop=0; loop<Wanted; loop++) {
dist=Dist(r, g, b,
Palette[loop][0], Palette[loop][1], Palette[loop][2]);
if(dist<shortest) {
shortest=dist;
sl=loop;
}
}
Jossa dist vain palauttaa tuon delta_r^2 + delta_g^2 + delta_b^2.
c) Lisätään lähimmän värin colorSum-taulukkoon väripallon
rgb-arvot kerrottuna väripallon pikseleiden määrällä (count
kappaletta tätä värisävyä). (x=lähin väri, c=looppi)
colorSum[x][0] += colorList[c].R * colorList[c].count;
colorSum[x][1] += colorList[c].G * colorList[c].count;
colorSum[x][2] += colorList[c].B * colorList[c].count;
d) Sitten täytyy vielä tallettaa montako kertaa niitä rgb-arvoja
sinne ynnättiinkään.
colorCount[x]+=colorList[c].count;
3) Nollataan liikelaskurimuuttujamme variance.
4) Käydään läpi kaikki värit peruspaletista (c = 0..255)
a) Jos värin värilaskuri colorCount on suurempi kuin nolla, niin
väri oli lähinnä ainakin yhtä väripalloa. Nyt vain laskemme
keskiarvon kaikista kutsuneista väreistä ottamalla keskiarvon
niistä. Koska colorSum sisältää aina n kappaletta värin c
rgb-arvoja ja colorCount sisältää tämän luvun n niin
keskiarvo saadaan yksinkertaisesti:
palette[c][0] = colorSum[c][0] / colorCount[c];
palette[c][1] = colorSum[c][1] / colorCount[c];
palette[c][2] = colorSum[c][2] / colorCount[c];
Jos et oikein ymmärtänyt ideaa, niin otetaan
esimerkki. Paletin väri 5 on lähinnä kahta väripalloa:
palloja A = (10, 20, 0) ja B = (10, 20, 5). Väriä A on
kvantisoitavassa kuvassa 100 ja väriä B 200.
Rutiinimmehän ensin lisää colorSum:iin värin A rgb-arvot
kerrottuna värin A määrällä kuvassa:
colorSum[5][0] = A.R * A.count = 10 * 100 = 1000
colorSum[5][1] = A.G * A.count = 20 * 100 = 2000
colorSum[5][2] = A.B * A.count = 0 * 100 = 0
colorCount[5] = 100
Nyt meillä on siis värin A rgb-arvot 100-kertaisena
tallessa. Koska väriä B on kaksinkertaisesti, saamme sen
200-kertaisena summataulukkoomme:
colorSum[5][0] = 1000 + 10*200 = 3000
colorSum[5][1] = 2000 + 20*200 = 6000
colorSum[5][2] = 0 + 5*200 = 1000
colorCount[5] = 100 + 200 = 300
Nyt laskemme sitten lopullisen värin paletille:
palette[5][0] = 3000 / 300 = 10
palette[5][1] = 6000 / 300 = 20
palette[5][2] = 1000 / 300 = 3.33...
Huomaamme, että koska väriä B oli kaksi kertaa enemmän, on
rgb-arvokin lähempänä väriä B. Näin siis se väri, jota on
kaikkein eniten, vaikuttaa suurimpana uuden värin
rgb-arvoihin. Helppoa, eikö totta?
b) Mittaamme paletin muutoksen alkup. väriin:
temp = 0;
temp += abs( R - palette[c][0] );
temp += abs( G - palette[c][1] );
temp += abs( B - palette[c][2] );
variance += temp;
c) Kirjataan väri lopullisesti uuteen palettiin.
palette[c][0] = R;
palette[c][1] = G;
palette[c][2] = B;
5) Nyt uusi paletti on taas generoitu ja muutos muuttujassa
variance. Nollaamme nyt colorSum- ja colorCount -taulukot.
6) Jos variance-muuttujan kertoma paletin muutos on vielä yli
sallitun rajan, niin hyppäämme kohtaan 2. Jos taas olemme jo
sallitun rajan alla, niin lopetamme. Täten mitä pienempi
MAX_VARIANCE on, sitä useammin pyörimme looppia ja sitä kauemmin
tämä kestää.
Siinäpä se. Vaan ei kuitenkaan. Hitain osuus nimittäin alkoi
nyt. Kvantisoitu paletti on kiva, vaan missä onkaan toivottu
värikuutio? Tai edes oikeat indeksit kullekin histogrammin värille? Ei
missään. Paletti on optimaalinen, mutta ei kerro lainkaan, mitkä värit
sitä ovat lähinnä. Jos haluamme tietää jollekin rgb-arvolle lähimmän
värin täytyy meidän yksinkertaisesti loopata lävitse paletin kaikki
256 väriä ja selvittää mihin etäisyys on lyhin. Ja koska haluamme
värikuution outoihin tarkoituksiimme, merkitsee se tässä tapauksessa
256:n värin läpikäyntiä 32768 kertaa...
Siitä vain laskemaan pojat. :) Fiksu idea voisikin olla tallentaa 64
kilon tulos tiedostoon, ettei sitä tarvitse pelin käynnistyessä
laskea. Säästätte hermojanne kummasti. Tosin, itse käytin kuutta
bittiä värille, jolloin sain huimaavan 262144-tavuisen värikuution. =)
No nyt jokainen varmasti osaa tehdä tuosta toimivan
kvantisointisysteemin. Ja esimerkkisorsa löytyy sitten
tiedätte-kyllä-mistä hakemistosta nimellä QUANT2.C. Ja hyvää joulua ja
onnellista uutta vuotta vain kaikille, sillä tässä kohtaa saan
kakkosversion valmiiksi, juuri tapaninpäivänä!
7.10 VESA 2.0, rakenteet
------------------------
Ohhoh, vaikuttaa taas siltä että on aika alkaa puurtamaan kun
kesäloman viimein alettua minunkin osaltani (neljä ekaa viikkoa
kesätöissä), en enää keksi hyviä selityksiä myöhästymiselle. Eli
homman nimi on SVGA-ohjelmointi ja tavan nimi VESA 2.0. Lähdekoodiakin
tulee juuri sopivasti jotta kauan toivottu asia, 640x480 -kokoisten
PCX-kuvien lataus ja näyttäminen ruudulla onnistuu. Ja jotta homma
olisi hieman haastavampi, muutamme 256-värisen paletillisen PCX-kuvan
vielä 16-bittiseksi ennen näyttämistä.
Vaan ensin tongimme hieman menneisyyttä. Ihmisten alkaessa hiljalleen
kypsymään 640x480 16-väriseen tilaan alkoivat näytönohjainvalmistajat
tekemään näyttökortteja, jotka tukivat 640x480-tilaa 256
värillä. Sitten pikkuhiljaa jokaiselta valmistajalta alkoi tippumaan
uusia kortteja, jotka pystyivät yhä parempiin näyttötiloihin ja olivat
mahdollisimman toimimattomia toistensa kanssa. Samoin on asian laita
nykyäänkin, suoraan korttia ohjelmoimalla päästään hyviin nopeuksiin,
mutta valitettavasti ei ole kovin mukavaa jos ohjelmasi toimii esim 2%
maailman SVGA:n alla toimivasta konekannasta, toistaiseksi.
Tilanteen muuttuessa yhä sekavammaksi markkinoilla hätiin riensi
VESA-niminen standardointijärjestö (Video Electronics Standards
Assocation tjsp.) ja luotiin yhtenäinen rajapinta jota käyttäen
voitaisiin ohjelmoida kaikkia sitä tukevia kortteja. Ja kehityksen
kehittyessä VESA on muodostunut varsin suosituksi tavaksi käyttää
korkeampia näyttötiloja. Etenkin versio 2.0 tarjoaa lähes lyömättömän
tavan tehdä näyttäviä graafisia sovelluksia. Ja miten tämä meihin
liittyy? No me tietenkin opettelemme käyttämään tätä rajapintaa!
Heti alkuun totean, että nämä kappaleet käsittelevät VESA 2.0:llaa
erittäin puutteellisesti. Oletan että näytönohjaimesi on VESA
2.0-yhteensopiva ja tukee juuri määrättyä tilaa, 640x480 16-bittisillä
väreillä. Jätän 90% standardin funktioista ja mahdollisuuksista
käyttämättä. Täydellisen, lähes 1400-rivisen englanninkielisen
dokumentaation löytää esim. MBnetistä nimellä VBE20.ZIP tai Scitech
softwaren kotisivuilta, joka dokumenttia tietääkseni levittääkin,
osoitteessa www.scitechsoft.com. Scitech Display Doctor, eli entinen
UniVBE on myös SE ohjelma jos näytönohjaimesi VESA 2.0-tuki on
vajaavainen (esim. oman korttini 20 VESA 2.0-näyttötilaa laajenevat
tuon myötä 58:aan).
Toisaalta kun VESA 2.0:llan info-rakenteen osaa ja ymmärtää
moodi-infon rakenteen ja tietää miten VESA-yhteensopiva tila
asetetaan, pystyy tekemään lähes millaisen ohjelman tahansa, uupumaan
jää vain mahdollinen hardware-tason tuki skrollaukselle ja muulle
vastaavalle (jota todellinen guru ei tietenkään tarvitse).
Tämä luku kattaa kahden olennaisimman VESA 2.0:llaan liittyvän
rakenteen määritelmän ja kaikkien kenttien selostuksen. Myöhemmissä
luvuissa tutkimme hieman käyttöä. Tässä ensimmäinen, ns. VESA
information struct, suoraan VESA-enginestäni revittynä:
/* DJGPP laajentaa esim. tavun kokoiset rakenteen kentät 4-tavuisiksi
nopeuden takia ilman tätä määrettä, jos tämä puuttuu
palautettavista rakenteista on tieto täysin päin seiniä. Voit
kokeilla. */
#define PACKED __attribute__ ((packed))
typedef unsigned long int dword; /* Kaksoissana 4 tavua */
typedef unsigned short int word; /* Sana 2 tavua */
typedef signed long int s_dword; /* Etumerkillinen kaksoissana 4 tavua */
typedef signed short int s_word; /* Etumerkillinen sana 2 tavua */
typedef unsigned char byte; /* Tavu */
typedef unsigned bit; /* Bitti */
typedef struct {
byte sign[4] PACKED;
word version PACKED;
dword OEMstring PACKED;
bit fixedDAC : 1 PACKED;
bit VGAcompatible : 1 PACKED;
bit RAMDACtype : 1 PACKED;
bit reserved1 : 29 PACKED;
dword videomodeptr PACKED;
word totalmemory PACKED;
word OEMsoftwarerev PACKED;
dword OEMvendornameptr PACKED;
dword OEMproductnameptr PACKED;
dword OEMproductrevptr PACKED;
byte reserved2[222] PACKED;
byte OEMdata[256] PACKED;
} VesaInformation;
sign täytetään merkkijonolla "VESA", jos näytönohjaimesi tukee
VESA-tiloja. Jos halutaan ns. laajennettu tietokenttä, tämä tulee
ennen kutsua asettaa olemaan "VBE2", jolloin VESA 2.0-toteutus
ymmärtää täyttää uudet VESA 2.0:llan mukanaan tuomat kentät.
version taas on BCD-muotoinen versionumero (käytetään vain heksoja
0h-9h, eli versio voi olla 0000h - 9999h). Ylemmät kaksi heksaa ovat
suurempi versionumero (haluamme sen olevan väh. 02h) ja alempi
pienempi (edellisessä versiossa 1.2 olisi luku 0102h).
OEMstring on reaalitilan (seg:off, eli segmentti ja offsetti)
osoitin NULL-päätteiseen valmistajan määrittelemään merkkijonoon.
OEMsoftwarerev kertoo version-kenttää vastaavasti versiotietoa, ja
OEMvendornameptr, OEMproductnameptr ja OEMproductrevptr taasen
ovat reaalitilan osoittimia valmistajan ja tuotteen nimeen sekä
tuotteen versionumeroon. VESA 2.0-yhteensopivien toteutusten tulisi
laittaa merkkijonot OEMdata-alueelle (ei ROM-muistiin tai muuallekaan
infoblokin 512 tavun ulkopuolelle), jolloin kopioidessasi infoblokin
ohjelman omaan datasegmenttiin DOS-muistialueelta voidaan nämä
pointteritkin muuttaa toimiviksi suojatun tilan osoitteiksi näytölle
tulostamista varten.
fixedDAC on 0 jos DAC on aina kuusi bittiä per värikomponentti, ja 1
jos se on vaihdettavissa kahdeksaan bittiin komponenttia kohden. DAC
hoitaa palettia.
VGAcompatible on 0 jos kontrolleri on VGA-yhteensopiva ja 1 jos
ei. Eli VGA-portit toimivat ja normaalit videotilat myös. Varmaan
jokaisessa PC-näytönohjaimessa 0.
RAMDACtype on 0 jos RAMDAC on "normaali", 1 tarkoittaa että
VESA-keskeytyksessä 09h tulee asettaa blank bitti. Käytännössä tämä ja
fixedDAC koskevat sinua vain jos käytät paletti-tilaa (256 väriä),
johon en tässä luvussa ainakaan puutu. Blank-bitti palettia
asetettaessa estää "lumisateen" ruudulla ajoittamalla paletin
muutokset muualle kuin piirron ajalle.
videomodeptr on kaikkein tärkein info version-kentän jälkeen, sillä se
osoittaa tilojen numerot sisältävään listaan, joka lopetetaan -1:llä
(0xFFFF). Ohjelman tehtävä on tarkistaa onko jokin moodi todella
olemassa, esimerkiksi UniVBE asettaa minulla tuonne tiloja joita ei
ole olemassa. Lisää virheentarkastuksesta alempana.
totalmemory kertoo käytössä olevan muistin määrän 64kilon palasissa,
eli 1 mega esimerkiksi on 16.
No nyt vielä se toinen tärkeä rakenne, moodi-info, jota voi pyytää
halutulle tilalle jos vain tietää sen numeron. Ja ne numerothan
löytyivät jo info-blokista, joten nyt vain rakennetta tutkimaan. Tämä
rakenne on hirviö:
typedef struct {
bit modesupported : 1 PACKED;
bit reserved : 1 PACKED;
bit TTYsupported : 1 PACKED;
bit colormode : 1 PACKED;
bit graphicsmode : 1 PACKED;
bit notVGAcompatible : 1 PACKED;
bit notVGAwindowmemory : 1 PACKED;
bit linearmodeavailable : 1 PACKED;
bit reserved1 : 8 PACKED;
bit Arelocatablewindows : 1 PACKED;
bit Areadablewindow : 1 PACKED;
bit Awritablewindow : 1 PACKED;
bit reserved2 : 5 PACKED;
bit Brelocatablewindows : 1 PACKED;
bit Breadablewindow : 1 PACKED;
bit Bwritablewindow : 1 PACKED;
bit reserved3 : 5 PACKED;
word windowgranularity PACKED;
word windowsize PACKED;
word windowAsegment PACKED;
word windowBsegment PACKED;
dword windowfunctionptr PACKED;
word bytesperscanline PACKED;
word horizontalresolution PACKED;
word verticalresolution PACKED;
byte characterwidth PACKED;
byte characterheight PACKED;
byte planes PACKED;
byte bitsperpixel PACKED;
byte banks PACKED;
byte memorymodel PACKED;
byte banksize PACKED;
byte imagepages PACKED;
byte reserved4 PACKED;
byte redbits PACKED;
byte redshift PACKED;
byte greenbits PACKED;
byte greenshift PACKED;
byte bluebits PACKED;
byte blueshift PACKED;
byte reservedbits PACKED;
byte reservedshift PACKED;
bit programmablecolorramp : 1 PACKED;
bit reservedbitsusable : 1 PACKED;
bit reserved5 : 6 PACKED;
dword physicalbasepointer PACKED;
dword offscreenmemoryoffset PACKED;
word offscreenmemorysize PACKED;
byte reserved6[206] PACKED;
} VesaModeInformation PACKED;
modesupported kertoo onko tila edes tuettu. (1 = tosi, 0 = epätosi)
TTYsupported kertoo tukeeko toteutus tekstin tulostusfunktioita
colormode on 1 jos tila on väritila, 0 jos mustavalkoinen
graphicsmode on 1 jos tila on grafiikkatila, 0 jos tekstitila
notVGAcompatible on 1 jos tila ei tue VGA-rekistereitä ja IO-portteja
notVGAwindowmemory on 0 jos banked-tilat ovat mahdollisia, 1 jos eivät
linearmodeavailable on 1 jos LFB on, 0 jos ei. Lisää termeistä alempana
A- ja B-alkuiset kolme kenttää, relocatablewindows, readablewindow ja
writeable window kertovat, voiko ikkunaa A tai B (riippuen nimen
alkukirjaimesta) siirtää, voiko sitä lukea ja voiko siihen
kirjoittaa. Nämä tiedot ovat ns. banked-tiloille, jossa videomuistia
katsotaan yleensä reaalitilassa olevista "ikkunoista", joita voidaan
sitten siirrellä. windowgranularity kertoo kilotavuina, kuinka
pienissä askelissa ikkunaa voidaan siirtää näyttömuistissa ja
windowsize kuinka suuri ikkuna on. windowAsegment ja B-vastaava
kertovat CPU-osoiteavaruudessa ikkunoiden osoitteet, A000h:ta
näytti minulla, eli ihan normaali VGA-muistisegmenttihän tuo yleensä
on (muista vain että segmentti kerrotaan kuudellatoista jotta saadaan
suojatun tilan offsetti, ja muista että selektori on _dos_ds, eli
pitää käyttää movedata, _farpoke* tai dosmemput-komentoja).
En puutu banked-tiloihin tässä luvussa, vaan oikaisen ja kerron
linear-tiloista. Banked-tiloissa tarvitaan paljon logiikkaa jotta
voidaan selvittää millaisissa pätkissä pitää näyttömuistissa liikkua,
kumpaa ikkunaa A vai B voi liikuttaa vai voiko molempia, saako niihin
kirjoittaa jne. Banked-tilassa kaksoispuskurin kopiointi ruudulle
tehdään siten, että siirretään ikkuna muistin alkuun, kirjoitetaan
ikkunan pituuden verran kaksoispuskurista, siirretään ikkunaa
eteenpäin granularity-muuttujan sallimissa rajoissa pitkin
näyttömuistia (esim. 64 kiloa kerrallaan), kirjoitetaan seuraava pala
jne.
Tarvittavalla älykkyysosamäärällä ja ehkä hitusella englannin kielen
taitoa varustettu yksilö kyllä pystyy tekemään banked-tuen
halutessaan. windowfunctionptr on reaalitilan osoitin (seg:off)
rutiiniin, joka vaihtaa nopeasti ikkunan sijaintia. Valitettavasti
tällaisen funktion kutsumiseen tehtävät valmistelut ovat sen verran
massiivista luokkaa (lue: en jaksa alkaa perehtymään asiaan), että en
ala niitä tässä esittelemään.
bytesperscanline on hyödyllinen tieto myös LFB:tä käytettäessä. Se
kertoo montako tavua yksi rivi näyttötilassa vie. Yleensä on
turvallista olettaa että se on suoraan vaakaresoluutio kerrottuna
tavuilla per pikseli, mutta joillakin korteilla, esim. Matroxilla
kuulemma nuo joskus ovat jotain ihan muuta. Tämä poistaa sinulta
mahdollisuuden käyttää koko kaksoispuskurin kopioimisen kerrallaan
näyttöpuskuriin, mikä tietenkin hidastaa ohjelmaa. Kannattaa ehkä
tehdä kaksi piirtofunktiota, joista vain toinen ottaa huomioon tämän
seikan.
horizontalresolution ja verticalresolution kertovat tilan vaaka- ja
pystyresoluution. Tekstitilassa nämä arvot ovat riveinä,
characterwidth ja characterheight -muuttujien ilmoittaessa merkin
leveyden ja korkeyden pikseleissä.
planes kertoo muistitasojen määrän tässä tilassa. Käytännössä näillä
on väliä vain 16-värisissä tiloissa ja mode-x:ää vastaavissa
VESA-tiloissa. Normaalisti tämä on 1.
bitsperpixel kertoo montako bittiä pikselille tässä tilassa on
varattu. 16-värisessä tämä on 4, 256-värisessä 8, 16 tai 15
highcolor-tiloissa ja 24 tai 32 truecolor -tiloissa. Jos käytät
tasoja, on bittien määrä / taso yleensä suoraan bitit/tasojen_määrä,
eli 4-tasoinen 256-värinen tila olisi 2 bittiä per taso.
banks kertoo montako scanline-bankkia kussakin tilassa
on. Esim. CGA:ssa kaksi ja Herculeksella neljä. Yleensä tämä on 1
memorymodel kertoo minkä tyyppinen muistimalli tilassa
on. Muistimalleja on 8:
00h Tekstitila
01h CGA
02h Hercules
03h Plane-tyyppinen
04h Pakattu pikseli (yleinen 256-värinen)
05h Non-chain 4, 256-värinen
06h Suora värimalli (jota me tulemme käyttämään)
07h YUV (värimalli, tyyliin RGB)
08h-0Fh = Varattuja VESA:n määriteltäviksi
10h-FFh = Varattuja valmistajan määriteltäviksi
banksize kertoo aiemmin kuvattujen scanline-bankkien määrän. Jos
jollakulla on jotain aavistusta näistä scanline-bankeista, tasoista ja
plane- pakatun pikselin, non-chain ja YUV-muistimalleista, olen
kiinnostunut tietämään, tässä moodi-info -rakenteessa on minulle
ainakin lähes outoja kenttiä ihan tarpeeksi.
imagepages kertoo montako ylimääräistä ruutua näyttömuistiin mahtuu,
eli ohjelma voi ladata useampia ruudullisia muistiin ja vaihdella
näiden välillä.
Sitten tuleekin varmaan meitä eniten kiinnostava osa. Suoran
värimallin tiloissa (memorymodel = 06h) 15-, 16-, 24- ja 32-bittisissä
tiloissa käytetään aina tietty määrä bittejä ilmaisemaan kutakin
värikomponentteja. Esimerkiksi 16-bittisessä tilassa yleensä on 5
bittiä punaiselle, sitten 6 vihreälle ja vielä 5 siniselle. Näin
meillä on 2^5 = 32 sävyä punaiselle ja siniselle ja 2^6 vihreälle
(jonka eri sävyjä silmä parhaiten erottaa). Jotta voimme koota ne
meidän täytyy vielä shiftata bittejä oikeille paikoilleen:
#define RGB16(r,g,b) ((r<<11) + (g<<5) + b)
Shiftaukset ja bittien määrä värikomponenttia kohden vaihtelevat ja
moodi-infossa jokaiselle värikomponentille on määritetty bittien määrä
ja shiftaus (redbits, redshift, bluebits...). Jos ohjelma ottaisi
kaikki mahdolliset yhdistelmät huomioon, olisi homma luultavasti
tuskallisen hidasta. Onneksi kuitenkin on varsin turvallista olettaa
seuraavia asioita:
15-bittinen värimalli on 5:5:5, eli kaava on (r<<10)+(g<<5)+b
16-bittinen värimalli on 5:6:5, eli kaava on (r<<11)+(g<<5)+b
24-bittinen värimalli on 8:8:8, eli kaava on (r<<16)+(g<<8)+b
32-bittinen värimalli on sama kuin 24, mutta 8 ylintä bittiä jäävät
käyttämättä ja näin ollen kaikkien pikselien muistiosoitteet ovat
neljällä jaollisia, paljon helpompaa kuin 24-bittisten tilojen
kolmella jaolliset.
Joskus tietenkin poikkeuksia voi olla ja on parasta tarkistaa ennen
moodin asettamista, täsmäävätkö shiftaukset ja bitit oletuksiin ja
tulostaa vaikka virheilmoitus, jos näin ei käy. Jos haluaa että
ohjelma varmasti toimii kaikilla korteilla, voi tehdä yleisluontoisen,
mutta kylläkin älyttömän hitaan muuttujia käyttävän systeemin.
Tiivistelmänä shift- ja bits-kentistä, että tee päätös bitsperpixelin
mukaan ja tarkista ennen moodiin siirtymistä vielä että shiftaukset
ovat oikeita moodi-infossa ja jos ne eivät täsmää, älä asetakaan
tilaa.
programmablecolorramp kertoo voiko väriramppia ohjelmoida. Jos arvo on
0 ei sitä voi muuttaa, mutta jos se on ohjelmoitava, voit säätää
punaiselle, vihreälle ja siniselle haluamasi muotoisen
värirampin. Voit esim. tehdä siten, että jos punaiselle on 64 sävyä
niin sen sijaan että 0 on ei yhtään ja 64 on maksimi, saavutetaan
maksimi jo 32:ssa ja loppu on tasaisen punaista. Tästä on hyötyä
käytännössä vain gamma-korjauksessa ja joissakin
erikoisefekteissä. Jos asia kiinnostaa kannattanee tutustua
VBE20.TXT:n keskeytykseen 09h.
reservedbitsusable on 1 jos yli jäävät (esim 32-bittisessä tilassa
yleensä 8 ylimmäistä) bitit ovat käytettävissä.
physicalbasepointer on LFB:n aloitusoffset fyysisessä
muistiavaruudessa (eli alkaen muistin alusta aina maksimiin 4
gigaan). Tämä on tärkeimpiä kenttiä jos et käytä banked-tiloja.
Jos LFB ei ollut tuettu tämä kenttä on 0.
offscreenmemoryoffset kertoo kuinka pitkällä näyttömuistin alusta on
ohjelman käytettävissä oleva ylimääräinen näyttömuisti ja
offscreenmemorysize kertoo montako kilotavua siinä on.
Huhhuh. Tämä käy työstä. Pahoittelen jos näissä kenttien selostuksissa
on jotain epäselvyyksiä, ilmoitathan asioista joita et ymmärrä
minulle, en tiedä kuinka selvää tämä on sellaiselle joka ei vielä
VESA-tiloja ole ohjelmoinut.
7.11 VESA 2.0, ensimmäiset keskeytykset
---------------------------------------
No niin, struktit ovat siis hallinnassa? Sitten hommiin. VESA 2.0:llan
käyttö on tiivistettynä seuraavanlaista:
1) Otetaan talteen info-structi ja tarkistetaan että kortti on
VESA 2.0-yhteensopiva (versionumerosta)
2) Luetaan structin lopusta tuettujen VESA-tilojen numeroiden lista
3) Tutkitaan mikä tuetuista numeroista on se tila jonka haluamme (eli
mennään yksitellen lävitse kaikki, pyydetään moodi-info, tutkitaan
ja jos ei ole oikea, jatketaan eteenpäin).
4) Löydettyämme oikean asetetaan tila.
Sitten hieman VESA-rajapinnan toiminnasta. VESA 2.0 on, kuten
edeltäjänsä, vanhan video-keskeytyksen 10h alle tehty joukko
keskeytyksiä. Tunnus VESA-keskeytykselle on arvo 4Fh
ah-rekisterissä. Al-rekisteriin asetetaan halutun toiminnon numero
(joista kolme ensimmäistä, 00h-02h selitetään tässä). Systeemi toimii
myös 16-bittisissä sovelluksissa, mikä tarkoittaa käytännössä sitä,
että kun keskeytykselle annetaan muistialueen osoite, jonne
informaatiorakenne tulee kirjoittaa, täytyy sen sijaita
perusmuistissa. Keskeytyksen kutsun jälkeen ax:ssä palautetaan
seuraavanlainen palautusarvo:
AL == 0x4F: Funktio on tuettu
AL != 0x4F: Funktio ei ole tuettu
AH == 0x00: Funktiokutsu onnistunut
AH == 0x01: Funktiokutsu epäonnistui
AH == 0x02: Softa tukee funktiota, mutta rauta ei
AH == 0x03: Funktiokutsu virheellinen nykyisessä näyttötilassa
Kaikki AH:n arvot muut kuin 0 täytyy tulkita yleisinä virhetiloina,
sillä myöhemmissä VESA:n versioissa virhemäärittelyjä saattaa tulla
lisää. Jotta voisimme käyttää normaalien rekisterien lisäksi
tarvittavia segmenttirekisterejä, käytämme __dpmi_int:iä ja rakennetta
__dpmi_regs. Alla esimerkkimalli VesaInt-komennosta, jolle annetaan
vain toiminnon numero.
int VesaInt(byte function, __dpmi_regs *regs) {
regs->h.ah=0x4F;
regs->h.al=function;
__dpmi_int(0x10, regs);
if(regs->h.al != 0x4F) {
puts("Funktio ei tuettu");
return -1;
}
switch(regs->h.ah) {
case 0x00:
break;
case 0x01:
puts("Funktiokutsu epäonnistui!");
return 1;
case 0x02:
puts("Softa tukee funktiota, mutta rauta ei!");
return 2;
case 0x03:
puts("Funktiokutsu virheellinen nykyisessä videotilassa!");
return 3;
default:
puts("Tuntematon virhe!");
return 4;
}
return 0;
}
__dpmi_regs-rakenteen h-kentän alta löytyy tavun kokoiset palat ja
x-osasta 16-bittiset rekisterit (ainakin). Muita emme tarvikaan.
Nyt olemme tarpeeksi evästettyjä kutsumaan funktiota 0h, joka
palauttaa VESA-infoblokin. Ah täytetään 4Fh:lla, al asetetaan nollaksi
ja es:di asetetaan osoittamaan puskuriin minne infoblokki sijoitetaan.
Jotta saisimme info-struktuurin talteen, täytyy meidän ensin varata
muistia megan alapuolelta tarvittavat 512 tavua (keskeytys jolla info
palautetaan haluaa reaalitilan osoitteen ja tämän takia meidän täytyy
varata DOS-muistia). Sekä ohjelman omalta muistialueelta saman
verran tilaa, emme halua käsitellä tietoja dos-muistissa jonkin
farpeekb:n avulla. Lisäksi sign täytyy asettaa VBE2:ksi, jotta
keskeytys tietää että haluamme version 2 mukaista tietoa.
Alla esimerkkikoodista pala, joka varaa dos-muistin, asettaa
tarvittavat asiat ja kutsuu keskeytystä:
int VesaInit() {
__dpmi_regs regs;
dosbuffer=(dword)__dpmi_allocate_dos_memory(64, (int *)&dosselector);
if(dosbuffer==-1) {
puts("Ei tarpeeksi perusmuistia VESA-infoblokille!");
return 1;
}
dosbuffer*=16; /* muutetaan lineaariseksi osoitteeksi (seg*16) */
vesainfo=(VesaInformation *)malloc(sizeof(VesaInformation));
memcpy(vesainfo->sign, "VBE2", 4);
dosmemput(vesainfo, sizeof(VesaInformation), dosbuffer);
regs.x.es=dosbuffer/16;
regs.x.di=0;
if(VesaInt(0x00, &regs)) {
puts("Virhe VESA-keskeytyksessä!");
return 1;
}
dosmemget(dosbuffer, sizeof(VesaInformation), vesainfo);
if(strnicmp(vesainfo->sign, "VESA", 4)!=0 || vesainfo->version<0x0200) {
puts("not found!");
return 1;
}
puts("found!");
return 0;
}
void VesaDeinit() {
__dpmi_free_dos_memory(dosselector);
}
Kuten selvästi näkyy, homma on varsin helppoa. Varataan muistit,
laitetaan "VBE2"-pala, kopioidaan DOS-muistiin, asetetaan rekisterit,
keskeytys, kopioidaan takaisin omaan muistiin ja se on siinä. Tutkimme
palautusarvon ja jos onnistuimme voimme jatkaa moodi-infojen
tiirailuun.
Moodi-infon lukemiseksi vain matkaamme lävitse halutun
alueen. Kaikkein varmin tapa tutkimiseen on hakea tieto
perusmuistista, jos jostain syystä lista ei olisikaan infoblokin
alueella, vaan jossain muualla. Käytämme vain dosmemget:iä niin
monesti että vastaan tulee -1 ja joka arvolle katsomme moodi-infon.
Allaoleva esimerkki etsii 640x480-tilan 16-bittisillä väreillä ja
asettaa tilan, varaa kaksoispuskurin ja palauttaa sen osoitteen tai
NULL jos ilmaantui virhe. Varsinainen monitoimityökalu, siis.
Ennen kuitenkin tutustumme käsitteeseen LFB, sillä se on se mitä
käytämme. Edellisissä versioissa käytettiin VGA-muistia, joka osoitti
aina haluttuun palaan videomuistia. Muistiin täytyi siis käydä käsiksi
64 kilon palasissa, mikä oli varsin tuskallista touhua. Versio 2.0 toi
kuitenkin mukanaan suojatun tilan käyttäjille uuden asian,
LFB:n. Systeemi on sellainen, että videomuisti sijoitetaan jonnekin
osaan muistiavaruutta. Homma on siis sama kuin osoitteen 0xA0000
kanssa, mutta nyt paikka on yleensä jossain 300 megan paikkeilla tai
kauempana ja kokoa on 64 kilon sijasta näyttömuistin verran, omalla
koneellani 4 megaa.
Osoitteen saimmekin jo infoblokissa, mutta jotta voisimme käyttää tätä
osoitetta, täytyy muistisuojauksista päästä eroon. Tarvitsemme siis
selektorin joka osoittaa halutun muistiosoitteen alkuun ja joka on
asetettu toimivaksi tarvittavan pitkälle matkalle, jottemme saa
segmentation faultia muistialueen ohi kirjoittamisen takia
kopioidessamme kaksoispuskuria ruudulle. Alla suoraan jostain pöllitty
funktio (kiitoksia tekijälle) mappaamiseen ja mappauksen poistoon:
/* Funktio ottaa fyysisen osoitteen muistiavaruudessa (physaddr) sekä
koon tavuissa (size) ja palauttaa linear-muuttujassa varatun alueen
lineaarisen offsetin (linear), sekä selektorin jota käytetään kun
halutaan käsitellä muistialuetta (segment, tätä käytettäessä offset
aina 0). Funktio palauttaa 0 jos onnistui, 1 jos ei */
int VesaMapPhysical(dword *linear, s_dword *segment,
dword physaddr, dword size) {
__dpmi_meminfo meminfo;
meminfo.address = physaddr;
meminfo.size = size;
if(__dpmi_physical_address_mapping(&meminfo) != 0)
return 1;
linear[0]=meminfo.address;
__dpmi_lock_linear_region(&meminfo);
segment[0]=__dpmi_allocate_ldt_descriptors(1);
if(segment[0]<0) {
segment[0]=0;
__dpmi_free_physical_address_mapping(&meminfo);
return 1;
}
__dpmi_set_segment_base_address(segment[0], linear[0]);
__dpmi_set_segment_limit(segment[0], size-1);
return 0;
}
Eli käytännössä tarvitaan vain palautettua segmenttiä, offset segmentin
alla on suoraan (y*leveys+x)*tavuja_per_pikseli, eli mitään lukujen
lisäyksiä ei tule, kuten asian laita VGA-tilojen kanssa on (0xA0000).
Sitten tietenkin vapautus loppuun:
/* Tämä taasen vapauttaa muistin käsittelyyn varatut kahvat, kutsutaan
kun palataan VESA-tilasta. */
void VesaUnmapPhysical(dword *linear, s_dword *segment) {
__dpmi_meminfo meminfo;
if(segment[0]) {
__dpmi_free_ldt_descriptor(segment[0]);
segment[0]=0;
}
if(linear[0]) {
meminfo.address=linear[0];
__dpmi_free_physical_address_mapping(&meminfo);
linear[0]=0;
}
}
Hieno homma, vaan mitenkäs näitä käytetään? No näemme kohta senkin,
hieman vain kärsivällisyyttä. Ensin tutkimme funktiot 01h ja 02h.
01h palauttaa cx-rekisterissä annettavan moodin tiedot, puskurin
ollessa jälleen es:di. Voimme käyttää mainiosti alustusfunktiossa
varattua muistialuetta dosbuffer. Käyttö on naurettavan helppoa:
/* Palauttaa 1 jos moodi ei ole olemassa */
int VesaGetModeInfo(word mode, VesaModeInformation *info) {
__dpmi_regs regs;
regs.x.cx=mode;
regs.x.es=dosbuffer>>4;
regs.x.di=0;
if(VesaInt(0x01, &regs))
return 1;
dosmemget(dosbuffer, sizeof(VesaModeInformation), info);
return 0;
}
Sitten vain tutkimme halutut arvot moodi-infosta ja jos oikea on
kohdalla, asetetaan tila. Keskeytyksen numero on 02h ja bx:ssä
annetaan tarvittava tieto moodista. Mukaan pakataan tieto haluammeko
lineaarisen tilan vain banked-tilan ja josko näyttömuisti tulee
tyhjentää ennen vaihtoa. Bitit on järjestelty näin:
0-8 Moodin numero
9-13 Nollaa (säästetty tulevaisuutta varten)
14 0 jos käytetään banked-tilaa, 1 jos lineaarinen, eli LFB-tila
15 0 jos tyhjennetään näyttömuisti, 1 jos ei
eli vaikkapa:
#define MODEFLAG_BANKED 0x0000
#define MODEFLAG_LINEAR 0x4000
#define MODEFLAG_CLEAR 0x0000
#define MODEFLAG_PRESERVE 0x8000
/* Jälleen ei-nolla arvo tarkoittaa virhettä */
int VesaSetMode(int mode) {
__dpmi_regs regs;
regs.x.bx = mode | MODEFLAG_LINEAR | MODEFLAG_CLEAR;
if(VesaInt(0x02, &regs))
return 1;
return VesaMapPhysical(&vesalfb_linear, &vesalfb_segment,
vesamodeinfo[modenum].physicalbasepointer,
vesamodeinfo[modenum].bytesperscanline*
vesamodeinfo[modenum].verticalresolution);
}
No niin, mitäs tässä enään on jäljellä. No ihan oikeassa olet, eipä
kai mitään. Vai häh? Ai mikä? Esimerkki?!? No kai se nyt vielä tähän
mahtuu. Täydelliset sorsat ja määrittelyt voit kaivaa tiedostosta (kai
nyt flipin osaa tehdä kuka tahansa kun tietää selektorin ja
näyttömuistin koon?) VESA20.C. Ja sitten miten homma todella hoidetaan
voit lukea seuraavasta luvusta. Mutta se moodin asetus:
/* Palauttaa 0 jos onnistui */
word * VesaSet640x480_16() {
VesaModeInformation modeinfo;
s_word mode=0;
dword addr=vesainfo->videomodeptr;
while(mode!=-1) {
dosmemget(addr, 2, &mode);
addr+=2;
if(mode!=-1) {
/* Jos virhe tulee jatketaan seuraavaan */
if(VesaGetModeInfo(mode, &modeinfo))
continue;
if(modeinfo.linearmodeavailable &&
modeinfo.horizontalresolution==640 &&
modeinfo.verticalresolution==480 &&
modeinfo.bitsperpixel==16) {
if(VesaSetMode(mode, &modeinfo))
return NULL;
vesascreen = (word *)malloc(640*480*sizeof(word));
return vesascreen;
}
}
}
return NULL;
}
Ja vielä se deinitti taitaapi puuttua.
void VesaReset() {
textmode(0x03);
VesaUnmapPhysical(vesalfb_linear, vesalfb_segment);
free(vesascreen);
}
Sitten vain niputetaan kaikki mitä on vastaan tullut, lisätään hieman
suolaa ja nautitaan PCX-kuvan kera. Hyvää ruokahalua!
7.12 Miten se todella pitäisi tehdä
-----------------------------------
Aiemmat kaksi lukua vain raapaisivat pintaa VESA-ohjelmoinnin
saralla. Tärkeimmät funktiot kuitenkin on selostettu ja niiden
pohjalta on jo varsin helppoa tehdä oma engine. Esimerkkikoodia ei
kannata suoraan käyttää, sillä se on käytännössä vain omasta
enginestäni kokoon parsittu kevytversio, joka sisältää tarpeeksi
esimerkkejä eri asioiden teosta, jotta oman systeemin teko
helpottuisi. Tässä luvussa hieman siitä miten järjestelmän voisi
toteuttaa.
Ensimmäiseksi kannattaa erotella VESA-rutiinit järkeviin
palasiin. Itselläni esimerkiksi yhdessä tiedostossa on Vesa-rutiinit
inforakenteiden lukemiseksi muistiin ja olennaisimmat funktiot, kuten
VesaInt. Toinen osa sitten hoitaa graafisen puolen, eli asettaa
halutun näyttötilan, ja hoita näytönpäivityksen. Luonnollisesti
funktioiden määrittelyt ja rakenteet ovat omissa .h-tiedostoissaan ja
koodi ja muuttujat taas .c-osissa. Ei ole yhtään tyhmä idea tehdä
kirjastosta yhtä pakettia, esim libvesa.a, jonka DJGPP:n
lib-hakemistoon sijoittamisen jälkeen voi sisällyttää johonkin
ohjelmaan pelkästään parilla #include-lauseella ja -lvesa
-parametrilla.
Toiseksi erittäin tärkeä asia on tehdä systeemistä tarpeeksi joustava,
jotta siitä olisi todella jotain hyötyä. Nykyisellään näytönohjainten
kirjo ja resoluutioiden määrä on niin suuri, että jo tästä syystä
VESA-esimerkki lienee ensimmäisiä tutoriaalin ohjelmia, joka ei tule
koskaan toimimaan kaikilla koneilla. Hyvä järjestelmä hoitaa asiat
siten, että kutsuva ohjelma on tyystin tietämätön siitä mitä raudassa
on. Unelmasysteemi on sellainen, että initialisoit moottorin alussa ja
deinitialisoit lopussa. Käytön aikana sinulla on puskuri jonne voit
laittaa grafiikan ja käsky jolla tavara heitetään näytölle. Ja
systeemin tulisi toimia näin vaikka alla ei edes olisi todellista
VESA-yhteensopivaa rautaa.
Miten tämän pystyy sitten saavuttamaan? No initit ja deinitit on
helppo hoitaa, mutta että vielä universaali piirtotapa, vaikka alla
olisi ihan toinen resoluutio ja värimäärä kuin mitä ohjelma luulee,
onko tämä mahdollista? Vastaus on myöntävä. Eikä ratkaisu edes ole
kovin vaikea. Taikasana: funktio-osoittimet (C++:ssalla
virtuaalimetodit ja eri resoluutioiden periyttäminen perusluokasta).
Oma systeemini sisältää tällaisen muuttujan:
void VESAREFRESH (*VesaScreenRefresh)()=NULL;
Käytännössä VESAREFRESH on vain määritelty tyhjäksi (#define
VESAREFRESH), mutta yllä esitetty systeemi osoittautuu aika
käytännölliseksi kun se haluttu tila ei löydykään. Systeemi toimii
näin:
Oletetaan että pelini on tarkoitus toimia 320x200-resoluutiossa
32-bittisillä väreillä. Systeemi on unelma, koska yksi pikseli on
dwordin kokoinen (=nopeaa) ja jokainen värikomponentti on tavun verran
ja ylimmäisen tavun jäädessä tyhjäksi. Vaan, ongelmana on, että vain
hyvin harvalla on 32-bittinen halutun resoluution näyttötila. No,
ongelma on helposti ratkaistu:
Yhden flipin sijasta tehdäänkin _useita_ päivitysrutiineja. Yksi
muuntaa värit 24-bittisiksi lennossa (käytännössä yhtä nopea kuin aito
32-bittinen tilakin), toinen muuttaa ne 16-bittisiksi, yksi voi jopa
käyttää korkean resoluution 32-bittistä tilaa emuloimaan joistakin
korteista puuttuvia 320x200-kokoisia korkeavärisiä tiloja (oma
Matroxini esim. tukee normaalisti tiloja vain resoluutiosta 640x480
ylöspäin). Varalle voidaan vielä tehdä kvantisoitua tilaa tai
harmaasävyjä käyttävä, 100% VGA-yhteensopiva flippi, joka käyttää
256-väristä tilaa. Initin aikana vain asetetaan VesaScreenRefresh
osoittamaan siihen päivitysfunktioon mitä asetettu näyttötila vastaa.
Ja kun flippi hoitaa kaksoispuskurin muuntamisen sellaiseen muotoon
että se on näytettävissä sillä hetkellä käytössä olevalla parhaiten
oikeaa vastaavalla näyttötilalla, ei ohjelman tarvitse kuin piirtää
tavara vesascreen-puskuriin, joka on aina saman suuruinen ja jossa on
aina sama värimäärä, sekä kutsua VesaScreenRefresh-funktiota. Näin
funktion ollessa oikea flippi tulee tavara ruudulle vaikka käyttäjällä
ei sattuisikaan olemaan 320x200 32bit -tilaa, vaan esim. 320x200
24bit. Ihanaa. Ja tässä pätkä omasta koodistani:
int VesaLowresInit(int flags) {
int loop;
if(!(vesaflag & VESA_INITIALIZED))
VesaError("Vesa low-resolution mode init", "Engine not initialized!");
vesascreen=(pointer)Jmalloc(320*200*sizeof(dword));
memset(vesascreen, 0, 320*200*sizeof(dword));
for(loop=0; loop<vesamodes; loop++)
if(vesamodeinfo[loop].linearmodeavailable &&
vesamodeinfo[loop].horizontalresolution==320 &&
vesamodeinfo[loop].verticalresolution==200 &&
vesamodeinfo[loop].bitsperpixel==32) {
printf("Initializing mode 320x200 32-bit colors...\n");
vesacurmodeinfo=&vesamodeinfo[loop];
VesaScreenRefresh=VesaLowres_320x200x32;
VesaSetMode(loop);
vesamode=VESAMODE_320x200x32;
return vesamode;
}
for(loop=0; loop<vesamodes; loop++)
if(vesamodeinfo[loop].linearmodeavailable &&
vesamodeinfo[loop].horizontalresolution==640 &&
vesamodeinfo[loop].verticalresolution==400 &&
vesamodeinfo[loop].bitsperpixel==32) {
printf("Initializing 640x400 to 320x200 emulation"
" with 32-bit colors...\n");
vesacurmodeinfo=&vesamodeinfo[loop];
VesaScreenRefresh=VesaLowres_640x400x32;
VesaSetMode(loop);
vesamode=VESAMODE_640x400x32;
return vesamode;
}
for(loop=0; loop<vesamodes; loop++)
if(vesamodeinfo[loop].linearmodeavailable &&
vesamodeinfo[loop].horizontalresolution==640 &&
vesamodeinfo[loop].verticalresolution==480 &&
vesamodeinfo[loop].bitsperpixel==32) {
printf("Initializing 640x480 to 320x200 emulation"
" with 32-bit colors...\n");
vesacurmodeinfo=&vesamodeinfo[loop];
VesaScreenRefresh=VesaLowres_640x480x32;
VesaSetMode(loop);
vesamode=VESAMODE_640x480x32;
return vesamode;
}
for(loop=0; loop<vesamodes; loop++)
if(vesamodeinfo[loop].linearmodeavailable &&
vesamodeinfo[loop].horizontalresolution==320 &&
vesamodeinfo[loop].verticalresolution==200 &&
vesamodeinfo[loop].bitsperpixel==24) {
printf("Initializing mode 320x200 24-bit colors...\n");
vesacurmodeinfo=&vesamodeinfo[loop];
VesaScreenRefresh=VesaLowres_320x200x24;
VesaSetMode(loop);
vesamode=VESAMODE_320x200x24;
return vesamode;
}
printf("Initializing mode 320x200 with greyscale palette...\n");
VesaScreenRefresh=VesaLowres_320x200x8;
textmode(0x13);
VesaGreyscalePalette();
vesamode=VESAMODE_320x200x8;
return vesamode;
}
Ja jokainen voi arvata kuinka monelta harmaalta hiukselta systeemi on
minut säästänyt, kun toinen demokooderimme omistaa näytönohjaimen,
jossa on vain 24-bittisiä tiloja ja minulla on näyttis, joka taasen
tukee vain 32-bittisiä.
Vihjeenä nopeaan greyscale-flippiin on, että kun kerran komponentteja
on kolme ja jakaminen muilla kuin kahden potensseilla on tuhottoman
hidasta, käytä paletista vain ensimmäiset 192 väriä musta-valkoinen
liukuun (asteen muutos kolmen värin jälkeen), ja jaa komponenttien
summa kolmen sijasta neljällä. Voi ehkä olla hyödyllistä vääntää
nousukäyrää hieman siten, että vaaleampiin sävyihin päästään
nopeammin.
Sen lisäksi että moottori tukee useita eri näyttötiloja saman puskurin
esittämiseen, olisi virheensietokyvyn olla niin hyvä kuin se voi
olla. Esimerkkiohjelman sietokyky on jo aika korkea, mutta parempikin
se vielä voisi olla. Ongelma on myös se milloin ei enää voida
jatkaa. Jokin pelin alkulogon näyttäminen korkeammassa resoluutiossa
ei vielä exit:tiä vaadi, mutta jos koko peli vaatii paljon värejä ja
tarkkuutta, ei pelkkä virheilmoitus riitä.
VESA-enginen seuraksi voi olla hyvä idea kerätä myös mittava joukko
sekalaisia apufunktioita, kuten spritejen piirrot, motion blur ja
muuta sellaista pientä kivaa. Ehkä jopa kuvatiedostojen saumaton
integrointi voisi olla hyödyllistä, sillä sekalaisten PCX-laturien ja
piirtorutiinien kaapiminen kovalevyn uumenista alkaa viimeistään
silloin olla tuskastuttavaa, kun käytät 32-bittisiä tiloja joskus,
toisinaan 16-bittisiä ja välillä vain korkean resoluution 256-värisiä
tiloja. Hyvä grafiikkamoottori säästää vaivalta ja sinne muutokset on
helppo tehdä keskitetysti, jonkin nerokkaan optimoinnin lisääminen on
helpompi tehdä yhteen kirjastoon kuin jokaiseen optimoitua funktiota
käyttävään ohjelmaan.
En minä taida enempää porista, lähettäkääpäs ihmiset kommentteja tästä
SVGA-osiosta, sillä jälleen kirjoittelen tätä side silmillä, en minä
tiedä ymmärrättekö tästä mitään, minähän vain teen tätä. ;-D Nyt
taidankin siirtyä aloittelemaan "Asioiden taustaa"-osiota. Lykkyä tykö
grafiikkasysteemien tekoon.
8.9 Polygoneista ja niiden fillauksesta
---------------------------------------
8.8 Lisää kivaa - zoomaus
-------------------------
8.7 Prekalkattuja pintoja - ja tunneli
--------------------------------------
8.6 Plasma tekee comebackin - wobblerit
---------------------------------------
8.5 Musiikkijärjestelmistä
--------------------------
8.4 Vektorit pelimaailmassa
---------------------------
8.3 Motion blur - sumeeta menoa
-------------------------------
8.2 Läpinäkyvyys ja sen vaihtoehto - shadebobit
-----------------------------------------------
8.1 Datatiedostot - miten?
--------------------------
No niin, on aika siis viimein aloittaa se mitä pitkän aikaa jo olen
suunnitellut. Eli lukijoiden pakottaminen koodaamaan rutiininsa
varmasti itse. :) Tästä lähtien ei koodia heru yhtä tai kahta riviä
enempää, joudutte tekemään tuttavuutta libc:n dokumentaation kanssa
enemmänkin (komento 'info libc').
Eli homman nimi on datatiedostot. Ja helppoahan tämä. Idea on luoda
oma pieni "levyjärjestelmä" ja laittaa tiedostot sen
alaisuuteen. Käytännössä FAT:in tyyppistä varausyksikkö-systeemiä ei
kannattane luoda, ellei aio myös kirjoittaa paljon tiedostoja ja
lisätä datatiedostossa oleviin tiedostoihin uutta tavaraa. Yleensä
riittää pelkkä read-only järjestelmä, johon vain pakataan senhetkiset
erilliset tiedostot haluttaessa.
Kaikkein yksinkertaisin on tehdä headeri, joka sisältää tiedoston
nimen, sen koon ja sijainnin datatiedostossa. Datatiedoston rakenne on
vain sellainen, että ensin tulee tiedostojen määrä datatiedostossa, ja
sitä seuraa määrän verran mainitun kaltaisia headereita, jotka luetaan
muistiin. Sitten vain kun ohjelma haluaa lukea jonkinnimisen tiedoston
datatiedostosta, etsitään tiedostonimeä vastaava headeri ja mennään
fseekillä headerin kertomaan sijaintiin ja luetaan tiedot. Luku voi
olla puhdas "kaikki-tai-ei-mitään", eli että tiedosto luettaisiin vain
kokonaisuudessaan puskuriin joka palautetaan (tyyliin
bufferi=GetFile("dummy.fil")).
Toinen mahdollisuus on luoda normaaleja f*-funktioita vastaavat
funktiot datatiedostossa seikkailemiseen (minulla esim. on jfopen,
jfclose, jfread, jfgetc ja jfseek). Jos aiotaan tukea usean tiedoston
yhtäaikaista aukipitoa ja liikkumista yhden tiedoston sisällä
muihinkin suuntaan kuin eteenpäin täytyy myös toteuttaa
tiedostokahva-järjestelmä. Tämäkin on helppoa, kahvahan on käytännössä
vain rakenne, joka kertoo missä kohdassa tiedostoa ollaan ja kuinka
pitkä tiedosto on jne. Yksinkertaisimmillaan systeemissäsi kahva
sisältää tiedostoheaderin numeron, josta voidaan noutaa pituus ja
nimi, sekä senhetkisen sijainnin. Lukufunktiot ottavat sitten huomioon
sen mistä kohdasta nyt pitäisi lukea.
Jos koodaat kahvat siten, että käytät fseekiä joka lukukerralla
siirtyäksesi oikeaan paikkaan ja päivität sitten kahvan sijaintia
luetun palan koon verran eteenpäin, käy tavupohjaisille lukijoille
kalpaten. Normaalisti fgetc nimittäin lukee hieman eteenpäin ja
seuraavalla kutsukerralla luettava tavu on jo puskurissa tallessa ja
se tulee nopeasti muistista. Vaan fseekin jälkeen puskuri tyhjätään ja
päädyt todella lukemaan tiedostoa tavu kerrallaan, etkä esim. 256
tavua kerrallaan, kuten asian laita normaalisti puskuroituna
olisi. Vaikutus näkyy esim. demossamme Bleedissä - 20 kertaa normaalia
hitaampi PCX-laturi.
Nopeuden vuoksi kannattaa joko toteuttaa itse puskurointi, eli kahvaan
myös pieni, esim. 20-tavuinen puskuri, joka kertoo seuraavien tavujen
sisällön. Näin todellinen levyhaku tapahtuu 20 kertaa harvemmin. Tai
ainakin tarkistaa ettei fseekata ellei oikean datatiedostoon
osoittavan kahvan sijainti ole muuttunut.
Hyvä idea edellisten estämiseksi voi myös olla yksinkertainen
järjestelmä, jossa fopen korvataan funktiolla, joka avaa uuden
FILE-kahvan datatiedostoon ja siirtää sen osoittamaan tiedoston
aloituspaikkaan datatiedostossa. Nyt lukufunktioihin tarvitaan vain
tarkistus, ettei lueta tiedoston pituutta pidemmälle eteenpäin
(LueKirjain vain lisäisi sijaintilaskuria yhdellä, palauttaisi EOF:in
jos oltaisiin tiedoston lopussa ja muussa tapauksessa palauttaisi
suoraan fgetc(handle):n). Fseek-funktiokin vain muuttaisi sijainnin
tiedostossa sijainniksi datatiedostossa (lisäisi alkuoffsetin jne.).
EXE-tiedostoon tallettaminenkin on helppoa. Tunget vain datan EXE:n
loppuun, sitten headerit ja lopuksi tiedostojen määrän. Eli käännät
normaalin datatiedoston toisinpäin ja lätkäiset EXE:n perään. Lukiessa
sitten käytät aluksi fseek(handle, -4, SEEK_END), jolloin pääset
käsiksi neljään viimeiseen tavuun jotka sisältävät headereiden määrän,
sitten seekkaat taas taaksepäin päästäksesi headerien alkuun ja luet
itsesi loppuun (tai siis, neljä tavua vaille, headerit loppuvat ja
viimeisenä tulisi vielä niiden määrä).
Tiedostojen lisääminen fiksusti datatiedostoon on itseasiassa astetta
vaikeampi homma kuin lukeminen (jos siis teet sen helpolla tavalla, et
käytä puskurointeja tai monimutkaista logiikkaa). Järkevää voi olla
tehdä ohjelma, jolle annetaan esim. tiedoston nimi jossa on lista
mukaan otettavista tiedostoista. Sen jälkeen ohjelma lukee jokaisen
tiedoston nimen headeriin ja pituuden, sekä laskee offsetit.
Ensimmäisen tiedoston sijainti datatiedostossa on tiedostojen määrän
kertovan muuttujan ja headerien jälkeen, eli laskennallisesti:
sizeof(headerien_määrä)+sizeof(header)*headerien_määrä
Seuraava sijaitsee sitten edellisen koon verran edellisen jälkeen,
kolmas on toisen koon verran toisen jälkeen jne. Kun headeriin on näin
laskettu koon lisäksi sijainti, niin voidaan kirjoittaa tiedostoon
headereiden määrä, sitten headerit ja lopuksi lisätään tiedostot
samassa järjestyksessä kuin niiden headerit tiedoston
jatkoksi. EXE:een lisätessä tapa tietenkin olisi käänteinen, ensin
tiedostot loppuun, sitten headerit ja lopuksi tiedostojen määrä. Tai
toinen mahdollisuus EXE-pohjaisessa olisi vain lisätä datatiedosto
EXE:n loppuun ja viimeiseksi vielä offsetti siihen kohtaan missä EXE
aiemmin loppui ja josta data nyt alkaa headerin määrineen ja
headereineen.
No sepä oli siinä, ei tässä oikeastaan mitään vaikeaa pitäisi olla,
kun vain ei anna tämän luvun sekoittaa päätä.
8.9 Polygoneista ja niiden fillauksesta
---------------------------------------
8.8 Lisää kivaa - zoomaus
-------------------------
8.7 Prekalkattuja pintoja - ja tunneli
--------------------------------------
8.6 Plasma tekee comebackin - wobblerit
---------------------------------------
8.5 Musiikkijärjestelmistä
--------------------------
8.4 Vektorit pelimaailmassa
---------------------------
8.3 Motion blur - sumeeta menoa
-------------------------------
8.2 Läpinäkyvyys ja sen vaihtoehto - shadebobit
-----------------------------------------------
Läpinäkyvyys on hieno efekti. Puoliksi läpi näkyvää esinettä
piirrettäessä piirretään puoliksi esine ja jätetään puolet taustasta
jäljelle. Todellinen läpinäkyvyys muodostetaan siten, että
valon läpäisemättömyys ilmoitetaan arvolla välillä 0..1 (jossa 0 on
näkymätön ja 1 täysin näkyvä), ja "pikselin" väri lasketaan kaavasta:
transparency * object_pixel + ( 1 - transparency ) * background_pixel
Valitettavasti täytyy todeta, että SVGA-tiloissa, joissa tätä
käytetään, täytyy jokainen värikomponentti ensinnäkin laskea erikseen
ja toisekseen että kertolasku ei ole kovin nopeaa hommaa. Vaikka
kuinka optimoit, jää käteen joka tapauksessa yksi kertolasku, yksi
addi ja shiftaus oikeaan (ja tämä vain jos trans*obj voidaan laskea
etukäteen, eli objekti ja läpinäkyvyys ei muutu).
Tosin jos muistia kulutetaan kaikille käytetyille läpinäkyvyyksille ja
prekalkataan taustatkin päästään yhdellä yhdeenlaskulla, joka on
nopeampaa kuin alhaalla esitettävä ratkaisu. Tämäkin hidastuu siinä
tapauksessa että objektin läpinäkyvyys voi vaihdella eri kohdissa,
jolloin joudutaan vaihtamaan lähdepikselin puskuria koko ajan. Ja
tietenkään päällekkäiset läpinäkyvät esineet eivät onnistu ellet laske
joka objektia kaikkiin mahdollisiin läpinäkyvyyksiin. Joka taas on
hidasta.
Mutta kuten nimeltä mainitsemattomassa pelissä Stan sanoo "But wait,
there is more!". Ja sen 'enemmän' nimi on shadebobit. Nämä 90-luvun
alkupuolen demoista vakioefektinä löytyvät vekkulit ovat varsin halpa
tapa toteuttaa maittavia läpinäkyvyysefektejä minimimäärällä
kertolaskuja, eli ilman ainoatakaan mullia. Ja mitenkä tämä onnistuu?
Shadebob eroaa normaalista spritestä vain siten, että sitä ei piirretä
päälle, vaan se lisätään. Jos sinulla on puhtaan sininen tausta ja
lennätät päällä punaista shadebobbia, niin bobin kohdalle syntyy
violettia, sillä (255,0,0) + (0,0,255) = (255,0,255)
(rgb-triplettejä). Esim. flaret ja muut toteutetaan usein
shadebobeilla.
Yksi ongelma kyllä on tässäkin tekniikassa. Ylivuodot. Joku teistä
varmaan on nähnyt efektin joka näyttää siltä että ruutu
"palaa". Käytännössä tämä on shadebobbi, joka on liian kauan samassa
paikassa, väriarvot kohoavat viimein 255:een ja kun ne menevät yli ne
pyörähtävät ympäri, ollaan takaisin nollassa ja jälki on
karua. Valkoisen kirkkaan täplän keskellä epämääräinen musta roso ei
oikein ole hienoa. Nopean shadebob-piirturin teko C:llä on jo taidetta
ja assylläkin hommaa on ihan liikaa. Ehtolause on periaatteessa
tällainen:
jos shadebob + tausta > yläraja
tausta = yläraja
muuten
tausta += shadebob
Assyllä voidaan käyttää hyväksi carry-flagia, joka menee päälle luvun
pyörähtäessä ympäri. Jnc hyppää jos carryä ei ole asetettu, jc taas
jos on. Myös adc ja sbc voivat olla mukavia, ne kun lisäävät
lisättävään/vähennettävään carry-flagin. Esimerkkinä varsin nopeasta
shadebob-rutiinista:
add al, bl
jnc .eiyli
mov al, 255
.eiyli:
Käytännössä tuo vie yhden kellon ja niissä tapauksissa kuin palamista
syntyy siltäkin vältytään toisen kellon menetyksellä. Ongelmallista
tosin on, että truecolor-tiloissa (24 ja 32) täytyy jokainen
komponentti lisätä erikseen, toisin kuin sopivaa palettia käyttävässä
256-värisessä tilassa. Ja 15- ja 16-bittisissä tiloissa homma alkaa jo
muistuttamaan masokismiä.
Joka tapauksessa läpinäkyvyyden kaksi vaihtoehtoista menetelmää ovat
molemmat varsin hyödyllisiä oikein käytettynä. Shadebob-systeemissä on
vain se vika, että puoliksi läpinäkyvää valkoista ei ole nähtykään, ja
todellisuudessa vain punaista lävitseen päästävä lasi sinisen taustan
päällä on mustaa. Siksi shadebobit sopivat parhaiten valoefektien
tekoon. Tosin läpinäkyvyyskin onnistuu tekemällä alpha-kanava (joka on
kuten bittikartan maski, mutta kertookin kunkin pikselin
läpinäkyvyyden). Piirrossa sitten taustasta vähennetään alpha-kanava
(valkoisella tausta on aina musta) ja bittikartasta 255-alpha
(mustalla ei muutu, valkoisella muuttuu "läpinäkyväksi", eli nollaksi)
ja sitten vasta lisätään bittikarttaan. Ja tarkistuksia ei enää
tarvita kun alpha-laskut ovat varmistaneet, että taustan ja
bittikartan summa ei voi olla yli 255. Tässäkin bittikartan voi
etukäteen laskea oikealle läpinäkyvyydelle alpha-kanavan suhteen, jos
se ei muutu.
Jännittäviä hetkiä tämänkin parissa, shadebobbeja ja läpinäkyvyyttä
käyttämällä voi kehittää vaikka millaisia viritelmiä, jos ette usko
niin katsokaa vaikka Orangen Mr. Black, arvatkaa kahdesti onko pyörivä
lonkero-mömmö muuta kuin taustakuva joka lonkeroiden kohdalta näkyy
läpi, tummentuen lonkeron reunoja kohti. Shadebobbeja tai
läpinäkyvyyttä, luultavasti ensimmäisiä. Oikaiskaa jos olet väärässä.
8.9 Polygoneista ja niiden fillauksesta
---------------------------------------
8.8 Lisää kivaa - zoomaus
-------------------------
8.7 Prekalkattuja pintoja - ja tunneli
--------------------------------------
8.6 Plasma tekee comebackin - wobblerit
---------------------------------------
8.5 Musiikkijärjestelmistä
--------------------------
8.4 Vektorit pelimaailmassa
---------------------------
8.3 Motion blur - sumeeta menoa
-------------------------------
Motion blur ei ehkä peleissä kovin hyödylliseksi muodostu, mutta
demoissa se on varsin suosittu, ja kyllä sitä voi vaikka
autopelissäkin käyttää. Joka tapauksessa idea tulee nyt, joten turha
pyristellä vastaan. Tämä ei satu kuin hetken.
Motion blur eli liikesumennus kuten joku sen voisi suomentaa
tarkoittaa sitä, että liikkuvista tavaroista jää jäljet ruutuun ja ne
häviävät vasta pikkuhiljaa. Efekti on tuttu Dubiuksen ja muiden
demojen lisäksi vaikkapa Jyrkistä, joissa ainakin aikoinaan valoista
jäi hirmuiset raidat ruutuun.
Tekniikkakin on helppo, käytännössä motion blur on melkein sama kuin
läpinäkyvyys, sillä toteutus sattuu olemaan sellainen, että tietty osa
uudesta ruudusta muodostuu juuri piirretystä informaatiosta ja tietty
osa edellisestä ruudusta (jossa taas oli jonkin verran sitä edellistä
jne. Näin syntyy pikkuhiljaa häipyvä efekti). Siinä se. Sekoitat
vanhaa ja uutta halutussa suhteessa ja olet valmis. Shadebobit eivät
tähän käy kauhean hyvin (taino, jos vähennät edellisestä aina
esim. 100 ja piirrät uuden framen skaalalla 0-155 ja lisäät ne, niin
mikäs siinä, itseasiassa voisi tämäkin toimia).
Yleisemmin käytetään kuitenkin aitoa läpinäkyvyyttä ja yksinkertaisia
perusjakoja, jotka menevät ilman kertolaskuja. Käytännössä tämä on 1:1
ja pienellä kikkailulla vaikka 1:3 ja 1:7 suhteet onnistuvat varsin
helposti. Lupasin olla antamatta sorsaa, joten vihjeenä, että
truecolor-tiloissa shiftaamalla ja poistamalla sopivalla and-maskilla
toisten komponenttien alueelle mahdollisesti eksyneet bitit saa
1:1-sekoituksen muutamassa kellossa. 1:3 onnistuu mukavasti
shiftaamalla neljällä molemmat ja kertomalla toisen lea-käskyä
käyttäen kolmella on homma vauhdikasta.
Jos käytät yhä jotain ankeaa kvantisoitua tilaa, tai jotain
muuta palettitilaa, niin voit olla onnellinen, voit laskea helposti
etukäteen 256x256-kokoisen taulukon, jonka jokainen alkio kertoo
pysty- ja vaakarivin mukaisten värien optimaalisen sekoituksen. Ja kun
taulukko prekalkataan voit valita sekoitussuhteen ihan
vapaasti. Rutiinikin on yksinkertainen, jokaista taulukon alkiota
kohden otat paletista pysty- ja vaakarivin mukaiset värit, sekoitat
halutussa suhteessa aidon läpinäkyvyyden mukaisesti ja etsit
tulokselle kvantisointikuutiosta (tai käyttäen lähimmän sopivan värin
etsintää, selostettu vektoreiden ohessa seuraavassa luvussa) lähimmän
sopivan värin.
Tämä oikeastaan tässä olikin. En tiedä kumpi on nopeampaa, blurraus
flipin sisällä (käytetään näyttöpuskuria toisena), vaiko nopeamman
keskusmuistin käyttö ja sitten vasta flippi ruudulle. Kokemuksia
otetaan vastaan.
8.9 Polygoneista ja niiden fillauksesta
---------------------------------------
8.8 Lisää kivaa - zoomaus
-------------------------
8.7 Prekalkattuja pintoja - ja tunneli
--------------------------------------
8.6 Plasma tekee comebackin - wobblerit
---------------------------------------
8.5 Musiikkijärjestelmistä
--------------------------
8.4 Vektorit pelimaailmassa
---------------------------
Vektorit ovat sen verran vekkuleja juttuja, että käsittelen niitä
lyhyesti ja vähemmän teoreettisesti tässä. Kärsivälliset odottavat
lukion kursseja, ja kärsimättömät mutta tiedonhaluiset kaivavat
syvemmän teorian vaikka lukion matikankirjasta tai 3Dicasta, sieltä
löytyy pistetulo, ristitulo ja muutkin tärkeät tiedot. Me keskitymme
vain peruskäsitteeseen ja vektorien muodostamiseen, skaalaamiseen ja
yhteenlaskuun.
Olet varmaan jo käyttänyt vektoreita pariin otteeseen. Vektorit ovat
vain tapa ajatella muita kuin skalaarisia suureita
(suuruudellisia). Hyvä esimerkki on jonkin esineen sijainti
ruudulla. Aiemmin olet ajatellut että sijainti koostuu x- ja
y-koordinaateista. Mutta voit ajatella sijaintia myös vektorina, jonka
x-komponentti on x-koordinaatti ja y-komponentti ja y-koordinaatti.
Vektori on nuoli. Ja kuten nuoli, vektori voi osoittaa mihin suuntaan
tahansa ja olla minä pituinen tahansa. 2d-peleissä käytät varmaan
yleensä 2d-vektoreita, sillä paperillekaan ei voi piirtää nuolia kuin
tasossa. 3-ulotteisessa avaruudessa taas nuoli voi osoittaa pysty- ja
vaakasuunnan lisäksi myös sisään ja ulos näytöstä ja kaikkialle näiden
välillä. Vektorit ovat itseasiassa vain niputettuja koordinaatteja ja
yleensä riittää että keskitytään origokeskeisiin vektoreihin, jotka
alkavat koordinaatiston keskipisteestä. Ajattele ruutupaperia, jossa
on koordinaatisto. Origokeskeinen vektori on nuoli, joka lähtee
keskipisteestä ja jonka kärki on missä tahansa koordinaatistossa. Sama
millaisen vektorin piirrät koordinaatistoosi on helppoa huomata että
origokeskeinen vektori voidaan aina ilmoittaa kahdella luvulla, x- ja
y-koordinaatilla mihin nuolen kärki sitten osuukin. Myös
tietokonemaailmassa vektori ilmoitetaan kahdella luvulla, ihan kuten
ennen teit x- ja y-koordinaattiesi kanssa.
Määrittely onnistuu lukutaulukkona, tai rakenteena. Suosittelen
taulukkoa, eroa structiin ei kuitenkaan nopeudessa ole:
float v[3]; /* 3-ulotteinen vektori, v[0] on x-komponentti, v[1] on y
ja z tietenkin v[2] */
Vektoreita voi muodostaa kaikkien pisteiden välille, muodostat vain
molemmista pisteistä vektorin (käytännössä ajattelet koordinaatteja
vektorin komponentteina) ja vähennät ne toisistaan ja tuloksesta
tulee vektori, joka on yhtä pitkä kuin mitä pisteiden välillä lyhin
matka (tätä voi käyttää esimerkiksi kvantisoinnissa, kahden värin
etäisyys voidaan selvittää muodostamalla niiden välille vektori ja
laskemalla sen pituus). Vektorien yhteen- ja vähennyslasku on helppoa,
jokainen komponentti vain käsitellään erikseen. Eli jos vektorin a
(päällä pitäisi olla viiva, mutta...) x- ja y-komponentit ovat 5 ja 2
ja vektorin b vastaavasti 3 ja 4, ovat niiden summavektorin
komponentit vastaavasti 5+3=8 ja 2+4=6. Yleensä vektori ilmoitetaan
kuten koordinaatti, eli a + b = (8,6) ja a - b = (2,-2). Vektorin
pituus lasketaan kaavasta sqrt(x*x + y*y). Vektorin pituutta voidaan
skaalata jollain luvulla kertomalla jokainen komponentti erikseen
luvulla. Tällöin pituus kasvaa <luku>-kertaiseksi, mutta suunta pysyy
ennallaan.
Kolmiulotteisessa erona on vain se, että mukaan tulee
z-komponentti. Pituuden kaavaan lisätään + z*z ja laskuissa myös tämä
komponentti pitää muistaa käsitellä. Helppoa kun sen osaa.
No nyt tiedät mikä on vektori. Vaan mitä sillä tekee? Paras vastaus
on, että ihan mitä tahansa. Vaikka autopelin. Tai Quaken. Vektori on
vain kätevä tapa niputtaa n-ulotteisen avaruuden koordinaatit yhteen
pakettiin.
Joku kysyi minulta vähän aikaa sitten miten autopelissä tai
luolalentelyssä tehdään liikkuminen. Olisi ollut todella helppoa
selittää asia jos olisin ollut varma että kysyjä ymmärtää mikä on
vektori, mutta kun jouduin selittämään asian x- ja y-koordinaateilla,
hommassa oli paljon enemmän tekemistä. Selitänpä nyt miten
luolalentely voitaisiin toteuttaa vektoreilla:
Aluksen sijainti on normaali 2-ulotteinen vektori. Lisäksi aluksella
on nopeusvektori ja kiihtyvyysvektori. Joka framella sijaintivektoriin
lisätään nopeusvektori. Nopeusvektorin suunta vastaa aluksen
kulkusuuntaa (eli ihan kuten x-nopeus ja y-nopeus, vain niputettuna)
ja pituus nopeutta. Voit hahmottaa liikettä piirtämällä esimerkiksi
sijaintivektorin ja sen kärjestä lähtien nopeusvektorin. Joka
kierroksella nopeusvektori lisätään sijaintiin, ja tulos on juuri se
mitä saat kun piirrät nopeusvektorin suuntaisen ja pituisen jatkeen
sijaintivektorille. Kokeile vaikka alkusijaintia (5,7) ja nopeutta
(2,-1) ja lisää edelliseen jälkimmäinen ja piirrä tämä uusi vektori
(9,6). Huomaat että uuden ja vanhan sijaintivektorin väli on juuri
nopeusvektorin suuntainen ja pituinen.
Lisäksi meillä on vielä kiihtyvyysvektori, joka ilmoittaa mihin
suuntaan alus on kiihtymässä. Tämä vektori kertoo mihin moottori
milläkin hetkellä alusta työntää (suunta) ja kuinka nopeasti
(pituus). Rakettimoottoreilla kiihtyvyys on aina vakio, joten
kiihtyvyyden määrittäminen onnistuu luomalla sinillä ja kosinilla
yksikkövektori (nimitys jota käytetään vektoreista joiden pituus on 1,
joka pätee kaikkiin vektoreihin joiden x-komponentti on kosini ja
y-komponentti sini, se on näiden trigonometristen funktioiden
perusluonne) ja skaalaamalla se rakettimoottorin teholla, eli esim:
kiihtyvyys[0] = cos(kulma_rad);
kiihtyvyys[1] = sin(kulma_rad);
kiihtyvyys[0] *= TEHO;
kiihtyvyys[1] *= TEHO;
Ja kiihtyvyys lisätään tietenkin joka vuorolla nopeuteen, eli kun
moottori ponnistelee kulkusuunnan mukaisesti vauhti kiihtyy ja jos
käännät aluksen nokan vastakkaiseen suuntaan ja painat kaasun pohjaan
(onko napeissa muka muita asentoja ?-) vauhti alkaa
hidastumaan. Täydellistä.
Ja koska vektorit ovat suoraan fysiikkaa varten luotuja, on
painovoiman, aseen rekyylin, törmäysten ja muiden lisääminen lasten
leikkiä. Painovoima on vain vakiosuuntainen (alas) kiihtyvyys. Kitka
pinnasta (ei luolalentelyissä, autopeleissä kylläkin) on tietyn
prosenttimäärän (1-kitkakerroin) mukainen vauhdin hidastuminen,
rekyyli ja törmäykset perustuvat siihen, että jos ammut panoksen
tiettyyn suuntaan (vektori), niin aluksen suunnanmuutos on
vastakkainen ja suoraan suhteessa massojen eroon. Esim. jos panos
painaa 1/1000 aluksesta, lisätään nopeuteen ammuksen suuntavektori
käännettynä (miinusmerkki joka komponentin eteen ja nuoli osoittaa
vastakkaiseen suuntaan) ja skaalattuna yhteen tuhannesosaan, eli
kerrottuna 0.001:llä. Jos olet perfektionisti voit pitää lukua
panoksista ja vähentää ne massasta. :) Täydellinen törmäys (molemmat
kappaleet jatkavat samaan suuntaan, esim. luodit) on ihan sama, mutta
käänteisesti, eli ei tarvitse kääntää ammuksen suuntavektoria, vaan
lisätään se vain massojen suhteessa. Alusten ja seinän väliset
törmäykset ovat hankalampia, niissä kun pitäisi molempien kolahtaa eri
suuntiin. Tähän saat vapaasti kehitellä omasi, luolalentelyiden
tekijät ovat tyytyneet yleensä pysäyttämään aluksen seinään osuessa ja
antavat alusten läpäistä toisensa.
Siinäpä kaikki tärkein vektoreista. Jos ymmärrät mitä ne ovat, miten
ne jakautuvat x-, y- ja z-komponentteihin ja tajuat että skaalaamalla
muutetaan niiden pituutta, ja osaat kaiken lisäksi lisätä, vähentää ja
skaalata niitä, olet vahvoilla. Oppitunti on päättynyt.
8.9 Polygoneista ja niiden fillauksesta
---------------------------------------
8.8 Lisää kivaa - zoomaus
-------------------------
8.7 Prekalkattuja pintoja - ja tunneli
--------------------------------------
8.6 Plasma tekee comebackin - wobblerit
---------------------------------------
8.5 Musiikkijärjestelmistä
--------------------------
Ne jotka haukkovat kotikatsomoissaan henkeä jo kuin kalat, saavat
aloittaa hengittämisen jälleen. Tiedossa ei vielä ole äänikorttien
saloja, eikä edes vaivaisia miksauksen perusteita, ne taidan laittaa
vasta 3.0-versioon, jos sellaista edes kannattaa siinä vaiheessa alkaa
tekemään. Nyt kuitenkin esittelen lyhyesti NE soittosysteemit, jotka
tällä hetkellä minun henk. koht. mielipiteeni mukaan ovat hyviä
vaihtoehtoja kun musiikkia pitää alkaa kuulumaan, muusikkojen tai
kohdeyleisön vaatimuksesta.
Alkusanoina totean, että kaikkein paras on tietenkin tehdä oma
systeemi. Mutta se suurin kompastuskivi on siinä, että levityksessä
olevien tasoisten (paraskaan ei soita IT- ja XM-kappaleita kaikkia
oikein) "playereiden" teko vie yhdestä viiteen vuotta. Tervetuloa
todellisuuteen. Vaatimukset nimittäin ovat hurjat, mitään yhtenäistä
standardia kun ei Windows Sound Systemiä lukuunottamatta DOS:in
puolella. Tai no, SB on vahva sana, Pro-mallia tukemalla tuet
luultavasti 99% tavoiteyleisösi äänikorteista.
Mutta edes yhden kortin koodaukseen vaadittava tieto- ja taitomäärä on
sen verran suuri, että jos viikossa saa sellaisen systeemin tehtyä,
että sillä voi soittaa looppaavia ja looppaamattomia sampleja,
katkottomasti, ilman naksahduksia ja vapaasti säädeltävällä vauhdilla
ja voluumilla, niin voi ajatella että kymmenesosa hommasta on jo
tehty. Sen jälkeen hyökätään FMODDOC:in kimppuun ja vietetään seuraava
viikko kyhäten jonkin formaatin moduuliloaderi (jos kyseessä on XM tai
IT suosittelen varaamaan pari viikkoa ja purkin Buranaa). Sen jälkeen
vielä pari kuukautta efektitukea väännellen (XM on dokumentaation
saatavuudessa suorastaan kuninkuusluokkaa, herrat FT2:n tekijät kun
ovat sitä mieltä että heidän trackerinsahan on melkein
itsedokumentoiva - tiedossa siis ainakin S3M- tai MOD-formaatin
dokumenttien luku ja hauskoja hetkiä niin heksaeditorin kuin FT2:nkin
parissa).
No nyt olette varmaan niin kauhuissanne että tämän luvun todellinen
asiasisältö menee kuin kuumille kiville. ;-D No ei, ei se
moduuliplayerin teko niin hirveää hommaa ole, täytyy vain omata
itsepäisyyttä, taito tehdä asiat tarpeeksi hyvin kerrasta (lukemalla
fmoddocin kerran läpi ennen aloitusta voi tähän syntyä kummasti
kiinnostusta) ja paljon paljon kärsivällisyyttä. Optimointitaitokaan
ei olisi pahitteeksi.
Jos kuitenkin lykkäät moduuliplayeriasi hieman kauemmaksi
tulevaisuuteen ja kokeilet ensin jotain muuta kuin kotikutoista
ratkaisua, kannattaa ensimmäiseksi kurkata Housemarquen sivuille
(www.housemarque.com/fi) ja imuroida Midaksen uusin versio. Midas on
moduuliplayereiden ehdoton Rolls Royce, jopa IT-tuki taisi löytyä, ja
XM:tkin soivat vain osaksi pieleen. Funktioita riittää vaikka muille
jakaa (tosin subrow-tarkkuista laskuria moduulin sijainnista ei saa
käyttöönsä :), timerista lähtien vga-tilojen asetukseen ja
näyttösynkronointeihin. DirectX-tuki löytyy niinikään.
Ainoa Midaksen ongelma on se, että sitä ei EHDOTTOMASTI saa käyttää
SW- tai muuhun kaupalliseen levitykseen. Ilmaisohjelmat ovat ok,
kunhan vain muistaa mainita käyttäneensä midasta, mutta jos otat siitä
rahaa, otat Midaksen myös pois ohjelmastasi. Kaupallisia lisenssejä
tosin on mahdollista hankkia, joten postia vain housemarquelle
sähköisessä muodossa. Aiemmin muistaakseni rekisteröintihinta oli
$500, mutta ehkä se on tippunut. :)
Jos SW kiinnostaa, tai haluat vaihtoehtoisen, DJGPP-optimoidun
systeemin peliisi, on Humppa (entinen Hubrmod), eli HUbris Module
Player PAckage tarkistamisen arvoinen, lähetät vaikka pelisi
lähdekoodit Kaikalle ja saat alkaa myymään peliä. :) Kuulemani mukaan
XM-tukikin on jo varsin hyvä ja sormeni syyhyävät päästä kokeilemaan
systeemiä, vaan en ole vielä ehtinyt.
Toinen hieman kalliimpi, mutta pitkät perinteet moduuliplayerien
saralla omaava vaihtoehto on MikMod, josta löytyy Midaksen tapaan tuki
melkein joka laitealustalle, mukaan lukien Linux. Rekisteröintihinta
oli varsin halpa, muistaakseni parikymmentä dollaria, ja sekin vain
siinä tapauksessa että haluat käyttää playeria kaupallisiin
tarkoituksiin, ilmaislevittäjät saavat käyttää softaakin
ilmaiseksi. Ja kuten Humpassa, myös MikModissa lähdekoodi tulee
mukana, joten mahdollisuudet omiin viritelmiin kohoavat huimasti.
Kaikkien kolmen mukana tulee varsin laadukas dokumentaatio, tai jos ei
sellaista löydy, niin esimerkkikoodia löytyy jokaisesta. Itse olen
kokeillut neljää tai viittä eri playeria ja jok'ikisen toimimaan
saanti ei ole vaatinut muuta kuin sopivan esimerkin räätälöimistä
omaan käyttöön sopivaksi. Uskaltakaa hyvät ihmiset kokeilla niitä, ja
jos ette kerta kaikkiaan ymmärrä niitä englanninkielisiä kommentteja
niin sanakirja tai taitava kaveri varmaan auttaa mielellään. :)
Tässä kaikki tältä erää, minulle saa postittaa ilmoituksia jos jokin
ehdottoman mahtava DJGPP-playeri puuttui (ei, se J. Hunterin DJGPP:lle
tehty SB Library tai mikä olikaan ei käy - edes modit eivät soi siinä
oikein).
8.9 Polygoneista ja niiden fillauksesta
---------------------------------------
8.8 Lisää kivaa - zoomaus
-------------------------
8.7 Prekalkattuja pintoja - ja tunneli
--------------------------------------
8.6 Plasma tekee comebackin - wobblerit
---------------------------------------
Yksi varsin hulvaton demoefekti ja peleissäkin kenties
hyödynnettävissä oleva vekkuli on nimeltään wobbler ja selostan
toiminnan lyhyesti tässä. Sen sijaan että ottaisin x:n mukaan parista
aallosta värin ja y:n mukaan parista aallosta, lisäätkin x-arvon
mukaisesti siniaallolla y-koordinaattia ja toisinpäin. Tuloksena
syntyy ihanasti vellova efekti, jota voi käyttää miten mieli sitten
tekeekään. Hyvää idea on käyttää 256x256-kokoista tekstuuria, jolloin
y- ja x-arvot on helppo saada menemään ympäri (y&255 ja assyllä
suoraan tavurekistereillä) ja oikeanlaisilla kartoilla ei reinoja
huomaa. (mikä olisi hyvä termi englanninkielessä käytetylle
tilingille? tiiliytyminen? tileytyminen?)
Voit myös kokeilla y:n lisäämistä y:n mukaan jolloin syntyy
venytystä. En myöskään tiedä millainen on tulos, jos et lisää näitä x-
ja y-arvoihin, vaan laitat ne sellaisenaan (eli ei x + ..., vaan vain
...).
8.9 Polygoneista ja niiden fillauksesta
---------------------------------------
8.8 Lisää kivaa - zoomaus
-------------------------
8.7 Prekalkattuja pintoja - ja tunneli
--------------------------------------
Jälleen uutta pikkukivaa efektien saralla. Tunneli. Idea on sellainen,
että sinulla on kaksi puskuria, samaa kokoa kuin ruutukin, sekä
tekstuuri (esim 256x256). Puskurista 1 otetaan samasta kohdasta kuin
ruudulle laitettava pikselikin ensin y-arvo, ja sitten puskurista 2
x-arvo. Tunnelin tapauksessa puskuriin 1 piirretään kuvio, joka on
säteittäisiä viivoja keskipisteestä ja kuvio lasketaan siten, että
mennään joka pikseli läpi, muodostetaan vektori pikselin ja
keskipisteen välille (x-komponentti on x-160 ja y-komponentti y-100)
ja selvitetään välillä oleva kulma. Tämä hoituu joko tangentilla,
jolloin homma on nopeampaa, tai jos et sitä osaa, teet sen siten, että
piirrät keskipisteestä tarpeeksi tiheään ympyröitä tähän tyyliin:
for(radius = 0; radius < 140; radius ++) {
for(angle = 0; angle < 2048; angle++) {
x = (int)(cos(3.1415*(float)angle/180.0)*radius);
y = (int)(sin(3.1415*(float)angle/180.0)*radius);
buffer1[y*320+x] = angle/8; // 0..255
}
}
Ja toinen taas on sarja ympyröitä keskipisteestä, värin ollessa
etäisyys keskipisteestä. Tämä on helppo ratkaista neliöjuurella. Eli:
for(y=0; y<200; y++) {
for(x=0; x<320; x++) {
tx = x-160;
ty = y-100;
buffer2[y*320+x] = (int)sqrt(tx*tx+ty*ty);
}
}
Sitten vain piirretään tunneli:
for(loop=0; loop<64000; loop++) {
screen[loop] = texture[ buffer2[loop] * 256 + buffer1[loop] ];
}
Ainiin, perspektiivinkin voi lisätä vaihtamalla y-arvon (buffer2)
laskuun sqrt:n tilalle 256/sqrt:n. Tällöin pitää tosin varmistaa ettei
sqrt ole alle 1, sillä muuten käy todella huonosti. Ja jos etäisyys ei
tuollaisena tyydytä sen voi kertoa halutun suuruisella luvulla. Niin
ja tunnelin saa liikkeelle kun lisää piirron aikana x- ja y-arvoihin
jotain. Tässä pitää kuitenkin huolehtia ettei kumpikaan mene yli 255:n
(eli käytännössä ((buffer2[loop]+yoff) & 255) + ...).
Muitakin kuvioita joissa tekstuuri liikkuu "pintaa" pitkin, voi
helposti luoda. Wormhole on yksi tällainen, eikä edes hirvittävän
vaikea, mietippäs vain. Ja Trauman Mindtrapissa luultavasti käytettiin
samaa tekniikkaa siinä pyörivän pallon kohdassa jossa ympärillä
pyöritään toiseen suuntaan.
8.9 Polygoneista ja niiden fillauksesta
---------------------------------------
8.8 Lisää kivaa - zoomaus
-------------------------
Jälleen sarjassa "helppo nakki kun käytät aivoja"-efektejä. Tällä
kertaa vuorossa vanha tuttumme reaaliaikainen suurennos. Ja mikäs sen
helpompaa.
Normaalissa bittikartan piirrossahan korotat ruudun offsettia yhdellä
ja bittikartan offsettia yhdellä. Entäs jos korottaisit bittikartan
offsettia kahdella? Bittikartta "loppuisi" puolet nopeammin, ja
tuloksena piirtäisit sen puoleen aiemmasta tilasta, sekä x- että
y-suunnassa. Pienensit juuri kuvaasi kahdella. Onnea. No entä
suurennos sitten? Helppoa, korotat ruudun offsettia kahdella? Totta,
mutta tuloksena syntyy hieman reikiä (aika kiva räjähdysefekti silti),
joten ehkä käytämme jotain muuta. No varmaan kaikki arvasivatkin jo -
korotetaan bittikartan offsettia puolella ja päästään samaan
tulokseen.
Vastaavalla tavalla pystyt suurentamaan ja pienentämään bittikarttoja
kaikilla kahden potensseilla. Mutta pystyt kyllä parempaankin ja
tiedät sen aivan varmasti. Nappaa käyttöön fixed-point luvut tai
vaikka floatit, niin yhtäkkiä voitkin tehdä sama minkä kokoisia
zoomauksia, portaattoman näköisiä vieläpä! (taino, siinä syntyy
sellaistä ärsyttävää pyöristysvirhekuviota, kokeile vaikka suurentaa
kuvaa pikkuhiljaa pienentämällä askelta mahdollisimman vähän framejen
välissä). Helppoa kun sen osaa.
Ainoa häiritsevä piirre tulee olemaan se, että karttasi lentelevät
ruudun ylitse tai loppuvat kesken. Niinpä piirrossa täytyy normaalin
for(... ; bitmap_x < x_size; bitmap_x++) -systeemin sijaan tarkistaa
pyörityksen aikana sekä ruudun että bittikartan x- ja
y-koordinaatit. Tai sitten klippaat ennen looppia, eli jos kartta
menee ruudun ylitse siirrät piirtoa alkamaan hieman myöhemmin kuin
ensimmäisen pikselin kohdalta. Jälleen tässä tulee tehdä sen verran
tarkka järjestelmä, ettei reunoilta jää satunnaisesti 2-5 kappaletta
pikseleitä tyhjäksi.
8.9 Polygoneista ja niiden fillauksesta
---------------------------------------
Minua on pitkän aikaa ruinattu tekemään 3D:stä juttua ja aina olen
käännyttänyt kysyjät 3Dican puoleen. Tai ainakin polygonijutuista. No
niin, samoin käy tällä kertaa. :) Tai melkein, tässä luvusta niin
lyhyesti ja ytimekkäästi polygonien fillaus ilman klippejä ja muita
kuin vain mahdollista. Ennen kuin taaperrat luvun läpi, hakkaa päähäsi
tieto miten piirretään muitakin kuin suoria viivoja. Eli lue se
interpolointi-juttu lävitse.
Polygonien täyttäminen on varsin helppoa. Ensin käymme lävitse
kolmioiden täyttämisen. Joka on itseasiassa _niin_ helppoa, että
keksin lineaarisen interpoloinnin ja kolmion täyttämisen idean ihan
itse, ilman kenenkään apua. Hienoa, lohduttiko?-) No joka tapauksessa,
asiaan, eli tarkemmin sanoen flat-polyfilleriin.
Tasaisella värillä täytetyn kolmion piirto on varsin helppoa. Kun
piirtelet vaikka 27 kappaletta kolmioita paperille, niin huomaat että
jos vedät kolmion pystytasossa katsottuna keskimmäisen pisteen kautta
kulkevalla vaakaviivalla halki, saat kaksi pienempää kolmiota (tai
erikoistapauksissa vain yhden, jos sinulla on jo tasapohjainen
kolmio), joista toisessa on tasainen pohja ja toisessa tasainen
"katto". Jos vielä ajattelet syntyneiden kolmioiden kylkiä viivoina ja
kuvittelet piirtäväsi ne ylhäältä alaspäin interpoloiden x:ää, huomaat
että olisi varsin helppoa aloittaa huipulta ja lisätä y:tä yhdellä,
interpoloida hieman alku- ja loppu- x-koordinaatteja ja piirtää
x-koordinaattien välille vaakaviiva. Itseasiassa jos mietit vielä
hetken huomaat, että se olisi enemmän kuin helppoa.
Siitä vain tekemään, ensimmäinen flat-fillerisi valmistui juuri. Eli
sorttaa pisteet y:n mukaiseen järjestykseen (menee kolmella
if-lauseella, bubblesorttia) siten että ensimmäisenä on ylin ruudulla,
sitten keskimmäinen ja lopuksi alin. Nyt lasket pisimmälle viivalle,
eli sille joka ulottuu ylimmästä alimpaan, x-askeleen
(kaava: (x3-x1)/(y3-y1)). Sitten tarkistat että y1 ja y2 eivät ole
samoja ja jos eivät, lasket vastaavan x-askeleen x1:n ja x2:n välille
ja menet loopilla välin y1-y2. Jos y1 oli sama kuin y2 niin et tee
tuota ja jatkat suoraan vastaavaan tarkistukseen y2:n ja y3:n kanssa,
ja jälleen jos ne ovat erisuuria jatkat pidemmän viivan piirtämistä
siitä mihin se jäi ja lasket x-askeleen vielä y2:n ja y3:n väliselle
matkalle. Koko ajan piirrät vaakaviivoja pisimmän viivan
x-koordinaatin ja ylemmän tai alemman kolmion viivan x-koordinaatin
välille, sille korkeudelle missä y-looppisi meneekään.
Vaakaviivassa voisi olla hyvä tarkistaa että x1 on pienempi kuin x2
(jos näin ei ole, vaihda pisteet keskenään) ja onko viiva edes
ruudulla (x2 >= 0 && x1 <= 319 && y>=0 && y<=199) ja ettet ala
piirtämään ruudun ulkopuolelta (siirrä x1 nollaan jos se on alle ja
laske x2 319:ään jos se on yli). Sitten vain helpolla for-loopilla
viiva täyteen väriä. Muista, että se se on for(x=x1; x<=x2; x++), eli
<=, eikä <!
Gouraud-kolmio on ihan samanlainen, mutta väriä täytyy interpoloida
samoin kuin x-koordinaattia. Tekstuurikolmiossa interpoloidaan värin
sijaan tekstuurin x- ja y-koordinaatteja. Huomioitavaa on, että jos
viivanpiirrossa siirrät x1-koordinaattia -5:stä 0:aan jottet piirtäisi
ruudun ohitse, täytyy myös tekstuurien/gouraud-väriarvojen
alkupisteitä siirtää viidellä askeleella eteenpäin. Ja texture ja
gouraud-fillereissä hlinessäkin pitää interpoloida samalla tavalla
viivaa piirrettäessä kuin tehdään kolmion sivujen kohdalla.
Sossiinä, mie meen saunaan.
8.10 Pari kivaa feikkimoodia
----------------------------
Ihmissilmää on helppo huijata, se on todistettu useampaan kertaan. Ja
huijauksen kohteeksi joutuu puolueellisen tutkimuksen mukaan varsin
useassa VESA-tiloja käyttämättömässä demossa. Ja mikäs siinä. Mode-X
-tiloja en käsittele, kun en jaksanut twiikata itsekään kuin 320x400
-tilan toimimaan, nykyään ne alkavat olla hieman turhia. Mutta, demoja
tehdessä etenkin arvostaa kunnon temppuja jolla saa ruudulle enemmän
värejä kuin mitä siellä oikeastaan voisi yhtä aikaa olla.
Ensimmäinen tapa onkin kaluttu jo lävitse, ja sen nimi on kvantisoidut
tilat. Niissä ongelmana tahtoo vain olla, että homma ei ole kovin
vauhdikasta. Demoihin asian kyllä voi hoitaa siten, että tekee
erikoisflipin, joka laskee kaikki käytetyt värit ja tekee niistä
värikuution ja paletin etukäteen, mutta erityisen värikkäillä
efekteillä alkaa homma maistua puulta, tai paletti näyttämään yhä
enemmän siltä perinteiseltä 3:3:2-ratkaisulta, eli käytetään
256-värisen tilan pikselin kolmea ylimmäistä bittia punaiseen, kolmea
keskimmäistä vihreään ja kahta alimmaista siniseen. Voit hyvin
kuvitella miltä väriliu'ut näyttävät. Täytyy kuitenkin todeta, että
onhan se kuitenkin kahdeksan/neljä eri värikomponenttia, toisin kuin
16-väristen tilojen värikomponenttien sekoitus, välivärit ja
tumma/vaalea -valinta. Argh. Perfektionistit sanovat ei väreille ja
tekevät 256-värisen tilansa tietenkin puhtailla harmaasävyillä, mutta
me (vai pitäisikö sanoa te? ;) muut ihmiset voimme käyttää erilaisia
pieniä kikkoja laadun parantamiseen.
Eräs hieno ja usein käytetty tapa on jakaa 256-värinen paletti
pehmeään sinisen, punaisen ja vihreän liukuun (á 64 sävyä) ja
muodostaa väri kolmesta vierekkäisestä pikselistä. Laadussa ei
todellakaan ole hurraamista, mutta välttämättä asiaa ei ihan ensi
silmäyksellä huomaa (minua ainakin on muutaman kerran vedetty
höplästä). Valkoinen väri on aika karu, mutta jääähän paletista 64
sävyä yli ja ne voi aina käyttää harmaasävyihin. Tehokasta jälkeä
tulee ainakin, jos twiikkaat käyttöösi jonkin 320x400-tyyppisen tilan,
jolla normaali tilan 13h pikselit tuplaantuvat ja päällekkäisiä
pikseleitä on suorastaan ihana käyttää tämäntyyppisiin
sekoituksiin. Jo pelkästään käyttämällä kahta 3:3:2-pikseliä
approksimoimaan värilänttiä saadaan teoriassa 4:4:3-tila, ja
kummallisemmilla värijaotteluilla (1 bitti ilmoittamaan onko ylä- vai
alapikselin paletti ja loput 7 bittiä molemmissa erikseen käyttöön,
jolloin saadaan 14-bittinen tila, teoriassa, ainakin) päästään jo
todella hyviin tuloksiin.
On myös olemassa todellisten virittelytilojen maineeseen päässyjä
moodeja, joissa käytetään kahta kuvaa joita välkytetään nopeasti ja
kaiken maailman muita virityksiä, mutta vauhti on yleensä huono
verrattuna lähes suoraan raudalla toteutettaviin illuusioihin ja
toimivuus joidenkin tilojen kohdalla satunnainen, riippuen koneen
näyttökortista ja Uranuksen kallistumiskulmasta (joka tietenkin on
vakio). Näihin en ole erityisemmin perehtynyt, mutta jos joku on, niin
minua voi vapaasti pommittaa informaatiolla. Joka tapauksessa hus vain
keksimään erikoisia tapoja saada värien määrää näennäisesti
korotettua!
9.1 Saatteeksi
--------------
Yli-guruiksi itsensä tuntevat, skip it.
Nyt et ole enää laama, vaan hatarasti pääasiat osaava, toivottavasti
innokas peliohjelmoijan alku. Tai kenties jopa vielä enemmän, jos
kaikki dokumentin asiat ovat hanskassa. Tämän dokumentin tarkoituksena
on ollut saattaa sinut vain alkuun. Ensimmäinen neuvoni aloittelevalle
peliohjelmoijalle, eli sinulle on, että älä jätä lukemistasi
tähän. Mikään ei korvaa tuhansia tunteja tutoriaalien ja kokiksen
ääressä vietettyjä tunteja. Samaa asiaa voi opetella useasta eri
lähteestä, jolloin tajuaa asiat paremmin, selkeämmin ja syvemmin kuin
yhden tutoriaalin luvulla.
Toisena on se, että englannin kieli on pakko opetella. Sen oppii
parhaiten sanakirjan kanssa tutoriaaleja kahlailemalla. Jos aiot
pärjätä hyvin peliohjelmoinnissa täytyy englantia osata. Jos et vielä
sitä osaa, niin työskentele oppiaksesi.
Kolmanneksi kaikkein tärkeimpänä ovat oman järjen käyttö, rautainen
tahto ja sammumaton tiedonhalu, sekä ahkeruus. Maailma on täynnä
laamoja, jotka eivät osaa mitään siksi koska eivät ole tosissaan
yrittäneet. Minä aloitin C-ohjelmoinnin alle kolme vuotta sitten ja
pelkällä kovalla yrittämisellä ja innostuneisuudellani opettelin
koodaamaan. Olen lukenut tuhansia ja tuhansia rivejä ohjelmointiasiaa,
enkä ole vielä sitä joutunut katumaan.
MBnetistä löytyy valtava määrä lähdekoodia ja tutoriaaleja lähes
kaikkiin ohjelmoinnin haaroihin. Netistä puhumattakaan. Ennenkuin
menetät toivosi tai menet kysymään mistään kahlaile sieltä kaikki
tarpeelliset alueet ("C/C++" ja "muut") läpi. Melkein kaikkeen pitäisi
vastaus noista dokumenteista löytyä (mistä muualta henkilöt joilta
kysytään olisivat ne saaneet selville kuin dokumenteista).
Lisäksi löytyy kymmeniä ohjelmointikirjastoja, joissa tulee lähdekoodi
mukana. Ja muista, että itsekin voi tehdä päätelmiä ja kokeiluja. Kyllä
aina jostain tarvittu tieto löytyy!
9.2 Hieman koodereiden jargonia
-------------------------------
Alta löytyy muutamia kivoja tietokoneslangin termejä, joiden merkitystä
on hieman valotettu lyhyellä selityksellä. Huomaa, että tässä on vain
tietokoneslangia, sanat optimaalinen, ideaalinen ja muu vastaava pitää
yhä hakea Suomen Sivistyssanakirjasta:
PIKSELI Yksi kuvaruudun piste. Piste voi olla mustavalkoinen,
harmaasävyinen tai värillinen riippuen näyttötilasta ja sen
muistinvienti vaihtelee 1 bitin ja 4 tavun välillä.
KAKSOISPUSKURI näyttöpuskurin (ks. NÄYTTÖPUSKURI) kokoinen puskuri, jonne
kaikki piirretään ennen ruudulle kopioimista, jolloin saadaan tavara
mahdollisimman "tiivissä" (ei hajanaisia kirjoituksia silloin tällöin)
paketissa hitaaseen näyttömuistiin.
SEGMENTTI reaalitilassa muisti on jaettu 64 kilon kokoisin segmentteihin.
Itseasiassa segmentti on 16 ylintä tavua 20-bittisessä (max. 1 mega
osoitettavaa muistiavaruutta) osoitteessa ja tähän lisätään sitten
vielä offsetti (ks. OFFSET). Muodostuskaava on seg*16+off.
Myös lähdekoodista käännettävissä objektitiedostoissa (ks. OBJEKTI)
on segmenttejä, mutta ne tarkoittavat tietojen säilömispalasia,
kuten koodille varattu segmentti, datalle varattu segmentti ja
numeromuuttujille varattu segmentti. Ks. myös SEGMENTTIREKISTERIT.
OFFSET reaalitilan 20-bittisen muistiosoitteen 16 alinta bittiä. Huomaa,
että segmenttirekisterin ollessa 16 ylintä on osa osoitteista
"päällekkäin" Näin ollen on useita tapoja osoittaa samaan kohtaan
muistia. Katso alhaalla olevaa esimerkkiä:
Huomaa, että segmentti on yhden heksan, eli 4 bittiä enemmän vasemmalla,
sillä 4 bitin bittisiirtohan vastaa kertomista 16:sta (2^4=16).
Offsetti: 1234 6784
Segmentti: + 5678 + 5123
------- -------
20-bittinen: 579B4 579B4
SELEKTORI Suojatussa tilassa prosessori jakaa todellisen fyysisen muistin
halutun kokoisiin loogisiin paloihin. Selektori kertoo prosessori mihin
loogiseen alueeseen halutaan osoittaa. Tähän liittyy hieman monimutkai-
sempaakin asiaa, mutta sen hyödyllisyys jää kyseenalaiseksi normaalissa
peliohjelmoinnissa. Loogisilla muistialueilla on myös koko tallessa ja
jos osoitellaan tätä pidemmälle muistialueella ohjelma kaatuu ja
tulostaa virheilmoituksen. (SIGSEGV muistaakseni) Selektoreja käytetään
usein kuten segmenttirekisterejäkin (ks. SEGMENTTIREKISTERIT), eli
ds:ssä on dataselektori, cs:ssä koodiselektori jne.
SEGMENTTIREKISTERIT Prosessorilla on muutama 16-bittinen segmenttirekisteri,
jota täytyy käyttää kun halutaan osoittaa tiettyyn segmenttiin tai
suojatussa tilassa selektoriin (ks. SELEKTORI). Näitä ovat cs, ds, es,
fs, gs ja ss. Rekisteri cs säilöö koodin segmenttiä, eli koodia
luettaessa prosessori katsoo aina segmentistä 'cs' tavaraa. Cs:n pari
on ip-rekisteri, joka on 16-bittinen ja osoittaa koodin offsetin (ks.
OFFSET). Lähes kaikilla segmenttirekistereillä on tällainen "pari",
joka hoitaa offset-puolen.
386-prosessorista lähtien näissä on myös extended osa, joka on myös
16-bittinen ja käytettäessä tätä extended-osaa lisätään e-kirjain
rekisterin eteen. Näitä käytetään etenkin suojatussa tilassa, jossa
64 kilotavua on hieman liian vähän, kun taas 32-bittinen (16-bittinen
extended osa normaalin jatkoksi) osoite riittää varsin hyvin, ainakin
toistaiseksi. Offset-rekisterejä ip, si, di, sp ja bp vastaavat siis
eip, esi, edi, esp ja ebp.
Seuraavaksi tulee ds, eli datasegmentti, joka on ns. oletussegmentti,
jota käytetään jos et määrittele toista segmenttiä assembler-käskyssäsi
(esim. mov [eax], ebx on sama kuin mov ds:[eax], ebx). Tämän "pari"
taasen on si/esi. Sitten es, fs ja gs, jotka ovat taas yleissegmentti-
rekisterejä, joista kaksi viimeistä lisättiin 368-prosessorin mukaan.
Ainoastaan es:llä on pari, di/edi. Sitten löytyy ss, eli pinosegmentti,
joka osoittaa siis ohjelman pinon (ks. PINO) segmenttiin ja tämän
offset-pari, sp/esp. Ylimääräistä bp/ebp -offsetrekisteriä käytetään
useasti osoittamaan funktion parametreihin ja omaan muistiin.
PINO Ohjelmalle on varattu pino, josta funktiot voivat varata muistia
siirtämällä ss:esp -parin osoittamaa osoitetta. Pinossa välitetään
myös parametrit. Pino toimii LIFO-periaatteella (last in, first out),
joka tarkoittaa, että viimeiseksi sinne pantu tieto tulee ensimmäisenä
pois. Pinoon tallennetaan ja sieltä poistetaan tavaraa push ja pop
käskyillä. Lisää tietoa kannattaa katsoa ulkoista assembleria
käsittelevästä luvusta.
OBJEKTI Tällä on käyttöyhteydestä riippuen muutamakin merkitys, mutta tär-
keimmät lienevät alhaalla. Tietokonemaailmassa merkitys ei ole aivan
sama kuin tosielämässä, esimerkiksi seksiobjektista puhuttaessa. =)
Objekti-sanaa käytetään puhuttaessa pelien esineistä, sekä myöskin
spriteistä ja bittikartoista, eli käytännössä objekti on jokin
yksittäinen esine (luultavasti pelien käyttämä nimitys on juuri peräisin
tästä bittikartta-merkityksestä, muistattehan te seikkailupelit?).
Myös olioille on joskus joissain yhteyksissä käytetty nimitystä
objekti, vaikkei se nyt olekaan enää kovin yleistä, ainakaan minun
tietääkseni.
Objektitiedosto taas on yksittäinen, käännetty lähdekoodi (C, assembler
tai joku muu), jossa on tiettyjä segmenttejä, jotka sisältävät koodia
ja lukumuuttujia. Lisäksi tällainen objektitiedosto sisältää paljon
tietoa funktioiden nimistä ja funktioiden sijainnista tiedostossa,
joita tarvitaan linkkauksessa (ks. LINKKAUS).
LINKKAUS Tämä tapahtuma on viimeinen osa ohjelman käännösvaiheessa. Siinä
konekielelle käännetyt objektitiedostot "linkataan", eli liitetään
yhteen ajettavaksi tiedostoksi. Omien objektitiedostojesi lisäksi gcc
linkkaa mukaan standardim C-kirjaston ja muut määrittelemäsi kirjastot,
sekä ns. stubin (ks. STUB).
STUB Sijaitsee EXE:n alussa ja käynnistyy ohjelman käynnistyessä. Sen
tehtävä on raivata itse ohjelmalle sen koodin tarvitsema muistialue
ja toimittaa muistipalvelut sun muut toimintakuntoon, jotta ohjelman
ei tarvitse huolehtia näistä. Stub tavallaan hoitaa ohjelman
suojatun tilan autuuteen sen omalle muistialueelle, jotta itse
pääohjelmalla on helppoa. Tästä syystä perusmuistin kulutus on suojatun
tilan ohjelmissa niin pientä, sillä vain stub pitää saada perusmuistiin
ja sieltä käsin se sitten hinaa pääohjelman jatkettuun muistin, megan
yläpuolelle.
REAALITILA PC:n "alkuperäinen" tila, jossa on segmentit ja offsetit ja 1
megan yläraja muistinkäytöllä. Tätä ollaan kierretty ohjelmilla kuten
EMM386 ja HIMEM, jotka vastaavasti toimittavat suojatun tilan
jatkomuistia reaalitilan ohjelmille (ks. EMS ja XMS).
SUOJATTU TILA Jo 286-prosessorien mukana esitelty tila, mutta paremmin
toteutettuna vasta 386:ssa (siksi DJGPP ei toimi vasta kuin sillä).
Suojatussa tilassa on kaikki muisti käytettävissä ja prosessori
tarjoaa useita palveluja, kuten muistin suojaus ja monta muuta kivaa
ominaisuutta, jolla yksittäiset ohjelmat eivät pääse niin helposti
kaatamaan konetta.
EMS Expanded Memory Services on tapa antaa jatkettua muistia reaalitilan
ohjelmille. Muistinhallintaohjelma, yleensä EMM386.EXE toimii siten,
että se käyttää suojattua tilaa hyväkseen mapaten yli megan alueelta
muistipalasia alle megan alueelle muistin kohtaan, jota kutsutaan
nimellä PAGE FRAME (katso vaikka DOS:sin helpeistä). Ohjelma voi varata
EMS-sivuja, joiden koko on 16 kilotavua ja sitten asettaa niistä
maksimissaan 4 kerralla näkyviksi (siksi page framen koko on yleensä
4*16=64kilotavua). Nopea tapa, muttei niin nopea kuin suora muistin
osoitus.
XMS Extended Memory Services taas on systeemi, joka perustuu siihen, että
järjestelmä tarjoaa joukon funktioita, joilla voit varata XMS-muistia
ja kopioida sitä perusmuisti<->jatkomuisti, ja
jatkomuisti<->jatkomuisti -alueilla. Ongelmana on se, että reaalitilan
ohjelma ei voi käsitellä jatkomuistia kuin kopioimalla sen ensin
perusmuistiin ja sitten takaisin jatkomuistiin, mikä tekee tästä usein
aika hitaan tavan.
PALETTI Tämä on näytönohjaimen muistissa oleva taulukko, jossa on
värinumeroiden (värin 0, värin 1, värin 2 jne.) väriarvot, eli se
paljonko mikäkin väri sisältää punaista, vihreää ja sinistä. Palettia
ei käytetä high-color ja true-color tiloissa (ks. HIGHCOLOR ja
TRUECOLOR), vaan ainoastaan 256- ja 16-värisissä tiloissa. Lisää
paletin asettamisesta ja lukemisesta palettia käsittelevästä luvusta.
HIGHCOLOR on väritila, jossa värejä on 65536 tai joissain tapauksissa
32768 kappaletta, eli 16-bittinen tai 15-bittinen pikseli
(ks. PIKSELI). Tämän pikselin värinumero on jaettu yleensä siten,
että numerosta 5 bittiä on tarkoitettu punaiselle, 6 vihreälle ja
5 siniselle (koska ihmisen silmä kai aistii tarkimmin vihreää),
joten erillistä palettia ei tarvita. 15-bittisessä tilassa
vastaavasti on vain 5 bittiä / väriarvo. Myös jotain virityksiä
14-bittisistä tiloista taitaa olla. Ks. myös PALETTI ja TRUECOLOR.
TRUECOLOR on väritila, jossa on 16.7 miljoonaa väriä, tarkemmin 2^24
väriä. Jako on kuten high-color tiloissa (ks. HIGHCOLOR), mutta
jokaiselle väriarvolle on 8 bittiä, eli 1 tavu punaiselle, vihreälle
ja siniselle. Ei varmaan tarvitse erikseen mainita, että tällaiset
tilat ovat ohjelmoijan taivas. Uudempina on myös 32-bittiset tilat,
joissa yksi tavu käytetään tietääkseni hukkaan. Tämä sen takia, että
32-bittinen pikseli (ks. PIKSELI) on paljon helpompi käsitellä, kun
rekisterit ovat 32-bittisiä, samoin kuin joidenkin assembler-käskyjen
käyttämät alkioiden koot. 24 tai 32 bittiä voidaan varmaan jakaa useilla
muillakin tavoilla (ks. CMYK ja RGB) ja tehokkaammin kuin kaksi edellä
esitettyä, mutta en tiedä kuinka paljon käytännössä käytetään
toisenlaisia bittien jakotapoja.
CMYK Cyan, Magenta, Yellow, black. Tämä on yksi tapa jakaa väriavaruus,
eli käytetään normaalin rgb-tripletin sijasta (ks. RGB) syaania,
magentaa, keltaista ja mustaa. Myös CMY-tyyppiä on näkynyt, josta siis
musta puuttuu. Tätä ei käytetä kovin paljoa pelimaailmassa, mutta
printtereiden ja skannereiden kanssa toiminut on varmaan tästä
kuullut. Muistaisin, että on vielä pari tapaa jakaa värit, jokin YMK
tai vastaava oli ainakin, mutta tiedän selittää vain CMYK-, CMY- ja
RGB-mallit.
RGB Red, Green, Blue. Tapa jakaa väriavaruus, eli jokainen väri sen puna-
viher- ja sinikomponentteihin. Vähän samaan tyyliin siis kun sekoitat
Punaisesta, sinisestä ja keltaisesta vesivärit ja muun vastaavan niin
tietokoneella ja televisioissa käytetään tätä tapaa. Tiedä sitten
miksi vihreä, luultavasti se soveltuu paljon helpommin sädeputkelle.
RGB-AVARUUS Väriavaruus ajatellaan kuutioksi, jossa XYZ-akseliston
korvaa RGB-akselisto. Kuutio on rajallinen ja rajat asettavat
värikomponenttien minimi- ja maksimiarvot. Esim. normaalin
VGA-paletin värit voidaan ajatella pisteiksi kuutiossa, jonka
alakulma on (0,0) ja vastakkainen kulma (63,63).
KVANTISOINTI Paletin kvantisointi on tapa optimoida käytössä olevaa
palettia. Useasti tarvittaisiin enemmän värejä käyttöön kuin mitä
niitä on käytettävissä ja tähän käytetään paletin värien "optimointia",
joissa yhdistellään toisiaan lähellä olevia värejä. Kvantisoinnin
tehtävä on siis lyhyesti etsiä optimaalinen n väriä sisältävä paletti
jolla voidaan näyttää mahdollisimman alkuperäistä vastaavasti
m-värinen kuva.
MOODI Yleisesti käytetty lyhenne näyttötilasta, screen mode.
HEKSA 16-kantainen, eli HEKSAdesimaalinen luku, käytetään monesti muisti-
osoitteissa ja porttien numeroissa. Lisää tietoa tiedostosta LUVUT.TXT
FYYSINEN OSOITE Ks. SELEKTORI
LOOGINEN OSOITE Ks. SELEKTORI
SIIRROSOSOITE Ks. OFFSET
POINTTERI Hiiren kursori tai yleensä ohjelmoinnissa tietoalkio, joka
sisältää muistiosoitteen. Pointteri näyttömuistiin on siis alkio,
jonka arvo on näyttömuistin osoite (ks. OFFSET, SEGMENTTI).
Hyödylliseksi pointterin tekee se, että sitä voidaan indeksoida,
eli sitä voidaan käyttää kantaosoitteena johonkin muistialueeseen.
Yhden indeksin osoittaman alkion pituus on pointterityypin pituus.
Jos pointteri on char-tyyppinen niin sen yksi alkio on yhtä pitkä
kuin yksi char-alkio, eli 1 tavu. Indeksi 10 olisi siis 10 tavua
pointterin osoittamasta muistista eteenpäin.
Tätä käytetään hyväksi esimerkiksi kaksoispuskurissa
(ks. KAKSOISPUSKURI), jossa pointteri osoittaa sen alkuun (samoin
kuin indeksi 0) ja sen 600. alkio kaksoispuskurin 600. tavuun,
tässä tapauksessa tulevan framen (ks. FRAME) 600. pikseliin (ks.
PIKSELI), olettaen että ollaan 256-värisessä tilassa.
Lyhesti: Pointteri on muistiosoite, indeksointi on tapa saada indeksin
määrämä alkio pointterin osoittamalta muistialueelta. Esim. 12. tavu
pointterin alusta saataisiin pointterin indeksillä 11 (indeksi 0 on
1. tavu). Muista, että int-tyyppisen pointterin yhden indeksin
osoittaman alkion pituus on sizeof(int), eli 4 tavua, jolloin indeksissä
0 on 1., 2., 3. ja 4. tavu ja indeksissä 1 vastaavasti 5., 6., 7. ja 8.
FRAME Tarkoittaa yhtä näyttöruudullista, yhtä näyttöruudun päivityskertaa.
Jos käytät kaksoispuskuria on frame se, minkä kopioit näyttömuistiin.
Lyhyesti frame siis on valmis, näytettäväksi tarkoitettu ruudullinen
kuva-informaatiota. Katso selvennykseksi myös kohta FRAMERATE.
FRAMERATE On se montako framea (ks. FRAME) jokin ohjelma voi tuottaa
tietyssä ajassa, yleensä sekunnissa (tätä framea/sekunti kutsutaan
myös nimellä FPS). Jos matopelisi esimerkiksi pystyisi päivittämään
madon sijainnin ruudulla, esteet ja muut objektit vaikka 10 kertaa
sekunnissa niin sen "FPS" olisi näinollen 10.
FPS, Frames per second. Ks. FRAMERATE.
VIRKISTYSTAAJUUS Luku ilmoitetaan yleensä hertseinä (herzeinä, hertzeinä?)
ja se kertoo montako kertaa sekunnissa (hertsi, hZ tarkoittaa
värähtelyä/sekunti) monitori ja näytönohjain (heikoin lenkki ratkaisee)
pystyvät päivittämään näyttöruutua. Normaalissa VGA-tilassa luku on 70,
minkä takia yleensä sanotaan, että hyvän toimintapelin tulisi pyöriä
70 fps (ks. FRAMERATE). Tämä ei kuitenkaan estä sitä, että fps ei voisi
olla suurempi kuin virkistystaajuus, mutta jos ohjelmasi pyörittää
300 kuvaa ruudulle sekunnissa (sopiva määrä 3D-enginelle jollain yksin-
kertaisella varjostuksella, kuten phongilla) niin vain 70 näkyy.
ANTIALIAS Tämä termi esiintyy yleensä englanninkielisessä materiaalissa
muodossa antialising, joka tarkoittaa kuvioiden reunojen pehmentämistä
väreillä. Esimerkiksi kun piirrät vinon viivan vihreällä mustalle poh-
jalle se näyttää aika rujolta, mutta kun lisäät jokaiseen kulmaan hie-
man tummanvihreätä näyttää viiva huomattavasti pehmeämmältä. Käytännössä
tämä hoidetaan laskemalla paljonko viivan "arvio" (se mitä piirretään
ruudulle) poikkeaa oikean viivan sijainnista ja mitä enemmän se poikkeaa
sen enemmän reunoille laitetaan samaa väriä (väri siis sekoitetaan siinä
suhteessa missä viiva sijaitsee minkäkin pikselin päällä.
CROSSFADE Suomeksi termi voisi olla ehkä ristiliu'utus. Ideana on, että
toinen kuva ilmestyy toisen takaa pikkuhiljaa, ensin vain haaleana,
mutta voimistuen hiljalleen toisen häipyessä ja lopuksi ensimmäisestä
kuvasta onkin tullut jälkimmäinen.
PALETTE ROTATION Eli kauniimmin paletinpyöritys rullaa palettia ympäri siten,
että aiemmin värinä 3 toiminut muuttuu väriksi 2, väri 2 muuttuu väriksi
1, 1 muuttuu 0:ksi ja nolla menee väriksi 255, väri 255 väriksi 254 jne.
Eli siirretään koko palettia asken taaksepäin (tai eteenpäin) ja se joka
ei voi enää mennä edemmäs tai taaemmas laitetaan toiseen päähän
vapautuneelle paikalle. Myös osia paletista voidaan pyörittää. Tämä
aiheuttaa varsin kivan näköisiä efektejä, etenkin jos tausta sisältää
väriliu'utuksia. Tarkempaa tietoa palettia koskevasta kappaleesta ja
esimerkkiohjelmasta pal3.c.
RLE Taas uusi jännittävä lyhenne kokoelmaamme. Run Length Encoding
tarkoittaa käytännössä, että kun meillä on 5 kappaletta N-kirjaimia,
niin ilmoitamme ne tyyliin 5N. Näin säästämme 3 tavua tilaa jo
tuossakin. Idea on siis, että useat toistuvat merkit ilmoitetaan
numerona ja merkkinä. Toisaalta vaihteleva data on ongelma ja tähän
on useita kiertotapoja, kuten PCX:n lähestymistapa, jossa tavu, jonka
arvo on yli 192 tarkoittaa että seuraavaa tavua ilmestyy
<luettutavu>-192 kertaa, tai LBM-tyyli, jossa on yksi tavu, joka kertoo
joko kuinka monta pakkaamatonta pikseliä edessä on, tai kuinka monta
pakattua (muistaakseni jos n on alle 128 niin tarkoitetaan montako
pakkaamatonta edessä ja jos se on yli, niin sitten jotain tyyliin
n-127).
HANDLERI Tämä kummajainen on suomeksi sama kuin käsittelijä. Ohjelmassa
on usein monenlaisia handlereita, kuten näppishandleri, joka käsittelee
näppäinten painallukset tai esimerkiksi interrupt handleri, joka
käsittelee toiminnan painaessa CTRL-BREAK tai CTRL-C
-näppäinyhdistelmiä. Ks. myös KESKEYTYS.
KESKEYTYS PC-perusrakenteeseen kuuluvat keskeytykset, jotka osa ovat nk.
software-keskeytyksiä ja osa hardware-keskeytyksiä. Se kummantyyppinen
keskeytys on riippuu siitä aiheuttaako sen ohjelma itse (esimerkiksi
videokeskeytys 0x10 jolla voidaan vaihtaa vaikka näyttötilaa) vai
generoidaanko se laitteiston toimesta (kuten ajastinkeskeytys, joka
generoidaan halutuin välein). Keskeytyksen satuttua komento siirtyy
keskeytyskäsittelijään (ks. HANDLERI), joka hoitaa tarvittavat
toimenpiteet keskeytyksen satuttua. Käyttäjä voi itse koukuttaa
handlereita (ks. KOUKUTUS) ja näin tarjota keskeytyspalveluja tai
kutsua itse keskeytyksiä ja pyytää näiltä keskeytyskäsittelijöitä
palveluksia, kuten edellämainittu videotilan vaihto.
PC on aika keskeytyspohjainen tietokone ja esimerkiksi kovalevyn
lukeminen ja muu vastaava tehdään yleensä keskeytysten kautta. Monet
ajurit koukuttavat laitteen keskeytyksen ja kommunikoivat itse laitteen
kanssa, jolloin keskeytystä kutsuvan ohjelman ei tarvitse tietää
tarkasti miten laite toimii. Esimerkiksi hiirikeskeytyksen koukuttaa
hiiriajuri ja ajuri hoitaa suoran kommunikoinnin hiiren kanssa ohjelman
tarvitessa vain kutsua keskeytyskäsittelijää generoimalla
hiirikeskeytys.
KOUKUTUS (HOOKING) Keskeytyskäsittelijän muistiosoite sijaitsee taulukossa
aivan muistin alussa (luoja tietää onko se siellä suojatussa tilassa,
minä en ainakaan tiedä, mutta sillä ei onneksi ole väliä) ja koukutus
tarkoittaa sitä, että otat talteen alkup. keskeytyskäsittelijän
osoitteen ja sijoitat omasi sinne osoitteen tilalle, jolloin keskeytystä
kutsuttaessa käsky siirtyy omalle käsittelijällesi (ks. myös HANDLERI
ja KESKEYTYS). Voit myös kutsua vanhaa käsittelijää oman toimintasi
jälkeen.
LFB Tapaa osoittaa suoraan koko näyttömuistiin. Kuten VGA-segmentti
A000h, mutta sijaitsee kaukana 1 megan rajan yläpuolella, joten
kokorajoitus ei enää ole 64 kiloa.
BANKED-TILAT Toinen tapa päästä käsiksi yli 64 kilon näyttömuistiin on
tehdä pieni ikkuna (yleensä sama kuin VGA-segmentti ja koko 64
kiloa) jota liikutellaan pitkin näyttömuistia. Aika tuskainen
verrattuna LFB:hen (ks. LFB)
VESA eli Video Electronics Standards Assocation, jonka käsialaa ovat
mm. näytönohjaimien käsittelyyn yleisesti käytetty
VESA-standardi. Virallinen nimi standardille lienee kuitenkin VBE
(ks. VBE)
VBE eli VESA BIOS Extension on normaalin grafiikkakeskeytyksen 10h
rinnalle toteutettu joukko laajennuksia joka mahdollistaa
SVGA-tilojen näytönohjainriippumattoman käsittelyn. 1.2, 2.0 ovat
suosittuja ja 3.0 on ihan äskettäin saapunut.
9.3 Lähteet
-----------
Muutamia erityismaininnan ansaitsevia dokumentteja sähköisessä ja
paperimuodossa sekalaisessa järjestyksessä, joiden sisältämää
informaatiota on käytetty tämän tutoriaalin tekoon.
Tiedostot:
PCGPE10.ZIP
Jokaisen ohjelmoijan pakkoimurointi. Sekalainen kokoelma valittuja
paloja. Sisältää 10 ensimmäistä Aphyxian traineria!
FMODDOC2.ZIP
Kaikille äänikorteista ja MOD-playereistä kiinnostuineille hieman
vaikea (äänikortin ohjelmointi ei nimittäin aina ole helppoa)
tutoriaali sisältäen kaiken tarvittavan tiedon. Löytyy jokaisen
itseään kunnioittavan TosiKooderin kovalevyltä.
HELPPC21.ZIP
Mainio asioiden tarkistamiseen soveltuva lähdeteos.
HPC21_P5.ZIP
Päivitys edelliseen sisältäen Pentium-käskyt.
TUT*.ZIP
Asphyxian VGA-trainerit. Etsi hakusanalla Asphyxia.
3DICA*.ZIP
3D ohjelmoija-wannaben sekä kokeneemmankin raamattu. Suomen
kielellä kaiken lisäksi!
DJTUT255.ZIP
Selittää DJGPP:n AT&T-syntaksin ja inline-asseblerin
englanniksi. Korvaamaton jos haluaa käyttää assembleria
DJGPP-ohjelmissaan!
ASSYT.ZIP
Assemblerin alkeet suomeksi.
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 (095-osa tiedostonimessä).
ABEDEMO?.ZIP
Ruotsalainen demokoulu. Ei onneksi ruotsia, vaan
englantia. Ensimmäisiä lukemiani tutoriaaleja, joka auttoi minut
alkuun koodauksessa.
INTER*.ZIP
Ralph Brownin keskeytyslista. Sisältää hurjan määrän paketteja ja
kyllä tietoakin.
Kirjallisuus:
Opeta itsellesi C++ -ohjelmointi 21 päivässä
Jos et vielä osaa C++:ssaa tai C:tä, niin tämä voi olla
lainaamisen arvoinen teos. Kokeneemmalle ohjelmoijalle
suositeltavampi voi olla jokin muu, mutta monet on tämä kirja
auttanut alkuun.
486-ohjelmointi
Aina kun joku on kysynyt assembler-ohjelmointia käsittelevää
kirjaa, niin tällä hänet on vaiennettu. Omasta mielestänikin kelpo
kirja.
Assembler-ohjelmointi
Vaan joku kuitenkin oli sitä mieltä, että 486-ohjelmointi ei ollut
paras, vaan että tämä kirja olisi selkeämpi. Itse en ole tätä
lukenut.
Computer Graphics: Principles and Practice
Grafiikkaohjelmoijan raamattu. Sisältää paljon erilaisista
algoritmeistä sun muusta. Tietääkseni.
Zen of graphics programming: Second edition
Grafiikkaohjelmoijan koraani. Tosin nykyään VESA:n ja
huippumodernien 3d-engineiden aikana osa tiedosta on
vanhentunutta. Sisältää kuitenkin todella tehokkaita
optimointikikkoja sun muuta mukavaa.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment