Created
May 26, 2016 18:40
-
-
Save pekkavaa/59d66f6f8ca38c2a72de84cd39c72561 to your computer and use it in GitHub Desktop.
Laaman tie DJGPP-peliohjelmointiin versio 2.10. By Jokke / BAD KARMA
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ | |
█▓▒░ ░█ █▓▒░ ░▒▓▒█ █▒░ ░▒▓▒░ █ █░ ░▒▓▒░█ █▓▒░ ░▒▓█ █ ░▒▓▒░ ░▒█ | |
█▒░ ░▒█ █▒░ ░▒▓▒░█ █░ ░▒▓▒░ ░█ █ ░▒▓▒░ █ █▒░ ░▒▓▒█ █░▒▓▒░ ░▒▓█ | |
█░ ░▒▓█ █▒░ ░▒▓▒░ ░█ █░ ░▒▓▒░ ░▒▓█ █░▒▓▒░ ░▒█▒░ ░▒▓▒░█ █▒▓▒░ ░▒▓▒█ | |
█ ░▒▓▒█ █░ ░▒▓▒░ ░▒█ █ ░▒▓▒░ ░▒▓▒█ █▒▓▒░ ░▒▓▒░ ░▒▓▒░ █ █▒▓▒░ ░▒▓▒░ █ | |
█░▒▓▒░█ █ ░▒▓██ ░▒▓█ █░▒▓▒░██▒▓▒░█ █▓▒░ ░▒▓▒░ ░▒▓▒░ ░█ █▓▒░ ░██▒░ ░█ | |
█▒▓▒░ █ █░▒▓▒██░▒▓▒█ █▒▓▒░ ██▓▒░ ░██▒░ ░▒█▒░ ░▒█▒░ ░▒█ █▒░ ░▒██░ ░▒█ | |
█▓▒░ ░█ █░▒▓▒░██▒▓▒░ █▒▓▒░ ░██▒░ ░▒██░ ░▒▓█░ ░▒▓█░ ░▒▓█ █░ ░▒▓██ ░▒▓█ | |
█▒░ ░▒█▄▄▄█▒▓▒░ ░▒▓▒░ ░█▓▒░ ░▒▓▒░ ░▒▓██ ░▒▓▒█ ░▒▓▒█ ░▒▓▒██░ ░▒▓▒░ ░▒▓▒░█ | |
█░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░██▒▓▒██░▒▓▒░██ ░▒▓▒░ ░▒▓▒░ █ | |
█ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ██▓▒░██▒▓▒░ ██░▒▓▒░ ░▒▓▒░ ░█ | |
█░▒▓▒░ ░▒▓▒░ ░▒▓██ ░▒▓▒░ ░▒▓▒░██▒▓▒░ ░▒▓▒░ ░██▒░ ██▓▒░ ░██▒▓▒░ ░██▒░ ░▒█ | |
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀ | |
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, ®s)) { | |
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, ®s)) | |
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, ®s)) | |
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