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 ;). | |
joonas.pihlajamaa@mbnet.fi on sähköpostiosoite, josta minut pitäisi saada | |
kiinni. | |
Joonas Pihlajamaa on käyttäjätunnukseni MBnetissä, jolle voit kirjoittaa | |
yksityispostiin. Ainakin tällä hetkellä luen viestini keskimäärin 3 kertaa | |
viikossa, joten vastaus pitäisi tulla viikon sisällä (ellen ole | |
lomailemassa tai paastolla koneestani ;). | |
Joonas Pihlajamaa | |
Säveltäjäntie 40 | |
67300 Kokkola | |
Tämä on se osoite, jossa asun. Jos et aivan käymään viitsi tulla niin mikset | |
lähettäisi postikortilla terveisiä? Vastauksista kirjeisiin en tiedä, mutta | |
katsotaan nyt, ei ole ainakaan vielä tullut ainoatakaan kirjettä... | |
Kuulun gruuppiin BAD KARMA, joka tekee tällä hetkellä peliä nimeltään | |
SLiDER: Roadkill, joka on autopeli ja sen on tarkoitus hakata Slicks 'n' | |
Slide sekä muut vastaavat pelit mennen tullen. Kannattaa tutkia tarkasti | |
purkkien tiedostoalueita, jos vaikka ilmestyisi. Ilmestymisajankohta | |
on luultavasti (ensi?-) vuosituhannen loppupuolella. | |
1.8 Esimerkkien kääntäminen | |
--------------------------- | |
Tutoriaalin mukana seuraa sankka joukko esimerkkiohjelmia ja ne | |
löytyvät hakemistosta EXAMPLE. Jos sinulla on 'make', niin kääntö | |
sujuu yksinkertaisesti menemällä esimerkkikoodit sisältävään | |
hakemistoon ja ajamalla komennon 'make' ja sen jälkeen 'make test.exe' | |
jos sinulla on NASM. 'make clean' / 'make realclean' vastaavasti | |
tyhjentävät objektitiedostot / objekti- ja exetiedostot. | |
Kiitoksia Tero Kontkaselle makefile-esimerkistä. Tein sen pohjalta nyt | |
uuden, koska esimerkkiohjelmia oli tullut jonkin verran lisää. Jos | |
sinulla ei ole 'make'-ohjelmaa onnistuu kääntäminen käsinkin. Lähes | |
kaikki tiedostot ovat itsenäisiä eivätkä tarvitse muita | |
objektitiedostoja tai kirjastoja toimiakseen. Poikkeuksina | |
timertst.exe joka tarvitsee sekä timer.c:n ja timertst.c:n käännettynä | |
ja test.exe, joka tarvitsee test.asm:n ja test.c:n käännettynä. | |
Hauskaa kokeilua, minä menen nukkumaan! | |
2.1 DJGPP - vaikea, suuri, monimutkainen, omituinen, hidas? | |
----------------------------------------------------------- | |
Tutoriaali sivuaa koko ajan DJ Delorien ilmaista Gnu-kääntäjää | |
DOS:ille, eli DJGPP:tä, erityisesti sen kakkosversiota. Itse siirryin | |
puolessa välissä tätä tutoriaalia 2.0 -versiosta versioon 2.01 ja | |
luulisin, että esimerkit toimivat molemmilla näistä versioista ja | |
luultavasti uudemmillakin. Vanhemmat versiot eivät luultavastikaan | |
toimi näiden lähdekoodien kanssa. | |
Tämän mahtavan ilmaiskääntäjän löydät esimerkiksi internetistä | |
osoitteesta ftp://x2ftp.oulu.fi jostain | |
pub/msdos/programming-hakemiston alihakemistosta. Sen saa myös | |
MBnetistä, tarvittavat tiedostot ovat alueella PC-Ohjelmointi (area | |
8), tiedostoja on useita, ja ne löytyvät ko. alueelta löytyvästä | |
MBNETDJ2.TXT:stä. Myös kaikille Mikrobitin tilaajille tullut Huvi & | |
Hyötyromppu sisältää tämän kääntäjän hakemistossa MIKROBIT\DJGPP201\, | |
tosin sieltä puuttuu LGP2721B.ZIP (tarvitaan C++ koodin kääntämisessä), | |
jonka Käpyaho unohti laittaa mukaan. Halutessasi voit hakea puuttuvan | |
tiedoston MBnetistä. | |
DJGPP:n asennukseen purat vain kaikki tarvitsemasi paketit haluamaasi | |
hakemistoon (esim. D:\OHJELMAT\DJGPP) PKUNZIP:in -d parametrillä. Sen | |
jälkeen lisäät polkuun tuon hakemiston alihakemiston BIN (esim. | |
D:\OHJELMAT\DJGPP\BIN), ja vielä lopuksi teet uuden environment-muuttujan | |
DJGPP, joka osoittaa DJGPP:n juurihakemistossa olevaan DJGPP.ENV | |
-tiedostoon. Eli esim.: | |
SET DJGPP=D:\OHJELMAT\DJGPP\DJGPP.ENV | |
Nyt voit kokeilla toimivuutta tekemällä pienen C-ohjelman (vaikka | |
koe.c) ja kirjoittamalla: | |
GCC koe.c -o koe.exe | |
Lisää infoa GCC:n käännösoptioista ja kääntäjästä saat kirjoittamalla: | |
INFO GCC | |
Suosittelisin että lueskelet DJGPP:n dokumentaatiota ja teet tässä | |
vaiheessa paljon testiohjelmia ja opettelet käyttämään | |
info-lukijaa. Hyödyllinen hankinta on myös Rhide, joka on IDE | |
DJGPP:lle. Ohjelma löytyy MBnetistä alueelta 8 (ETSI RHIDE) sekä | |
H&H-Rompulta. Kun tunnet osaavasi käyttää vaivattomasti kääntäjää | |
palaa takaisin dokumentin pariin. | |
Jos et vielä C:tä osaa, niin hanki jostain, esimerkiksi kirjastosta hyvä | |
kirja ja opettele sen avulla C-ohjelmointi. En aio alkaa | |
selittämään kaikkein yksinkertaisimpia asioita esimerkkikoodeissa | |
taikka kommentoimaan liiemmälti koodia. | |
2.2 Grafiikkaa - mitä se on? | |
---------------------------- | |
No olet siis päättänyt edetä seuraavaan aiheeseen, joka näyttäisi | |
olevan grafiikan ohjelmointi DJGPP:llä. Aloittakaamme siis! Tiedoksi | |
nyt etukäteen, että muistiosoitteet ovat heksoina, vaikkei sitä | |
ilmoitetakaan. | |
Esimerkkinä käytän VGA:n perusmoodia, 13h (heksaluku, desimaalina | |
19), joka on erittäin helppokäyttöinen. Kun tarvitset muita moodeja | |
sinulla on varmasti jo tarpeeksi taitoa hankkia itse informaatiota, | |
mutta tämän neuvon ihan alusta alkaen. | |
Eli olipa kerran PC, jossa oli 16-bittinen muistiväylä, joka salli | |
vain 64 kilon osoittamisen kerralla, sillä 16-bittisellä osoitteella | |
voidaan maksimissaan osoittaa 2^16=65536 tavua muistia. PC:n oli | |
suunnitellut Intel, mutta PC:hen oli luvattu yli 64 kilotavua muistia | |
ja 32-bittinen muistiväylä oli niihin aikoihin kovin kallis. Joten | |
joku sai suorastaan neronleimauksen: Jaetaan koko muisti 64 kilon | |
palasiin! | |
En syvenny tekniikkaan sen kummemmin, vaan totean vain, että 8088 | |
prosessoriin perustuvassa PC:ssä muodostettiin muisti SEGMENTISTÄ ja | |
OFFSETISTA (SEG:OFF, esim B800:0000). Todellinen osoite muistissa | |
saatiin kertomalla SEGMENTTI kuudellatoista ja lisäämällä siihen | |
OFFSET. (B800:0000 = B800*16+0000 = B8000) Ja kun kummatkin olivat | |
16-bittisiä lukuja saatiin näin 20-bittinen siirrososoite. Ja koska 20 | |
bitillä voi ilmoittaa täsmäälleen kaksi potensiin 20 eri arvoa oli | |
maksimimäärä mitä voidaan osoittaa 1 megatavu. Kymmenen ensimmäistä | |
segmenttiä (eli 0000 1000 2000 3000 4000 5000 6000 7000 8000 ja 9000) | |
omistettiin ohjelmille ja nimettiin perusmuistiksi, jota oli siis | |
10*64=640 kilotavua. Sitten segmentistä A000 alkoi grafiikkamuisti. | |
No tietokoneet kehittyivät ja esiteltiin suojattu tila, eli PROTECTED | |
MODE (PM), joka käsitteli koko muistia selektoreilla ja offseteilla, | |
jotka olivat entisen 16 bitin sijasta 32-bittisiä (selektorit ovat | |
kuitenkin yhä 16-bittisiä). Vanhat segmenttien varastoimiseen tarkoitetut | |
SEGMENTTIREKISTERIT varattiin nyt selektoreille, jotka kertoivat | |
prosessorille, mitä LOOGISTA muistialuetta käsiteltiin. DJGPP, joka on | |
suojatun tilan kääntäjä esim. antaa ohjelmalle alussa 2 selektoria, toinen | |
osoittaa dataan ja toinen koodiin. Tästä pidemmälle en tiedä tarkasti, | |
mutta riittää tietää, että selektorin osoittaessa dataan ei offset 1234 | |
todellakaan ole muistissa kohdassa 1234, vaan se on ohjelman oman | |
data-alueen 1234. tavu. | |
Ja mikä meitä kiinnostaa, on perusmuistin 11. segmentti, jonka osoite | |
siis oli A000:0000. Siirrososoite on siis A000*16+0000 = A0000. Mutta, | |
kuten muistamme, ei onnistu, että vain tekisimme pointterin, joka osoittaa | |
tuonne osoitteeseen, sillä ohjelman datahan on aivan toisessa | |
selektorissa kuin perusmuisti. Meidän täytyy ensin löytää oikea | |
selektori, jonka osoittama looginen muistialue vastaisi PC:n | |
perusmuistia. Ja tällainen löytyykin nimellä _dos_ds. Tämän selektorin | |
osoittaman muistialueen 0. tavu on perusmuistin 0. tavu, 1. tavu on | |
perusmuistin 1. tavu ja niin jatkuu edelleen, kunnes tavu numero A0000 | |
on ensimmäinen VGA:n grafiikkamuistin tavu. | |
Nyt meillä on siis tiedossa segmentin A000, eli VGA-kortin | |
muistialueen siirrososoite, A0000 ja oikea selektori, _dos_ds. Mutta | |
miten laitamme tavun tuonne? Hyvä kysymys. Se onnistuu vähintään 5:llä | |
eri tavalla, mutta perehdymme helpompaan. Kirjaston sys/farptr.h | |
funktioon _farpokeb(selektori, siirrososoite, tavu), jolla pääsemme | |
käsiksi tuonne. Normaalin pointterin tekohan ei onnistu, vaan meillä | |
pitää olla funktio, joka kykenee osoittamaan toisen selektorin | |
alueelle. | |
Näinollen esimerkkiohjelma, joka asettaa VGA-muistin 235. tavun arvoon | |
100 on tämän näköinen (PIXEL1.C): | |
#include <go32.h> /* muistathan, _dos_ds on määritelty täällä! */ | |
#include <sys/farptr.h> /* täältä löytyy _farpokeb */ | |
int main() { | |
int selektori=_dos_ds, | |
siirros=0xA0000 + 235, | |
arvo=100; | |
_farpokeb( selektori, siirros, arvo ); | |
return 0; | |
} | |
Arvaan, että ehkä menit ja kokeilit tuota ja petyit, kun mitään ei | |
tapahtunutkaan. Ei se mitään, niin pitääkin tapahtua, sillä olimme | |
tekstitilassa. Jotta jotain tapahtuisi meidän pitää olla oikeassa | |
tilassa, joka oli siis 0x13 (heksanumero 13 C:ssä, desimaalimuodossa | |
19). Tämän tilan rakenne onkin seuraava mihin perehdymme. Ole huoleti, | |
valitsin tämän tilan, sillä se on KAIKKEIN yksinkertaisin tila | |
PC-yhteensopivalla tietokoneella. Resoluutio on 320 riviä vaakatasossa | |
ja 200 pystytasossa. Jokaista pikseliä merkitään yhdellä tavulla, eli | |
sillä voi olla 256 erilaista arvoa. Näyttö alkaa aivan ruudun | |
vasemmasta yläkulmasta (miksi? sitä ei kukaan oikein tiedä, menee | |
filosofiaksi) ja jatkuu tavu tavulta (pikseli pikseliltä) päättyen | |
lopulta oikeaan alakulmaan. Eli ensimmäiset 320 tavua ovat ensimmäisen | |
rivin kaikki vaakatasossa olevat pikselit, sitten seuraavat 320 ovat | |
toisen rivin pikselit, kunnes lopulta ollaan ruudun alakulmassa. | |
Ja kun muistamme, että ensimmäinen tavu on kohdassa A0000 (heksa siis | |
tämäkin), eli 0 tavua alusta eteenpäin, niin me voimmekin tehdä hienon | |
kaavion: | |
Pikselit: Sijainti: | |
.......................... | |
0...319 1. rivi | |
320...639 2. rivi | |
... | |
63680...63999 200. rivi | |
Näin meillä onkin hieno kaava, jolla saamme selville pikselin | |
sijainnin: | |
alkio = rivi * 320 + sarake eli: | |
offset = y*320+x | |
Muista, että C:ssä 1. rivi olisi tietenkin rivi numero 0! | |
Nyt yhdistämme tietomme: VGA:n muisti sijaitsee selektorissa _dos_ds, | |
alkaen osoitteesta A0000 (heksa, C:ssä 0xA0000) ja siitä lähtee 64000 | |
tavua, joka on näyttömuisti. Pikselin osoite tässä muistissa voidaan | |
laskea kaavalla y*320+x. Selektorin kanssa voidaan muistia asettaa | |
komennolla _farpokeb(selektori, siirros, arvo). Tarvittava moodi on | |
0x13 ja siinä on 256 väriä ja resoluutio 320 x 200. | |
Mutta miten pääsemme sinne? Vastaus on helppo: conio.h:n funktiolla | |
textmode(moodi)! Ja kun vielä yhdistämme tähän funktion getch(), joka | |
odottaa napinpainallusta (löytyy myöskin kirjastosta conio.h), sekä | |
palaamme lopuksi tekstitilaan (0x3, eli heksa 3, eli desimaali 3) on | |
meillä jo aika kiva ohjelma kasassa (PIXEL2.C): | |
#include <go32.h> /* _dos_ds ! */ | |
#include <sys/farptr.h> /* _farpokeb(selektori, siirros, arvo) */ | |
#include <conio.h> /* textmode(moodi), getch() */ | |
int main() { | |
int selektori=_dos_ds, siirros=0xA0000, y=100, x=160, | |
graffa=0x13, texti=0x3, color=100; | |
textmode(graffa); | |
_farpokeb(selektori, siirros+y*320+x, color); | |
getch(); | |
textmode(texti); | |
return 0; | |
} | |
Tietenkin olisi ollut helpompaa sijoittaa arvo suoraan parametrin | |
kohdalle: | |
textmode(0x13); | |
_farpokeb(_dos_ds, 0xA0000+100*320+160, 100); | |
getch(); | |
textmode(0x3); | |
Mutta katsoin aiemman tavan havainnollisemmaksi. Kaiken tekemiseksi | |
oikein helpoksi teemme tästä pikselinsytytyksestä makron | |
#define-komennolla. Tämä ei hidasta ohjelmaa yhtään, mutta varmasti | |
selventää koodia. Se määrittelee makron putpixel(x, y, c), jonka | |
kääntäjä muuttaa käännösvaiheeksa _farpokeb-funktioksi. x tarkoittaa | |
saraketta väliltä 0-319 ja y riviä väliltä 0-199, sekä c väriä väliltä | |
0-255. Muista, että vaikka teetkin makron sinun pitää silti | |
sisällyttää mukaan kirjastot sys/farptr.h ja go32.h! Sulut makron | |
farpokeb-funktion muuttujien x ja y ympärillä selittyvät sillä, että | |
koska makro puretaan suoraan kutsukohtaan niin esim. komento: | |
putpixel(50, 40+a, 100) purkautuisi muotoon: _farpokeb( _dos_ds, | |
0xA0000+40+a*320+50, 100), joka ei tietenkään ole haluttu tulos, sillä | |
40+a pitää käsitellä ennen sijoitusta, eli sulut vain ympärille! Tässä | |
se siis on: | |
#define putpixel(x, y, c) _farpokeb( _dos_ds, 0xA0000+(y)*320+(x), c) | |
Kun haluat käyttää sitä, niin teet vaikka seuraavanlaisen | |
koodinpätkän (PIXEL3.C): | |
#include <sys/farptr.h> | |
#include <go32.h> | |
#include <conio.h> /* textmode(moodi) ja getch() löytyvät täältä! */ | |
#define putpixel(x, y, c) _farpokeb( _dos_ds, 0xA0000+y*320+x, c) | |
int main() { | |
textmode(0x13); | |
putpixel(319, 199, 150); | |
getch(); | |
textmode(0x3); | |
return 0; | |
} | |
Ohjelma sytyttää pikselin aivan ruudun alareunaan. Jos et enää muista, | |
miten ohjelma käännettiin DJGPP:llä, on tämän kokeilemiseksi | |
tarvittava komento: "GCC PIXEL3.C -o PIXEL3.EXE" ja sitten kokeilu | |
komennolla "PIXEL3". | |
Painu nyt kokeilemaan ohjelmaa ja muuntelemaan sitä! Laita se tekemään | |
ruksi, pystyviiva, vaakaviiva, tai vaikka ympyrä jos osaat, tai | |
yhdistä se randomin kanssa ja tee näytönsäästäjä! Kokeilemalla tulet | |
parhaiten sinuiksi uuden asian kanssa. Ja kun olet valmis, siirrymme | |
seuraavaan aiheeseen, palettiin. | |
2.3 Paletti - hörhelöhameita ja tanssia? | |
---------------------------------------- | |
Kuten edellisessä luvussa opimme, voi tilassa 13h olla 256 erilaista | |
väriä. Teit ehkä jo ohjelman, joka piirtää pikselin jokaisella värillä | |
viivaa ja huomasit, että käytössä olevat värit ovat huonoja, | |
puuttelisia, kirkkaita, tummia tai muuten vain inhottavia. Mutta ei | |
hätää - niitä voi muuttaa! Ja vaikka paletissa ei mielestäsi olisikaan | |
mitään vikaa haluat ehkä tehdä sellaisia efektejä kuten häivytys, | |
plasma, "crossfade" (toinen kuva ilmestyy toisen alta pikkuhiljaa)... | |
Näissä kaikissa tarvitaan enemmän tai vähemmän itse tehtyä palettia ja | |
siksi meidän pitääkin opetella nämä asiat ennenkuin menemme pidemmälle. | |
Kaiken ytimenä on VGA ja sen paletti, etenkin sen asettaminen, mutta | |
ehkä myös sen lukeminen. Tässä luvussa teemme funktiot, yhden tai | |
useamman värin, asettamiseen ja lukemiseen, sekä tutustumme | |
paletinpyöritykseen (palette rotation). | |
Ensin taas vähän teoriaa efektien ja paletin takana. Kuten ehkä | |
tiedätkin, valo voidaan koostaa komponenteista. Tietokoneella | |
jokaisella värillä on yleensä kolme komponenttia: punainen, vihreä ja | |
sininen (red, green, blue). Tätä kutsutaan nimellä RGB. Itseasiassa | |
jokainen moodin 13h väri on vain osoite taulukkoon, jonka jokainen | |
alkio sisältää värin punaisen, virheän ja sinisen komponentin määrän, | |
eli vahvuuden. | |
Jos meillä olisi puhtaan punainen väri, sen arvot olisivat seuraavat: | |
r=63, g=0 ja b=0. Sininen taas olisi 0,0 ja 63. Violetti, joka on | |
sinisen ja punaisen yhdistelmä, voisi olla vaikkapa 63,0 ja 63 (eli | |
täysi määrä punaista ja sinistä). Jos taas haluaisimme tumman punaisen | |
värin, olisivat sen väriarvot vaikka 30, 0, 0. Koska 30 on vähemmän | |
kuin puolet kirkkaan punaisen puna-arvosta, on tämä väri siis yli | |
puolet tummempi! Helppoa! Ja miksi maksimimäärä on vain 63? Siksi, | |
koska VGA:n rekistereissä värille on varattuna vain 6 bittiä, jolla | |
voidaan esittää numerot välillä 0...63. Tämä joudutaan huomioimaan | |
esim. PCX:n paletin latauksessa, sillä siinä värit ovat välillä | |
0...255. Tässä joudutaan jakamaan väriarvot neljällä, jotta saadaan | |
toimiva luku. | |
Eli ymmärrämme nyt, että jokaisella värillä on itseasiassa punainen, | |
vihreä ja sininen komponentti, mutta mitä siitä? Vastaus on helppo, | |
jos haluamme, voimme muuttaa mitä tahansa tilan 0x13 (tai miksei | |
muunkin tilan) väriä helpolla joukolla komentoja. Meidän tarvitsee | |
vain kirjoittaa asetettavan värin numero porttiin 3C8h (h lopussa | |
siis tarkoittaa heksalukua, C:ssä 0x3C8) ja sitten porttiin 3C9 ensin | |
punainen komponentti, sitten vihreä komponentti ja lopuksi sininen | |
komponentti. Tämän jälkeen VGA korottaa väri-indeksiä automaattisesti | |
yhdellä, eli jos ensin syötämme porttiin 3C8h värinumeron 5 ja sitten | |
punaisen, virheän ja sinisen porttiin 3C9h korottuu VGA:n sisäinen | |
laskuri yhdellä, ja voimme halutessamme tunkea heti seuraavan värin | |
RGB arvot porttiin 3C9. | |
Nyt olemme jauhaneet teoriaa tarpeeksi. Menkäämme pikkuiseen | |
esimerkkiin. Esittelemme tietorakenteen RGB, joka sisältää värin | |
RGB-arvot ja sitten funktion, jolle annetaan parametrinä osoitin | |
tällaiseen rakenteeseen ja värin numero jolle nämä väriarvot | |
asetetaan. Myöhemmin yhdistämme tämän pieneen esimerkkiohjelmaamme, | |
mutta (PALETTE.H): | |
typedef struct { | |
char red; | |
char green; | |
char blue; | |
} RGB; | |
void setcolor(int index, RGB *newdata) { | |
outportb(0x3C8, index); | |
outportb(0x3C9, newdata->red); | |
outportb(0x3C9, newdata->green); | |
outportb(0x3C9, newdata->blue); | |
} | |
Huomiosi ehkä kiinnittyy vielä outoon funktioon outportb, jolle | |
annetaan ensimmäisenä portin numero ja sitten sinne syötettävä | |
tavu. Funktion käyttämiseksi sisällytät mukaan kirjaston dos.h. | |
Ehkä sinua kiinnostaisi myös tämän käyttö? No olkoon, tehkäämme | |
esimerkkiohjelma kokonaisuudessaan. Kun edellinen pikku koodinpätkä on | |
nimellä PALETTE.H, voimme helposti sisällyttää sen seuraavaan | |
esimerkkiohjelmaamme kuten ihan tavallisen kirjaston. Muista vain, | |
että kirjaston täytyy olla samassa hakemistossa ohjelman kanssa, | |
muuten ei esimerkki käänny. Eli tässä sitten itse koodiosa, joka | |
tuikkaa keskelle ruutua värin 50. Sitten se odottaa napinpainallusta | |
ja muuttaa funktiollamme värin punaiseksi. Huomaa, että vain alussa | |
kajotaan näyttömuistiin. Toinen kohta hoidetaan värinvaihdolla! | |
Eli (PAL1.C): | |
#include <conio.h> | |
#include <sys/farptr.h> | |
#include <go32.h> | |
#include <dos.h> | |
#include "palette.h" | |
#define putpixel(x, y, c) _farpokeb(_dos_ds, 0xA0000+y*320+x, c) | |
int main() { | |
RGB newcolor; | |
textmode(0x13); | |
putpixel(160, 100, 50); | |
getch(); | |
newcolor.red=63; | |
newcolor.green=0; | |
newcolor.blue=0; | |
setcolor(50, &newcolor); | |
getch(); | |
textmode(0x3); | |
return 0; | |
} | |
Seuraavana huomionkohteenamme onkin sitten väriarvojen luku, joka on | |
yhtä suoraviivaista kuin edellinenkin (tosin tarpeellisuus on | |
kyseenalaista, tätä ei tarvitse jos on itse asettanut paletin). | |
Erotuksena on, että väriarvo kirjoitetaankin porttiin 3C7h ja portista | |
3C9h _luetaan_ värin arvo. Jälleen tripletin (kolme alkiota, RGB) luvun | |
jälkeen indeksi kohoaa, joten voisimme lukea seuraavat värit. Luku | |
portista tapahtuu funktiolla inportb(portti). Muuta tietoa emme | |
tarvitsekaan. | |
Lisätkäämme nyt kirjastoomme (PALETTE.H) kolme uutta funktiota. | |
getcolor(int index, RGB *color) lukee värin <index> väriarvot ja | |
asettaa ne RGB-rakenteeseen <color>. setpal(char *palette) asettaa | |
koko paletin kerralla hyväksikäyttäen automaattista indeksin korotusta | |
(indeksi nollataan aluksi ja syötetään koko data perään, indeksi | |
korottuu jokaisen rgb-arvon jälkeen). getpal(char *palette) taas lukee | |
vastaavasti koko paletin. Niiden käytöstä sitten | |
esimerkkiohjelmassamme, joka seuraa ajallaan. Eli uutuudet kirjastoon | |
PALETTE.H: | |
void getcolor(int index, RGB *color) { | |
outportb(0x3C7, index); | |
color->red=inportb(0x3C9); | |
color->green=inportb(0x3C9); | |
color->blue=inportb(0x3C9); | |
} | |
void setpal(char *palette) { | |
int c; | |
outportb(0x3C8, 0); | |
for(c=0; c<256*3; c++) | |
outportb(0x3C9, palette[c]); | |
} | |
void getpal(char *palette) { | |
int c; | |
outportb(0x3C7, 0); | |
for(c=0; c<256*3; c++) | |
palette[c]=inportb(0x3C9); | |
} | |
Kuten huomasit, ei viimeisissä funktiossa ole lainkaan enää | |
RGB-rakennetta. Tämä siksi, että koko paletti on huomattavasti | |
helpompi käsitellä näin. Jos olet sitä mieltä, että RGB oli parempi | |
tai haluat muuttaa loputkin pointtereiksi, en sitä | |
estä. Char-pointteriversiossa on aina kolme tavua peräkkäin | |
ilmoittamassa RGB-triplettiä. Toisen värin r alkaa siis 4. tavusta, | |
eli indeksistä 3. Jos haluat jonkin värin r-arvon, niin lasket: | |
"palette[number*3+0]". Vihreällä korotat tuota yhdellä (number*3+1) ja | |
sinisen kanssa kahdella. Helppoa tämäkin. | |
Nyt on kaikki tärkein katettu VGA:n paletista, joten kysytkin ehkä | |
(aina sinä sitten olet kysymässä ;) mihin näitä nyt sitten voi | |
käyttää. Itseasiassa paletilla on loputtomasti | |
käyttömahdollisuuksia. Ensimmäinen on 256-väristen kuvien paletin | |
asettaminen, sillä väärällä paletilla kuvat yleensä näyttävät enemmän | |
tai vähemmän sotkulta. Toisena on häivytysefekti, sekä feidaus | |
valkoiseen. Palettiliutuksesta käytetään usein termiä feidaus, joka | |
tarkoittaa, että palettia liutetaan sävy sävyltä toiseen väriin, | |
jolloin saadaan vaikka hieno ruudun tummeneminen. Kokeilemmekin sitä | |
ihan kohta, kunhan selitän vielä yhden efektin, palettirotaation. | |
Palettirotaatiossa on paletti, jonka väriarvoja pyöritetään | |
ympäri. Eli käytännössä väri, joka ennen oli numerolla 5 onkin | |
rotaation jälkeen värinumerossa 6. Tätä jatketaan koko ajan, ja väri | |
matkaa koko paletin lävitse, ja kun se on lopussa niin se siirretään | |
paletin alkuun. Yleensä väriä 0 ei kuitenkaan siirretä, sillä se on | |
taustaväri ja yleensä musta. Usein käytetään myös palettia, jossa on | |
useampia värejä kuin 256, jolloin erona on vain se, että ainoastaan | |
osa väreistä näkyy ruudulla. | |
"JA MIHIN TÄTÄ", kuulen sinun kysyvän. Olet kenties nähnyt plasman, | |
jonka värit vaihtuvat koko ajan (kunnon plasmassa on kyllä lisäksi | |
mukana muutakin kuin pyörivä paletti, mutta pyörityksellä saadaan | |
kummasti lisäeloa muuten liikkuvaan plasmaan). Tai tunnelin, jossa | |
värit siirtyvät kauemmaksi tai lähemmäksi. Tällaisia efektejä voidaan | |
helposti toteuttaa palettirotaatiolla. Ennenkuin ymmärrät voit ehkä | |
tarvita pienen demonstraation. Kohta teemmekin esimerkin, joka piirtää | |
vaakatasossa viivoja, jokainen eri värillä alkaen yhdestä päättyen | |
255:teen. Sitten teemme hienon liukupaletin ja alamme pyörittämään | |
sitä. Eli tehkäämme vielä funktio (lisätään kirjastoon PALETTE.H): | |
void rotatepal(int startcolor, int endcolor, char *pal) { | |
char r, g, b; | |
int c; | |
r=pal[startcolor*3+0]; /* tallennamme ensimmäiset värit ja siirrämme */ | |
g=pal[startcolor*3+1]; /* ne lopuksi loppuun. Tämä paletti pyörii siten, */ | |
b=pal[startcolor*3+2]; /* että viimeinen väri kulkeutuu kohti alkua */ | |
for(c=startcolor*3; c<endcolor*3; c++) | |
pal[c]=pal[c+3]; /* muista, että uusi väri on kolmen välein, | |
sillä välissähän on aina kolme tavua, r, | |
g ja b, joita ei saa sekoittaa, muuten | |
saisimme aikaan vaikkapa sinisen paloauton! | |
(kiinnostava tavoite sinänsä) */ | |
pal[endcolor*3+0]=r; | |
pal[endcolor*3+1]=g; | |
pal[endcolor*3+2]=b; | |
} | |
Vielä ennen esimerkkiä tarvitsemme yhden rutiinin, joka tekee | |
efektistämme edes jotenkin siedettävän. Palettia pitää nimittäin | |
vaihtaa ennen kuin ruudulle aletaan piirtää, tai muuten voi edessä | |
olla aika huonolaatuinen efekti (normaalipaletissa ei ole mitään | |
väriliukuja). Varsinkin näin yksinkertaisessa ohjelmassa voi nopealla | |
näytönohjaimella/koneella nopeus olla liiankin suuri, joten hidastamme | |
vähän rutiinia odottamalla signaalia, jonka VGA antaa päästessään | |
ruudun loppuun ja lähtiessään palaamaan yläkulmaan aloittaakseen taas | |
piirron. Tähän teemme funktion, joka odottaa kunnes piirto on valmis | |
ja kuvaruudulle voi kopioida pelkäämättä kesken piirron muutoksia | |
tehdessä aiheutuvia ongelmia. Lisätkäämme seuraava funktio kirjastoon | |
PALETTE.H: | |
void waitsync() { | |
while( (inportb(0x3DA)&8) != 0); | |
while( (inportb(0x3DA)&8) == 0); | |
} | |
Nyt sitten hienoon esimerkkiohjelmaamme, joka piirsi niitä viivoja ja | |
pyöritti palettia. Huomaa funktio genpal(char *palette), joka asettaa | |
paletin liukuväreillä tehdyksi, sekä waitsync()-funktion käyttö | |
(kokeile vaikka ilman waitsync():iä, niin näet eron)! Eli tässä se | |
olisi (PAL2.C): | |
#include <conio.h> | |
#include <sys/farptr.h> | |
#include <go32.h> | |
#include <dos.h> | |
#include "palette.h" | |
#define putpixel(x, y, c) _farpokeb(_dos_ds, 0xA0000+y*320+x, c) | |
void genpal(char *palette) { | |
char r=0, g=0, b=0; | |
int c, color=0; | |
for(c=0; c<64; c++) { /* MUSTA (0,0,0) - PUNAINEN (63,0,0) */ | |
palette[color++]=r; | |
palette[color++]=g; | |
palette[color++]=b; | |
if(r<63) r++; | |
} | |
for(c=0; c<64; c++) { /* PUNAINEN (63,0,0) - VIOLETTI (63,0,63) */ | |
palette[color++]=r; | |
palette[color++]=g; | |
palette[color++]=b; | |
if(b<63) b++; | |
} | |
for(c=0; c<64; c++) { /* LILA (63,0,63) - VALKOINEN (63,63,63) */ | |
palette[color++]=r; | |
palette[color++]=g; | |
palette[color++]=b; | |
if(g<63) g++; | |
} | |
for(c=0; c<64; c++) { /* VALKOINEN (63, 63, 63) - MUSTA (0,0,0) */ | |
palette[color++]=r; | |
palette[color++]=g; | |
palette[color++]=b; | |
if(r) r--; | |
if(g) g--; | |
if(b) b--; | |
} | |
} | |
int main() { | |
int x, y; | |
char palette[256*3]; | |
textmode(0x13); | |
genpal(palette); | |
setpal(palette); | |
for(y=0; y<200; y++) for(x=0; x<320; x++) | |
putpixel(x, y, y); | |
while(!kbhit()) { | |
rotatepal(1, 255, palette); | |
waitsync(); /* odotetaan että piirto on valmis ennen uuden | |
paletin asettamista! */ | |
setpal(palette); | |
} | |
getch(); | |
textmode(0x3); | |
return 0; | |
} | |
Huomasit varmaan, että ruudun onnettoman geometrian takia kaikki värit | |
EIVÄT mahtuneet ruudulle. No niin. Ja mitäs kivaa seuraavaksi? | |
Seuraavaksi tutustumme viimeiseen palettikikkaan, jonka periaatteen | |
olet jo voinut keksiäkin, eli feidauksen. | |
Genpal-funktio olisi voinut käyttää myös erillistä rutiinia jolle | |
annetaan parametreina monenko värin matkalla liu'utaan väristä toiseen. | |
Kuitenkin koska tuo oli yksinkertaisemman näköinen tein sen tuolla | |
tapaa. | |
Teemme minimaalisia lisäyksiä PALETTE.H:hon, sekä pikkuisen | |
esimerkkiohjelman, joka demonstroi efektiä käytännössä. Ideahan on | |
erittäin yksinkertainen. Meillä on paletti, jossa on sekailaisia | |
värejä ja haluamme häivyttää sen. Miten? No tietenkin muuttamalla | |
ruudun mustaksi. Miten se tapahtuu? Nollaamme jokaisen värin, mutta | |
emme kerralla, vaan vähennämme joka kierroksella ja asetamme uuden | |
paletin. Tästä funktiosta voit tehdä helposti muitakin efektejä, | |
kuten feidauksen valkoiseen (korotetaan jokaista väriä joka | |
kierroksella kunnes ollaan värissä 63) tai vaikka paletista toiseen | |
(jos kohdepaletin vastaava komponentti on suurempi niin korotetaan | |
arvoa, jos pienempi niin vähennetään). Esittelen tässä vain | |
häivytyksen, mutta löydät kirjastosta PALETTE.H toteutettuna myös | |
valkoiseen ja toiseen palettiin feidauksen. Voit myös itse tehdä | |
hauskoja efektejä, kuten feidata valkoiseen, tehdä valkoisen paletin | |
ja feidata sen mustaan. Kokeile! Mutta, tässä rutiinimme: | |
void fadetoblack(char *palette) { | |
char temppal[256*3]; | |
int c, color; | |
memcpy(temppal, palette, 256*3); | |
for(c=0; c<63; c++) { /* tarvitsemme maksimissaan 63 muutosta */ | |
for(color=0; color<256*3; color++) | |
if(temppal[color]) temppal[color]--; | |
waitsync(); | |
setpal(temppal); | |
} | |
} | |
Sitten yhdistämme efektin lopuksi edelliseen esimerkkiohjelmaamme | |
lisäämällä sen juuri ennen tekstitilaan vaihtoa: | |
fadetoblack(palette); | |
Kokonaisuudessaan ja toimivana, vanhat osat mukana on esimerkkimme | |
tiedostossa PAL3.C. Siihen on tehty myös pari muuta muutosta, kuten | |
se, että aluksi paletti feidataan valkoiseen, asetetaan oikeasti val- | |
koiseksi (muuten feidatessa mustaan paletti välähtää hetken normaaliväri- | |
senä, tätäkin SAA kokeilla). | |
No niin. Pahin tiedonnälkäsi lienee tältä erältä tyydytetty! Viihdy | |
esimerkkien parissa ja tee mitä vain mieleen juolahtaa niillä. Muista, | |
että palettifunktiot toimivat myös tekstitilassa. Tämän voit kokeilla | |
vaikka käyttämällä fadetoblack-funktiota. Muista kuitenkin laittaa | |
loppuun textmode(0x3), vaikket moodia olisi vaihtanutkaan, sillä et | |
välttämättä pidä DOS-kehotteestasi jokainen väri mustana... | |
3.1 Kaksoispuskuri - luonnonoikku, horoskooppi? | |
----------------------------------------------- | |
No niin, olet näemmä sulattanut jo kaiken edellisen tiedon. Mainiota! | |
Tänään pääsemme (tai miten nyt haluamme asian ilmaista) yhteen | |
peliohjelmoinnin perustempuista, kaksoispuskuriin. Periaate tämän | |
takana on aivan naurettavan yksinkertainen, ja itseasiassa minä opin | |
tämän erään lehden lähdekoodia vilkaisemalla (Mikrobitin | |
grafiikkaohjelmointikurssi, numero 11/95). Eli tähän asti olemme | |
tunkeneet grafiikkaamme suoraan näyttöpuskuriin tavu | |
kerrallaan. Valitettavasti tässä on haittoja. Ensimmäisenä on se, että | |
meillä on kiire. Nimittäin käytössä on vain lyhyt aika kun näyttöä ei | |
piirretä monitorille ja jos siinä ajassa ei ehdä piirtää näyttöä niin | |
näyttö alkaa välkkymään, ilmestyy lumisadetta (varsinkin paletinvaihdon | |
kanssa!) ja muitakin ei-toivottavia ilmiöitä esiintyy. | |
Lisäksi on todettava valitettava tosiasia: Näyttömuisti on | |
HIDASTA. Jos haluamme tehdä sen kaikkein tehokkaimmin niin kopioimme | |
kaiken tavaran kerralla näytölle. Eli sen sijaan, että läiskisimme | |
pikseleitä sinne, toisia tänne kopioimme tavaran näytölle näytön | |
alusta loppuun neljän tavun (kaksoissana) kokoisina palasina. Mutta | |
miten saamme ruudulle pikseleitä sinne tänne, kun kaikki pitäisi | |
kopioida kerralla? Vastaus on, että käytämme kaksoispuskuria! | |
Kaksoispuskuri, englanniksi doublebuffer on saman kokoinen kuin | |
näyttömuisti, mutta sille on varattu tilaa keskusmuistista, joten se | |
on nopeampaa kuin hidas, kortilla sijaitseva näyttömuisti (näin vain | |
on, uskokaa pois). Sinne pikselinpiirto tapahtuu huomattavasti | |
sutjakammin, ja kaiken lisäksi meillä ei ole mitään kiirettä. Vaikka | |
piirrämme uuden pikselin, ei se näy näytöllä ennenkuin kaksoispuskuri | |
on kopioitu, eli flipattu näyttömuistiin. | |
DJGPP:llä näyttömuisti varataan vaikka malloc-käskyllä ja vapautetaan | |
suorituksen loppuessa free-käskyllä. Kokoa pitää puskurilla olla | |
tilassa 13h 64000 tavua. Eroja oikeaan näyttömuistiin | |
kaksoispuskurissa on DJGPP:llä: | |
- Se on nopeampaa. | |
- Se sijaitsee omassa muistissa, joten se voidaan taulukoida. Ei | |
enää putpixel-makroja, vaan dblbuf[y*320+x]=color. | |
- Se voidaan kopioida nopealla _dosmemputl-rutiinilla, joka on | |
viimeiseen saakka optimoitu (hidas se on siltikin, mutta se on | |
näyttökortin ja VGA:n rakenteen vika.) | |
- Se ei näy ruudulla ennenkuin käsketään. | |
- Se ei vilku. | |
- Se säilyy muistissa vaikka käytäisiin tekstitilassa. | |
- Paljon muuta kivaa. | |
Voit käyttää myös dynaamisen muistinvarauksen (malloc tai C++:ssalla | |
new-operaattori) tilalla taulukkoa, kuten joissakin esimerkeissä on | |
tehty, tällöin käytät muotoa char dblbuf[64000] (tai unsigned | |
char...). Mallocin käyttö on kuitenkin suositeltavampaa kuin tällainen | |
valtavien taulukoiden ottaminen pinosta. | |
Muttamutta, tarvitsisimme esimerkin. Mistä saamme sellaisen? No tässä | |
pieni esimerkki. Mukana on makro flip(char *buffer), joka kopioi 64000 | |
tavua puskuria näyttömuistiin DJGPP:n _dosmemputl-komennolla, joka | |
löytyy kirjastosta sys/movedata.h ja tarvitsee myös _dos_ds:ää ja | |
siten kirjastoa go32.h. Eli tässä tällaista (DOUBLE1.C): | |
#include <go32.h> | |
#include <sys/movedata.h> | |
#include <time.h> | |
#include <stdlib.h> | |
#include <conio.h> | |
#include <dos.h> | |
#include <stdio.h> | |
#define flip(c) _dosmemputl(c, 64000/4, 0xA0000) | |
char *dblbuf; | |
void varaamuisti() { | |
dblbuf=(char *)malloc(64000); | |
if(dblbuf==NULL) { | |
printf("Ei tarpeeksi muistia kaksoispuskurille!\n"); | |
exit(1); | |
} | |
} | |
int main() { | |
int x, y; | |
varaamuisti(); | |
srand(time(NULL)); /* alustetaan satunnaislukugeneraattori */ | |
textmode(0x13); | |
while(!kbhit()) { | |
for(y=0; y<200; y++) | |
for(x=0; x<320; x++) | |
dblbuf[y*320+x]=rand()%256; | |
flip(dblbuf); | |
} | |
getch(); | |
textmode(0x3); | |
return 0; | |
} | |
Kokeile myös ohjelmaa DOUBLE2.C, joka on toteutettu ilman | |
kaksoispuskurointia, jos eroa ei vielä huomaa, tulee se | |
joka tapauksessa vielä esiin, ja on muitakin hyödyllisiä asioita missä | |
kaksoispuskuri, tai kolmoispuskurikin on tarpeen. Mutta, kokeile tämän | |
käyttöä ja palaa tämän dokumentin pariin VASTA kun osaat täydellisesti | |
kaksoispuskurin käytön (oikeammin ymmärrät miten se toimii, miten sitä | |
käytetään, mihin se perustuu ja miten siihen piirretään | |
pisteitä). Sitten syöksymmekin uuteen tuntemattomaan. Katsotaan nyt | |
mihin... | |
3.2 PCX-kuvien lataus - vain vähän oikaisemalla | |
----------------------------------------------- | |
Noniin, kaikki wannabe gamekooderit. Nyt on aika mennä vaikeimpaan | |
aiheeseemme, johon monen kooderin taidot ovat viimein tyssänneet ja | |
jota minäkään en vielä täysin ymmärrä, enkä tiedä osaanko sitä | |
selittää. | |
Se on hyväuskoisuus, sillä PCX:n sisältä löytyy looginen ja helposti | |
ymmärrettävä rakenne. Ja vaikkei sitäkään täysin ymmärrä, voi | |
aina vain käyttää samaa rutiinia (kuten minä) PCX:n lataamiseen. | |
Esittelenkin tässä kappaleessa lyhyesti tämän yhden yleisimmistä | |
kuvaformaateista olevan tiedostotyypin saloja. 256-värisen tyypillisen | |
PCX:n rakenne voidaan jakaa karkeasti neljään (4) osaan: | |
- 128 ensimmäistä tavua headeria sisältäen info kuvasta | |
- kuvadata RLE-pakattuna | |
- salaperäinen tavu 12 | |
- palettidata, viimeiset 768 tavua | |
Ensimmäisenä ja kaikkein vaikeimpana on headeri, jonka loikimme lähes | |
kokonaan yli, sillä tosipelikooderi tietää lataavansa oikeaa | |
PCX-kuvaa, joka on oikeaa formaattia oikeankokoiseen puskuriin ja | |
jättää selittämättömät kaatumiset muiden harteille! Tai itseasiassa en | |
sitä selitä kun en siihen ole perehtynyt syvemmin. Kiinnostuneille | |
PCGPE:ssä on tämäkin formaatti selitettynä lahjakkaan kryptisesti | |
englannin kamalalla mongerruksella. Kaikki sitä haluavat hankkivat sitten | |
tiedoston PCGPE10.ZIP, joka sisältää kaikkea hyödyllistä | |
peliohjelmointiasiaa, englanniksi siis. | |
Headerista tahdomme tietää vain sen, että PCX-kuvan koko lasketaan | |
seuraavasti: | |
- Mennään offsettiin 8 (fseek(handle, 8, SEEK_SET)). | |
- Luetaan kaksi tavua ja tehdään niistä sana (unsigned short int, | |
katsomme latauskoodia kohta) ja meillä on koko vaakatasossa. | |
- Luetaan toiset kaksi tavua ja tehdään niille samoin kuin | |
edellisille, nyt meillä on y-koko. | |
Sitten onkin vaikein pala PCX:n rakenteessa. Sitä kutsutaan nimellä | |
RLE-koodaus (run length encoding) ja se tarkoittaa sitä, että jos | |
meillä on peräkkäin 10 pikseliä väriä 15 emme kirjoitakaan PCX:ään | |
kymmentä kertaa numeroa 15, vaan kirjoitamme sinne tavun 192+10=202 ja | |
sen perään tavun 15. Nyt kun PCX-lukija lukee ensimmäisen tavun se | |
katsoo, että ahaa, nyt tulee toistoa ja toistaa seuraavaa tavua | |
puskuriin tavu-192 kertaa (202-192=10). Näin me teemmekin | |
yksinkertaisen pseudorungon: | |
- Lue tavu1 | |
- Jos tavu1 on suurempi kuin 192 niin lue tavu2 ja toista tavua 2 | |
tavu1-192 kertaa. | |
- Jos tavu1 ei ollut suurempi laita puskuriin tavu1. | |
Näin helppoa, nyt vielä paletti. Sekin on helppoa, kunhan muistamme | |
kaksi seikkaa: | |
1) Etsimme paletin tiedoston LOPUSTA päin (fseek(handle, -768, SEEK_END)) | |
2) Jaamme värikomponentit neljällä, sillä PCX:ssä väriarvot ovat | |
väliltä 0-255, VGA:ssa 0-63 (255/4=63). | |
Nyt yhdistämme taas kaiken tietomme, ja teemme funktion, joka ottaa | |
argumenttinaan PCX:n nimen ja puskurin jonne se ladataan. Ohjelma EI | |
VARAA MUISTIA puskurille, vaan se pitää varata etukäteen. Voit itse | |
tehdä muutokset ohjelmaan jos haluat. Yleensä kuitenkin etukäteen on | |
tiedossa kuvan koko, kun PCX:iä käytetään | |
peleissä. Kuvankatseluohjelmaa tehdessä pitää kuitenkin koko ottaa | |
selville jo viimeistään sen vuoksi, että kuva näytetään oikein, vaikka | |
puskurissa olisikin tilaa. | |
Eli tässä meillä on valmiiksi pureskeltu PCX-lataajan runko, teemme | |
sille oikein oman kirjaston PCX.H. Kirjasto tarvitsee stdio.h:n | |
tiedostonkäsittelyfunktioita ja niiden tietorakenteita: | |
void loadpcx(char *filename, char *buffer) { | |
int xsize, ysize, tavu1, tavu2, position=0; | |
FILE *handle=fopen(filename, "rb"); | |
if(handle==NULL) { | |
printf("Virhe PCX-tiedoston avauksessa: Tiedostoa ei löydy!\n"); | |
exit(1); | |
} | |
fseek(handle, 8, SEEK_SET); | |
xsize=fgetc(handle)+(fgetc(handle)<<8)+1; | |
ysize=fgetc(handle)+(fgetc(handle)<<8)+1; | |
fseek(handle, 128, SEEK_SET); | |
while(position<xsize*ysize) { | |
tavu1=fgetc(handle); | |
if(tavu1>192) { | |
tavu2=fgetc(handle); | |
for(; tavu1>192; tavu1--) | |
buffer[position++]=tavu2; | |
} else buffer[position++]=tavu1; | |
} | |
fclose(handle); | |
} | |
void loadpal(char *filename, char *palette) { | |
FILE *handle=fopen(filename, "rb"); | |
int c; | |
if(handle==NULL) { | |
printf("Virhe PCX-tiedoston palettia luettaessa:" | |
" Tiedostoa ei löydy!\n"); | |
exit(1); | |
} | |
fseek(handle,-768,SEEK_END); | |
for(c=0; c<256*3; c++) | |
paletti[c] =fgetc(handle)/4; | |
fclose(handle); | |
} | |
Kuten jo varmasti huomasit ovat paletin ja PCX:n latausrutiinit | |
erillisinä. Tämä siksi, että joskus on huomattavasti kätevämpää ladata | |
vain kuva, jos palettia ei mihinkään tarvita. Seuraavaksi seuraa | |
kappaleen esimerkkiohjelma, joka käyttää hyväkseen tutoriaalin | |
varrella esiteltyjä rutiineja ja muodostaa pienen esityksen. Ohjelma | |
lataa PCX-kuvan PICTURE.PCX ja paletin siitä. Sitten se läiskäisee sen | |
ruudulle. Lopuksi kuva himmenee tyhjyyteen ja palataan | |
tekstitilaan. Esimerkki olettaa kuvan olevan kokoa 320x200, | |
256-värinen ja paletin sisältävä PCX-kuva RLE-pakattuna. Voit korvata | |
kuvan millä haluat joko muuttamalla lähdekoodia tai kopioimalla oman | |
kuvasi PICTURE.PCX:n päälle. | |
Huomaa, että ohjelmassa luodaan kaksoispuskuri, johon kuva | |
ladataan. Näyttömuistin vänkääminen parametriksi aiheuttaa 100% | |
varmasti kaatumisen, tai jos jotenkin säästyt siltä niin ainakaan | |
mitään ei ilmesty näytölle. Mutta asiaan (PCX1.C): | |
#include <go32.h> | |
#include <conio.h> | |
#include <stdio.h> | |
#include <sys/movedata.h> | |
#include <dos.h> | |
#include "palette.h" | |
#include "pcx.h" | |
#define flip(c) _dosmemputl(c, 64000/4, 0xA0000) | |
int main() { | |
char palette[256*3]; | |
char dblbuf[64000]; | |
textmode(0x13); | |
loadpcx("PICTURE.PCX", dblbuf); | |
loadpal("PICTURE.PCX", palette); | |
setpal(palette); | |
flip(dblbuf); | |
getch(); | |
fadetoblack(palette); | |
textmode(0x3); | |
return 0; | |
} | |
Toivottavasti ymmärsit tästä luvusta ainakin käyttöperiaatteen. Eli | |
loadpcx(nimi, puskuri) lataa kuvan puskuriin ja flip(puskuri) laittaa | |
sen näytölle (jos kuva on kokoa 320x200). Paletti ladataan tarvittaessa | |
funktiolla loadpal(nimi, palettipuskuri) ja asetetaan aktiiviseksi | |
komennolla setpal(palettipuskuri). Huomaa, että esimerkissä asetetaan oikea | |
paletti ENNEN kuvan laittamista ruudulle. Huomataksesi miksi vaihda | |
setpal- ja flip-funktioiden paikkaa ja lisää väliin getch(), jotta ehdit kat- | |
sella rauhassa muutosta. Tällaista tässä kappaleessa. Mene nyt kokeilemaan | |
PCX-kuvien latausta. Seuraavassa kappaleessa tutustummekin sitten johonkin | |
peliohjelmoijaa lähellä olevaan asiaan... | |
4.1 Bitmapit - eikai vain suunnistusta? | |
--------------------------------------- | |
Tänään siis teemme pienen bitmap-enginen C:llä. Itse olen aiemmin tehnyt | |
kaikki sprite- ja bitmap -rutiinini C++:ssalla, mutta tällä kertaa | |
käytämme C:tä, sillä haluan näiden esimerkkien toimivan ilman plussiakin. | |
Eli mitä on bitmap? | |
Bitmap, eli bittikartta on määrätyn kokoinen suorakulmion muotoinen esine, | |
jolla on puskuri muistissa sisältäen sen värit, kuten näyttöpuskurinkin | |
kanssa on. Hyödylliseksi bitmapin tekee se, että laitamme siihen pyyhkimis- | |
ja piirtotoiminnot, sekä liikutustoiminnot, joilla voimme siirrellä bitmap- | |
piamme ympäri ruutua. Lisäksi teemme siihen värin, joka tarkoittaa ettei | |
sitä kohtaa bitmapista tarvitse kopioida ruudulle. Näin saamme tehtyä bit- | |
mappiimme reikiä, eli teemme sen osittain läpinäkyväksi. Mutta miten tämä | |
kaikki sitten tehdään? Koko asia on, kuten kaikki asiat ohjelmoinnissa lo- | |
pulta ovat - naurettavan helppo. | |
Eli, menkäämme takaisin kaksoispuskurin aikoihin. Siinä meillä on | |
puskuri, jonka koko on 320x200 pikseliä ja se kopioidaan kokonaan näytön | |
päälle. Bittikartassa on muutama selkeä ero: | |
- Se voi alkaa mistä tahansa kohdasta ruutua, vaikka koordinaateista | |
15, 123. | |
- Se voi olla minkä kokoinen tahansa (yleensä kuitenkin ruutua pienempi). | |
- Sen peittämä tausta tallennetaan ja palautetaan kun bittikartta | |
pyyhitään pois, mikä mahdollistaa liikuttelemisen. | |
- Siinä on läpinäkyvä väri, meillä 0, jota ei piirretä ruudulle. Jos siis | |
koko bittikartta olisi väriä 0, emme näkisi ruudulla mitään! | |
Eli itseasiassa bittikartta on pari puskuria, joille on varattu tilaa | |
siten, että jokainen bittikartan väri voidaan säilöä | |
puskuriin. Puskureita on perusbittikartassa kaksi, eli itse kuvan | |
sisältävä kartta, joka on järjestelty aivan samoin kuin | |
esim. kaksoispuskuri, mutta koko on bittikartan mukainen. Toinen on | |
taustapuskuri, joka on muuten sama, mutta sinne vain säilötään | |
piirrettäessä alle jääneet pikselit, jotta ne voidaan bittikarttaa | |
ruudulta pyyhkiessä palauttaa sieltä. | |
Eli tällainen voisi olla 3x3 kokoinen bittikartta: | |
Bittikartta: Taustapuskuri (mitä bittikartan alle on | |
piirrettäessä jäänyt): | |
30 20 19 0 0 0 | |
19 23 42 0 0 0 | |
12 32 43 0 0 0 | |
Kuten huomaatte bittikartta on piirretty mustalle pohjalle, sillä | |
taustapuskuri eli se mitä bittikartan alle jäi on täynnä mustaa, eli | |
väriä 0. Bittikartta on kaikkein helpointa määritellä omaan | |
datarakenteeseensa, joka sisältää tarvittavat tiedot kartan piirtelyyn | |
ja pyyhkimiseeen, nimetään se vaikka structiksi BITMAP. | |
Koordinaattien määrittely saavutetaan siten, että meillä on rakenteessamme | |
X-ja Y-koordinaatit, joista piirto kaksoispuskuriin aloitetaan. Koko | |
taas on helpompi. Jos kaksoispuskurin koko oli 320x200, niin kaava | |
oikean pikselin hakemiseksi oli y*320+x. Jos meillä on bitmap kokoa | |
ysize * xsize, niin oikea koordinaatti on y*xsize+x. Piirrettäessä | |
loopataan X:ää ja Y:tä siten, että luemme yksi kerrallaan pikselin | |
bittikartasta, ja jos se on jokin muu kuin väri 0 (yleensä musta, tämä | |
oli siis läpinäkyväksi sovittu väri), otamme ensin sen alle jäävän | |
pikselin talteen taustapuskuriin ja laitamme sitten vasta bittikartan | |
värin ruudulle oikeaan kohtaan (bittikartan värit sisältävästä | |
puskurista). | |
Eli tarvittavat tiedot bittikarttarakenteeseen ovat: | |
- bittikartan värit (char * -pointteri) | |
- taustan värit (char * -pointteri) | |
- x-sijainti ruudulla (int) | |
- y-sijainti ruudulla (int) | |
- koko x-suunnassa (int) | |
- koko y-suunnassa (int) | |
Lisäksi meillä on xspeed ja yspeed, joita käytetään esimerkeissä | |
säilömään bittikartan liikenopeutta x- ja y-suunnassa. Näillä | |
tempuilla meillä on nyt teoria liikuteltavan bitmapin tekemiseksi. | |
Ensin määrittelemme rakenteen, joka sisältää kaiken tarvittavan tiedon | |
bittikartastamme (BITMAP.H): | |
typedef struct { | |
char *bitmap; | |
char *background; | |
int x; | |
int y; | |
int xsize; | |
int ysize; | |
int xspeed; | |
int yspeed; | |
} BITMAP; | |
Sitten tehtävänämme on tehdä "interface", eli käyttöliittymä | |
bitmap-engineemme. Siihen sisällytämme seuraavat funktiot: | |
- bdraw(BITMAP *b) piirtää bittikartan kohtaan BITMAP.x, BITMAP.y | |
- bhide(BITMAP *b) tyhjentää edellisellä piirtokerralla piirretyn bitti- | |
kartan. Huomaa, että JOKAISEN PIIRRON JÄLKEEN ON TULTAVA TYHJENNYS | |
ja että BITTIKARTTAA EI LIIKUTETA SEN OLLESSA RUUDULLA (todellisuudessa | |
tietenkin kaksoispuskurissa, joka kopioidaan ruudulle kun kaikki bitti- | |
kartat ovat näkyvissä, sanoinhan, että hyödymme vielä siitä!) | |
- bmove(BITMAP *b) lisää X-koordinaattiin muuttujan BITMAP.xspeed ja | |
Y-koordinaattiin vastaavasti muuttujan BITMAP.yspeed. | |
- bsetlocation(BITMAP *b, int x, int y) asettaa uudet X- ja | |
Y-koordinaatit. | |
- bsetspeed(BITMAP *b, int xspeed, int yspeed) asettaa uudet X- ja | |
Y-nopeudet. Huomaa, että liike ylös saavutetaan negatiivisella | |
Y-nopeudella ja vastaavasti liike vasemmalle negatiivisellä | |
X-nopeudella. | |
- bload(BITMAP *b, int x, int y, int xspeed, int yspeed, int xsize, | |
int ysize, char *bitmapbuffer, int bufferx, int buffery, | |
int bufferxs), jossa 8. parametristä lähtien kertoo | |
latauspuskurista, jona tulemme käyttämään 320x200 kokoista PCX, kuvaa, | |
sisältäen kaikki bitmapit mitä pitää ladata. Jos kuvan x-koko ja y-koko, | |
sekä aloituskoordinaatit kuvassa on ilmoitettu oikein, onnistuu lataus | |
suorakulmion muotoiselta alueelta täysin onnistuneesti, eikä lataus- | |
rutiinin käyttö vaadi kovin paljoa miettimistä. Lisää käytöstä ajal- | |
laan tulevassa esimerkissä. | |
No niin. Lähtekäämme tekemään kirjastoamme BITMAP.H yksi funktio kerrallaan. | |
Rakenne BITMAP on jo esitelty, joten alkakaamme keräämään sen perään | |
käsittelyfunktioita. Ensimmäisenähän oli vuorossa bdraw(), joka onkin | |
helpoimpia ja tärkeimpiä funktioita. Katsellaanpas esimerkkikoodia: | |
void bdraw(BITMAP *b) { | |
int y=b->y, | |
x=b->x, | |
yy, xx; | |
/* Eli loopataan koko suorakulman kokoinen alue. bitmap- ja | |
ja background -puskureissahan lasketaan sijainti seuraavasti: | |
y * b->xsize + x. */ | |
for(yy=0; yy<b->ysize; yy++) { | |
for(xx=0; xx<b->xsize; xx++) { | |
/* eli värillä 0 tämä vertailu alla ei ole tosi, joten värillä | |
0 merkittyjä kohtia EI piirretä! */ | |
if(b->bitmap[yy*b->xsize+xx]) { | |
/* doublebuffer muuttuja osoittaa kaksoispuskuriin. Huomaa, että | |
yläkulma on y*320+x, mutta koska haluamme vielä piirtää useita | |
rivejä, lisäämme yy-looppimme y-arvoon, kutenn myös xx-looppi | |
x-arvoon. Jos et ymmärtänyt niin poista väliaikaisesti kohdat | |
ja näet mitä tapahtuu */ | |
b->background[yy*b->xsize+xx]= | |
doublebuffer[ (y+yy) * 320 + (x+xx) ]; | |
/* sitten vain asetetaan bittikartasta oikea kohta ruudulle, | |
alle peittyvä osa on jo tallessa puskurin background vastaa- | |
valla kohdalla. */ | |
doublebuffer[ (y+yy) * 320 + (x+xx) ]= | |
b->bitmap[yy*b->xsize+xx]; | |
} | |
} | |
} | |
} | |
Koska joiltakin on esiintynyt valituksia siitä, että koodi jää hämärän | |
peittoon, niin esittelen tässä saman pseudona, jos se olisi hieman | |
selvempää: | |
funktio bdraw | |
kokonaisluvun kokoiset kierroslaskurit a ja b | |
looppaa a välillä 0 - <y-koko> | |
looppaa b välillä 0 - <x-koko> | |
bittikarttasijainti = a * <x-koko> + b | |
ruutusijainti = ( <y-sijainti> + a ) * 320 + b + <x-sijainti> | |
jos bittikartta(bittikarttasijainti) ei ole 0 niin | |
tausta(bittikarttasijainti) = kaksois(ruutusijainti) | |
kaksois(ruutusijainti) = bittikartta(bittikarttasijainti) | |
end jos | |
end looppi b | |
end looppi a | |
end funktio | |
Kun lähdet korvaamaan a:n muuttujalla yy ja b:n muuttujalla xx ja | |
korvaat bittikartan sisäiset muuttujat <y-koko>, <x-koko>, | |
<y-sijainti> ja <x-sijainti> BITMAP-rakenteen muuttujilla b->ysize, | |
b->xsize, b->y ja b->x sekä tausta:n ja bittikartan:n | |
b->background:illa ja b->bitmap:illa, kaksois-muuttujan | |
kaksoispuskurisi nimellä niin olet aikalailla ensimmäisessä, | |
alkuperäisessä sorsassa. Jos yhtään selventää niin voit poistaa | |
kommentit alkuperäisestä sorsasta kokonaan ja siirtää sijainnin laskut | |
sieltä []-sulkeiden sisästä juuri tuollaisiin | |
bittikarttasijainti-tyylisiin apumuuttujiin, jolloin koodi selvenee | |
hieman. Olkoot, tässä se on: | |
void bdraw(BITMAP *b) { | |
int a, b, bitmapsijainti, ruutusijainti; | |
for(a=0; a < b->ysize; a++) { | |
for(b=0; b < b->xsize; b++) { | |
bitmapsijainti=a * b->xsize + b; | |
ruutusijainti = ( b->y + a ) * 320 + b + b->x; | |
if(b->bitmap[bitmapsijainti] != 0) { | |
b->background[bitmapsijainti] = doublebuffer[ruutusijainti]; | |
doublebuffer[ruutusijainti] = b->bitmap[bitmapsijainti]; | |
} | |
} | |
} | |
} | |
Varaa aikaa edellisten tutkimiseen, sillä on tärkeää, että ymmärrät periaat- | |
teen. Tietenkin saat lisäselvyyttä kokeilemalla muuttaa noita kohtia, jol- | |
loin näet muutoksen kääntämällä uudelleen esimerkkiohjelman, jonka | |
myöhemmin esittelemme ja ajamalla muunnellun version. Seuraavana onkin | |
huomattavasti nopeammin tehty pyyhintäfunktio, joka eroaa vain siten, että | |
sen sijaan, että säilöisimme taustan ja korvaisimme ruudun pikselin | |
bitmap-puskurin arvolla laitammekin background-puskuriin tallennetun pikse- | |
lin takaisin kaksoispuskuriin, joka on piilotusfunktion jälkeen samassa | |
kunnossa kuin ennen piirtoakin! | |
void bhide(BITMAP *b) { | |
int y=b->y, | |
x=b->x, | |
yy, xx; | |
/* Eli loopataan koko suorakulman kokoinen alue. bitmap- ja | |
ja background -puskureissahan lasketaan sijainti seuraavasti: | |
y * b->xsize + x. */ | |
for(yy=0; yy<b->ysize; yy++) { | |
for(xx=0; xx<b->xsize; xx++) { | |
/* eli värillä 0 tämä vertailu alla ei ole tosi, joten värillä | |
0 merkittyjä kohtia EI piirretä! */ | |
if(b->bitmap[yy*b->xsize+xx]) { | |
doublebuffer[ (y+yy) * 320 + (x+xx) ]= | |
b->background[yy*b->xsize+xx]; | |
} | |
} | |
} | |
} | |
Tuohon ette varmaan enää pseudoja tarvitse, koska sehän eroaa | |
edellisestä vain tuon sijoituksen osalta, eli ensimmäinen sijoitus | |
draw-funktiosta käännetään vain toisinpäin, niin alkup. tausta | |
palautuu. | |
Seuraavaksi kolme helponta funktiota heti rivissä, sillä niiden toteuttami- | |
nen on helppoa ja ymmärtäminen vielä helpompaa, muista, että X-ja Y-koor- | |
dinaatteja vähennetään negatiivisill nopeuksilla, sillä X+(-1)=X-1: | |
void bmove(BITMAP *b) { | |
b->x+=b->xspeed; | |
b->y+=b->yspeed; | |
} | |
void bsetlocation(BITMAP *b, int x, int y) { | |
b->x=x; | |
b->y=y; | |
} | |
void bsetspeed(BITMAP *b, int xspeed, int yspeed) { | |
b->xspeed=xspeed; | |
b->yspeed=yspeed; | |
} | |
Seuraava onkin vaikea pala, joten lisään koodia saadakseni siitä vähän | |
selvemmäksi. Idea siis on, että otamme pikselin tuplapuskuriin ladatus- | |
ta ja laitamme sen bitmap-puskuriin. Eli oikeastaan käänteisesti näyt- | |
töfunktioon nähden. Eli katsotaanpas: | |
void bload(BITMAP *b, int x, int y, int xspeed, int yspeed, int xsize, | |
int ysize, char *bitmapbuffer, int bufferx, int buffery, | |
int bufferxs) { | |
int yy, xx; | |
bsetlocation(b, x, y); | |
bsetspeed(b, xspeed, yspeed); | |
b->xsize=xsize; | |
b->ysize=ysize; | |
b->bitmap=(char *)malloc(xsize*ysize); | |
b->background=(char *)malloc(xsize*ysize); | |
if(b->background==NULL || b->background==NULL) { | |
printf("Ei tarpeeksi muistia bitmap-puskureille!\n"); | |
exit(1); | |
} | |
/* Eli loopataan koko suorakulman kokoinen alue. bitmap- | |
puskurissahan lasketaan sijainti seuraavasti: | |
y * b->xsize + x. */ | |
for(yy=0; yy<ysize; yy++) { | |
for(xx=0; xx<xsize; xx++) { | |
/* doublebuffer muuttuja osoittaa kaksoispuskuriin. Huomaa, että | |
yläkulma on y*320+x, mutta koska haluamme vielä piirtää useita | |
rivejä, lisäämme yy-looppimme y-arvoon, kutenn myös xx-looppi | |
x-arvoon. Jos et ymmärtänyt niin poista väliaikaisesti kohdat | |
ja näet mitä tapahtuu */ | |
b->bitmap[yy*xsize+xx]= | |
bitmapbuffer[ (buffery+yy) * bufferxs + (bufferx+xx) ]; | |
} | |
} | |
} | |
bload on itseasassa täysin sama kuin ensimmäinenkin funktio, mutta | |
alussa meillä on pari alustusta jotta BITMAP-rakenne saadaan halutuksi | |
(muistinvarausta, sijainnin nollausta, koon alustus...). Vain | |
piirtofunktio on korvattu versiolla, joka ei piirrä ruudulle, vaan | |
lataa ruudulta (bitmapbuffer tässä tapauksessa, jottei tarvi oikeaa | |
kaksoispuskuria välttämättä käyttää) pikselit. Ei se loppujenlopuksi | |
ole sen vaikeampi. | |
Nyt kun lisäämme kaikki yhteen kirjastoomme BITMAP.H ja teemme lopuksi | |
vielä pienen esimerkkiohjelman, joka liikuttelee palloa | |
ruudulla. Koska kirjastomme ei kykene estämään ruudun yli menemisiä, | |
niin meidän pitää kääntää liikkuvan pallon suuntaa ennenkuin alareuna | |
osuu ruudun alareunaan ja menee sitten siitä yli (eli jos bittikartan | |
koko, sijainti ja nopeus yhteenlaskettuna on yli ruudun koon, tai | |
bittikartan sijainti ja nopeus yhteenlaskettuna on pienempi kuin | |
0). Eli kun jompikumpi edellisistä ehdoista täyttyy niin käännetään | |
pallon suuntaa ja saadaan pallo "pomppimaan" reunoista. | |
Mutta, olemme taas puhuneet ihan tarpeeksi. Menkäämme nyt esimerkkiohjel- | |
mamme pariin (BITMAP1.C). Siinä lataamme bittikartan tiedostosta BITMAP.PCX | |
ja tausta tiedostosta BITBACK.PCX. Näin näemme läpinäkyvyyden toiminnassa | |
(muutenhan pallo olisi neliönmuotoinen). Lisäksi tietenkin käytämme jo va- | |
kioiksi muuttuneita palettifunktiota ohjelmamme koristukseksi: | |
#include <go32.h> | |
#include <sys/movedata.h> | |
#include <conio.h> | |
#include <stdio.h> | |
#include <dos.h> | |
#include <stdlib.h> | |
char *doublebuffer; | |
#include "palette.h" | |
#include "pcx.h" | |
#include "bitmap.h" | |
#define flip(c) _dosmemputl(c, 64000/4, 0xA0000) | |
int main() { | |
char palette[768]; | |
BITMAP bitmap; | |
doublebuffer=(char *)malloc(64000); | |
if(doublebuffer==NULL) { | |
printf("Ei tarpeeksi muistia kaksoipuskurin varaukseen!\n"); | |
return 1; | |
} | |
textmode(0x13); | |
loadpcx("BITMAP.PCX", doublebuffer); | |
loadpal("BITMAP.PCX", palette); | |
setpal(palette); | |
bload(&bitmap, 160, 100, 1, 1, 16, 16, doublebuffer, 1, 1, 320); | |
loadpcx("BITBACK.PCX", doublebuffer); | |
/* Lataus vasta kun bittikartta on otettu edellisestä tiedostosta. | |
Ei ladata palettia koska se on sama kuin edellisessä PCX:ssä. */ | |
while(!kbhit()) { | |
bdraw(&bitmap); | |
waitsync(); | |
flip(doublebuffer); | |
bhide(&bitmap); | |
bmove(&bitmap); | |
if((bitmap.x+bitmap.xsize+bitmap.xspeed)>320 || | |
bitmap.x+bitmap.xspeed<0) | |
bitmap.xspeed= -bitmap.xspeed; | |
if((bitmap.y+bitmap.ysize+bitmap.yspeed)>200 || | |
bitmap.y+bitmap.yspeed<0) | |
bitmap.yspeed= -bitmap.yspeed; | |
} | |
getch(); | |
fadetoblack(palette); | |
textmode(0x3); | |
return 0; | |
} | |
Varaa kunnolla aikaa ja tutki lähdekoodeja, mieti teoriaa ja kokeile kaikkea | |
käytännössä mitä mieleen tulee. Kun luulet keksineesi idean niin palaa | |
takaisin dokumentin ääreen, ja siirrymme seuraavaan aiheesemme. Menehän | |
siitä! Jos vieläkin tuntui siltä ettet tajunnut niin ota yhteyttä ja | |
kysy mikä jäi mietityttämään, niin tarkennan sitten vielä tätä. | |
4.2 Animaatiot | |
-------------- | |
Tämänkertainen aiheemme on pieni parannus koodiin, joka on paljon näy- | |
töllä ja jonka jälkeen on tämän tutoriaalin bittikarttarutiinit lähes kä- | |
sitelty. Tulemme kyllä hyväksikäyttämään edellisen kappaleen koodia | |
tehdessämme fonttiengineä, sekä parantelemme koodia tehdessämme törmäys- | |
tarkistuksen, mutta itse animointi- ja bittikarttateoria käsitellään | |
kokonaan tässä ja edellisessä kappaleessa. | |
Eli tänään tutustumme ensimmäisenä animaatiohin. Mitä animaatiot sitten | |
ovat? No itseasiasas animaatio on vain sarja kuvia, joita vaihdellaan | |
ja saadaan kuva liikkeestä. Animaatiota voidaan käyttä lähes kaikkeen | |
pelissä. Sillä voidaan tehdä pyörivä alusanimaatio, jonka jokainen | |
kuva on yksi aluksen suunta. Jokaisella suunnalla voisi olla vielä oma | |
animaationsa, joka saa vaikka rakettimoottorit hehkumaan ja laserit | |
aiheuttamaan välähdyksiä aluksen pinnassa. Pienellä mielikuvituksella | |
ja taitavalla graafikolla päästään ihmeisiin. Tässä kappaleessa esi- | |
telty kirjasto ei varmaankaan käy suoraan moneen tarkoitukseen tai ole | |
tarpeeksi nopea peliin, mutta enginen onkin vain tarkoitus näyttää | |
pääperiaatteita animoinnin ja muiden olennaisien asioiden takana. | |
Eli animaatio on kuvasarja, jotka näytetään tietyssä järjestyksessä. Miten | |
sitten toteutamme tämän. Tässä on tapa jolla minä olen sen tehnyt. Meillähän | |
on täysin toimivat rutiinit yhden kuvan näyttämiseen. Tehkäämme vain | |
animointikoodi, joka vaihtaa pointterin bitmap osoittamaan seuraavaan | |
kuvaa, eli frameen. Tätä täytyy kutsua silloin kun spriteä, joksi kutsumme | |
animoivaa bittikarttaamme tästälähin ei ole piirretty puskuriin. Jälleen | |
voit kokeilla siirtää animointikoodin kutsun kohtaan jossa esine on piir- | |
rettynä, mutta se ei tule näyttämään hyvältä (jos objektin peittämän alueen | |
muoto muuttuu). Eli siis tarvitsemme uuden rakenteen, joka voi säilöä | |
useita kuvia, koodin joka vaihtaa bitmap-pointterin osoittamaan seuraavaan | |
kuvaan, laskurin joka kertoo monennessako kuvassa mennään ja toisen muuttu- | |
jan joka kertoo montako kuvaa meillä on animaatiossa, sekä lopulta uuden | |
latausfunktion, joka osaa ladata useita kuvia käsittävän animaation. | |
Tähän kaikkeen voimme kopioida vanhaa koodiamme ja lisäillä sinne tar- | |
peellisia osia. Eli teemme nyt uuden rakenteen, jossa voi olla maksimis- | |
saan MAXFRAME määrä frameja, eli kuvia (tämä toteutuksen helpottamiseksi): | |
#define MAXFRAME 64 | |
typedef struct { | |
char *frame[MAXFRAME]; | |
int curfrm; | |
int frames; | |
char *bitmap; | |
char *background; | |
int x; | |
int y; | |
int xsize; | |
int ysize; | |
int xspeed; | |
int yspeed; | |
} SPRITE; | |
Se olikin helppoa. Nämä rutiinit tulevat kirjastoon SPRITE.H, josta löydät | |
myös joukon vanhoja tuttujamme uudelleennimettynä ja vähän | |
muunneltuina (sdraw, shide...). Seuraavaksi sitten animointirutiini: | |
void sanimate(SPRITE *s) { | |
s->curfrm++; | |
if(s->curfrm >= s->frames) | |
s->curfrm=0; | |
s->bitmap=s->frame[s->curfrm]; | |
} | |
Radikaaleja muutoksia tarvinnee myös latausrutiinimme. Tärkeimmät muutok- | |
set siinä on, että se lukee framet rivistä. Katso SPRITE.PCX esimerkkinä | |
tällaisesta animaatiosta. Jos ihmettelet outoja kertolaskuja joissain | |
kohdin se johtuu siitä, että jokaisen framen jälkeen hypätään 1 pikseli | |
yli, sillä teemme rajat animaatioiden väliin selvennykseksi. Eli tässä | |
olisi latauskoodimme, uusi parametri on animaatioiden määrä: | |
void sload(SPRITE *s, int x, int y, int xspeed, int yspeed, int xsize, | |
int ysize, char *bitmapbuffer, int bufferx, int buffery, | |
int bufferxs, int frames) { | |
int yy, xx, current; | |
ssetlocation(s, x, y); | |
ssetspeed(s, xspeed, yspeed); | |
s->xsize=xsize; | |
s->ysize=ysize; | |
s->curfrm=0; | |
s->frames=frames; | |
for(current=0; current<frames; current++) { | |
s->frame[current]=(char *)malloc(xsize*ysize); | |
if(s->frame[current]==NULL) { | |
printf("Ei tarpeeksi muistia sprite-puskureille!\n"); | |
exit(1); | |
} | |
} | |
s->background=(char *)malloc(xsize*ysize); | |
s->bitmap=s->frame[s->curfrm]; | |
if(s->background==NULL) { | |
printf("Ei tarpeeksi muistia sprite-puskureille!\n"); | |
exit(1); | |
} | |
/* Eli loopataan koko suorakulman kokoinen alue. bitmap- | |
puskurissahan lasketaan sijainti seuraavasti: | |
y * s->xsize + x. Uloimpana looppina on uutena framelooppi, | |
joka on lisätty koska meidän pitää ladata usea kuva. */ | |
for(current=0; current<frames; current++) | |
for(yy=0; yy<ysize; yy++) { | |
for(xx=0; xx<xsize; xx++) { | |
/* doublebuffer muuttuja osoittaa kaksoispuskuriin. Huomaa, että | |
yläkulma on y*320+x, mutta koska haluamme vielä piirtää useita | |
rivejä, lisäämme yy-looppimme y-arvoon, kutenn myös xx-looppi | |
x-arvoon. Jos et ymmärtänyt niin poista väliaikaisesti kohdat | |
ja näet mitä tapahtuu */ | |
s->frame[current][yy*xsize+xx]= | |
bitmapbuffer[ (buffery+yy) * bufferxs + (bufferx+xx) + | |
(xsize+1)*current ]; | |
} | |
} | |
} | |
Kirjastoon SPRITE.H lisätään vielä bdraw, bhide, bmove, bsetlocation ja | |
bsetspeed nimettynä nimillä sdraw, shide, smove, ssetlocation ja ssetspeed | |
funktioiden erottamiseksi bitmap-rutiineista (jos vaikka halutaan käyttää | |
molempia). Muitakin pikkumuutoksia on tehty. Huomaat ne helposti | |
kurkkaamalla kirjaston sisään. Nyt meillä onkin animaatiot taitava engine, | |
jota meidän täytyy tietenkin heti kokeilla. Tässä on esimerkkiohjelmamme | |
SPRITE1.C, joka havainnoi funktioiden käyttöä: | |
#include <go32.h> | |
#include <sys/movedata.h> | |
#include <conio.h> | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <dos.h> | |
char *doublebuffer; | |
#include "palette.h" | |
#include "pcx.h" | |
#include "sprite.h" | |
#define flip(c) _dosmemputl(c, 64000/4, 0xA0000) | |
int main() { | |
char palette[768]; | |
SPRITE sprite; | |
doublebuffer=(char *)malloc(64000); | |
if(doublebuffer==NULL) { | |
printf("Ei tarpeeksi muistia kaksoipuskurin varaukseen!\n"); | |
return 1; | |
} | |
textmode(0x13); | |
loadpcx("SPRITE.PCX", doublebuffer); | |
loadpal("SPRITE.PCX", palette); | |
setpal(palette); | |
sload(&sprite, 160, 100, 1, 1, 16, 16, doublebuffer, 1, 1, 320, 8); | |
loadpcx("BITBACK.PCX", doublebuffer); | |
/* Lataus vasta kun bittikartta on otettu edellisestä tiedostosta. | |
Ei ladata palettia koska se on sama kuin edellisessä PCX:ssä. */ | |
while(!kbhit()) { | |
sdraw(&sprite); | |
waitsync(); | |
waitsync(); | |
flip(doublebuffer); | |
shide(&sprite); | |
smove(&sprite); | |
sanimate(&sprite); | |
if((sprite.x+sprite.xsize+sprite.xspeed)>320 || | |
sprite.x+sprite.xspeed<0) | |
sprite.xspeed= -sprite.xspeed; | |
if((sprite.y+sprite.ysize+sprite.yspeed)>200 || | |
sprite.y+sprite.yspeed<0) | |
sprite.yspeed= -sprite.yspeed; | |
} | |
getch(); | |
fadetoblack(palette); | |
textmode(0x3); | |
return 0; | |
} | |
Luultavasti huomaat nykimistä, sillä täysin optimoimaton sprite-enginemme | |
ei aivan pysty 70 frameen sekunnissa. Siksi laitoin ohjelmamme odottamaan | |
kahta vertical retracea, jotta nykiminen ei olisi niin häiritsevää | |
(P75:lläni kahdella waitilla meno näyttää paljon tasaisemmalta, eikä yhden | |
framen hyppy näy läheskään niin selvästi). Jos kuitenkin sinulla on hidas | |
kone niin poista toinen tai kummatkin odotuksista, se nopeuttaa koodia | |
paljon, mutta voit joutua laittamaan delay-komennolla viivettä säätääksesi | |
pyörimistä tasaisemmaksi. Pienellä optimoinnilla olisimme toki saaneet | |
moninkertaisesti lisää nopeutta, mutta koodi olisi menettänyt luettavuut- | |
taan, joka esimerkkiohjelmien tarkoitus on. Tietenkin kun alat tekemään | |
omaa peliäsi teet uudet ja paremmin tarkoitukseesi sopivat rutiinit ke- | |
räämiesi tietojen pohjalta. | |
Nyt onkin tämän kappaleen aika loppua ja sinun on aika paneutua uuden | |
asian pariin. Seuraavassa luvussamme käsitelläänkin sitten viimeistä | |
kysymystä spritejen parissa, monen spriten käyttöä, niiden törmäyksiä | |
ja ylitseliukumisia. Mutta nyt jätän sinut rauhaan. Näemme seuraavassa | |
luvussa! | |
4.3 Pitääkö spriten törmätä? Entä coca-colan? | |
--------------------------------------------- | |
Nyt pääsemmekin vihoviimeiseen vaiheeseen teoriassamme ja ryyditämme sitä | |
pienin, tai ehkä niinkään pienin muutoksin SPRITE.H-kirjastoomme. Nimit- | |
täin jokainen vähänkään vakavasti pelintekoa harkinnut tarvitsee useampia | |
kuin yhden spriten. Mutta mitä tapahtuu kun ne ovat menossa päällekäin? | |
Jos teet vain loopin, joka piirtää spriten ja toisen, joka pyyhkii ne | |
samassa järjestyksessä olet varmaan huomannut, että se ei aiheuta toivot- | |
tuja tuloksia. Muutos mitä tarvitaan on pieni ja yksinkertainen, mutta | |
ajatellaanpas esimerkkiämme. | |
Ajatellaan, että sinulla on kolme pikseliä. Punainen, sininen ja keltainen. | |
Haluat laittaa ne samaan kohtaan ruudulle. Laitat ne edellä olevassa | |
järjestyksessä mustalle ruudulle ja laitat lapulle muistiin punaisen koh- | |
dalle, että sen alla oli musta, sinisen kohdalle, että sen alla oli | |
punainen ja keltaisen kohdalle, että sen alla oli sininen. | |
Nyt haluat poistaa ne. Ottaisitko ne nyt samassa järjestyksessä, eli ensin | |
punainen, sitten sininen ja lopuksi keltainen? Et, sillä jos ottaisit lopuksi | |
keltaisen, katsoisit lapustasi sen alla olleen sinisen värin ja ruutu | |
muuttuisikin siniseksi. Tässä meidän täytyykin mennä käänteisesti, eli | |
keltainen, sininen ja sitten vasta punainen, jonka tilalle laitat lopulta | |
mustan ja kaikki on hyvin. | |
Eli jos sinulla olisi 10 bittikarttaa taulukossa SPRITE s[10], niin niiden | |
piirto ja pyyhkiminen tapahtuisi seuraavasti: | |
for(c=0; c<10; c++) sdraw(s[c]); | |
flip(doublebuffer); | |
for(c=10; c>=0; c--) shide(s[c]); | |
Ja ei enää toimimattomia koodinpätkiä, vaan hienosti toistensa ylitse | |
liukuvat spritet. | |
Mutta aina ei haluta kaikkien vain liukuvan toistensa ylitse. Miltä | |
näyttäisi matopeli, jossa madot kiltisti liukuvat toistensa ylitse? | |
Ei kovin oikealta, sanoisin. Meidän täytyy siis tehdä rutiini, joka | |
tarkistaa törmäyksen kahden spriten välillä. Olkoon sen kutsutapa | |
seuraava: scollision(SPRITE *a, SPRITE *b) ja se palauttaa arvon | |
1 jos törmäys on tapahtunut, muuten se palauttaa nollan. Jos siis | |
haluat tehdä törmäyksen tultua jotakin, niin koodi menisi suurinpiirtein | |
näin: | |
if(scollision(sprite[0], sprite[1])) | |
tee_jotain_kun_tulee_pamahdus(); | |
Mutta, miten toimii tämä salaperäinen funktiomme? Itseasiassa minä en | |
saanut siitä mitään selvää luettuani sen aikoinani Mikrobitin grafiikka- | |
ohjelmointikurssin toisesta osasta, mutta luulisin nyt pystyväni teke- | |
mään samanlaisen, ja jos onnistumme pystynen selittämäänkin toimintaperi- | |
aatteen. | |
int scollision(SPRITE *a, SPRITE *b) { | |
/* Lasketaan spritejen yläkulmien väliset etäisyydet. Huomaa, että tässä | |
lasketaan mukaan nopeudet, eli palautusarvo 1 kertoo spritejen | |
törmäävän ENSI vuorolla. Näin ehditään päällekkäin meneminen estää | |
ajoissa. */ | |
int xdistance= (a->x+a->xspeed) - (b->x+b->xspeed); | |
int ydistance= (a->y+a->yspeed) - (b->y+b->yspeed); | |
int xx, yy; | |
/* Jos x- tai y-etäisyys on suurempi kuin suuremman leveys eivät | |
spritet voi mitenkään olla toistensa päällä. */ | |
if(xdistance>a->xsize && xdistance>b->xsize) return 0; | |
if(ydistance>a->ysize && ydistance>b->ysize) return 0; | |
for(xx=0; xx< a->xsize; xx++) | |
for(yy=0; yy< a->ysize; yy++) | |
if(xx+xdistance < b->xsize && xx+xdistance>=0 && | |
yy+ydistance < b->ysize && yy+ydistance>=0) | |
if(a->bitmap[ yy * a->xsize + xx ] && | |
b->bitmap[ (yy+ydistance) * b->xsize + (xx+xdistance) ]) | |
return 1; | |
return 0; | |
} | |
Loopissa ideana on se, että laskuilla saadaan b-spriten vastaava koordinaatti | |
selville ja jos se on siis positiivinen ja spriten b rajoissa (pienempi | |
kuin leveys tai y-koordinaatin ollessa kyseessä korkeus). Tarkemmin en | |
ala selittämään. Jos välttämättä haluat saada selville miten pätkä toimii | |
niin piirrä pari tilannetta paperilla ja katso miten niiden kanssa tapah- | |
tuu. Nyt meillä onkin käsiteltynä kaikki tärkein spriteistä ja voimme | |
mennä viimeiseen pelkästään spritejä käyttävään ohjelmaamme. Tämä ohjelma | |
on pienimuotoinen peli, jossa liikutaan edellisen esimerkin palikoilla. Pe- | |
laajia on 2 ja tarkoitus on leikkiä hippaa. Eli toinen yrittää pakoon ja | |
toinen yrittää ottaa kiinni. Peli loppuu kun pelaajat törmäävät. Kontrol- | |
lit ovat pelaajalla 1 wsad ja pelaajalla 2 ujhk. Tämä on vain pieni esi- | |
merkki siitä mitä näillä taidoilla voisi tehdä. Lisäksi nappeina on | |
+ ja - nopeuden säätöön (nyt ei odoteta waitsyncillä) sekä ESC lopetuk- | |
seen kesken. Eli SPRITE2.C: | |
#include <go32.h> | |
#include <sys/movedata.h> | |
#include <conio.h> | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <dos.h> | |
char *doublebuffer; | |
#include "palette.h" | |
#include "pcx.h" | |
#include "sprite.h" | |
#define flip(c) _dosmemputl(c, 64000/4, 0xA0000) | |
int main() { | |
char palette[768]; | |
SPRITE pl1, pl2; | |
int quit=0, waittime=0; | |
doublebuffer=(char *)malloc(64000); | |
if(doublebuffer==NULL) { | |
printf("Ei tarpeeksi muistia kaksoipuskurin varaukseen!\n"); | |
return 1; | |
} | |
textmode(0x13); | |
loadpcx("SPRITE.PCX", doublebuffer); | |
loadpal("SPRITE.PCX", palette); | |
setpal(palette); | |
sload(&pl1, 100, 100, 0, 0, 16, 16, doublebuffer, 1, 1, 320, 8); | |
sload(&pl2, 220, 100, 0, 0, 16, 16, doublebuffer, 1, 1, 320, 8); | |
loadpcx("BITBACK.PCX", doublebuffer); | |
while(!quit) { | |
sdraw(&pl1); | |
sdraw(&pl2); | |
flip(doublebuffer); | |
shide(&pl1); | |
shide(&pl2); | |
smove(&pl2); | |
smove(&pl1); | |
sanimate(&pl1); | |
sanimate(&pl2); | |
if((pl1.x+pl1.xsize+pl1.xspeed)>320 || | |
pl1.x+pl1.xspeed<0) | |
pl1.xspeed= -pl1.xspeed; | |
if((pl1.y+pl1.ysize+pl1.yspeed)>200 || | |
pl1.y+pl1.yspeed<0) | |
pl1.yspeed= -pl1.yspeed; | |
if((pl2.x+pl2.xsize+pl2.xspeed)>320 || | |
pl2.x+pl2.xspeed<0) | |
pl2.xspeed= -pl2.xspeed; | |
if((pl2.y+pl2.ysize+pl2.yspeed)>200 || | |
pl2.y+pl2.yspeed<0) | |
pl2.yspeed= -pl2.yspeed; | |
if(scollision(&pl1, &pl2)) | |
quit=2; /* 2 tarkoittaa, että toinen saatiin kiinni */ | |
while(kbhit()) { /* tyhjennetään näppispuskuri */ | |
switch(getch()) { | |
case 'w': pl1.yspeed=-1; pl1.xspeed=0; break; | |
case 's': pl1.yspeed=1; pl1.xspeed=0; break; | |
case 'a': pl1.xspeed=-1; pl1.yspeed=0; break; | |
case 'd': pl1.xspeed=1; pl1.yspeed=0; break; | |
case 'u': pl2.yspeed=-1; pl2.xspeed=0; break; | |
case 'j': pl2.yspeed=1; pl2.xspeed=0; break; | |
case 'h': pl2.xspeed=-1; pl2.yspeed=0; break; | |
case 'k': pl2.xspeed=1; pl2.yspeed=0; break; | |
case '+': if(waittime) waittime--; break; | |
case '-': waittime++; break; | |
case 27: quit=1; break; | |
} | |
} | |
delay(waittime); | |
} | |
if(quit==2) { /* jos kiinni, niin feidataan ensin valkoiseen (räjähdys) */ | |
fadetowhite(palette); | |
for(waittime=0; waittime<256*3; waittime++) | |
palette[waittime]=63; | |
} | |
fadetoblack(palette); | |
textmode(0x3); | |
return 0; | |
} | |
Tässä oli sitten sellainen lähdekoodi, jota kukaan vähänkään omanarvontuntoa | |
omaava peliohjelmoija, taikka muukaan ohjelmoija EI TEE. Jos pelistä to- | |
della halutaan selvä ja helposti laajennettava ei tehdä jokaiselle pelaa- | |
jalle eri spriteä eri nimellä, vaan kaikki pelaajaspritet ovat | |
taulukossa. Ja muutenkin esimerkkikoodi ainoastaan demonstroi mahdolli- | |
suuksia oppimiemme asioiden käyttämiseen, ei suinkaan minkälainen pelin | |
runko pitäisi olla. Siihen me palaamme myöhemmin. Mutta meneppäs pelaamaan | |
ja näytä kavereillesi minkälaisia pelejä osaisit jo tehdä. =) Äläkä | |
palaa takaisin ennenkuin tämän kappaleen asiat ovat hallussa. Sillä niiden | |
osaamista luultavasti tullaan vaatimaan seuraavissakin luvuissa. Mutta jos | |
olet malttamaton, niin on tietenkin mahdollista palata takaisin opettelemaan, | |
mutta turhauttavaa se on. | |
Jälkikäteen kaiken sprite, animaatio ja bittikarttanäpräilyn jälkeen totean, | |
että kaikissa kohdissahan ei käytetty täsmälleen oikeita termejä. Bittikart- | |
tahan on käytännössä vain kuvadata ja mahdollisesti hieman lisätietoa, ani- | |
maatio on yleensä peräkkäisiä bittikarttoja osaksi yhteisellä datalla, | |
olio on yleensä sitten se mikä osaa pyyhkiä itsensä ja joka tietää mitkä | |
bittikartat ja muut vastaavat sille kuuluvat, joka voi pyyhkiä itsensä ja | |
tehdä monia muitakin kivoja asioita. Sprite on sitten jotain siellä jossain | |
välillä tai päässä, en tiedä kovin tarkasti mutta käytin nyt tätä nimitystä | |
täysin toimivasta oliosta joka kykenee itsensä käsittelyyn. | |
4.4 Maskatut spritet | |
-------------------- | |
Vähän aikaa sitten kerroin PC-Ohjelmointi -alueella tämän kurssin sisällöstä | |
ja eikös vain joku mennyt kysymään minulta selittikö tutoriaali maskatut | |
vai maskaamattomat spritet. Minähän en ollut edes kuullut moisesta asiasta | |
ja utelin ideaa sen takana. Sainkin kuulla se ja tein sen pohjalta assemb- | |
lerilla nopean rutiinin. Pienellä nopeuskokeella se osoittautui 11 kertaa | |
nopeammaksi kuin muutama luku sitten tekemämme rutiini. Aion nyt selittää | |
idean tämän tekniikan takana, joten kiinnittäkää turvavyönne ja valmistau- | |
tukaa! | |
Maskatuiden spritejen ideana on se, että niiden piirrossa ei tarvita pikse- | |
likohtaisia vertailulauseita lainkaan, jolloin voidaan käyttää assembleril- | |
la neljän tavun kanssa operoivia funktioita. Mutta miten sitten kierrämme | |
vertailulausekkeet säilyttäen silti läpinäkyvyyden nollavärin kanssa? | |
Idea perustuu bittioperaattoreihin. | |
Jokaiselle spriten framelle tehdään etukäteen maski, joka on nolla kohdissa | |
joissa on pikseli ja 255 läpinäkyvissä kohdissa. Nyt sitten vain suoritamme | |
kaksoispuskurin pikselille loogisen AND-operaation: | |
Maski spritelle FF 00 FF FF | |
Näyttö 4F 3C 93 5A | |
---------------------------- | |
Tulos 4F 00 93 5A | |
Kuten huomaatte, jäävät läpinäkyvät kohdat (FF) jäljelle. Sitten vain | |
käytämme OR-operaattoria sytyttämään spriten pikselit, sillä ne kohdat | |
ovat juuri äsken nollautuneet, joten looginen OR asettaa juuri oikeat | |
bitit: | |
Sprite 00 46 00 00 | |
Maskattu näyttö 4F 00 93 5A | |
---------------------------- | |
Tulos 4F 46 93 5A | |
Lopun saat toteuttaa aivan itse. Huomattavaa tässä on se, että jos haluat | |
käyttää tehokkaita 4 tavun (dword) operaatioita on bittikartan leveyden | |
oltava jaollinen neljällä. Huipputehoon tarvitset assembleria, sillä C:llä | |
on vaikea kontrolloida edellä mainittuja asioita. Jos et vielä osaa assemb- | |
leria, varsinkaan DJGPP:n AT&T syntaksia, suosittelen seuraavia tiedostoja: | |
ASSYT.ZIP Assemblerin alkeet suomeksi. | |
PCGPE10.ZIP PCGPE sisältää kaiken muun lisäksi assemblytutoriaalin. | |
DJTUT2_4.ZIP Jos osaat Intel-syntaksin, muttet AT&T-syntaksia | |
(movd %eax, %ebx). Sisältää myös muuta kiinnostavaa | |
materiaalia, jota tässäkin tutoriaalissa on sivuttu. | |
NASM095B.ZIP Tällä voit tehdä Intel-syntaksin assemblerilla DJGPP:n | |
COFF-muotoisia objektitiedostoja. Tiivistettynä TASM joka | |
osaa myöskin DJGPP:n objektiformaatin. Huomaa, että uusin | |
versio voi olla muutakin kuin 0.95 (NASM095B.ZIP). | |
Lisäksi voisi olla hyvä idea lainata kirjastosta kirja 486-ohjelmointi, | |
joka on suomenkielinen assembler-ohjelmointia käsittelevä kirja ja kaiken | |
lisäksi hyvä sellainen! | |
Loppulisäyksenä jälleen kiva vinkki Pekka Nurmiselta. Kaksoispuskuri | |
kannattaa tarvittaessa tehdä sen verran leveämmäksi, että jos spitea | |
ei saada katki juuri neljän tavun kohdalta ei tuo tule toisesta reunasta | |
vastaan. Eli jättää sinne neljä tavua ruudun reunoihin, jota ei vain | |
sitten kopioida näytälle. Näin kaksoispuskurin kooksi tulisi 328x200. | |
5.1 Näppäimistön käsittely - ja nyt meillä on hauskaa | |
----------------------------------------------------- | |
Jos pelasit ahkerasti esimerkkipeliämme, niin ehkä huomasit, että painaessasi | |
useita nappia ilmenee myös useita ongelmia. Näihin voivat kuulua näppäimis- | |
tön jumiutuminen, nappien huomiotta jättäminen jne. Tarvitsemme siis ru- | |
tiinin joka päästäisi meidät pälkähästä. Tarvitsemme näppishandlerin! | |
Tämä perustuu siihen, että joka kerta kun nappia painetaan kutsutaan | |
keskeytystä 9, joka lukee merkin näppäimistöltä portista 60h (0x60) ja | |
muuntaa sen ASCII:ksi ja laittaa näppäimistöpuskuriin. Mutta mepäs ohi- | |
tammekin tämän ja teemme oman handlerin, joka ei muutakaan mitään miksi- | |
kään ASCII:ksi, vaan laittaa näppäimistötaulukon vastaavan kohdan arvoon | |
1, josta peli voi sitten sen tarkistaa. Ja kun nappi päästetään tulee | |
myös keskeytys, tällä kertaa tulee napin arvo + 128, joten vähennämme | |
luetusta arvosta 128 ja nollaamme vastaavan kohdan taulukosta. Ja millainen | |
on tämä taulukko? | |
Taulukossa on 128 alkiota, yksi jokaiselle SCAN KOODILLE, jollaisia näppäi- | |
mistö syytää. Olen tehnyt näistä numeroista kirjaston, jossa esimerkiksi | |
ESC-näppäimen scan koodi on nimellä SxESC ja sen arvo on 1. Jos siis haluat | |
pelissäsi tietää onko ESC painettuna, osoitat näppäimistöpuskuriin: | |
if(keybuffer[SxESC]==1) printf("ESC painettu!\n"); | |
Kirjasto on nimellä D_SCAN.H. Ja sitten tarvitsemme siis koodia, joka lukee | |
tavun portista 60h ja jos se on alle 128 se laittaa vastaavan kohdan | |
taulukosta ykköseksi ja jos se on yli tai yhtäsuuri kuin 128, niin laitamme | |
alkion tavu-128 nollaksi. Lopuksi lähetämme signaalin PIC:ille, että kes- | |
keytyksemme on valmis, eli outtaamme tavun 20h porttiin 20h. Tällainen on | |
siis handlerimme (KEYBOARD.H): | |
void keyhandler() { | |
register unsigned char tavu=inportb(0x60); | |
if(tavu<128) keybuffer[tavu]=1; | |
else keybuffer[tavu-128]=0; | |
outportb(0x20, 0x20); | |
} | |
Tämä onkin oikeastaan helpoin osa tehtäväämme. Vaikeampi (joskin esimerkki- | |
koodin takia helppo) on koukuttaa tarvitsemamme näppäimistökeskeytys ja | |
palauttaa se kun tarvitaan näppäimistörutiineja (gets, getch...) tai pois- | |
tutaan ohjelmasta. Lisäksi tarvitsemme joukon apumuuttujia, jotka ovat | |
tässä: | |
volatile unsigned char keybuffer[128], installed; | |
_go32_dpmi_seginfo info, original; | |
Keybuffer säilöö näppäinten tilat, installed kertoo onko tämä handleri a- | |
sennettuna ja estää samalla uudelleenasentamisen. Kaksi viimeistä muuttujaa | |
info ja original ovat koukuttamiseen ja koukutuksen (hooking) poistamiseen | |
tarvittavia rakenteita, joista infoa käytetään oman asentamiseen ja origi- | |
naliin säilötään alkup. handlerin osoite ja muut tarpeelliset tiedot. | |
Tässä on koukutukseen ja palautukseen tarvittava koodi, johon emme perehdy | |
kovinkaan tarkasti, lisäinfoa asiasta saat vaikka DJGPP:n FAQ:sta hakusanalla | |
handler: | |
int setkeyhandler() { | |
int c; | |
for(c=0; c<0x80; c++) | |
keybuffer[c]=0; /* nollataan napit */ | |
if(!installed) { | |
_go32_dpmi_get_protected_mode_interrupt_vector(0x0009, &original); | |
info.pm_offset=(unsigned long int)keyhandler; | |
info.pm_selector=_my_cs(); | |
_go32_dpmi_allocate_iret_wrapper(&info); | |
_go32_dpmi_set_protected_mode_interrupt_vector(0x0009, &info); | |
installed=1; | |
return 1; | |
} else return 0; | |
} | |
int resetkeyhandler() { | |
if(installed) { | |
_go32_dpmi_set_protected_mode_interrupt_vector(0x0009, &original); | |
installed=0; | |
return 1; | |
} else return 0; | |
} | |
Lisäämme kaikki kolme funktiota ja globaalit muuttujamme tiedostoon | |
KEYBOARD.H. Nyt meillä on tarpeen vaatiessa täydellisen toimiva näppäimis- | |
töhandleri (jota ehkä myöhemmin tulemme käyttämään). | |
5.2 Fixed point matematiikka | |
---------------------------- | |
Alamme pikkuhiljaa lähestyä kurssimme loppua (tai ken tietää, todellista | |
alkua?), joten käsittelen tässä hieman pelin optimointiin vaikuttavia | |
tekijöitä ja parannuksia aiemmin esittelemiimme kirjastoihin (omaan peliin | |
kun kannattaa kuitenkin tehdä osa kirjastoista uusiksi). Selitän fixed- | |
pointin, lookupin idean ja pari muuta nopeuttavaa temppua sekä mainitsen | |
pullonkauloja joita nopeuttamalla saadaan aikaan dramaattisia muutoksia. | |
Siis fixed point, mitä se on? Kuten tiedät, C:n int-tyyppi on kokonaisluku, | |
eli sillä ei voi ilmoittaa desimaalilukuja. Monesti desimaaliluvu olisivat | |
tarpeellisia, esimerkiksi sprite-enginessä, jos halutaan että eri spritet | |
liikkuvat eri nopeuksilla. Näyttää nimittäin todella typerältä jos ohjus | |
pomppii kymmenen pikseliä eteenpäin, koska se on 10 kertaa nopeampi kuin | |
pelin hitain sprite. Tarvitsemme siis nopeudeksi desimaaliluvun, jolloin | |
ohjuksen nopeus voisi olla 1 ja kilpikonnan 0.1 (jolloin se liikkuisi yhden | |
pikselin joka 10. frame). Valitettavasti float-tyyppisten muuttujien kä- | |
sittely on moninkertaisesti hitaampaa (tosin pentium-optimoitu peli voi | |
niitä käyttää, ainakin assemblerilla voidaan pentiumin matematiikkapro- | |
sessoria käyttää täysipainoisesti ja peliä nopeuttaa). Niinpä meidän täy- | |
tyisi pystyä esittämään kokonaisluvuilla desimaalilukuja. Onko tämä mahdol- | |
listakaan? | |
Kyllä se on, katsokaamme hieman toisella tavalla normaaleja lukujamme. | |
Meidän luvuissamme on kokonaislukuosa ja desimaaliosa sekä välissä piste. | |
Kokonaislukuosalla voidaan ilmaista 10^<numeroja> lukua, eli jos | |
kokonaislukuosassa on 3 numeroa niin voimme ilmaista sillä 10^3=1000 | |
erilaista lukua, välillä 0-999. Pisteen toisella puolella on kaikki muuten | |
samalla tavalla, mutta meidän täytyy ajatella käänteisesti. Voimme ilmaista | |
desimaaliosalla desimaalin, joka on yksi 10^<numeroja>:sosa. Tämä näyttää | |
sekavalta, mutta oletetaan että meillä on 2-numeroinen desimaaliosa, niin | |
pienin desimaali on 1/10^2, eli yksi SADASOSA. Seuraava kaavio varmaan sel- | |
ventää asiaa: | |
1234.123 = 1234 + 123/10^3 = 1234 + 123/1000 = 1234.123 | |
Nyt menemme vähän pidemmälle. Oletetaan, että meillä olisi luvussa pilkku | |
AINA samalla kohdalla ja desimaalia esittäviä lukuja 3. Takaisin voisimme | |
sen palauttaa vain jakamalla kokonaisluku tuhannella (kolme desimaalinumeroa, | |
eli siis 10^3=1000): | |
1234123 = 1234123/1000 = 1234.123 | |
Kuten huomaat pilkku voidaan ajatella sinne nelosen ja ykkösen väliin. | |
Nyt kysyt ehkä että mitä hyötyä tästä on. Siitä on seuraava hyöty: Meillä | |
on kaksi lukua, 0.1 ja 5.4, jotka haluamme laskea yhteen. Muunnetaanpa ne | |
oikeaan muotoon: 0.1*1000=100 ja 5.4*1000=5400. Haluamme laskea ne yhteen: | |
100+5400 = 5500. Nyt muuntakaamme takaisin: | |
5500/1000 = 5.5 = 5.5 (5.4 + 0.1 = 5.5). | |
Eli meillä on sama tulos! Vähennyslasku toimii ihan yhtä hyvin. Voimme las- | |
kea desimaalilukuja kokonaisluvuilla. Mutta tarvitsemme vielä kaksi laskua, | |
kerto- ja jakolaskun. Koska lukumme ovat kummatkin 1000-kertaisia todelli- | |
suuteen nähden niin ne kertomalla saamme 1000000-kertaisen tuloksen, joten | |
lopuksi meidän täytyy jakaa tulos tuhannella. Eli: | |
5400*100 = 540000 => 540000/1000 = 540 => 540/1000 = 0.54 | |
(5.4 * 0.1 = 0.54) | |
Ja tadaa! Meillä onkin oikea tulos. Vielä jakolasku, siinähän jaamme vain | |
numerot toisillamme, mutta tässä häviää meiltä desimaaliosa, eli meidän pi- | |
täisi kertoa tulos lopuksi tuhannella. Tarkemman tuloksen saamme kun | |
kerromme ensin jaettavan tuhannella ja sitten vasta jaamme: | |
(5400*1000) / 100 = 54000 => 54000/1000 = 54 (5.4 / 0.1 = 54). | |
Nyt meidän täytyy sitten syventyä siihen miten toteutamme nopeasti edelliset | |
asiat tietokoneen binäärijärjestelmällä. Se on erittäin helppoa. Teemme | |
vaikka 32-bittisen luonnollisen (unsigned int), josta 16 alinta bittiä on | |
varattu desimaaliosalle. Koska binäärijärjestelmä on 2-kantainen, niin | |
meidän täytyy vain muuttaa pikku laskumme kahden potensseilla leikkimisiksi. | |
Tällaisella luvulla voimme siis esittää 16-bittisen kokonaislukuosan, | |
maksimissaan 2^16=65536 ja 16-bittisen desimaaliosan, joten pienin desimaali | |
n 1/2^16 = 1/65536 = n. 0.000015228. | |
Entiset laskumme toimivat ihan hyvin, muunnamme vain luvut kertomalla ne | |
65536:llä ja palautamme jakamalla 65536:llä. Nopeuttamisessa apuna ovat | |
vielä bittisiirrot, joiden avulla voimme kertoa nopeasti 65536:lla | |
siirtämällä bittejä 16 vasemmalle ja jakaa siirtämällä niitä oikealle. | |
Tässä on pieni esimerkkiohjelma, joka demonstroi fixedin käyttöä: | |
#include <stdio.h> | |
int main() { | |
unsigned int a, b, tulos; | |
a=(unsigned int)(5.4 * 65536.0); | |
b=(unsigned int)(0.1 * 65536.0); | |
tulos=a+b; | |
printf("A+B=%f\n", tulos/65536.0); | |
tulos=a-b; | |
printf("A-B=%f\n", tulos/65536.0); | |
tulos=(a*b)/65536; | |
printf("A*B=%f\n", tulos/65536.0); | |
tulos=(a/b)*65536; | |
printf("A/B=%f\n", tulos/65536.0); | |
return 0; | |
} | |
Mieti nyt kaikkea ihan rauhassa. Jos luulet ymmärtäneesi edes jotain niin | |
hyvä, jos et ymmärtänyt mitään niin lue uudelleen ja uudelleen ja kokeile | |
paperilla. Jos et siltikään ymmärtänyt niin lue jostain toisesta dokumentis- | |
ta! Fixed-pointissa on huomattava pari asiaa: | |
1) Luvut voivat mennä yli ja tulee ihmeellisiä tuloksia. Jakolaskuesimerkis- | |
säni en voinut kertoa a:ta ensin 65536:lla, sillä muuten olisi luku men- | |
nyt ympäri. Kannattaa aina varmistaa ettei luku voi mennä ympäri. | |
2) Käytä bittioperaatioita aina kuin mahdollista. 32-bittisestä | |
16.16-fixedistä (tarkoittaa, 16 bittiä kokonais- ja 16 bittiä desimaali- | |
osalle) saat desimaaliosan halutessasi AND-funktiolla maskin 0xFFFF | |
kanssa. Voit käyttää kaikkia nerokkaita optimointikikkoja jos vain kek- | |
sit niitä. Myös pyörähdystä voi käyttää hyväksi (jotenkin). | |
3) Signed luvut toimivat samoin, mutta ylin bitti merkkaakin etumerkkiä, | |
eli 16.16-luku int-tyyppinä onkin oikeasti 15.16. | |
4) Valitse itse pilkun paikka. Mitä enemmän bittejä desimaaleille sitä tar- | |
kempia lukuja. Mitä enemmän bittejä kokonaisluvuille sitä suurempia ja | |
epätarkempia lukuja. | |
5.3 Lookup-tablet ja muita optimointivinkkejä | |
--------------------------------------------- | |
Lookup-tableissa, eli lookupeissa ei ole oikeastaan muuta selittämistä, kuin | |
että niissä toistuvia, vain yhtä (tai joskus kahtakin) muuttujaa käyttävis- | |
sä monimutkaisissa laskutoimituksissa (tai muuten vain hidastavissa) | |
lasketaan tulokset etukäteen taulukkoon käyttäen indeksinä sitä lukua joka | |
oli muuttuvana laskutoimituksessa. Tähän käy esimerkkinä sinin laskeminen | |
taulukkoon. Sin-funktio on hidas laskea ja siinä pitää aina suorittaa pitkä | |
konversio asteista radiaaneiksi (3.14*2*aste/256, 256:n ollessa suurin | |
kulma + 1, 360-asteisella ympyrällä luku olisi 360 ja suurin kulma 359) ja | |
lopuksi vielä ottaa siitä sini. Nyt laskemmekin kaikki 256 arvoa taulukkoon | |
(fixed-point-sellaiseen, muoto 1.14, 16-bittinen signed, muuntoluku 16384): | |
for(c=0; c<256; c++) | |
sin_table[c] = (short)(sin(3.141592654*2*c/256.0)*16384); | |
Nyt jos haluamme kulman 15 sinin, niin osoitamme vain sin_table[15], emmekä | |
(short)(sin(3.141592654*2* 15 /256.0)*16384). | |
Sitten sekalaisia optimointivinkkejä: | |
1) Suuria määriä dataa käsittelevät loopit assemblerilla. Lisää tietoa | |
inline-assemblerin käytöstä DJGPP:llä tiedostosta DJTUT*.ZIP, | |
vaikka MBnetistä, tai tämän tutoriaalin Nasmia käsittelevästä | |
luvusta. | |
2) Kaikki muuttumattomat vertailulausekkeet loopin ulkopuolelle: | |
for(c=0; c<1000000; c++) if(a==b) puskuri[c]=0; onkin: | |
if(a==b) for(c=0; c<1000000; c++) puskuri[c]=0; | |
Vähennämme näin 1000000 vertailua. | |
3) Älä tuhlaa aikaasi optimoimalla suuria määriä logiikkaa, ellei siitä | |
todella ole hyötyä. Esimerkkinä vaikka kaksoispuskurin tyhjennyksen | |
tekeminen inlinenä memsetin sijaan säästää kyllä aikaa, mutta kun | |
ajansäästö funktiokutsun jäämisessä pois on jotain 1/10000 siitä | |
mitä aikaa memsetissä menee joka tapauksessa, on hyödyttömyys | |
varsin ilmeistä. | |
4) Käytä fixediä floatin tilalla aina kuin mahdollista. | |
5) Laske kaikki toistuva konemainen laskenta taulukkoihin. | |
6) Käytä DJGPP:n käännösvalitsinta -O2, tai jopa -O3 (joka kyllä suurentaa | |
ohjelmaasi reilusti). | |
Yleensäkin kannattaa uhrata paljon aikaa grafiikkakirjastojen ja äänikirjas- | |
tojen optimointiin ja pitää itse runko selkeänä C-kielisenä kutsujen joukko- | |
na. Tämä ei paljoa hidasta ja selventää uskomattomasti koodia ja nopeuttaa | |
kehitystä. | |
5.4 Väliaikatulokset ja fontteja | |
-------------------------------- | |
Tässä vaiheessa osaat nyt kaikki tärkeimmät niksit mitä peliohjelmointiin | |
tarvitaan. Tästä luvusta lähtien alan tietoisesti vähentämään, ellen | |
jopa joissain kohdissa poistamaan esimerkkiohjelmia. Mitä tästä lähtien | |
tarvitset on maalaisjärkeä ja kykyä osata soveltaa oppimiasi asioita. | |
Eli tänään meillä on siis jotain, mitä kutsutaan nimellä fontit? Idea fon- | |
tienginen teossa on tehdä tavallaan karsittu bittikarttaengine. Fontti- | |
enginen voit tehdä esimerkiksi poistamalla sprite-koodistamme pyyhkimisen | |
(halutessasi voit myös poistaa läpinäkyvyyden tai jättää pyyhkimisen jos | |
tarvitset sitä, sinun pitää siinä tapauksessa vain tehdä erikoisjärjeste- | |
lyjä) ja käyttää animaationa kuvasarjaa jossa on piirrettynä merkit a-z, | |
A-Z, 0-9 ja sitten joitakin mahdollisesti tarvittavia välimerkkejä, kuten | |
.!?,;:'" ja muut vastaavat. Sitten vain teet funktion, joka vaihtaa framek- | |
si oikean kuvan ja piirtää sen, jonka jälkeen se korottaa x-arvoa merkin | |
leveydellä (plus jonkin verran väliä seuraavan merkin ja viimeisen välille) | |
ja ottaa käsittelyyn seuraavan merkkijonon merkin. | |
Koodi voisi näyttää vaikka tältä: | |
void printString(char *string, int x, int y) { | |
int c; | |
for(c=0; c<strlen(string); c++ { | |
if(string[c]>'a' && string[c]<'z') { | |
setframe(string[c]-'a'); /* a olisi frame 0 */ | |
drawchar(x+c*9, y); /* merkin leveys 8 + 1 pikseli erottamaan */ | |
} else if(string[c]>'A' && string[c]<'Z') { | |
setframe(string[c]-'A' + 'z'-'a' + 1); | |
/* eli suomeksi A-kirjaimella olisi paikka heti viimeisen pienen | |
kirjaimen jälkeen, joka on 'z'-'a' */ | |
drawchar(x+c*9, y); | |
} else if(string[c]>'0' && string[c]<'9') { | |
setframe(string[c]-'0' + 'z'-'a' + 1 + 'Z'-'A' + 1); | |
/* tämä taas tulee pienien JA isojen kirjaimien jälkeen */ | |
drawchar(x+c*9, y); | |
} else if(c == '.') { /* jos c on erikoismerkki */ | |
setframe('9'-'0' + 1 + 'z'-'a' + 1 + 'Z'-'A' + 1); | |
drawchar(x+c*9, y); | |
/* ideana siis, että piste tulee kaikkien kirjainten ja | |
numeroiden jälkeen */ | |
} | |
... | |
} | |
} | |
Kuten ehkä huomasit tuli koodista aivan kammottavaa sekasotkua ja on ihme | |
jos sait siitä jotain selvää. Lisäksi koodi ei ole erityisen nopeaakaan, | |
saati sitten että se edes välttämättä toimii. Mutta miten voisimme nopeuttaa | |
tätä? Vastaus on lookup-tablet. Sillä mehän tiedämme, että C:llä kirjain on | |
vain numero välillä 0-255. Niinpä teemme taulukon jonka jokainen alkio | |
osoittaa indeksin mukaisen ASCII-kirjaimen framenumeroon. Jos et ymmärtänyt | |
niin tässä on esimerkki taulukon käytöstä: | |
frame = asciitaulukko['a']; | |
Asciitaulukon alkio 'a' (numerona 97) olisi 0, joten framenumeroksi tulisi | |
näinollen tämä luku. Sitten vain framenvaihto: "setframe(frame)". | |
Tietenkin tuo kannattaisi käyttää näin: "setframe(asciitaulukko['a'])"... | |
Mutta miten sitten taulukko alustetaan? Tapoja on monia, jotkin ovat seka- | |
vampia ja jotkin vähän selvempiä, mutta annan sinun itsesi päättää mikä on | |
paras. Mahdollisuutena olisi ensin täyttää taulukko nollalla (joka olisi | |
tyhjä frame) ja sitten loopata aakkoset a-z täyttäen taulukon kohdat 'a'-'z' | |
oikeilla framearvoilla (1...26), sitten loopataan 'A'-'Z' täyttäen ne alkiol- | |
la 27...52 jne. Myös lataaminen kannattaa automatisoida. | |
Muista lisäksi huomioonottaa erikoismerkit enginessäsi. Tarpeellisia voivat | |
olla välilyönti (32), rivinvaihto (\n), tabulaattori (\t) jne. Ja lisäksi | |
saat aivan vapaasti päättää onko fontin väri mahdollista vaihtaa vai käytät- | |
kö aina samanlaisia fontteja, joka mahdollistaa vähän hienommat, vaikka moni- | |
väriset fontit. | |
5.5 Hiirulainen, jokanörtin oma lemmikki | |
---------------------------------------- | |
Tänään, tytöt ja pojat, setä puhuu hieman kotieläimistä. Ne ovat sellaisia | |
pieniä valkoisia ötököitä, joilla on häntä ja jotka viipottavat matolla. | |
Sen lisäksi niitä voi myös painella. Ei, nyt ei ole kyse mistään karvaisesta, | |
vaan ihan aidosta tietokoneen lisälaitteesta, jota hiireksikin kutsutaan. | |
Tällä karvattomalla ystävällämme on säädyttömän monia haaroja sukupuussaan. | |
Löytyy Logitechia, Microsoftia, Targaa ja ties mitä vimputinta ja kaiken | |
kukkuraksi rautatasolla käskyttäminenkin on suorastaan säädyttömän | |
epästandardia. Onneksi hätiin rientää kymmenisen vuotta vanha apu nimel- | |
tään _hiirikeskeytys_, kiinnostavemmin ilmaistuna keskeytys 33h. Tätä | |
keskeytystä käyttäen saadaan kaikkien hiireen tungettujen vimpainten, kuten | |
nappien ja pohjassa (yleensä) pyörivän pallukan tila. Nämä tiedot ovat helpon | |
saatavuuden lisäksi myös naurettavan helppokäyttöisiä, kunhan vain tietää | |
miten niitä käyttää. | |
Jos et vielä tiedä miten keskeytyksiä käytetään tulee tässä tiivistettynä | |
niiden käyttö DJGPP:llä. Keskeytykselle annetaan parametrit rekistereissä | |
ja ne saadaan rekistereissä. Jos DJGPP oli yhtä huoleton kuin Borland | |
Turbo-kääntäjineen olisi meilläkin rekisteri ax nimellä _AX jne. Mutta koska | |
kaikki on tehty rakkaalla kääntäjällämme hipun vaikeammaksi teemme sen | |
standardilla tavalla. Alhaalla näet tarvittavat askeleen keskeytyksen kut- | |
sumiseksi ja rekisterien näpläykseksi. Esimerkki käyttää yhtä kymmenistä kes- | |
keytyksen aiheuttavista funktiosta int86(...) kirjastosta dos.h: | |
1) Tarvitset rekisterit muuttujinaan sisältävän unionin, int86:n tapauksessa | |
unioni on nimeltään REGS ja sen sisällä on pari structia joihin | |
tutustut vaikka selaamalla ko. kirjastoa. En ala perehtymään syvemmin | |
näihin x, d ja w-rakenteisiin. Tässä kuitenkin käytämme viimeistä, joka | |
on 16-bittiset rekisterit. | |
union REGS rekisterit; | |
2) Tunge kaikki parametrit uuteen muuttujaasi. | |
rekisterit.w.ax=jotain; | |
rekisterit.w.di=muuta; | |
rekisterit.w.cs=kivaa; | |
3) Kutsu funktiota int86(vektori, inputti rekisterit, outputti rekisterit) | |
int86( keskeytys, &rekisterit, &rekisterit ); | |
4) Kaivele esiin muuttuneet rekisterisi ja tallenna ne muuttujiin. | |
ihan=rekisterit.w.bx; | |
helppo=rekisterit.w.ds; | |
homma=rekisterit.w.cx; | |
Tehdessäsi hiiriohjattua ohjelmaa sinun pitää tietysti hiiren koordinaattien | |
ja nappien käsittelyn lisäksi piirtää kursori ruudulle, ellet sitten halua | |
käyttää (amatöörimäisen näköistä) kursoria, jonka ajuri piirtelee ruudullesi. | |
Grafiikkatilassa tämä onnistuu vaikka tekemällä hiirestä yksi spriteistä ja | |
liikuttelemalla sitä. Antaa paljon paremman kuvan ohjelman tekijästäkin! | |
Tekstitilassa vaihdat vaikka ko. kohdan väriä. Tähän ihmeelliseen tilaan | |
tutustumme kohtapuolin, eli jatka lukemistasi jos haluat tehdä tekstitila- | |
ohjelman, joka käyttää kursoria... | |
Tässä nyt olisivat nämä kaikkein käytännöllisimmät ja alkuun auttavat funk- | |
tiot. Lisää löydät vaikkapas Ralph Brownin interruptilistasta tai kenties | |
jopa HelpPC:stä. RB:n lista on MBnetissä nimellä INTERxxy.ZIP, jossa xx on | |
versionumero (kai 48 tarkoittaen 4.8:aa) ja y paketin numero, itse listassa | |
A-E tjsp. ja muitakin kirjaimia on sisältäen muunmuassa selailuohjelman, | |
konvertoinnin Windowsin help-muotoon jne.. Mutta, kuten lupasin: | |
Funktio 0 - Hiiren alustus | |
Parametrit: AX=0 | |
Palauttaa: AX=0 jos ajuria ei ole installoitu, FFFFh jos on installoitu. | |
Funktio 1 - Näytä kursori (se kauhea siis) | |
Parametrit: AX=1 | |
Palauttaa: - | |
Funktio 2 - Piilota kursori (se kauhea siis) | |
Parametrit: AX=2 | |
Palauttaa: - | |
Funktio 3 - Anna koordinaatit ja nappien tila | |
Parametrit: AX=3 | |
Palauttaa: CX=x-koordinaatti (0...639) | |
DX=y-koordinaatti (0...199) | |
BX=nappien tila (bitti 0 vasen nappi, bitti 1 oikea ja | |
bitti 2 keskimmäinen nappi) | |
Funktio 4 - Aseta kursorin koordinaatit | |
Parametrit: AX=4, CX=x-koordinaatti, DX=y-koordinaatti | |
Palauttaa: - | |
Funktio 5 - Nappien painallukset | |
Parametrit: AX=5, | |
BX=mikä nappi (0 vasen, 1 oikea ja 2 keskimmäinen) | |
Palauttaa: Muuten kuten funktio 3, mutta koordinaatit kertovat kursorin | |
sijainnin viime painalluksella ja BX kertoo ko. napin painal- | |
luksien määrän sitten viime kutsun. | |
Funktio 6 - Nappien vapautukset | |
Parametrit: AX=6, | |
BX=mikä nappi (0 vasen, 1 oikea ja 2 keskimmäinen) | |
Palauttaa: Muuten kuten funktio 5, mutta vapautuksen tiedot. | |
Funktio 7 - Vaakarajoitukset | |
Parametrit: AX=7, | |
CX=pienin sallittu X-sijainti, | |
DX=suurin sallittu X-sijainti | |
Palauttaa: - | |
Funktio 8 - Pystyrajoitukset | |
Parametrit: AX=8, | |
CX=pienin sallittu Y-sijainti, | |
DX=suurin sallittu Y-sijainti | |
Palauttaa: - | |
Funktio B - Liikemäärä | |
Parametrit: AX=B | |
Palauttaa: CX=vaakamikkien määrä | |
DX=pystymikkien määrä | |
Funktio F - Mikkejä pikseliä kohden | |
Parametrit: AX=F | |
CX=vaakamikkien määrä | |
DX=pystymikkien määrä | |
Palauttaa: - | |
Lisäksi on vielä ainakin funktio C, joka asettaa oman käsittelijän, mutta | |
koska se ei luultavasti kiinnosta kovin monta (rm-osoitetta odottava käsit- | |
telijä ei ehkä oikein toimi PM:ssä kunnolla jne...) jätän sen tässä väliin. | |
Sitten vain tekemään kaiken maailman testiohjelmia. Esimerkkejä ei tule | |
tässä lainkaan, sillä oletan jokaisen pystyvän edellisten ohjeiden perusteel- | |
la kyhäämään itseään tyydyttävän ohjelman. | |
Jos homma ei kuitenkaan ota luonnistuakseen tai tässä kappaleessa oli muita | |
epäselvyyksiä niin otahan yhteyttä niin kaivelen lisää tietoa aiheesta. | |
Erityiskiitos tämän kappaleen teon auttamisesta kuuluu nyt kyllä MB:n numerol- | |
le 4/96 josta katsoin nopeasti tiivistelmän hiirifunktioista. | |
Ja ensi kappaleessa onkin uudet kujeet, näyttäisi olevan tekstitilan hallinta | |
seuraavana edessä... | |
5.6 Tekstitilan käsittely suoraan | |
--------------------------------- | |
Tästä kappaleesta tulee tulemaan äärimmäisen lyhyt. Ainoa meitä kiinnostava | |
seikkahan on tekstimuistin osoite (tila 3, 80x25, myös muut voivat toimia) | |
ja rakenne. Osoite on perusmuistin segmentti B800h, eli lineearinen osoite | |
selektorin _dos_ds osoittamassa muistissa olisi C:llä 0xB8000. Rakenne | |
on myös naurettavan yksinkertainen. Erona VGA:han (ks. kappale | |
"Grafiikkaa - mitä se on?" jos et muista) on vain se, että yksi alkio | |
koostuu kahdesta tavusta (joista ensimmäinen on merkin ASCII ja toinen | |
merkin väri) ja ruudun leveys on 80 merkkiä. Jos ei mennyt päähän niin | |
tutustu vielä kerran VGA:ta käsittelevään kappaleeseen ja tutkaile seuraavia | |
makroja: | |
#define putchar(x, y, c) _farpokeb(_dos_ds, 0xB8000+(y*80+x)*2, c); | |
#define putcolor(x, y, c) _farpokeb(_dos_ds, 0xB8000+(y*80+x)*2+1, c); | |
Vielä jos olit kiinnostunut hiiren kursorin tekemisestä tekstitilaan voisi | |
seuraava funktio olla sinulle omiaan: | |
void inline addcolor(int x, int y, char c) { | |
int originalc=_farpeekb(_dos_ds, 0xB8000+(y*80+x)*2+1); | |
putcolor(x, y, originalc+c); | |
} | |
Sitten vain "piirrät" kursorin lisäämällä väriarvoon - sanotaan vaikka 17 | |
ja pyyhit kursorin lisäämällä siihen saman arvon vastaluvun (-17), eli | |
toisinsanoen vähennät siitä 17: | |
#define CShow(x, y, c) addcolor(x, y, c) | |
#define CHide(x, y, c) addcolor(x, y, -c) | |
Makrojen käyttö sitten komennoilla "CShow(17)" ja "CHide(17)"... | |
Lopuksi vielä sananen merkin värin muodosta. Se on XYYYZZZZ, jossa jokainen | |
kirjain edustaa yhtä bittiä väritavussa. X ilmaisee vilkkuuko merkki (1). | |
YYY ilmaisee taustan värin (0-7) ja ZZZZ ilmaisee tekstin värin (0-15). | |
Tässä vielä pikkuruinen makro, joka voi osoittautua hyödylliseksi: | |
#define BuildC(blink, fore, back) ( (blink<<7) + (back<<4) + (fore) ) | |
Sitten vain vaikka komento "putcolor(x, y, BuildC(0,15,1))", joka aiheuttaisi | |
välkkymättömän valkoisen tekstin sinisellä pohjalla (31). | |
Sellaista tällä kertaa. Nyt painun suihkuun ja katsomaan X-Filesia. Jatketaan | |
taas vaikka huomenna! | |
6.1 Projektien hallinta - useat tiedostot | |
----------------------------------------- | |
Nyt seuraakin sitten jakso lukuja (tai yksi luku, katsotaan nyt), | |
joissa käsitellään kaikkea tärkeää mitä pelejä ohjelmoidessa pitää | |
osata sen hardwaren tuntemuksen lisäksi. Tarkoituksena on käydä läpi | |
useiden c-tiedostojen käyttö, headerien teko, Rhiden projektit, | |
makefileet, ulkoisen assyn ja assyn yleensäkin käyttö, engineiden | |
teko, kirjastojen luonti. Kaikki suhteellisen kevyttä kamaa kun ne | |
vain kerran opettelee, joten aloitamme. | |
Tähän asti olen opettanut teille huonoja tapoja joita itselläni oli | |
tapana käyttää vielä puolitoista vuotta sitten (ja vasta viime aikoina | |
olen päässyt lopullesesti niistä eroon). Olen nimittäin laittanut | |
koodia noihin .h-tiedoistoihin ja tehnyt niistä kirjastoja, joiden | |
rutiineja on sitten helppo käyttää. Laajempien projektien ja miksei | |
hieman suppeampienkin kanssa alkaa kuitenkin ennenpitkää esiintyä | |
suorastaan ärsyttävän hidasta kääntämistä. Ajattele seuraavaa | |
tapausta: | |
Peliprojektissa on ääniengine sound.h (yksinkertainen, vain vähän alle | |
3000 riviä), sprite-engine sprite.hh (minimaalinen toiminta, hieman | |
inline-assyä, 800 riviä), sekalaisia hardware-rutiineja | |
(kellokeskeytys, näppishandleri jne. 1000 riviä) sekä itse pelin | |
koodia 2000 riviä. Näin joka kerta käännämme vähän alle 7000 riviä | |
C-koodia. Mutta miksi kääntää kaikki joka kerta kun vain yksi muuttuu | |
yleensä kerrallaan? Muuttakaamme hieman lähestymistapaa löytääksemme | |
parempi keino. | |
Keinoa kutsutaan projekteiksi, usean C-tiedoston käytöksi ja ties | |
miksi. Ideana on, että jokainen looginen kokonaisuus on jaettu omaan | |
.c-tiedostoonsa ja .h-tiedostoonsa. Tällaisia voisivat olla | |
näppishandleri, timerhandleri, sprite-rutiinit, modien lataus, | |
äänienginen ohjelmointirajapinta, sb-osa koodista, gus-osa koodista | |
jne.. Jokaiselle tiedostolle olisi sitten oma .h-tiedostonsa, jossa | |
määritellään kaikki c-tiedoston funktiot ja globaalit muuttujat (jos | |
niitä tarvitaan). Sitten toiset c-tiedostot jotka tarvitsevat tuon | |
tiedoston funktiota tai muuttujia ottaisivat vain includella | |
h-tiedoston mukaan ja kääntäjän linkkeri huolehtisi siitä, että | |
ohjelmakutsut menevät oikeisiin osoitteisiinsa. | |
Katsotaanpas pientä esimerkki h-tiedostoa ja c-tiedostoa. En väitä | |
tämän olevan ainoa oikea tapa, tämä on vain yksi tapa hoitaa homma: | |
ESIM.H: | |
#ifndef __ESIM_H | |
#define __ESIM_H | |
#include <stdio.h> | |
#define ESIMTEKSTI "Moikka, olen esimerkki!" | |
void Esimteksti(); | |
extern int Kutsukertoja; | |
#endif | |
ESIM.C: | |
#include "esim.h" | |
int Kutsukertoja=0; | |
int Oma=666; | |
void Esimteksti() { | |
puts(ESIMTEKSTI); | |
Kutsukertoja++; | |
} | |
Lähdetäänpäs askeltamaan ESIM.H-tiedostoamme lävitse. Ensimmäisenä | |
rivi #ifndef __ESIM_H, joka ilmoittaa C-koodin esikäsittelijälle, että | |
jos __ESIM_H ei ole määritelty (IF Not DEFined, IFNDEF) niin osio | |
#ifndef:in ja #endif:in välissä tulee ottaa mukaan. Sen jälkeen | |
määritellään tuo kyseinen muuttuja, jotta H-tiedostoa ei pureta | |
kahteen kertaan (voi sattua kaikkea hassua jos vaikka h-tiedostot | |
kutsuvat toisiaan). Sitten tulee tämän C-tiedoston tarvitsemien | |
funktioiden kirjastot ja #definet (kirjastot voitaisiin sijoittaa myös | |
C-tiedostoon, mutta joskus tästä tulee ongelmia, jos käytetään makroja | |
tai muuta vastaavaa). | |
Sitten tulevat muuttujat ja funktiot. Muuttujien eteen TULEE laittaa | |
extern-määre, joka kertoo että ne on oikeasti määritelty jossain | |
muualla, jottei kääntäjä varaa muistia näille joka H-tiedoston | |
includettamisen kohdalla, jolloin linkatessa useissa C-tiedostoissa on | |
varattu muistia samannimiselle globaalille muuttujalle -> ongelmia. | |
Funktioiden edessä extern ei ole pakollinen ja sen voikin jättää pois | |
ja lisätä extern-määreen jos ko. funktio on ulkoisessa | |
assembler-tiedostossa. | |
Funktion parametrien nimet voi halutessa jättää määrittelyistä pois, | |
mutta se ei ole suositeltavaa. Muista myös, että globaalit muuttujat | |
esitellään ja alustetaan VAIN ja AINOASTAAN C-tiedostossa, ei | |
H-tiedossa! | |
C-tiedosto sisältää vastaavat H-tiedostossa "luvatut" funktiot ja | |
muuttujat. Jos haluat tehdä globaaleja muuttujia jotka eivät näy | |
muihin C-tiedostoihin, niin jätät sen esittelyn H-tiedostosta pois, | |
jolloin headerin sisällyttävät muut C-tiedostot eivät tiedä mitään | |
ko. muuttujan olemassaolosta eikä vahingossa tule virheitä. Tällainen | |
on esimerkki C-tiedoston muuttuja Oma. | |
Useita C-tiedostoja käyttäessäsi teet siis jokaisesta loogisesta | |
kokonaisuudesta oman "paketin", joka sisältää C-tiedoston, joka on | |
toimiva kokonaisuutensa ja H-tiedoston, joka tarjoaa muille | |
C-tiedostoille mahdollisuuden käyttää tämän paketin rutiineja. | |
Muista, että käyttäessäsi includea tuollaisen tiedoston kohdalla | |
käytetään heittomerkkejä normaalin <>-parin sijasta, jottei kääntäjä | |
lähde hakemaan ESIM.H:ta omasta include-hakemistostaan, vaan jotta se | |
hakisi tiedoston senhetkisestä työskentelyhakemistosta. | |
Mieti nyt nämä asiat selviksi, jotta ymmärrät miten tehdään useita | |
tiedostoja ja käytetään ilman ongelmia, niin voit sen jälkeen jatkaa | |
seuraavaan lukuun, jossa kerrotaan miten niistä muodostetaan ajettavia | |
ohjelmia, kirjastoja ja objektitiedostoja. | |
6.2 Useiden tiedostojen projektit - kääntäminen ja hallinta | |
----------------------------------------------------------- | |
No niin, osaat nyt tehdä C-tiedostoja ja H-tiedostoja, mutta sillä ei | |
varmaankaan pitkälle pötkitä. Lähdemme nyt tutkimaan hieman | |
kääntäjämme, GCC:n sielunelämää ja tutustumme muutamaan elintärkeään | |
tietoon joita ilman ei voi edes elää. Nimittäin janoamme tietoa | |
formaateista. | |
Tiedostot joiden kanssa pyörimme DJGPP:n kanssa voidaan jakaa helposti | |
pelkistäen neljään (4) kategoriaan. Tässä ne ovat: | |
1. Lähdekooditiedostot (c, cc, s, asm). Kääntäjä muuttaa koodin | |
konekieleksi ja tekee muut tarvittavat tehtävät tuottaen | |
objektitiedoston. | |
2. Objektitiedosto (O). Sisältää koodin ja symboleja (eli funktioiden | |
ja muuttujien nimiä) ja kaikkea muuta kivaa infoa jotka liittyvät | |
olennaisesti rutiinien käskyihin ja dataan. Linkkeri linkkaa kaikki | |
objektitiedostot yhteen ja lisää tarvittavaa käynnistyskoodia sun | |
muuta luodakseen ajettavan tiedoston. Nämä ovat eräänlaisia | |
rakennuspalikoita, joissa kaikki on jo binäärimuodossa. | |
3. Archive (A). Tätä voidaan halutessa käyttää useiden objektien | |
säilömiseen, eli paketoidaan monta objektitiedostoa yhteen kasaan | |
jotka voidaan liittää sitten yhtenä pakettina | |
kääntäjälle. Objekteista siis kootaan nippu jota voidaan käsitellä | |
yhtenä kokonaisuutena. | |
4. Ajettava tiedosto. Sisältää objektitiedostoista tehdyn EXE:n, jossa | |
on lisäksi tarvittava koodi ohjelman käynnistämiseen. | |
GCC:n toimintaperiaate EXE:n käännössä on seuraava: Lähdetään | |
kääntämällä lähdekooditiedostot objektitiedostoiksi. Tässä vaiheessa | |
siis laajennetaan makrot, includet ja esikäsittelijän komennot (kaikki | |
#ifndef-rakenteet sun muut). Sitten käännetään koodi konekielelle ja | |
tehdään objektitiedostot. | |
Seuraavaksi kutsutaan linkkeri joka liittää objektitiedostot yhteen ja | |
lisää tarvittavat kirjastot (LIBC.A tulee EXE:en aina mukaan ja | |
lisäksi muut -l<nimi> parametreillä annetut kirjastot) sekä | |
aloituskoodin, joka kutsuu main-funktiota, jonka oletetaan löytyvän | |
jostain O-tiedostosta. | |
Itseasiassa tuo ei mene aivan noin yksinkertaisesti, mutta tärkeintä | |
on ymmärtää, että lähdekoodista tehdään rakennuspalikoita, | |
objektitiedostoja joista voidaan myöhemmin koota ajettavia tiedostoja. | |
Jos meillä siis olisi C-tiedostot main.c ja apu.c (mahdollisesti | |
vastaavine H-tiedostoineen), joista main.c sisältäisi main-funktion ja | |
pääkoodin ja apu.c kaikkia tarpeellisia rutiineja, niin voisimme | |
kääntää ne objektitiedostoiksi ja aina kun jompaakumpaa muunnetaan, | |
niin kääntäisimme tämän lähdekooditiedoston uudelleen. EXE | |
muodostettaisiin erikseen toisella komennolla jolloin muutos toisessa | |
tiedostossa vähentäisi käännettävän koodin määrää (tosin linkkaustyö | |
pysyisi ennallaan). | |
Miten sitten näitä erilaisia tiedostoja tehdään? Hyvä kysymys. Alla | |
näette kaikkein komentoja objektitiedostojen, EXE:jen ja archivejen | |
luontiin, lähdekoodit osaatte varmaan jo. =) | |
Objektitiedosto GCC:llä: | |
gcc -c koodi.c -o objekti.o (halutessa lähdetiedostoja voi olla useampia) | |
Archive-tiedosto objektitiedostoista: | |
ar rs archive.a objekti1.o ... (kaikki halutut objektit vain perään) | |
Ajettava tiedosto archive-, objekti- ja lähdekooditiedostoista (GCC | |
osaa käsitellä ne päätteiden mukaan): | |
gcc <tiedostot> -o tulos.exe <parametrit> | |
Lisää infoa sitä haluaville löytyy englanninkielisenä komennolla | |
INFO. Sitä löytyy aika paljon enkä todellakaan halua tästä | |
tutoriaalista mitään DJGPP:n komentoriviparametrien selitystä. =) | |
Eli kerrataan vielä vaiheet joita käytätte "oikeaoppisen" projektin | |
tekoon: | |
1. Luo C- ja H-tiedostot ja muu tarvittava lähdekoodi | |
2. Käännä ne O-tiedostoiksi (tyyliin gcc -c koodi.c -o objekti.o) | |
3. Jos haluat tehdä kirjastoja, niin tee objektitiedostoista ar:llä | |
niitä. Esimerkiksi grafiikkaenginen objektitiedostot voisi liittää | |
yhteen ja nimetä libgraf.a:ksi ja siirtää DJGPP:n LIB-hakemistoon. | |
Myöhemmin nuo enginen objektit olisi helppo lisätä EXE:een pelkällä | |
-lgraf -parametrilla. | |
4. Käännä ajettava ohjelma objektitiedostoista ja archive-tiedostoista | |
(gcc <tiedostot> -o tulos.exe <parametrit>). Archive-tiedoston | |
nimen voi antaa joko tiedostojen mukana tai parametrinä -l<nimi> | |
JOS archive on DJGPP:n LIB-hakemmistossa nimellä lib<nimi>.a. | |
Grafiikkaenginekin voi olla projekti, jolloin jätätte EXE:ksi | |
kääntämisen kokonaan pois, ja teette vain archive-tiedoston. Tai jos | |
tarvit vain yhden .o -tiedoston, niin mikäs siinä, valinta on vapaa. | |
Nyt sinun pitäisi osata tehdä objektitiedostoja lähdekoodista, | |
kirjastotiedostoja objekteista ja ajettava ohjelma objekteista (ja | |
mahdollisesti myös kirjastoista). Kun hallitset nämä asiat jatkamme | |
jälleen taivaltamme. | |
6.3 Hieman automaatiota - tapaus Rhide | |
-------------------------------------- | |
No tällä hetkellä me osaamme kaikki tarvittavat taidot komentoriviltä, | |
mutta uusien tiedostojen nimien muistaminen ei aina ole kivaa ja | |
komentorivillä vääntäminen sopii vain perusteiden harjoitteluun. Rhide | |
on tapa päästä koko roskasta helpolla ilman perusteita edes | |
objektitiedostoista, mutta koska teillä tulee olemaan niin paljon | |
helpompaa kun ne osaatte niin olen katsonut tarpeelliseksi ne myös | |
neuvoa. (sillä Rhidenkin kanssa kunnon projekteilla tarvitaan tuota | |
osaamista). | |
Ainahan pääsee helpolla, mutta valitettava tosiasia on, että se joka | |
hyppäsi edelliset kappaleet ylitse onkin sormi suussa kun tulee | |
ongelma eteen. Mikään ei korvaa tietoa ja kokemusta, ei edes hyvä | |
ohjelmointiväline. | |
Eli tämän kappaleen tarjoama informaatio käsittelee Rhideä ja sen | |
projekteja projektien hallinnassa. Jos teitä ei Rhide kiinnosta niin | |
voitte hypätä yli, lupaan että seuraava kappale kiinnostaa teitä, | |
sillä makefilejen käyttö on vaihtoehtoinen (ja gurumpi, elegantimpi ja | |
yleisempikin) tapa automatisoida projektien kääntäminen. Mutta te | |
joita kiinnostaa yksi tämän hetken parhaimmista DOS-ympäristön | |
IDE-ohjelmista pysykää kappaleessa, tosin asia voi olla joillekin jo | |
vanhaa leipää. | |
Eli Rhiden sisältää makefileiden kaltaisen järjestelmän projektien | |
hallintaan, mutta toisin kuin make se sisältää tekoälyä, joka osaa | |
projektille valitusta kohteesta päätellä millainen tulos halutaan ja | |
projektin tiedostojen päätteistä minkätyyppinen tiedosto on kyseessä | |
ja miten se pitää kääntää. Koska Rhide on aika yksinkertainen | |
järjestelmä käsittelen vain lyhyesti sen perusasiat, eli projektien | |
teon, availun, käsittelyn, Rhiden kustomoinnin ja kohteiden | |
määräämisen. | |
Eli aloittakaamme tekemällä oletusprojekti Rhidelle. Ensimmäinen | |
tehtäväsi lienee installoida Rhide, joka yleensä koostuu purkamisesta | |
DJGPP-hakemistoon ja ohjelman käynnistämisestä kokeeksi. Dokumenttien | |
lukeminenkaan ei ole pahasta, mutta kyllä ilmankin voi pärjätä, tosin | |
vaikeuksien sattuessa ne ovat usein korvaamattomia. Rhiden jotkin | |
versiot ovat olleet enemmän tai vähemmän bugisia, mutta ainakin | |
versiot 1.1 (bugikorjattuna!), 1.2 ja 1.3 ovat toimineet minulla hyvin, | |
joten joko Altavistaan hakusanalla Rhide, MBnettiin tai MB:n | |
H&H-rompulle. | |
Sitten kun Rhide toimii niin menette DJGPP:n BIN-hakemistoon ja | |
kirjoitatte "rhide rhide". Tämä tarkoitus on luoda/muuttaa | |
BIN-hakemistossa olevaa rhide-nimistä projektia, jonka asetukset | |
ladataan AINA kun rhide käynnistetään ilman projektia ja jotka | |
toimivat uusien projektien oletusasetuksina. Muuttele rhide-projektia | |
niin paljon kuin haluat/uskallat/viitsit ja lopeta sen jälkeen | |
rhide. Voit kokeilla vielä asetusten toimivuutta menemällä jonnekin | |
hakemistoon missä on jokin muu määrä kuin yksi projekteja (jos niitä | |
on vain yksi niin se ladataan automaattisesti) ja käynnistämällä | |
Rhiden. | |
Nyt pitäisi kaiken olla valmista uuden projektin teolle. Ota | |
Project-valikosta Open project ja kirjoita avautuvan ikkunan | |
Name-sarakkeeseen haluamasi projektin nimi. Ruudun alalaitaan avautuu | |
ikkuna joka kertoo projektin tiedostot. Aktivoimalla tämän ikkunan ja | |
painamalla insert-nappia (tai Project-valikosta Add item) saat | |
lisättyä uusia tiedostoja. Kun olet valmis paina Cancel-nappia. | |
Tällä tavalla lisäät haluamasi tiedostot (lähdekooditiedostot, tosin | |
jos ehdottomasti haluat voit laittaa jonkin valmiiksi käännetynkin O- | |
tai A-tiedoston mukaan) projektiin. | |
Mukaan lisättäviä kirjastoja voit määrittää Options-valikon | |
Libraries-kohdasta. Muista, että tämä hakee kirjastoja VAIN DJGPP:n | |
LIB-hakemistosta, ja että kirjaston nimeen lisätään aina kääntäjän | |
toimesta eteen LIB ja loppuun .A, eli älä kirjoita koko kirjaston | |
nimeä tyyliin LIBJOKIN.A, vaan JOKIN. Sellainen erikoisuus kyllä | |
kääntäjästä löytyy, että ylipitkät (yli 5 merkkiä) kirjaston nimet | |
katkaistaan, joten IOSTREAM antaa tiedoston LIBIOSTR.A, eikä | |
virheellistä LIBIOSTREAM.A:ta (joka olisi siis liian pitkä). | |
Kun olet tyytyväinen kaikkeen muuhun niin ota vielä Project-valikosta | |
main targetname ja määritä kohteen nimi. Jos olet tekemässä | |
ääniengineä, niin sinulla on äänienginen C-tiedostot projektissasi ja | |
kohteena (esim.) LIBSND.A. Jos taas teet C++ EXE:ä, niin sinulla on | |
C-tiedostot joita käytetään, kohteena (esim.) PLUSPLUS.EXE ja | |
mahdollisesti kirjastossa IOSTR ja jotain muuta. .A-päätteestä Rhide | |
osaa automaattisesti kääntää archive-muotoisen tiedoston ja | |
.EXE-päätteestä ajettavan. Muutkin voivat toimia (O ainakin), mutten | |
ole kokeillut koskaan, sillä siihen ei yleensä ole tarvetta. | |
Projektin kääntäminen onnistuu napilla F9, jolloin Rhide osaa | |
automaattisesti katsoa tiedoston päiväyksistä mitkä tiedostot ovat | |
muuttuneita (lähteen päivämäärä uudempi kuin kohteen) ja kääntää näin | |
vain tarpeellisen. Aikaa säästyy ja hermoja samoin. Kääntämisen | |
jälkeen hakemistostasi löytyy luultavasti kasa objektitiedostoja, | |
joita voidaan käyttää myöhemmin linkkauksessa (jos vastaava | |
lähdekooditiedosto ei ole muuttunut). | |
Sellaista tällä kertaa. Aika perusasiaa ja itsekin pääteltävissä, | |
mutta joskus vain käy siten ettei jotain perusasiaa itse hoksaa, tai | |
ainakin säästää aikaa kun ei tarvitse kaikkea kokeilla. Nyt hallussa | |
pitäisi olla projektien teko Rhidellä ja niiden toimimaan saaminen, ei | |
sen kummempaa tällä kertaa. Voit jatkaa halutessasi seuraavaan jos | |
tuntuu että osaat tämänkin kappaleen materiaalin. | |
6.4 Todellista guruutta - salaperäinen make | |
------------------------------------------- | |
Make on kuin suoraan Unix-maailmasta tullut. Jos pelkkä vilkaisu sen | |
info-sivuille (INFO MAKE) saa aloittelijan vapisemaan horkassa. Mutta | |
ei hätää, minä kävin siellä ja selvisin elossa - tosin en ole enää | |
ollut sama itseni sen jälkeen. Olen nimittäin huomattavasti gurumpi | |
jälleen sillä voin käännellä projektini halutessani hienosti | |
komentoriviltä automatisoituna. Ja se onnistuu maken | |
makefileillä. Tässä luvussa kerron miten niitä tehdään, tosin en | |
mitään monimutkaisempaa valota kun mitään ihmekonsteja harvemmin | |
normaalissa perustyöskentelyssä tarvitsee. | |
Eli ensimmäisenä tehtävänä on jälleen kaivaa make jostain, paikat ja | |
keinot ovat samat kuin Rhiden kohdalla, mutta toisin kuin Rhide maken | |
pitäisi toimia ilman manuaaliin vilkaisua (koska se on huomattavasti | |
yksinkertaisempi systeemi). Ideana on tehdä projektille ns. makefile, | |
jonka make osaa tulkita ja tehdä sen mukaan tiedostossa käsketyt | |
asiat. | |
Mutta tehdäksemme oikeanlaisia makefilejä meidän täytyy ensin hieman | |
ymmärtää filosofiaa maken takana. | |
Normaali makefile koostuu yleensä alussa olevasta kasasta | |
muuttujamäärittelyjä, joita myöhemmin käytetään kääntämisessä. Sen | |
jälkeen on kasa ohjeita, jotka koostuvat muutamasta | |
komponentista. Tässä on ohjeen muoto ja esimerkki yhdestä: | |
kohde: riippuvuudet | |
komento kohteen tekoon | |
esim. | |
ohjelma.exe: ohjelma.o | |
gcc ohjelma.o -o ohjelma.exe -s -Wall -v -O2 | |
Eli ensimmäisenä on kohde joka kertoo makelle, että tässä on ohje | |
miten teet tämän. Sitten on riippuvuudet, joka kertoo, että näiden | |
pitää olla kunnossa ennenkuin tätä ohjetta aletaan | |
toteuttamaan. Seuraavalla rivillä on yksi TAB:in painallus ja komento | |
jolla kohde tehdään (komentoja voi olla useampiakin, jokainen omalla | |
rivillään alkaen TAB:illa). Huomaa, että tarvitsemme EHDOTTOMASTI | |
oikean TAB:in, emme mitääs MSDOS EDIT:in lelutabbeja, jotka eivät | |
itseasiassa ole kuin määrätty määrä välilyöntejä. Eli pitää olla | |
jonkinlainen editori, joka osaa käyttää aitoja TAB-merkkejä. | |
En taida alkaa miettimään syvällisemmin maken toimintaa, mutta ideana | |
on, että esittelet ensin pääkohteen ja sen riippuvuudet ja sen jälkeen | |
esittelet nämä uudet riippuvuudet ja niiden riippuvuudet jatkaen | |
pohjalle asti kunnes lopulta sinulla on kohteena objektitiedosto ja | |
lähteenä lähdekooditiedosto ja alla komento tämän kääntämiseksi, | |
jolloin make katsoo päivämäärän mukaan tarvitseeko tämä kohde | |
päivittämistä. Jos lähde on uudempi kuin kohde niin käsky suoritetaan | |
mutta jos kohde on uudempi niin se on täydytty kääntää lähteen | |
muuttamisen jälkeen eikä kääntöä tarvita. Tällä tavalla vain | |
muuttuneiden tiedostojen aiheuttamat käännöstarpeet hoidetaan eikä | |
ylimääräistä työtä tehdä. | |
Yleensä makefilessä on ensin kohde all, jossa riippuvuuksina on kaikki | |
mitä makefilen tulee saada tuloksena valmiiksi (EXE:t, kirjastot), | |
sitten on näiden tuloksien ohjeet riippuvuuksina objekti- ja | |
archive-tiedostot, sitten archive-tiedostot riippuvuuksina | |
objektitiedostot ja lopuksi objektitiedostot riippuvuuksina | |
lähdekooditiedostot. Tässä on esimerkki joka varmaan valaisee aika | |
sekavaa selitystäni. =) Huomaa myös makrot, jotka määritellään alussa | |
ja joita muuttamalla on helppo vaihtaa käännöksessä tarvittavia | |
parametrejä ja kääntäjien nimiä: | |
CC=gcc | |
CFLAGS=-s -Wall | |
AR=ar | |
ARFLAGS=rs | |
all: esim.exe libx.a | |
esim.exe: esim.o libx.a | |
$(CC) $(CFLAGS) esim.o libx.a -o esim.exe | |
libx.a: x1.o x2.o | |
$(AR) $(ARFLAGS) libx.a x1.o x2.o | |
esim.o: esim.c | |
$(CC) $(CFLAGS) -c esim.c -o esim.o | |
x1.o: x1.c | |
$(CC) $(CFLAGS) -c x1.c -o x1.o | |
x2.o: x2.c | |
$(CC) $(CFLAGS) -c x2.c -o x2.o | |
Kun tämän tiedoston tallentaa nimelle makefile tarvitsee sinun vain | |
antaa komento make niin ohjelma osaa automaattisesti kääntää kaikki | |
makefilessä määritellyt tiedostot. Käyttääksesi muita makefilen nimiä | |
pitää maken komentoriville antaa parametri -f<makefile>. | |
Esimerkki oli hyvin yksinkertaistettu ja vältin käyttämästä paria | |
hauskaa kikkaa jotka tekevät makefilestä paljon lyhyemmän (ja | |
sotkuisemman näköisen). Jos kuitenkin toiminta on epävarmaa, niin | |
selostetaan se tässä vielä kertaalleen: | |
1. Make aloittaa lausekkeesta all (komentorivillä voit halutessasi | |
määrätä mikä ohje tulee tehdä, esim make libx.a ei koskisi esim.* | |
-tiedostoihin) ja etenee tekemään esim.exe:ä. | |
2. Esim.exe:n teko tarvitsee ensin esim.o:n, siirrytään siihen. | |
3. Esim.o tarvitsee esim.c:n, mutta sille ei löydy ohjetta, joten | |
suoritetaan ensimmäinen käännös. Makrot CC ja CFLAGS puretaan | |
komentoriville ja se suoritetaan ja kaiutetaan näytölle. Jatketaan | |
esim.exe:n riippuvuuksien tutkimista. | |
4. Esim.exe:n teko tyssää kun siihenkin pitää tehdä libx.a, joten | |
siirrytään tekemään sitä. | |
3. Libx.a:han pitää olla x1.o ja x2.o, joten siirrytään niihin. | |
4. Riippuvuudelle x1.c ei ole ohjetta, joten suoritetaan x1.o:n | |
komento (näissä kohtaa olisi päivämäärätarkistus, mutta koska | |
noita objektitiedostoja ei vielä ole olemassa niin...) ja palataan | |
takaisin. | |
5. x2.o tehdään samaan tapaan kuin edellinen ja palataan libx.a:n | |
pariin | |
6. Riippuvuudet kunnossa, tehdään kirjasto libx.a, palataan esim.exe:n | |
kimppuun. | |
7. Esim.exe:n riippuvuudetkin ovat hanskassa, joten tehdään se ja | |
palataan kohtaan all. | |
8. Libx:kin on tehty juuri, joten kaikki on valmista, poistutaan. | |
No niin, kyllä toiminta varmaankin selvisi, ja jos ei niin paljon | |
pidemmät ja selvemmät tekstit löytää englanniksi komennolla info make | |
(no selvemmistä en itseasiassa tiedä :). | |
Mutta make ei vielä ole ohitse, en uskalla päästää teitä kappaleesta | |
ennenkuin osaatte tehdä ohjeita jotka tekevät vaikka 30 | |
objektitiedostoa kerralla, ne kun ovat kovin mukavia systeemejä | |
verrattuna siihen että joutuisit kirjoittamaan jokaista varten oman | |
ohjeen. | |
Ideana tässä on eräänlainen nimentäydennys. Make osaa poistaa päätteen | |
nimestä ja korvata sen toisella, jota ominaisuutta käytetään juuri | |
tähän useiden samankaltaisten tiedostojen tekoon kerralla. Jos siis | |
sinulla on 10 objektitiedostoa ja jokainen käännetään | |
vastaavannimisestä lähdekooditiedostosta (o1.o ja o1.c, o2.o ja o2.c | |
jne.), niin niiden kääntö onnistuu seuraavalla tyylillä (aika maken | |
infoista pöllittyä ja suoraan käännettyä tavaraa mutta who cares?-): | |
KOHTEET: KOHDE-PATTERN: RIIPPUVUUS-PATTERN ... | |
OBJECTS=object0.o object1.o object2.o object3.o object4.o object5.o | |
object6.o object7.o object8.o object9.o | |
$(OBJECTS): %.o: %.c | |
$(CC) $(CFLAGS) -c $< -o $@ | |
Eli ensimmäisenä tulee lista (OBJECTS) tehtävistä kohteista, sitten | |
tulee %-merkki, joka esiintyy kohde-patternissa vain kerran, ja | |
maken infosivut käyttävät siitä nimeä "stem". Tämä vastaa mitä tahansa | |
kohtaa yhden kohteen nimestä, kaikki muut kohteen nimessä (.o tässä | |
tapauksessa) täytyy vastata täysin. | |
Jos siis kohteena olisi foo.o ja kohde-pattern olisi %.o, niin "stem" | |
(anteeksi minulla ei ole sanakirjaa käsillä ;) saisi arvon foo. Jos | |