Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- ▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄
- █▓▒░ ░█ █▓▒░ ░▒▓▒█ █▒░ ░▒▓▒░ █ █░ ░▒▓▒░█ █▓▒░ ░▒▓█ █ ░▒▓▒░ ░▒█
- █▒░ ░▒█ █▒░ ░▒▓▒░█ █░ ░▒▓▒░ ░█ █ ░▒▓▒░ █ █▒░ ░▒▓▒█ █░▒▓▒░ ░▒▓█
- █░ ░▒▓█ █▒░ ░▒▓▒░ ░█ █░ ░▒▓▒░ ░▒▓█ █░▒▓▒░ ░▒█▒░ ░▒▓▒░█ █▒▓▒░ ░▒▓▒█
- █ ░▒▓▒█ █░ ░▒▓▒░ ░▒█ █ ░▒▓▒░ ░▒▓▒█ █▒▓▒░ ░▒▓▒░ ░▒▓▒░ █ █▒▓▒░ ░▒▓▒░ █
- █░▒▓▒░█ █ ░▒▓██ ░▒▓█ █░▒▓▒░██▒▓▒░█ █▓▒░ ░▒▓▒░ ░▒▓▒░ ░█ █▓▒░ ░██▒░ ░█
- █▒▓▒░ █ █░▒▓▒██░▒▓▒█ █▒▓▒░ ██▓▒░ ░██▒░ ░▒█▒░ ░▒█▒░ ░▒█ █▒░ ░▒██░ ░▒█
- █▓▒░ ░█ █░▒▓▒░██▒▓▒░ █▒▓▒░ ░██▒░ ░▒██░ ░▒▓█░ ░▒▓█░ ░▒▓█ █░ ░▒▓██ ░▒▓█
- █▒░ ░▒█▄▄▄█▒▓▒░ ░▒▓▒░ ░█▓▒░ ░▒▓▒░ ░▒▓██ ░▒▓▒█ ░▒▓▒█ ░▒▓▒██░ ░▒▓▒░ ░▒▓▒░█
- █░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░██▒▓▒██░▒▓▒░██ ░▒▓▒░ ░▒▓▒░ █
- █ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ░▒▓▒░ ██▓▒░██▒▓▒░ ██░▒▓▒░ ░▒▓▒░ ░█
- █░▒▓▒░ ░▒▓▒░ ░▒▓██ ░▒▓▒░ ░▒▓▒░██▒▓▒░ ░▒▓▒░ ░██▒░ ██▓▒░ ░██▒▓▒░ ░██▒░ ░▒█
- ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀
- Laaman tie DJGPP-peliohjelmointiin versio 2.10. By Jokke / BAD KARMA
- Copyright (C) Joonas Pihlajamaa 1997. All rights reserved.
- Sisällysluettelo:
- 1. Esittely
- 1.1 Disclaimer
- 1.2 Mistä uusin versio?
- 1.3 Huomattavaa lukijalle
- 1.4 Kenelle tämä on tarkoitettu?
- 1.5 Kreditsit
- 1.6 Versiohistoria
- 1.7 Yhteystiedot
- 1.8 Esimerkkien kääntäminen
- 2. Alkeet
- 2.1 DJGPP - vaikea, suuri, monimutkainen, omituinen, hidas?
- 2.2 Grafiikkaa - mitä se on?
- 2.3 Paletti - hörhelöhameita ja tanssia?
- 3. Peruskikkoja
- 3.1 Kaksoispuskuri - luonnonoikku, horoskooppi?
- 3.2 PCX-kuvien lataus - vain vähän oikaisemalla
- 4. Bittikartat ja animaatiot
- 4.1 Bitmapit - eikai vain suunnistusta?
- 4.2 Animaatiot
- 4.3 Pitääkö spriten törmätä? Entä coca-colan?
- 4.4 Maskatut spritet
- 5. Hieman kehittyneempää yleistavaraa
- 5.1 Näppäimistön käsittely - ja nyt meillä on hauskaa
- 5.2 Fixed point matematiikka
- 5.3 Lookup-tablet ja muita optimointivinkkejä
- 5.4 Väliaikatulokset ja fontteja
- 5.5 Hiirulainen, jokanörtin oma lemmikki
- 5.6 Tekstitilan käsittely suoraan
- 6. Projektinhallinta
- 6.1 Projektien hallinta - useat tiedostot
- 6.2 Useiden tiedostojen projektit - kääntäminen ja hallinta
- 6.3 Hieman automaatiota - tapaus Rhide
- 6.4 Todellista guruutta - salaperäinen make
- 6.5 Ammattimaista meininkiä - enginen teko
- 7. Kehittyneemmät yksityiskohdat
- 7.1 Vauhtia peliin - ulkoisen assyn käyttö
- 7.2 PIT - aikaa ja purkkaa
- 7.3 Miten peli toimii yhtä nopeasti kaikilla koneilla
- 7.4 Yleistä asiaa pelin levityksestä
- 7.5 Interpolointi ja viivoja
- 7.6 Vapaa skrollaus
- 7.7 Sinit ja kosinit sekä plasmaa
- 7.8 Paletin kvantisointi ja rekursio - Median cut
- 7.9 Lisää paletin kvantisointia - Local K Mean
- 7.10 VESA 2.0, rakenteet
- 7.11 VESA 2.0, ensimmäiset keskeytykset
- 7.12 Miten se todella pitäisi tehdä
- 8. Asioiden taustaa
- 8.1 Datatiedostot - miten?
- 8.2 Läpinäkyvyys ja sen vaihtoehto - shadebobit
- 8.3 Motion blur - sumeeta menoa
- 8.4 Vektorit pelimaailmassa
- 8.5 Musiikkijärjestelmistä
- 8.6 Plasma tekee comebackin - wobblerit
- 8.7 Prekalkattuja pintoja - ja tunneli
- 8.8 Lisää kivaa - zoomaus
- 8.9 Polygoneista ja niiden fillauksesta
- 8.10 Pari kivaa feikkimoodia
- 9. Liitteet, jatkeet ja muu roina
- 9.1 Saatteeksi
- 9.2 Hieman koodereiden jargonia
- 9.3 Lähteet
- 1.1 Disclaimer
- --------------
- Tämän dokumentin ja kaikkien muiden paketin tiedostojen tekijänoikeudet
- kuuluvat Joonas Pihlajamaalle, ellei tiedostossa ole toisin ilmoitettu
- ja nämä ehdot pätevät kaikkiin paketin tiedostoihin jotka eivät
- sisällä erillisiä ehtoja tai joista ei ole näissä ehdoissa erikseen
- mainittu. Paketin sisältämän materiaalin käyttö on sallittu vain
- allaolevien ehtojen rajoissa. Jos käyttäjä ei hyväksy ehtoja tulee
- hänen poistaa tämä paketti ja sen tiedostot. Paketin sisältämän
- materiaalin käyttö tarkoittaa käyttäjän hyväksyneen levitysehdot.
- Dokumentin levitys, monistus ja muu jakelu on sallittu vain
- alkuperäisessä, muuttamattomassa muodossa, lukuunottamatta file_id.diz
- -tiedostoa, joka voidaan halutessa uudelleennimetä .old- tai
- .org-päätteiseksi ja lisätä uusi .diz-tiedosto, jotta kuvaus sopisi
- levitettävän BBS-järjestelmän käyttämään formaattiin.
- Minkäänlaista maksua ei saa periä lukuunottamatta kopiointi- ja
- levityskustannuksia, niin kauan kuin niiden yhteenlaskettu summa ei
- ylitä 20 suomen markkaa. Lähdekoodin käyttö on sallittu omissa
- ohjelmissa, mutta ohjelman dokumentaatiossa täytyy mainita lähdekoodin
- lähde. Tutoriaalin kautta opittu tieto on täysin vapaasti
- sovellettavissa.
- Tekijä ei ota minkäänlaista vastuuta paketin tiedostojen toiminnasta tai
- tietojen oikeellisuudesta. Minkäänlaista takuuta tutoriaalin sisältämän
- informaation käytännöllisyydestä ja virheettömyydestä ei anneta.
- Jos paketti aiotaan sisällyttää jonkin suuren tiedostopalvelimen,
- CD-ROM levyn tai muun vastaavan massalevitykseen tarkoitetun median
- jonka oletetaan leviävän suuria määriä olisi tekijälle hyvä ilmoittaa
- sähköpostitse tapahtumasta.
- Tutoriaaliin liittyy myös rajoitettu tyytyväisyystakuu. Jos et jostain
- syystä pidä tuotteesta voit poistaa sen määräämättömän ajan jälkeen
- ohjelman asennuksesta. Vapautat kiintolevytilaasi ja saat ilman
- erillistä maksua kokea tutoriaalin poistamisesta aiheutuvan henkisen
- tyydytyksen.
- Epäselvyydet, puutteet ja huomautukset disclaimerista pyydetään
- lähettämään tekijälle.
- 1.2 Mistä uusin versio?
- -----------------------
- Tutoriaalin teko alkoi alunperin MBnetin FAQ-jahkailusta, kun veikkailtiin
- tehtäisiinkö PC-Ohjelmointi -alueen kysymyksistä FAQ vai eikö. Minä päätin
- sitten tehdä ainakin jotain ja niinpä uusin versio pitäisi olla aina
- saatavilla MBnetistä PC-Ohjelmointi -alueelta. Alue tullaan jakamaan
- jossain vaiheessa, mutta Apajalta se löytyy ainakin.
- Lisäksi Laamatutin virallinen kotisivu löytyy osoitteen www.mbnet.fi/~jokke
- alta. Tästä osoitteesta pitäisi myöskin löytyä Laamatutin uusin versio
- nopeasti ja helposti (jopa nopeammin kuin mitä se tulee MBnettiin).
- Tiedostonimi on aina LAAMAxyy.ZIP, jossa x on suurempi (major) versionumero
- ja yy pienempi (minor). Pitäkäähän silmä tarkkana!
- 1.3 Huomattavaa lukijalle
- -------------------------
- Dokumentin koko on alkuperäisestä jo viisinkertaistunut ja public
- betasta ei voine enää puhua. Silti kommentteja täydellisyyksistä,
- virheistä ja puutteista tarvitaan ehkä jopa enemmän kuin beta-aikoina,
- kun alue on liian laaja yksin tarkistettavaksi. Olen myös kiinnostunut
- mahdollisista lisäjuttujen tekijöistä, jolloin luonnollisesti minun ei
- tarvitse kirjoittaa kaikkea. Korvauksena pääset sitten kreditseihin ja
- dokumenttisi julkaistaan tämän mukana.
- Teemu Keinonen on jo osallistunut Laamatutin tekoon ja on näinollen
- ansainnut erityiskiitokseni samat kiitokset kuuluvat myös 3D-starfield
- -selostuksen tehneelle Erik Seesjärvelle. Heidän teoksensa löytyvät
- myöskin tästä päähakemistosta nimillä LUVUT.TXT ja STARFLD.TXT. Herra
- Seesjärvi koodaa nykyään kunniallisena ihmisenä 3D-engineä ja
- pyynnöistä huolimatta starfield säilyy kunniakkaana osana
- tutoriaalia. Lisäksi kiitoksen jo tässä ansaitsee Pekka Nurminen
- lukuista tarkennuksista ja lisäehdotuksista joidenkin asioiden
- suhteen, sekä Tero Kontkanen maanmainion "Laama"-logon teosta.
- Eli kun törmäät johonkin epäselvyyteen, päällekkäisyyteen,
- epäloogisuuteen, toistoon, virheeseen tai puutteeseen niin
- ilmoittelehan heti minulle. Osoite tuolta tiedoston lopusta. Vastaan
- postiin mahdollisuuksieni mukaan (vastaan siis jokaiseen ellen sitten
- huku postiin). Jokainen kommentti tekee minut iloiseksi, sillä on aina
- mukavaa nähdä jos joku on tutoriaalista hyötynyt.
- Minulle saa lähettää viestimuotoisen kannustuksen lisäksi myös rahaa
- ja 20 markkaa olisi oikein hauska yllätys joskus löytää postiluukusta,
- tosin vähemmän ja enemmänkin voi halutessaan lähettää. =) Myös pelkkä
- postikortti tai e-maili on mukavaa. Rahan takia en tätä tee, saldo
- taitaa tähän mennessä olla yksi lahjoitus Erikiltä. :)
- Jatkossa tulen julkaisemaan uusia versioita sitä mukaa kun asiaa tulee
- lisää. Eli pidä silmä tarkkana ja mieli valppaana tutkiessasi
- käyttämiesi purkkien tiedostoalueita. Uusimman version löytäminen on
- selostettu tarkemmin luvussa 1.2. Muista, että Laamatutin levittäminen
- on suorastaan toivottua muuttamattomassa muodossa, joten älä epäröi
- lähettää sitä suosikkipurkkeihisi!
- 1.4 Kenelle tämä on tarkoitettu?
- --------------------------------
- Aloitin dokumentin kirjoittamisen Ilkka Pelkosen mainion suomenkielisen
- 3d-tutoriaalin innoittamana ja toivon, että tästä on hyötyä monille
- aloitteleville peli/demokoodereille DJGPP:llä. Tutoriaali kattaa
- DJGPP:n asennuksen ja monia grafiikkaohjelmoinnin perusniksejä, joskus
- lähdekoodinkin kanssa. Myöhempi osa alkaa menemään pikkuhiljaa yhä
- teoreettisemmalle tasolle eikä tahattomasti, sillä kunnon
- ohjelmointiin kuuluu paljon muutakin kuin hardware-tuntemus. Pyrin
- myös valaisemaan asioita jotka kuuluvat vähemmänkin peliohjelmointiin,
- mutta joista ei kunnollista juttua mistään muualta ole saatavilla.
- Ehdotuksia saa aina lähettää.
- Lähtövaatimuksena tämän lukijalle on siedettävä matematiikan taito
- (kertolaskut pitää olla hallussa, kuten myös jotkin muut
- peruskäsitteet, kuten kokonaisluvut, desimaaliluvut jne.) Sekä
- C-kielen taitaminen. Assemblerikin voi olla hyödyllinen. Tätä
- kirjoittaessani en vielä tiedä millainen tutoriaalista tulee, joten
- katsotaan nyt... Teemu Keinonen on kirjoittanut tähän tutoriaaliin
- mainion pikku dokumentin lukujärjestelmistä ja bittioperaatioista, joten
- jos et niitä vielä hallitse niin lue ensin tiedosto LUVUT.TXT!
- Tutoriaalin esimerkit EIVÄT MISSÄÄN NIMESSÄ ole tarkoitettu
- käytettäviksi peleissä suoraan. Niitä kyllä saa käyttää, mutta ne ovat
- hitaita ja ne ovat esimerkkiohjelmia, eivät juuri yhdenlaiseen
- pelityyppiin sopivia räätälöityjä rutiineja. Sitäpaitsi mikään ei
- voita kokemusta ja kirjoittaessasi omat rutiinisi opit asian paremmin
- kuin mitenkään muuten. Jos minä olisin käyttänyt muiden rutiineja niin
- en olisi nyt tässä selittämässä ideaa niiden takana, vaan tekisin
- alkeellisia pelejä, koska en osaisi muunnella muiden koodeja peleihini
- sopiviksi. Eli tämä dokumentti ei kirjoita sinulle valmiiksi parhaita
- ja sopivimpia rutiineja, vaan ainoastaan demonstroi mahdollisia
- toteutustapoja, joka tulisi pitää mielessä dokumenttia lukiessa.
- Huomaa myös, että tämän on kirjoittanut OikeaIhminen(tm), jolla on
- myös Sähköpostiosoite, jolla voit ottaa häneen yhteyttä. Mikään ei ole
- minulle mieluisampaa kuin nähdä, että edes joku on tyytyväinen tai
- tyytymätön tähän tutoriaaliin.
- Ja tietenkin koska olen oikea ihminen voit kysyä minulta epäselväksi
- jääneitä kohtia ja katson voinko selventää tätä ja kenties lisään
- vastauksen myös seuraavaan versioon tutoriaalista ja autat siten muita
- aloittelijoita. Voit jopa saada nimesi jonnekin, ken tietää? Eli kun
- tulee jotain mieleen niin mene dokumentin loppuun ja lue
- yhteystietoni. Myös kirjoitusvirheistä, huonosta / hyvästä tekstistä
- tai selvästä tekstistä kannattaa ilmoittaa, en nimittäin ole ainakaan
- vielä lukenut tätä kokonaan lävitse (lukuunottamatta kun kirjoitin
- tämän). Ja kaikki enemmän osaavat voivat ilmoittaa tarkennuksia ja
- oikaisuja tutoriaalin tekstiin. =)
- Koulutus Kokkolan Yhteislyseon lukiossa (eli lyhyemmin Länsipuistossa)
- on nyt sitten viimein alkanut, jonka jälkeen edessä on jokin
- teknillinen korkeakoulu ja DI:n arvo, jos luoja suo. :)
- 1.5 Kreditsit
- -------------
- Ennenkuin aloitamme, haluaisin tervehtiä joukkoa tuntemiani
- henkilöitä. Tiedoksi kaikki MBnetin ohjelmointi-alueen lukijoille,
- että ainakin yritin muistaa niin monta kuin vain mahdollista, jos
- siis nimeäsi ei ole listassa ja tunnet sinne kuuluvasi niin ilmoittele!
- Teemu Keinonen: Erityiskiitokset lukujärjestelmät -jutustasi!
- Erik Seesjärvi: Kiitoksia starfieldistä ja onnea 3D-enginelle. =)
- Pekka Nurminen: Kiitos mainiosta palautteesta ja avusta monessa asiassa.
- Tero Kontkanen: Mahtava logo! Muistinpas vihdoin lisätä senkin.
- Sami Kuronen: Alias pysyy, I hope. Jatka vain lukemista! ;)
- Jyri Pieniniemi: Tällä dokumentilla voi olla laksatiivisia vaikutuksia!
- Ilkka Pelkonen: Sinun takiasi jouduin tällaista kirjoittamaan... Tsemppiä!
- Tommi Kemppainen: Koodaus, skene ja elämä. Pitääkö muuta sanoa?-)
- Johan Brandt: Täytyyhän meidän nörttien pitää yhtä!
- Asko Soukka: Onnea sen C++:ssan opettelun kanssa, toivottavasti onnistut!
- Jari Karppanen: Filekamu, vain 2 vuotta myöhässä?-) Muistin nyt sinutkin!
- Tero Karras: Jos joinaat Doomsdayhin niin katso, että Bad Karmaa greetataan!
- Jere Sanisalo: Terveisiä vain sinnekin, toivottavasti Kaboomia on rekattu! ;)
- Kaj Björklund: Toivon RC:n imevän monta sielua ja seuraavan version! :)
- Aleksi Kallio: Näpit irti siitä Watcomista! DJGPP ja herneet 4ever!
- Juhana Venäläinen: Hmm, kai tagisaarto ES:ää vastaan on vielä voimassa?-)
- Marko Åkerberg: Menikö nimi oikein?-) BLAST 'EM RULEZ, JEE!!!
- Jarmo Muukka: Miten ikinä JAKSAT kirjoittaa yli sadan rivinohjelmaesimerkkejä?
- Jukka Vuokko: Huomentapäivää. Aiotko tehdä Emacsiin sprite-enginen?-)
- Petteri Järvinen: Tsemiä autopeliin! Toivottavasti kirje saapui perille. :)
- Ilja Bräysy: No toivottavasti sait jotain tolkkua jostakin =)
- Henri Pyyny: Toivottavasti ette huku lumeen siellä Lapissa!
- Lasse Laurila: Kyllä minä vielä saan sinut kirjoitetuissa messuissa kiinni!
- Santeri Saarimaa: Yhä NNY?
- Äiti&Isi: Mitä te tätä luette?!?
- Tomi Jutila: Olet sinäkin siis päättänyt alkaa kooderiksi?-)
- Timo Jutila: Quakee?!?!
- Teemu Kellosalo: Älä vain väitä että aiot lukea tämän?
- Kalle Liukkonen: Muistin sitten sinutkin. =) Shefun oikat hanskassa?-)
- Juho Östman: No laitoinpas sinutkin tänne. Yllätyitkö?-)
- The Pihlajamaa: Hemmetti, etunimi pääsi unohtumaan, tsemiä!
- Viznut / PwP: Onko sinulla jokin oikea nimikin?-) No mitä tuosta...
- Erityiskiitoksen ansaitsevat vielä koko MBnetin ylläpito, sillä ilman ko.
- purkkia ei minulle olisi koskaan ollut mahdollista oppia niin paljon
- ohjelmoinnista, että voisin kirjoittaa tämän. Näistäkin ylläpitäjistä
- mainitsen vielä erikseen Jere Käpyahon, Tarmo Toikkasen ja Rasmus Wickholmin,
- jotka ovat ahkerasti olleet mukana PC-Ohjelmointialueella. Kiitos!
- 1.6 Versiohistoria
- ------------------
- Kehitystä on jälleen tapahtunut ja mikäs sen mukavampi paikka
- nauttia niistä etukäteen kuin tämä luku. Uusi termikin on ilmaantunut,
- "uusi tausta" tarkoittaa selostusta toiminnasta Asioiden taustaa
- -osaan.
- Versio 2.1:
- + Jälleen korjauksia, pitäisi alkaa olla jo aika virheetöntä
- tavaraa, poistin //-kommentit ja kaikki mainit nyt tyyppiä int
- + Uusi luku VESA 2.0-rakenteista
- + Uusi luku VESA 2.0-keskeytyksistä
- + Uusi luku grafiikkaenginen teosta
- + Asioiden taustaa -osa, jossa kerron vain mikä on homman nimi,
- koodia ei enää tipu
- + Uusi tausta datatiedostoista
- + Uusi tausta läpinäkyvyydestä ja shadebobeista
- + Uusi tausta motion blurrista
- + Uusi tausta vektoreista pelimaailmassa
- + Uusi tausta musiikkijärjestelmistä
- + Uusi tausta wobblereista
- + Uusi tausta tunneli-efektistä
- + Uusi tausta zoomauksesta
- + Uusi tausta polygoneista ja niiden fillauksest
- + Uusi tausta feikkimoodeista
- Versio 2.01:
- + Joukko korjauksia enemmän tai vähemmän kriittisiin asioihin
- + Ei julkisessa levityksessä
- Versio 2.0: The Joulu Edition Enhanced
- + Ei enää READJUST.NOW -tiedostoa
- + Vaikeaselkoisempi disclaimer-teksti
- + Pikku korjauksia materiaaliin ja joitakin tarkennuksia
- + Mahtava, tuore versionumero
- + Uusi, hieno ja selkeä lukujako ja joitain järjestelyjä
- + Uusi, laaja (?) slangisanasto
- + Lisää kiinnostavia ja selkeitä ohjelmaesimerkkejä
- + Uusi luku interpoloinnista ja viivanpiirrosta
- + Uusi luku skrollauksesta
- + Uusi luku sineistä, kosineista ja plasmasta
- + Uusi luku kvantisoinnista median cut -algoritmilla
- + Uusi luku kvantisoinnista local K mean -algoritmilla
- Versio 1.3: Assembly-mix, jotain purtavaa myös demokoodereille
- + Tarkennuksia ja parannuksia VGA:n muistista kertovaan osaan
- + Lisää koodia pseudona bitmap-osuuteen ja muutenkin enemmän
- selvennystä ko. kohtaan. Kiitoksia selvennyspyynnöistä.
- + Uusi luku useiden C-tiedostojen käytöstä
- + Uusi luku objekti- ja archive-tiedostojen teosta
- + Uusi luku Rhiden konffauksesta ja projektinhallinnasta
- + Uusi luku makefileiden käytöstä
- + Uusi luku enginen teosta
- + Uusi luku ulkoisen assyn käytöstä
- + Uusi luku timerin koukutuksesta C:llä
- + Uusi luku frameskipistä
- + EJS:n starfield-esimerkki ja -selostus.
- Versio 1.2: Kesä-release, toinen julkisesti levitetty versio
- + Hiiren käsittely
- + Tekstitilan käsittely
- + Lisää korjauksia, kiitos ahkeran palautteen
- Versio 1.1: Bugikorjaus-release, ei yleisesti levityksessä
- + Lukuisia korjauksia enemmän tai vähemmän vialliseen
- tietoon siellä sun täällä tutoriaalissa
- Versio 1.0: Ensimmäinen julkaistu versio
- + DJGPP:n asenuns
- + Grafiikka
- + Paletti
- + Kaksoispuskuri
- + PCX-kuvat
- + Bittikartat
- + Animaatiot
- + Spritet
- + Näppäimistö
- + Fixed point
- + Lookup-tablet
- + Fontit
- + Maskatut spritet
- 1.7 Yhteystiedot
- ----------------
- Hyvä, olet siis päättänyt ottaa yhteyttä minuun. Yhteyden minuun saat
- useallakin tavalla, mutta tässä ovat ne joita luultavimmin tarvitset:
- www.mbnet.fi/~jokke/ sisältää minun, Bad Karman ja sen tuotosten, sekä
- Laamatutin viralliset kotisivut sekä joukon linkkejä maailmalle (ainakin
- jossain vaiheessa ;).
- joonas.pihlajamaa@mbnet.fi on sähköpostiosoite, josta minut pitäisi saada
- kiinni.
- Joonas Pihlajamaa on käyttäjätunnukseni MBnetissä, jolle voit kirjoittaa
- yksityispostiin. Ainakin tällä hetkellä luen viestini keskimäärin 3 kertaa
- viikossa, joten vastaus pitäisi tulla viikon sisällä (ellen ole
- lomailemassa tai paastolla koneestani ;).
- Joonas Pihlajamaa
- Säveltäjäntie 40
- 67300 Kokkola
- Tämä on se osoite, jossa asun. Jos et aivan käymään viitsi tulla niin mikset
- lähettäisi postikortilla terveisiä? Vastauksista kirjeisiin en tiedä, mutta
- katsotaan nyt, ei ole ainakaan vielä tullut ainoatakaan kirjettä...
- Kuulun gruuppiin BAD KARMA, joka tekee tällä hetkellä peliä nimeltään
- SLiDER: Roadkill, joka on autopeli ja sen on tarkoitus hakata Slicks 'n'
- Slide sekä muut vastaavat pelit mennen tullen. Kannattaa tutkia tarkasti
- purkkien tiedostoalueita, jos vaikka ilmestyisi. Ilmestymisajankohta
- on luultavasti (ensi?-) vuosituhannen loppupuolella.
- 1.8 Esimerkkien kääntäminen
- ---------------------------
- Tutoriaalin mukana seuraa sankka joukko esimerkkiohjelmia ja ne
- löytyvät hakemistosta EXAMPLE. Jos sinulla on 'make', niin kääntö
- sujuu yksinkertaisesti menemällä esimerkkikoodit sisältävään
- hakemistoon ja ajamalla komennon 'make' ja sen jälkeen 'make test.exe'
- jos sinulla on NASM. 'make clean' / 'make realclean' vastaavasti
- tyhjentävät objektitiedostot / objekti- ja exetiedostot.
- Kiitoksia Tero Kontkaselle makefile-esimerkistä. Tein sen pohjalta nyt
- uuden, koska esimerkkiohjelmia oli tullut jonkin verran lisää. Jos
- sinulla ei ole 'make'-ohjelmaa onnistuu kääntäminen käsinkin. Lähes
- kaikki tiedostot ovat itsenäisiä eivätkä tarvitse muita
- objektitiedostoja tai kirjastoja toimiakseen. Poikkeuksina
- timertst.exe joka tarvitsee sekä timer.c:n ja timertst.c:n käännettynä
- ja test.exe, joka tarvitsee test.asm:n ja test.c:n käännettynä.
- Hauskaa kokeilua, minä menen nukkumaan!
- 2.1 DJGPP - vaikea, suuri, monimutkainen, omituinen, hidas?
- -----------------------------------------------------------
- Tutoriaali sivuaa koko ajan DJ Delorien ilmaista Gnu-kääntäjää
- DOS:ille, eli DJGPP:tä, erityisesti sen kakkosversiota. Itse siirryin
- puolessa välissä tätä tutoriaalia 2.0 -versiosta versioon 2.01 ja
- luulisin, että esimerkit toimivat molemmilla näistä versioista ja
- luultavasti uudemmillakin. Vanhemmat versiot eivät luultavastikaan
- toimi näiden lähdekoodien kanssa.
- Tämän mahtavan ilmaiskääntäjän löydät esimerkiksi internetistä
- osoitteesta ftp://x2ftp.oulu.fi jostain
- pub/msdos/programming-hakemiston alihakemistosta. Sen saa myös
- MBnetistä, tarvittavat tiedostot ovat alueella PC-Ohjelmointi (area
- 8), tiedostoja on useita, ja ne löytyvät ko. alueelta löytyvästä
- MBNETDJ2.TXT:stä. Myös kaikille Mikrobitin tilaajille tullut Huvi &
- Hyötyromppu sisältää tämän kääntäjän hakemistossa MIKROBIT\DJGPP201\,
- tosin sieltä puuttuu LGP2721B.ZIP (tarvitaan C++ koodin kääntämisessä),
- jonka Käpyaho unohti laittaa mukaan. Halutessasi voit hakea puuttuvan
- tiedoston MBnetistä.
- DJGPP:n asennukseen purat vain kaikki tarvitsemasi paketit haluamaasi
- hakemistoon (esim. D:\OHJELMAT\DJGPP) PKUNZIP:in -d parametrillä. Sen
- jälkeen lisäät polkuun tuon hakemiston alihakemiston BIN (esim.
- D:\OHJELMAT\DJGPP\BIN), ja vielä lopuksi teet uuden environment-muuttujan
- DJGPP, joka osoittaa DJGPP:n juurihakemistossa olevaan DJGPP.ENV
- -tiedostoon. Eli esim.:
- SET DJGPP=D:\OHJELMAT\DJGPP\DJGPP.ENV
- Nyt voit kokeilla toimivuutta tekemällä pienen C-ohjelman (vaikka
- koe.c) ja kirjoittamalla:
- GCC koe.c -o koe.exe
- Lisää infoa GCC:n käännösoptioista ja kääntäjästä saat kirjoittamalla:
- INFO GCC
- Suosittelisin että lueskelet DJGPP:n dokumentaatiota ja teet tässä
- vaiheessa paljon testiohjelmia ja opettelet käyttämään
- info-lukijaa. Hyödyllinen hankinta on myös Rhide, joka on IDE
- DJGPP:lle. Ohjelma löytyy MBnetistä alueelta 8 (ETSI RHIDE) sekä
- H&H-Rompulta. Kun tunnet osaavasi käyttää vaivattomasti kääntäjää
- palaa takaisin dokumentin pariin.
- Jos et vielä C:tä osaa, niin hanki jostain, esimerkiksi kirjastosta hyvä
- kirja ja opettele sen avulla C-ohjelmointi. En aio alkaa
- selittämään kaikkein yksinkertaisimpia asioita esimerkkikoodeissa
- taikka kommentoimaan liiemmälti koodia.
- 2.2 Grafiikkaa - mitä se on?
- ----------------------------
- No olet siis päättänyt edetä seuraavaan aiheeseen, joka näyttäisi
- olevan grafiikan ohjelmointi DJGPP:llä. Aloittakaamme siis! Tiedoksi
- nyt etukäteen, että muistiosoitteet ovat heksoina, vaikkei sitä
- ilmoitetakaan.
- Esimerkkinä käytän VGA:n perusmoodia, 13h (heksaluku, desimaalina
- 19), joka on erittäin helppokäyttöinen. Kun tarvitset muita moodeja
- sinulla on varmasti jo tarpeeksi taitoa hankkia itse informaatiota,
- mutta tämän neuvon ihan alusta alkaen.
- Eli olipa kerran PC, jossa oli 16-bittinen muistiväylä, joka salli
- vain 64 kilon osoittamisen kerralla, sillä 16-bittisellä osoitteella
- voidaan maksimissaan osoittaa 2^16=65536 tavua muistia. PC:n oli
- suunnitellut Intel, mutta PC:hen oli luvattu yli 64 kilotavua muistia
- ja 32-bittinen muistiväylä oli niihin aikoihin kovin kallis. Joten
- joku sai suorastaan neronleimauksen: Jaetaan koko muisti 64 kilon
- palasiin!
- En syvenny tekniikkaan sen kummemmin, vaan totean vain, että 8088
- prosessoriin perustuvassa PC:ssä muodostettiin muisti SEGMENTISTÄ ja
- OFFSETISTA (SEG:OFF, esim B800:0000). Todellinen osoite muistissa
- saatiin kertomalla SEGMENTTI kuudellatoista ja lisäämällä siihen
- OFFSET. (B800:0000 = B800*16+0000 = B8000) Ja kun kummatkin olivat
- 16-bittisiä lukuja saatiin näin 20-bittinen siirrososoite. Ja koska 20
- bitillä voi ilmoittaa täsmäälleen kaksi potensiin 20 eri arvoa oli
- maksimimäärä mitä voidaan osoittaa 1 megatavu. Kymmenen ensimmäistä
- segmenttiä (eli 0000 1000 2000 3000 4000 5000 6000 7000 8000 ja 9000)
- omistettiin ohjelmille ja nimettiin perusmuistiksi, jota oli siis
- 10*64=640 kilotavua. Sitten segmentistä A000 alkoi grafiikkamuisti.
- No tietokoneet kehittyivät ja esiteltiin suojattu tila, eli PROTECTED
- MODE (PM), joka käsitteli koko muistia selektoreilla ja offseteilla,
- jotka olivat entisen 16 bitin sijasta 32-bittisiä (selektorit ovat
- kuitenkin yhä 16-bittisiä). Vanhat segmenttien varastoimiseen tarkoitetut
- SEGMENTTIREKISTERIT varattiin nyt selektoreille, jotka kertoivat
- prosessorille, mitä LOOGISTA muistialuetta käsiteltiin. DJGPP, joka on
- suojatun tilan kääntäjä esim. antaa ohjelmalle alussa 2 selektoria, toinen
- osoittaa dataan ja toinen koodiin. Tästä pidemmälle en tiedä tarkasti,
- mutta riittää tietää, että selektorin osoittaessa dataan ei offset 1234
- todellakaan ole muistissa kohdassa 1234, vaan se on ohjelman oman
- data-alueen 1234. tavu.
- Ja mikä meitä kiinnostaa, on perusmuistin 11. segmentti, jonka osoite
- siis oli A000:0000. Siirrososoite on siis A000*16+0000 = A0000. Mutta,
- kuten muistamme, ei onnistu, että vain tekisimme pointterin, joka osoittaa
- tuonne osoitteeseen, sillä ohjelman datahan on aivan toisessa
- selektorissa kuin perusmuisti. Meidän täytyy ensin löytää oikea
- selektori, jonka osoittama looginen muistialue vastaisi PC:n
- perusmuistia. Ja tällainen löytyykin nimellä _dos_ds. Tämän selektorin
- osoittaman muistialueen 0. tavu on perusmuistin 0. tavu, 1. tavu on
- perusmuistin 1. tavu ja niin jatkuu edelleen, kunnes tavu numero A0000
- on ensimmäinen VGA:n grafiikkamuistin tavu.
- Nyt meillä on siis tiedossa segmentin A000, eli VGA-kortin
- muistialueen siirrososoite, A0000 ja oikea selektori, _dos_ds. Mutta
- miten laitamme tavun tuonne? Hyvä kysymys. Se onnistuu vähintään 5:llä
- eri tavalla, mutta perehdymme helpompaan. Kirjaston sys/farptr.h
- funktioon _farpokeb(selektori, siirrososoite, tavu), jolla pääsemme
- käsiksi tuonne. Normaalin pointterin tekohan ei onnistu, vaan meillä
- pitää olla funktio, joka kykenee osoittamaan toisen selektorin
- alueelle.
- Näinollen esimerkkiohjelma, joka asettaa VGA-muistin 235. tavun arvoon
- 100 on tämän näköinen (PIXEL1.C):
- #include <go32.h> /* muistathan, _dos_ds on määritelty täällä! */
- #include <sys/farptr.h> /* täältä löytyy _farpokeb */
- int main() {
- int selektori=_dos_ds,
- siirros=0xA0000 + 235,
- arvo=100;
- _farpokeb( selektori, siirros, arvo );
- return 0;
- }
- Arvaan, että ehkä menit ja kokeilit tuota ja petyit, kun mitään ei
- tapahtunutkaan. Ei se mitään, niin pitääkin tapahtua, sillä olimme
- tekstitilassa. Jotta jotain tapahtuisi meidän pitää olla oikeassa
- tilassa, joka oli siis 0x13 (heksanumero 13 C:ssä, desimaalimuodossa
- 19). Tämän tilan rakenne onkin seuraava mihin perehdymme. Ole huoleti,
- valitsin tämän tilan, sillä se on KAIKKEIN yksinkertaisin tila
- PC-yhteensopivalla tietokoneella. Resoluutio on 320 riviä vaakatasossa
- ja 200 pystytasossa. Jokaista pikseliä merkitään yhdellä tavulla, eli
- sillä voi olla 256 erilaista arvoa. Näyttö alkaa aivan ruudun
- vasemmasta yläkulmasta (miksi? sitä ei kukaan oikein tiedä, menee
- filosofiaksi) ja jatkuu tavu tavulta (pikseli pikseliltä) päättyen
- lopulta oikeaan alakulmaan. Eli ensimmäiset 320 tavua ovat ensimmäisen
- rivin kaikki vaakatasossa olevat pikselit, sitten seuraavat 320 ovat
- toisen rivin pikselit, kunnes lopulta ollaan ruudun alakulmassa.
- Ja kun muistamme, että ensimmäinen tavu on kohdassa A0000 (heksa siis
- tämäkin), eli 0 tavua alusta eteenpäin, niin me voimmekin tehdä hienon
- kaavion:
- Pikselit: Sijainti:
- ..........................
- 0...319 1. rivi
- 320...639 2. rivi
- ...
- 63680...63999 200. rivi
- Näin meillä onkin hieno kaava, jolla saamme selville pikselin
- sijainnin:
- alkio = rivi * 320 + sarake eli:
- offset = y*320+x
- Muista, että C:ssä 1. rivi olisi tietenkin rivi numero 0!
- Nyt yhdistämme tietomme: VGA:n muisti sijaitsee selektorissa _dos_ds,
- alkaen osoitteesta A0000 (heksa, C:ssä 0xA0000) ja siitä lähtee 64000
- tavua, joka on näyttömuisti. Pikselin osoite tässä muistissa voidaan
- laskea kaavalla y*320+x. Selektorin kanssa voidaan muistia asettaa
- komennolla _farpokeb(selektori, siirros, arvo). Tarvittava moodi on
- 0x13 ja siinä on 256 väriä ja resoluutio 320 x 200.
- Mutta miten pääsemme sinne? Vastaus on helppo: conio.h:n funktiolla
- textmode(moodi)! Ja kun vielä yhdistämme tähän funktion getch(), joka
- odottaa napinpainallusta (löytyy myöskin kirjastosta conio.h), sekä
- palaamme lopuksi tekstitilaan (0x3, eli heksa 3, eli desimaali 3) on
- meillä jo aika kiva ohjelma kasassa (PIXEL2.C):
- #include <go32.h> /* _dos_ds ! */
- #include <sys/farptr.h> /* _farpokeb(selektori, siirros, arvo) */
- #include <conio.h> /* textmode(moodi), getch() */
- int main() {
- int selektori=_dos_ds, siirros=0xA0000, y=100, x=160,
- graffa=0x13, texti=0x3, color=100;
- textmode(graffa);
- _farpokeb(selektori, siirros+y*320+x, color);
- getch();
- textmode(texti);
- return 0;
- }
- Tietenkin olisi ollut helpompaa sijoittaa arvo suoraan parametrin
- kohdalle:
- textmode(0x13);
- _farpokeb(_dos_ds, 0xA0000+100*320+160, 100);
- getch();
- textmode(0x3);
- Mutta katsoin aiemman tavan havainnollisemmaksi. Kaiken tekemiseksi
- oikein helpoksi teemme tästä pikselinsytytyksestä makron
- #define-komennolla. Tämä ei hidasta ohjelmaa yhtään, mutta varmasti
- selventää koodia. Se määrittelee makron putpixel(x, y, c), jonka
- kääntäjä muuttaa käännösvaiheeksa _farpokeb-funktioksi. x tarkoittaa
- saraketta väliltä 0-319 ja y riviä väliltä 0-199, sekä c väriä väliltä
- 0-255. Muista, että vaikka teetkin makron sinun pitää silti
- sisällyttää mukaan kirjastot sys/farptr.h ja go32.h! Sulut makron
- farpokeb-funktion muuttujien x ja y ympärillä selittyvät sillä, että
- koska makro puretaan suoraan kutsukohtaan niin esim. komento:
- putpixel(50, 40+a, 100) purkautuisi muotoon: _farpokeb( _dos_ds,
- 0xA0000+40+a*320+50, 100), joka ei tietenkään ole haluttu tulos, sillä
- 40+a pitää käsitellä ennen sijoitusta, eli sulut vain ympärille! Tässä
- se siis on:
- #define putpixel(x, y, c) _farpokeb( _dos_ds, 0xA0000+(y)*320+(x), c)
- Kun haluat käyttää sitä, niin teet vaikka seuraavanlaisen
- koodinpätkän (PIXEL3.C):
- #include <sys/farptr.h>
- #include <go32.h>
- #include <conio.h> /* textmode(moodi) ja getch() löytyvät täältä! */
- #define putpixel(x, y, c) _farpokeb( _dos_ds, 0xA0000+y*320+x, c)
- int main() {
- textmode(0x13);
- putpixel(319, 199, 150);
- getch();
- textmode(0x3);
- return 0;
- }
- Ohjelma sytyttää pikselin aivan ruudun alareunaan. Jos et enää muista,
- miten ohjelma käännettiin DJGPP:llä, on tämän kokeilemiseksi
- tarvittava komento: "GCC PIXEL3.C -o PIXEL3.EXE" ja sitten kokeilu
- komennolla "PIXEL3".
- Painu nyt kokeilemaan ohjelmaa ja muuntelemaan sitä! Laita se tekemään
- ruksi, pystyviiva, vaakaviiva, tai vaikka ympyrä jos osaat, tai
- yhdistä se randomin kanssa ja tee näytönsäästäjä! Kokeilemalla tulet
- parhaiten sinuiksi uuden asian kanssa. Ja kun olet valmis, siirrymme
- seuraavaan aiheeseen, palettiin.
- 2.3 Paletti - hörhelöhameita ja tanssia?
- ----------------------------------------
- Kuten edellisessä luvussa opimme, voi tilassa 13h olla 256 erilaista
- väriä. Teit ehkä jo ohjelman, joka piirtää pikselin jokaisella värillä
- viivaa ja huomasit, että käytössä olevat värit ovat huonoja,
- puuttelisia, kirkkaita, tummia tai muuten vain inhottavia. Mutta ei
- hätää - niitä voi muuttaa! Ja vaikka paletissa ei mielestäsi olisikaan
- mitään vikaa haluat ehkä tehdä sellaisia efektejä kuten häivytys,
- plasma, "crossfade" (toinen kuva ilmestyy toisen alta pikkuhiljaa)...
- Näissä kaikissa tarvitaan enemmän tai vähemmän itse tehtyä palettia ja
- siksi meidän pitääkin opetella nämä asiat ennenkuin menemme pidemmälle.
- Kaiken ytimenä on VGA ja sen paletti, etenkin sen asettaminen, mutta
- ehkä myös sen lukeminen. Tässä luvussa teemme funktiot, yhden tai
- useamman värin, asettamiseen ja lukemiseen, sekä tutustumme
- paletinpyöritykseen (palette rotation).
- Ensin taas vähän teoriaa efektien ja paletin takana. Kuten ehkä
- tiedätkin, valo voidaan koostaa komponenteista. Tietokoneella
- jokaisella värillä on yleensä kolme komponenttia: punainen, vihreä ja
- sininen (red, green, blue). Tätä kutsutaan nimellä RGB. Itseasiassa
- jokainen moodin 13h väri on vain osoite taulukkoon, jonka jokainen
- alkio sisältää värin punaisen, virheän ja sinisen komponentin määrän,
- eli vahvuuden.
- Jos meillä olisi puhtaan punainen väri, sen arvot olisivat seuraavat:
- r=63, g=0 ja b=0. Sininen taas olisi 0,0 ja 63. Violetti, joka on
- sinisen ja punaisen yhdistelmä, voisi olla vaikkapa 63,0 ja 63 (eli
- täysi määrä punaista ja sinistä). Jos taas haluaisimme tumman punaisen
- värin, olisivat sen väriarvot vaikka 30, 0, 0. Koska 30 on vähemmän
- kuin puolet kirkkaan punaisen puna-arvosta, on tämä väri siis yli
- puolet tummempi! Helppoa! Ja miksi maksimimäärä on vain 63? Siksi,
- koska VGA:n rekistereissä värille on varattuna vain 6 bittiä, jolla
- voidaan esittää numerot välillä 0...63. Tämä joudutaan huomioimaan
- esim. PCX:n paletin latauksessa, sillä siinä värit ovat välillä
- 0...255. Tässä joudutaan jakamaan väriarvot neljällä, jotta saadaan
- toimiva luku.
- Eli ymmärrämme nyt, että jokaisella värillä on itseasiassa punainen,
- vihreä ja sininen komponentti, mutta mitä siitä? Vastaus on helppo,
- jos haluamme, voimme muuttaa mitä tahansa tilan 0x13 (tai miksei
- muunkin tilan) väriä helpolla joukolla komentoja. Meidän tarvitsee
- vain kirjoittaa asetettavan värin numero porttiin 3C8h (h lopussa
- siis tarkoittaa heksalukua, C:ssä 0x3C8) ja sitten porttiin 3C9 ensin
- punainen komponentti, sitten vihreä komponentti ja lopuksi sininen
- komponentti. Tämän jälkeen VGA korottaa väri-indeksiä automaattisesti
- yhdellä, eli jos ensin syötämme porttiin 3C8h värinumeron 5 ja sitten
- punaisen, virheän ja sinisen porttiin 3C9h korottuu VGA:n sisäinen
- laskuri yhdellä, ja voimme halutessamme tunkea heti seuraavan värin
- RGB arvot porttiin 3C9.
- Nyt olemme jauhaneet teoriaa tarpeeksi. Menkäämme pikkuiseen
- esimerkkiin. Esittelemme tietorakenteen RGB, joka sisältää värin
- RGB-arvot ja sitten funktion, jolle annetaan parametrinä osoitin
- tällaiseen rakenteeseen ja värin numero jolle nämä väriarvot
- asetetaan. Myöhemmin yhdistämme tämän pieneen esimerkkiohjelmaamme,
- mutta (PALETTE.H):
- typedef struct {
- char red;
- char green;
- char blue;
- } RGB;
- void setcolor(int index, RGB *newdata) {
- outportb(0x3C8, index);
- outportb(0x3C9, newdata->red);
- outportb(0x3C9, newdata->green);
- outportb(0x3C9, newdata->blue);
- }
- Huomiosi ehkä kiinnittyy vielä outoon funktioon outportb, jolle
- annetaan ensimmäisenä portin numero ja sitten sinne syötettävä
- tavu. Funktion käyttämiseksi sisällytät mukaan kirjaston dos.h.
- Ehkä sinua kiinnostaisi myös tämän käyttö? No olkoon, tehkäämme
- esimerkkiohjelma kokonaisuudessaan. Kun edellinen pikku koodinpätkä on
- nimellä PALETTE.H, voimme helposti sisällyttää sen seuraavaan
- esimerkkiohjelmaamme kuten ihan tavallisen kirjaston. Muista vain,
- että kirjaston täytyy olla samassa hakemistossa ohjelman kanssa,
- muuten ei esimerkki käänny. Eli tässä sitten itse koodiosa, joka
- tuikkaa keskelle ruutua värin 50. Sitten se odottaa napinpainallusta
- ja muuttaa funktiollamme värin punaiseksi. Huomaa, että vain alussa
- kajotaan näyttömuistiin. Toinen kohta hoidetaan värinvaihdolla!
- Eli (PAL1.C):
- #include <conio.h>
- #include <sys/farptr.h>
- #include <go32.h>
- #include <dos.h>
- #include "palette.h"
- #define putpixel(x, y, c) _farpokeb(_dos_ds, 0xA0000+y*320+x, c)
- int main() {
- RGB newcolor;
- textmode(0x13);
- putpixel(160, 100, 50);
- getch();
- newcolor.red=63;
- newcolor.green=0;
- newcolor.blue=0;
- setcolor(50, &newcolor);
- getch();
- textmode(0x3);
- return 0;
- }
- Seuraavana huomionkohteenamme onkin sitten väriarvojen luku, joka on
- yhtä suoraviivaista kuin edellinenkin (tosin tarpeellisuus on
- kyseenalaista, tätä ei tarvitse jos on itse asettanut paletin).
- Erotuksena on, että väriarvo kirjoitetaankin porttiin 3C7h ja portista
- 3C9h _luetaan_ värin arvo. Jälleen tripletin (kolme alkiota, RGB) luvun
- jälkeen indeksi kohoaa, joten voisimme lukea seuraavat värit. Luku
- portista tapahtuu funktiolla inportb(portti). Muuta tietoa emme
- tarvitsekaan.
- Lisätkäämme nyt kirjastoomme (PALETTE.H) kolme uutta funktiota.
- getcolor(int index, RGB *color) lukee värin <index> väriarvot ja
- asettaa ne RGB-rakenteeseen <color>. setpal(char *palette) asettaa
- koko paletin kerralla hyväksikäyttäen automaattista indeksin korotusta
- (indeksi nollataan aluksi ja syötetään koko data perään, indeksi
- korottuu jokaisen rgb-arvon jälkeen). getpal(char *palette) taas lukee
- vastaavasti koko paletin. Niiden käytöstä sitten
- esimerkkiohjelmassamme, joka seuraa ajallaan. Eli uutuudet kirjastoon
- PALETTE.H:
- void getcolor(int index, RGB *color) {
- outportb(0x3C7, index);
- color->red=inportb(0x3C9);
- color->green=inportb(0x3C9);
- color->blue=inportb(0x3C9);
- }
- void setpal(char *palette) {
- int c;
- outportb(0x3C8, 0);
- for(c=0; c<256*3; c++)
- outportb(0x3C9, palette[c]);
- }
- void getpal(char *palette) {
- int c;
- outportb(0x3C7, 0);
- for(c=0; c<256*3; c++)
- palette[c]=inportb(0x3C9);
- }
- Kuten huomasit, ei viimeisissä funktiossa ole lainkaan enää
- RGB-rakennetta. Tämä siksi, että koko paletti on huomattavasti
- helpompi käsitellä näin. Jos olet sitä mieltä, että RGB oli parempi
- tai haluat muuttaa loputkin pointtereiksi, en sitä
- estä. Char-pointteriversiossa on aina kolme tavua peräkkäin
- ilmoittamassa RGB-triplettiä. Toisen värin r alkaa siis 4. tavusta,
- eli indeksistä 3. Jos haluat jonkin värin r-arvon, niin lasket:
- "palette[number*3+0]". Vihreällä korotat tuota yhdellä (number*3+1) ja
- sinisen kanssa kahdella. Helppoa tämäkin.
- Nyt on kaikki tärkein katettu VGA:n paletista, joten kysytkin ehkä
- (aina sinä sitten olet kysymässä ;) mihin näitä nyt sitten voi
- käyttää. Itseasiassa paletilla on loputtomasti
- käyttömahdollisuuksia. Ensimmäinen on 256-väristen kuvien paletin
- asettaminen, sillä väärällä paletilla kuvat yleensä näyttävät enemmän
- tai vähemmän sotkulta. Toisena on häivytysefekti, sekä feidaus
- valkoiseen. Palettiliutuksesta käytetään usein termiä feidaus, joka
- tarkoittaa, että palettia liutetaan sävy sävyltä toiseen väriin,
- jolloin saadaan vaikka hieno ruudun tummeneminen. Kokeilemmekin sitä
- ihan kohta, kunhan selitän vielä yhden efektin, palettirotaation.
- Palettirotaatiossa on paletti, jonka väriarvoja pyöritetään
- ympäri. Eli käytännössä väri, joka ennen oli numerolla 5 onkin
- rotaation jälkeen värinumerossa 6. Tätä jatketaan koko ajan, ja väri
- matkaa koko paletin lävitse, ja kun se on lopussa niin se siirretään
- paletin alkuun. Yleensä väriä 0 ei kuitenkaan siirretä, sillä se on
- taustaväri ja yleensä musta. Usein käytetään myös palettia, jossa on
- useampia värejä kuin 256, jolloin erona on vain se, että ainoastaan
- osa väreistä näkyy ruudulla.
- "JA MIHIN TÄTÄ", kuulen sinun kysyvän. Olet kenties nähnyt plasman,
- jonka värit vaihtuvat koko ajan (kunnon plasmassa on kyllä lisäksi
- mukana muutakin kuin pyörivä paletti, mutta pyörityksellä saadaan
- kummasti lisäeloa muuten liikkuvaan plasmaan). Tai tunnelin, jossa
- värit siirtyvät kauemmaksi tai lähemmäksi. Tällaisia efektejä voidaan
- helposti toteuttaa palettirotaatiolla. Ennenkuin ymmärrät voit ehkä
- tarvita pienen demonstraation. Kohta teemmekin esimerkin, joka piirtää
- vaakatasossa viivoja, jokainen eri värillä alkaen yhdestä päättyen
- 255:teen. Sitten teemme hienon liukupaletin ja alamme pyörittämään
- sitä. Eli tehkäämme vielä funktio (lisätään kirjastoon PALETTE.H):
- void rotatepal(int startcolor, int endcolor, char *pal) {
- char r, g, b;
- int c;
- r=pal[startcolor*3+0]; /* tallennamme ensimmäiset värit ja siirrämme */
- g=pal[startcolor*3+1]; /* ne lopuksi loppuun. Tämä paletti pyörii siten, */
- b=pal[startcolor*3+2]; /* että viimeinen väri kulkeutuu kohti alkua */
- for(c=startcolor*3; c<endcolor*3; c++)
- pal[c]=pal[c+3]; /* muista, että uusi väri on kolmen välein,
- sillä välissähän on aina kolme tavua, r,
- g ja b, joita ei saa sekoittaa, muuten
- saisimme aikaan vaikkapa sinisen paloauton!
- (kiinnostava tavoite sinänsä) */
- pal[endcolor*3+0]=r;
- pal[endcolor*3+1]=g;
- pal[endcolor*3+2]=b;
- }
- Vielä ennen esimerkkiä tarvitsemme yhden rutiinin, joka tekee
- efektistämme edes jotenkin siedettävän. Palettia pitää nimittäin
- vaihtaa ennen kuin ruudulle aletaan piirtää, tai muuten voi edessä
- olla aika huonolaatuinen efekti (normaalipaletissa ei ole mitään
- väriliukuja). Varsinkin näin yksinkertaisessa ohjelmassa voi nopealla
- näytönohjaimella/koneella nopeus olla liiankin suuri, joten hidastamme
- vähän rutiinia odottamalla signaalia, jonka VGA antaa päästessään
- ruudun loppuun ja lähtiessään palaamaan yläkulmaan aloittaakseen taas
- piirron. Tähän teemme funktion, joka odottaa kunnes piirto on valmis
- ja kuvaruudulle voi kopioida pelkäämättä kesken piirron muutoksia
- tehdessä aiheutuvia ongelmia. Lisätkäämme seuraava funktio kirjastoon
- PALETTE.H:
- void waitsync() {
- while( (inportb(0x3DA)&8) != 0);
- while( (inportb(0x3DA)&8) == 0);
- }
- Nyt sitten hienoon esimerkkiohjelmaamme, joka piirsi niitä viivoja ja
- pyöritti palettia. Huomaa funktio genpal(char *palette), joka asettaa
- paletin liukuväreillä tehdyksi, sekä waitsync()-funktion käyttö
- (kokeile vaikka ilman waitsync():iä, niin näet eron)! Eli tässä se
- olisi (PAL2.C):
- #include <conio.h>
- #include <sys/farptr.h>
- #include <go32.h>
- #include <dos.h>
- #include "palette.h"
- #define putpixel(x, y, c) _farpokeb(_dos_ds, 0xA0000+y*320+x, c)
- void genpal(char *palette) {
- char r=0, g=0, b=0;
- int c, color=0;
- for(c=0; c<64; c++) { /* MUSTA (0,0,0) - PUNAINEN (63,0,0) */
- palette[color++]=r;
- palette[color++]=g;
- palette[color++]=b;
- if(r<63) r++;
- }
- for(c=0; c<64; c++) { /* PUNAINEN (63,0,0) - VIOLETTI (63,0,63) */
- palette[color++]=r;
- palette[color++]=g;
- palette[color++]=b;
- if(b<63) b++;
- }
- for(c=0; c<64; c++) { /* LILA (63,0,63) - VALKOINEN (63,63,63) */
- palette[color++]=r;
- palette[color++]=g;
- palette[color++]=b;
- if(g<63) g++;
- }
- for(c=0; c<64; c++) { /* VALKOINEN (63, 63, 63) - MUSTA (0,0,0) */
- palette[color++]=r;
- palette[color++]=g;
- palette[color++]=b;
- if(r) r--;
- if(g) g--;
- if(b) b--;
- }
- }
- int main() {
- int x, y;
- char palette[256*3];
- textmode(0x13);
- genpal(palette);
- setpal(palette);
- for(y=0; y<200; y++) for(x=0; x<320; x++)
- putpixel(x, y, y);
- while(!kbhit()) {
- rotatepal(1, 255, palette);
- waitsync(); /* odotetaan että piirto on valmis ennen uuden
- paletin asettamista! */
- setpal(palette);
- }
- getch();
- textmode(0x3);
- return 0;
- }
- Huomasit varmaan, että ruudun onnettoman geometrian takia kaikki värit
- EIVÄT mahtuneet ruudulle. No niin. Ja mitäs kivaa seuraavaksi?
- Seuraavaksi tutustumme viimeiseen palettikikkaan, jonka periaatteen
- olet jo voinut keksiäkin, eli feidauksen.
- Genpal-funktio olisi voinut käyttää myös erillistä rutiinia jolle
- annetaan parametreina monenko värin matkalla liu'utaan väristä toiseen.
- Kuitenkin koska tuo oli yksinkertaisemman näköinen tein sen tuolla
- tapaa.
- Teemme minimaalisia lisäyksiä PALETTE.H:hon, sekä pikkuisen
- esimerkkiohjelman, joka demonstroi efektiä käytännössä. Ideahan on
- erittäin yksinkertainen. Meillä on paletti, jossa on sekailaisia
- värejä ja haluamme häivyttää sen. Miten? No tietenkin muuttamalla
- ruudun mustaksi. Miten se tapahtuu? Nollaamme jokaisen värin, mutta
- emme kerralla, vaan vähennämme joka kierroksella ja asetamme uuden
- paletin. Tästä funktiosta voit tehdä helposti muitakin efektejä,
- kuten feidauksen valkoiseen (korotetaan jokaista väriä joka
- kierroksella kunnes ollaan värissä 63) tai vaikka paletista toiseen
- (jos kohdepaletin vastaava komponentti on suurempi niin korotetaan
- arvoa, jos pienempi niin vähennetään). Esittelen tässä vain
- häivytyksen, mutta löydät kirjastosta PALETTE.H toteutettuna myös
- valkoiseen ja toiseen palettiin feidauksen. Voit myös itse tehdä
- hauskoja efektejä, kuten feidata valkoiseen, tehdä valkoisen paletin
- ja feidata sen mustaan. Kokeile! Mutta, tässä rutiinimme:
- void fadetoblack(char *palette) {
- char temppal[256*3];
- int c, color;
- memcpy(temppal, palette, 256*3);
- for(c=0; c<63; c++) { /* tarvitsemme maksimissaan 63 muutosta */
- for(color=0; color<256*3; color++)
- if(temppal[color]) temppal[color]--;
- waitsync();
- setpal(temppal);
- }
- }
- Sitten yhdistämme efektin lopuksi edelliseen esimerkkiohjelmaamme
- lisäämällä sen juuri ennen tekstitilaan vaihtoa:
- fadetoblack(palette);
- Kokonaisuudessaan ja toimivana, vanhat osat mukana on esimerkkimme
- tiedostossa PAL3.C. Siihen on tehty myös pari muuta muutosta, kuten
- se, että aluksi paletti feidataan valkoiseen, asetetaan oikeasti val-
- koiseksi (muuten feidatessa mustaan paletti välähtää hetken normaaliväri-
- senä, tätäkin SAA kokeilla).
- No niin. Pahin tiedonnälkäsi lienee tältä erältä tyydytetty! Viihdy
- esimerkkien parissa ja tee mitä vain mieleen juolahtaa niillä. Muista,
- että palettifunktiot toimivat myös tekstitilassa. Tämän voit kokeilla
- vaikka käyttämällä fadetoblack-funktiota. Muista kuitenkin laittaa
- loppuun textmode(0x3), vaikket moodia olisi vaihtanutkaan, sillä et
- välttämättä pidä DOS-kehotteestasi jokainen väri mustana...
- 3.1 Kaksoispuskuri - luonnonoikku, horoskooppi?
- -----------------------------------------------
- No niin, olet näemmä sulattanut jo kaiken edellisen tiedon. Mainiota!
- Tänään pääsemme (tai miten nyt haluamme asian ilmaista) yhteen
- peliohjelmoinnin perustempuista, kaksoispuskuriin. Periaate tämän
- takana on aivan naurettavan yksinkertainen, ja itseasiassa minä opin
- tämän erään lehden lähdekoodia vilkaisemalla (Mikrobitin
- grafiikkaohjelmointikurssi, numero 11/95). Eli tähän asti olemme
- tunkeneet grafiikkaamme suoraan näyttöpuskuriin tavu
- kerrallaan. Valitettavasti tässä on haittoja. Ensimmäisenä on se, että
- meillä on kiire. Nimittäin käytössä on vain lyhyt aika kun näyttöä ei
- piirretä monitorille ja jos siinä ajassa ei ehdä piirtää näyttöä niin
- näyttö alkaa välkkymään, ilmestyy lumisadetta (varsinkin paletinvaihdon
- kanssa!) ja muitakin ei-toivottavia ilmiöitä esiintyy.
- Lisäksi on todettava valitettava tosiasia: Näyttömuisti on
- HIDASTA. Jos haluamme tehdä sen kaikkein tehokkaimmin niin kopioimme
- kaiken tavaran kerralla näytölle. Eli sen sijaan, että läiskisimme
- pikseleitä sinne, toisia tänne kopioimme tavaran näytölle näytön
- alusta loppuun neljän tavun (kaksoissana) kokoisina palasina. Mutta
- miten saamme ruudulle pikseleitä sinne tänne, kun kaikki pitäisi
- kopioida kerralla? Vastaus on, että käytämme kaksoispuskuria!
- Kaksoispuskuri, englanniksi doublebuffer on saman kokoinen kuin
- näyttömuisti, mutta sille on varattu tilaa keskusmuistista, joten se
- on nopeampaa kuin hidas, kortilla sijaitseva näyttömuisti (näin vain
- on, uskokaa pois). Sinne pikselinpiirto tapahtuu huomattavasti
- sutjakammin, ja kaiken lisäksi meillä ei ole mitään kiirettä. Vaikka
- piirrämme uuden pikselin, ei se näy näytöllä ennenkuin kaksoispuskuri
- on kopioitu, eli flipattu näyttömuistiin.
- DJGPP:llä näyttömuisti varataan vaikka malloc-käskyllä ja vapautetaan
- suorituksen loppuessa free-käskyllä. Kokoa pitää puskurilla olla
- tilassa 13h 64000 tavua. Eroja oikeaan näyttömuistiin
- kaksoispuskurissa on DJGPP:llä:
- - Se on nopeampaa.
- - Se sijaitsee omassa muistissa, joten se voidaan taulukoida. Ei
- enää putpixel-makroja, vaan dblbuf[y*320+x]=color.
- - Se voidaan kopioida nopealla _dosmemputl-rutiinilla, joka on
- viimeiseen saakka optimoitu (hidas se on siltikin, mutta se on
- näyttökortin ja VGA:n rakenteen vika.)
- - Se ei näy ruudulla ennenkuin käsketään.
- - Se ei vilku.
- - Se säilyy muistissa vaikka käytäisiin tekstitilassa.
- - Paljon muuta kivaa.
- Voit käyttää myös dynaamisen muistinvarauksen (malloc tai C++:ssalla
- new-operaattori) tilalla taulukkoa, kuten joissakin esimerkeissä on
- tehty, tällöin käytät muotoa char dblbuf[64000] (tai unsigned
- char...). Mallocin käyttö on kuitenkin suositeltavampaa kuin tällainen
- valtavien taulukoiden ottaminen pinosta.
- Muttamutta, tarvitsisimme esimerkin. Mistä saamme sellaisen? No tässä
- pieni esimerkki. Mukana on makro flip(char *buffer), joka kopioi 64000
- tavua puskuria näyttömuistiin DJGPP:n _dosmemputl-komennolla, joka
- löytyy kirjastosta sys/movedata.h ja tarvitsee myös _dos_ds:ää ja
- siten kirjastoa go32.h. Eli tässä tällaista (DOUBLE1.C):
- #include <go32.h>
- #include <sys/movedata.h>
- #include <time.h>
- #include <stdlib.h>
- #include <conio.h>
- #include <dos.h>
- #include <stdio.h>
- #define flip(c) _dosmemputl(c, 64000/4, 0xA0000)
- char *dblbuf;
- void varaamuisti() {
- dblbuf=(char *)malloc(64000);
- if(dblbuf==NULL) {
- printf("Ei tarpeeksi muistia kaksoispuskurille!\n");
- exit(1);
- }
- }
- int main() {
- int x, y;
- varaamuisti();
- srand(time(NULL)); /* alustetaan satunnaislukugeneraattori */
- textmode(0x13);
- while(!kbhit()) {
- for(y=0; y<200; y++)
- for(x=0; x<320; x++)
- dblbuf[y*320+x]=rand()%256;
- flip(dblbuf);
- }
- getch();
- textmode(0x3);
- return 0;
- }
- Kokeile myös ohjelmaa DOUBLE2.C, joka on toteutettu ilman
- kaksoispuskurointia, jos eroa ei vielä huomaa, tulee se
- joka tapauksessa vielä esiin, ja on muitakin hyödyllisiä asioita missä
- kaksoispuskuri, tai kolmoispuskurikin on tarpeen. Mutta, kokeile tämän
- käyttöä ja palaa tämän dokumentin pariin VASTA kun osaat täydellisesti
- kaksoispuskurin käytön (oikeammin ymmärrät miten se toimii, miten sitä
- käytetään, mihin se perustuu ja miten siihen piirretään
- pisteitä). Sitten syöksymmekin uuteen tuntemattomaan. Katsotaan nyt
- mihin...
- 3.2 PCX-kuvien lataus - vain vähän oikaisemalla
- -----------------------------------------------
- Noniin, kaikki wannabe gamekooderit. Nyt on aika mennä vaikeimpaan
- aiheeseemme, johon monen kooderin taidot ovat viimein tyssänneet ja
- jota minäkään en vielä täysin ymmärrä, enkä tiedä osaanko sitä
- selittää.
- Se on hyväuskoisuus, sillä PCX:n sisältä löytyy looginen ja helposti
- ymmärrettävä rakenne. Ja vaikkei sitäkään täysin ymmärrä, voi
- aina vain käyttää samaa rutiinia (kuten minä) PCX:n lataamiseen.
- Esittelenkin tässä kappaleessa lyhyesti tämän yhden yleisimmistä
- kuvaformaateista olevan tiedostotyypin saloja. 256-värisen tyypillisen
- PCX:n rakenne voidaan jakaa karkeasti neljään (4) osaan:
- - 128 ensimmäistä tavua headeria sisältäen info kuvasta
- - kuvadata RLE-pakattuna
- - salaperäinen tavu 12
- - palettidata, viimeiset 768 tavua
- Ensimmäisenä ja kaikkein vaikeimpana on headeri, jonka loikimme lähes
- kokonaan yli, sillä tosipelikooderi tietää lataavansa oikeaa
- PCX-kuvaa, joka on oikeaa formaattia oikeankokoiseen puskuriin ja
- jättää selittämättömät kaatumiset muiden harteille! Tai itseasiassa en
- sitä selitä kun en siihen ole perehtynyt syvemmin. Kiinnostuneille
- PCGPE:ssä on tämäkin formaatti selitettynä lahjakkaan kryptisesti
- englannin kamalalla mongerruksella. Kaikki sitä haluavat hankkivat sitten
- tiedoston PCGPE10.ZIP, joka sisältää kaikkea hyödyllistä
- peliohjelmointiasiaa, englanniksi siis.
- Headerista tahdomme tietää vain sen, että PCX-kuvan koko lasketaan
- seuraavasti:
- - Mennään offsettiin 8 (fseek(handle, 8, SEEK_SET)).
- - Luetaan kaksi tavua ja tehdään niistä sana (unsigned short int,
- katsomme latauskoodia kohta) ja meillä on koko vaakatasossa.
- - Luetaan toiset kaksi tavua ja tehdään niille samoin kuin
- edellisille, nyt meillä on y-koko.
- Sitten onkin vaikein pala PCX:n rakenteessa. Sitä kutsutaan nimellä
- RLE-koodaus (run length encoding) ja se tarkoittaa sitä, että jos
- meillä on peräkkäin 10 pikseliä väriä 15 emme kirjoitakaan PCX:ään
- kymmentä kertaa numeroa 15, vaan kirjoitamme sinne tavun 192+10=202 ja
- sen perään tavun 15. Nyt kun PCX-lukija lukee ensimmäisen tavun se
- katsoo, että ahaa, nyt tulee toistoa ja toistaa seuraavaa tavua
- puskuriin tavu-192 kertaa (202-192=10). Näin me teemmekin
- yksinkertaisen pseudorungon:
- - Lue tavu1
- - Jos tavu1 on suurempi kuin 192 niin lue tavu2 ja toista tavua 2
- tavu1-192 kertaa.
- - Jos tavu1 ei ollut suurempi laita puskuriin tavu1.
- Näin helppoa, nyt vielä paletti. Sekin on helppoa, kunhan muistamme
- kaksi seikkaa:
- 1) Etsimme paletin tiedoston LOPUSTA päin (fseek(handle, -768, SEEK_END))
- 2) Jaamme värikomponentit neljällä, sillä PCX:ssä väriarvot ovat
- väliltä 0-255, VGA:ssa 0-63 (255/4=63).
- Nyt yhdistämme taas kaiken tietomme, ja teemme funktion, joka ottaa
- argumenttinaan PCX:n nimen ja puskurin jonne se ladataan. Ohjelma EI
- VARAA MUISTIA puskurille, vaan se pitää varata etukäteen. Voit itse
- tehdä muutokset ohjelmaan jos haluat. Yleensä kuitenkin etukäteen on
- tiedossa kuvan koko, kun PCX:iä käytetään
- peleissä. Kuvankatseluohjelmaa tehdessä pitää kuitenkin koko ottaa
- selville jo viimeistään sen vuoksi, että kuva näytetään oikein, vaikka
- puskurissa olisikin tilaa.
- Eli tässä meillä on valmiiksi pureskeltu PCX-lataajan runko, teemme
- sille oikein oman kirjaston PCX.H. Kirjasto tarvitsee stdio.h:n
- tiedostonkäsittelyfunktioita ja niiden tietorakenteita:
- void loadpcx(char *filename, char *buffer) {
- int xsize, ysize, tavu1, tavu2, position=0;
- FILE *handle=fopen(filename, "rb");
- if(handle==NULL) {
- printf("Virhe PCX-tiedoston avauksessa: Tiedostoa ei löydy!\n");
- exit(1);
- }
- fseek(handle, 8, SEEK_SET);
- xsize=fgetc(handle)+(fgetc(handle)<<8)+1;
- ysize=fgetc(handle)+(fgetc(handle)<<8)+1;
- fseek(handle, 128, SEEK_SET);
- while(position<xsize*ysize) {
- tavu1=fgetc(handle);
- if(tavu1>192) {
- tavu2=fgetc(handle);
- for(; tavu1>192; tavu1--)
- buffer[position++]=tavu2;
- } else buffer[position++]=tavu1;
- }
- fclose(handle);
- }
- void loadpal(char *filename, char *palette) {
- FILE *handle=fopen(filename, "rb");
- int c;
- if(handle==NULL) {
- printf("Virhe PCX-tiedoston palettia luettaessa:"
- " Tiedostoa ei löydy!\n");
- exit(1);
- }
- fseek(handle,-768,SEEK_END);
- for(c=0; c<256*3; c++)
- paletti[c] =fgetc(handle)/4;
- fclose(handle);
- }
- Kuten jo varmasti huomasit ovat paletin ja PCX:n latausrutiinit
- erillisinä. Tämä siksi, että joskus on huomattavasti kätevämpää ladata
- vain kuva, jos palettia ei mihinkään tarvita. Seuraavaksi seuraa
- kappaleen esimerkkiohjelma, joka käyttää hyväkseen tutoriaalin
- varrella esiteltyjä rutiineja ja muodostaa pienen esityksen. Ohjelma
- lataa PCX-kuvan PICTURE.PCX ja paletin siitä. Sitten se läiskäisee sen
- ruudulle. Lopuksi kuva himmenee tyhjyyteen ja palataan
- tekstitilaan. Esimerkki olettaa kuvan olevan kokoa 320x200,
- 256-värinen ja paletin sisältävä PCX-kuva RLE-pakattuna. Voit korvata
- kuvan millä haluat joko muuttamalla lähdekoodia tai kopioimalla oman
- kuvasi PICTURE.PCX:n päälle.
- Huomaa, että ohjelmassa luodaan kaksoispuskuri, johon kuva
- ladataan. Näyttömuistin vänkääminen parametriksi aiheuttaa 100%
- varmasti kaatumisen, tai jos jotenkin säästyt siltä niin ainakaan
- mitään ei ilmesty näytölle. Mutta asiaan (PCX1.C):
- #include <go32.h>
- #include <conio.h>
- #include <stdio.h>
- #include <sys/movedata.h>
- #include <dos.h>
- #include "palette.h"
- #include "pcx.h"
- #define flip(c) _dosmemputl(c, 64000/4, 0xA0000)
- int main() {
- char palette[256*3];
- char dblbuf[64000];
- textmode(0x13);
- loadpcx("PICTURE.PCX", dblbuf);
- loadpal("PICTURE.PCX", palette);
- setpal(palette);
- flip(dblbuf);
- getch();
- fadetoblack(palette);
- textmode(0x3);
- return 0;
- }
- Toivottavasti ymmärsit tästä luvusta ainakin käyttöperiaatteen. Eli
- loadpcx(nimi, puskuri) lataa kuvan puskuriin ja flip(puskuri) laittaa
- sen näytölle (jos kuva on kokoa 320x200). Paletti ladataan tarvittaessa
- funktiolla loadpal(nimi, palettipuskuri) ja asetetaan aktiiviseksi
- komennolla setpal(palettipuskuri). Huomaa, että esimerkissä asetetaan oikea
- paletti ENNEN kuvan laittamista ruudulle. Huomataksesi miksi vaihda
- setpal- ja flip-funktioiden paikkaa ja lisää väliin getch(), jotta ehdit kat-
- sella rauhassa muutosta. Tällaista tässä kappaleessa. Mene nyt kokeilemaan
- PCX-kuvien latausta. Seuraavassa kappaleessa tutustummekin sitten johonkin
- peliohjelmoijaa lähellä olevaan asiaan...
- 4.1 Bitmapit - eikai vain suunnistusta?
- ---------------------------------------
- Tänään siis teemme pienen bitmap-enginen C:llä. Itse olen aiemmin tehnyt
- kaikki sprite- ja bitmap -rutiinini C++:ssalla, mutta tällä kertaa
- käytämme C:tä, sillä haluan näiden esimerkkien toimivan ilman plussiakin.
- Eli mitä on bitmap?
- Bitmap, eli bittikartta on määrätyn kokoinen suorakulmion muotoinen esine,
- jolla on puskuri muistissa sisältäen sen värit, kuten näyttöpuskurinkin
- kanssa on. Hyödylliseksi bitmapin tekee se, että laitamme siihen pyyhkimis-
- ja piirtotoiminnot, sekä liikutustoiminnot, joilla voimme siirrellä bitmap-
- piamme ympäri ruutua. Lisäksi teemme siihen värin, joka tarkoittaa ettei
- sitä kohtaa bitmapista tarvitse kopioida ruudulle. Näin saamme tehtyä bit-
- mappiimme reikiä, eli teemme sen osittain läpinäkyväksi. Mutta miten tämä
- kaikki sitten tehdään? Koko asia on, kuten kaikki asiat ohjelmoinnissa lo-
- pulta ovat - naurettavan helppo.
- Eli, menkäämme takaisin kaksoispuskurin aikoihin. Siinä meillä on
- puskuri, jonka koko on 320x200 pikseliä ja se kopioidaan kokonaan näytön
- päälle. Bittikartassa on muutama selkeä ero:
- - Se voi alkaa mistä tahansa kohdasta ruutua, vaikka koordinaateista
- 15, 123.
- - Se voi olla minkä kokoinen tahansa (yleensä kuitenkin ruutua pienempi).
- - Sen peittämä tausta tallennetaan ja palautetaan kun bittikartta
- pyyhitään pois, mikä mahdollistaa liikuttelemisen.
- - Siinä on läpinäkyvä väri, meillä 0, jota ei piirretä ruudulle. Jos siis
- koko bittikartta olisi väriä 0, emme näkisi ruudulla mitään!
- Eli itseasiassa bittikartta on pari puskuria, joille on varattu tilaa
- siten, että jokainen bittikartan väri voidaan säilöä
- puskuriin. Puskureita on perusbittikartassa kaksi, eli itse kuvan
- sisältävä kartta, joka on järjestelty aivan samoin kuin
- esim. kaksoispuskuri, mutta koko on bittikartan mukainen. Toinen on
- taustapuskuri, joka on muuten sama, mutta sinne vain säilötään
- piirrettäessä alle jääneet pikselit, jotta ne voidaan bittikarttaa
- ruudulta pyyhkiessä palauttaa sieltä.
- Eli tällainen voisi olla 3x3 kokoinen bittikartta:
- Bittikartta: Taustapuskuri (mitä bittikartan alle on
- piirrettäessä jäänyt):
- 30 20 19 0 0 0
- 19 23 42 0 0 0
- 12 32 43 0 0 0
- Kuten huomaatte bittikartta on piirretty mustalle pohjalle, sillä
- taustapuskuri eli se mitä bittikartan alle jäi on täynnä mustaa, eli
- väriä 0. Bittikartta on kaikkein helpointa määritellä omaan
- datarakenteeseensa, joka sisältää tarvittavat tiedot kartan piirtelyyn
- ja pyyhkimiseeen, nimetään se vaikka structiksi BITMAP.
- Koordinaattien määrittely saavutetaan siten, että meillä on rakenteessamme
- X-ja Y-koordinaatit, joista piirto kaksoispuskuriin aloitetaan. Koko
- taas on helpompi. Jos kaksoispuskurin koko oli 320x200, niin kaava
- oikean pikselin hakemiseksi oli y*320+x. Jos meillä on bitmap kokoa
- ysize * xsize, niin oikea koordinaatti on y*xsize+x. Piirrettäessä
- loopataan X:ää ja Y:tä siten, että luemme yksi kerrallaan pikselin
- bittikartasta, ja jos se on jokin muu kuin väri 0 (yleensä musta, tämä
- oli siis läpinäkyväksi sovittu väri), otamme ensin sen alle jäävän
- pikselin talteen taustapuskuriin ja laitamme sitten vasta bittikartan
- värin ruudulle oikeaan kohtaan (bittikartan värit sisältävästä
- puskurista).
- Eli tarvittavat tiedot bittikarttarakenteeseen ovat:
- - bittikartan värit (char * -pointteri)
- - taustan värit (char * -pointteri)
- - x-sijainti ruudulla (int)
- - y-sijainti ruudulla (int)
- - koko x-suunnassa (int)
- - koko y-suunnassa (int)
- Lisäksi meillä on xspeed ja yspeed, joita käytetään esimerkeissä
- säilömään bittikartan liikenopeutta x- ja y-suunnassa. Näillä
- tempuilla meillä on nyt teoria liikuteltavan bitmapin tekemiseksi.
- Ensin määrittelemme rakenteen, joka sisältää kaiken tarvittavan tiedon
- bittikartastamme (BITMAP.H):
- typedef struct {
- char *bitmap;
- char *background;
- int x;
- int y;
- int xsize;
- int ysize;
- int xspeed;
- int yspeed;
- } BITMAP;
- Sitten tehtävänämme on tehdä "interface", eli käyttöliittymä
- bitmap-engineemme. Siihen sisällytämme seuraavat funktiot:
- - bdraw(BITMAP *b) piirtää bittikartan kohtaan BITMAP.x, BITMAP.y
- - bhide(BITMAP *b) tyhjentää edellisellä piirtokerralla piirretyn bitti-
- kartan. Huomaa, että JOKAISEN PIIRRON JÄLKEEN ON TULTAVA TYHJENNYS
- ja että BITTIKARTTAA EI LIIKUTETA SEN OLLESSA RUUDULLA (todellisuudessa
- tietenkin kaksoispuskurissa, joka kopioidaan ruudulle kun kaikki bitti-
- kartat ovat näkyvissä, sanoinhan, että hyödymme vielä siitä!)
- - bmove(BITMAP *b) lisää X-koordinaattiin muuttujan BITMAP.xspeed ja
- Y-koordinaattiin vastaavasti muuttujan BITMAP.yspeed.
- - bsetlocation(BITMAP *b, int x, int y) asettaa uudet X- ja
- Y-koordinaatit.
- - bsetspeed(BITMAP *b, int xspeed, int yspeed) asettaa uudet X- ja
- Y-nopeudet. Huomaa, että liike ylös saavutetaan negatiivisella
- Y-nopeudella ja vastaavasti liike vasemmalle negatiivisellä
- X-nopeudella.
- - bload(BITMAP *b, int x, int y, int xspeed, int yspeed, int xsize,
- int ysize, char *bitmapbuffer, int bufferx, int buffery,
- int bufferxs), jossa 8. parametristä lähtien kertoo
- latauspuskurista, jona tulemme käyttämään 320x200 kokoista PCX, kuvaa,
- sisältäen kaikki bitmapit mitä pitää ladata. Jos kuvan x-koko ja y-koko,
- sekä aloituskoordinaatit kuvassa on ilmoitettu oikein, onnistuu lataus
- suorakulmion muotoiselta alueelta täysin onnistuneesti, eikä lataus-
- rutiinin käyttö vaadi kovin paljoa miettimistä. Lisää käytöstä ajal-
- laan tulevassa esimerkissä.
- No niin. Lähtekäämme tekemään kirjastoamme BITMAP.H yksi funktio kerrallaan.
- Rakenne BITMAP on jo esitelty, joten alkakaamme keräämään sen perään
- käsittelyfunktioita. Ensimmäisenähän oli vuorossa bdraw(), joka onkin
- helpoimpia ja tärkeimpiä funktioita. Katsellaanpas esimerkkikoodia:
- void bdraw(BITMAP *b) {
- int y=b->y,
- x=b->x,
- yy, xx;
- /* Eli loopataan koko suorakulman kokoinen alue. bitmap- ja
- ja background -puskureissahan lasketaan sijainti seuraavasti:
- y * b->xsize + x. */
- for(yy=0; yy<b->ysize; yy++) {
- for(xx=0; xx<b->xsize; xx++) {
- /* eli värillä 0 tämä vertailu alla ei ole tosi, joten värillä
- 0 merkittyjä kohtia EI piirretä! */
- if(b->bitmap[yy*b->xsize+xx]) {
- /* doublebuffer muuttuja osoittaa kaksoispuskuriin. Huomaa, että
- yläkulma on y*320+x, mutta koska haluamme vielä piirtää useita
- rivejä, lisäämme yy-looppimme y-arvoon, kutenn myös xx-looppi
- x-arvoon. Jos et ymmärtänyt niin poista väliaikaisesti kohdat
- ja näet mitä tapahtuu */
- b->background[yy*b->xsize+xx]=
- doublebuffer[ (y+yy) * 320 + (x+xx) ];
- /* sitten vain asetetaan bittikartasta oikea kohta ruudulle,
- alle peittyvä osa on jo tallessa puskurin background vastaa-
- valla kohdalla. */
- doublebuffer[ (y+yy) * 320 + (x+xx) ]=
- b->bitmap[yy*b->xsize+xx];
- }
- }
- }
- }
- Koska joiltakin on esiintynyt valituksia siitä, että koodi jää hämärän
- peittoon, niin esittelen tässä saman pseudona, jos se olisi hieman
- selvempää:
- funktio bdraw
- kokonaisluvun kokoiset kierroslaskurit a ja b
- looppaa a välillä 0 - <y-koko>
- looppaa b välillä 0 - <x-koko>
- bittikarttasijainti = a * <x-koko> + b
- ruutusijainti = ( <y-sijainti> + a ) * 320 + b + <x-sijainti>
- jos bittikartta(bittikarttasijainti) ei ole 0 niin
- tausta(bittikarttasijainti) = kaksois(ruutusijainti)
- kaksois(ruutusijainti) = bittikartta(bittikarttasijainti)
- end jos
- end looppi b
- end looppi a
- end funktio
- Kun lähdet korvaamaan a:n muuttujalla yy ja b:n muuttujalla xx ja
- korvaat bittikartan sisäiset muuttujat <y-koko>, <x-koko>,
- <y-sijainti> ja <x-sijainti> BITMAP-rakenteen muuttujilla b->ysize,
- b->xsize, b->y ja b->x sekä tausta:n ja bittikartan:n
- b->background:illa ja b->bitmap:illa, kaksois-muuttujan
- kaksoispuskurisi nimellä niin olet aikalailla ensimmäisessä,
- alkuperäisessä sorsassa. Jos yhtään selventää niin voit poistaa
- kommentit alkuperäisestä sorsasta kokonaan ja siirtää sijainnin laskut
- sieltä []-sulkeiden sisästä juuri tuollaisiin
- bittikarttasijainti-tyylisiin apumuuttujiin, jolloin koodi selvenee
- hieman. Olkoot, tässä se on:
- void bdraw(BITMAP *b) {
- int a, b, bitmapsijainti, ruutusijainti;
- for(a=0; a < b->ysize; a++) {
- for(b=0; b < b->xsize; b++) {
- bitmapsijainti=a * b->xsize + b;
- ruutusijainti = ( b->y + a ) * 320 + b + b->x;
- if(b->bitmap[bitmapsijainti] != 0) {
- b->background[bitmapsijainti] = doublebuffer[ruutusijainti];
- doublebuffer[ruutusijainti] = b->bitmap[bitmapsijainti];
- }
- }
- }
- }
- Varaa aikaa edellisten tutkimiseen, sillä on tärkeää, että ymmärrät periaat-
- teen. Tietenkin saat lisäselvyyttä kokeilemalla muuttaa noita kohtia, jol-
- loin näet muutoksen kääntämällä uudelleen esimerkkiohjelman, jonka
- myöhemmin esittelemme ja ajamalla muunnellun version. Seuraavana onkin
- huomattavasti nopeammin tehty pyyhintäfunktio, joka eroaa vain siten, että
- sen sijaan, että säilöisimme taustan ja korvaisimme ruudun pikselin
- bitmap-puskurin arvolla laitammekin background-puskuriin tallennetun pikse-
- lin takaisin kaksoispuskuriin, joka on piilotusfunktion jälkeen samassa
- kunnossa kuin ennen piirtoakin!
- void bhide(BITMAP *b) {
- int y=b->y,
- x=b->x,
- yy, xx;
- /* Eli loopataan koko suorakulman kokoinen alue. bitmap- ja
- ja background -puskureissahan lasketaan sijainti seuraavasti:
- y * b->xsize + x. */
- for(yy=0; yy<b->ysize; yy++) {
- for(xx=0; xx<b->xsize; xx++) {
- /* eli värillä 0 tämä vertailu alla ei ole tosi, joten värillä
- 0 merkittyjä kohtia EI piirretä! */
- if(b->bitmap[yy*b->xsize+xx]) {
- doublebuffer[ (y+yy) * 320 + (x+xx) ]=
- b->background[yy*b->xsize+xx];
- }
- }
- }
- }
- Tuohon ette varmaan enää pseudoja tarvitse, koska sehän eroaa
- edellisestä vain tuon sijoituksen osalta, eli ensimmäinen sijoitus
- draw-funktiosta käännetään vain toisinpäin, niin alkup. tausta
- palautuu.
- Seuraavaksi kolme helponta funktiota heti rivissä, sillä niiden toteuttami-
- nen on helppoa ja ymmärtäminen vielä helpompaa, muista, että X-ja Y-koor-
- dinaatteja vähennetään negatiivisill nopeuksilla, sillä X+(-1)=X-1:
- void bmove(BITMAP *b) {
- b->x+=b->xspeed;
- b->y+=b->yspeed;
- }
- void bsetlocation(BITMAP *b, int x, int y) {
- b->x=x;
- b->y=y;
- }
- void bsetspeed(BITMAP *b, int xspeed, int yspeed) {
- b->xspeed=xspeed;
- b->yspeed=yspeed;
- }
- Seuraava onkin vaikea pala, joten lisään koodia saadakseni siitä vähän
- selvemmäksi. Idea siis on, että otamme pikselin tuplapuskuriin ladatus-
- ta ja laitamme sen bitmap-puskuriin. Eli oikeastaan käänteisesti näyt-
- töfunktioon nähden. Eli katsotaanpas:
- void bload(BITMAP *b, int x, int y, int xspeed, int yspeed, int xsize,
- int ysize, char *bitmapbuffer, int bufferx, int buffery,
- int bufferxs) {
- int yy, xx;
- bsetlocation(b, x, y);
- bsetspeed(b, xspeed, yspeed);
- b->xsize=xsize;
- b->ysize=ysize;
- b->bitmap=(char *)malloc(xsize*ysize);
- b->background=(char *)malloc(xsize*ysize);
- if(b->background==NULL || b->background==NULL) {
- printf("Ei tarpeeksi muistia bitmap-puskureille!\n");
- exit(1);
- }
- /* Eli loopataan koko suorakulman kokoinen alue. bitmap-
- puskurissahan lasketaan sijainti seuraavasti:
- y * b->xsize + x. */
- for(yy=0; yy<ysize; yy++) {
- for(xx=0; xx<xsize; xx++) {
- /* doublebuffer muuttuja osoittaa kaksoispuskuriin. Huomaa, että
- yläkulma on y*320+x, mutta koska haluamme vielä piirtää useita
- rivejä, lisäämme yy-looppimme y-arvoon, kutenn myös xx-looppi
- x-arvoon. Jos et ymmärtänyt niin poista väliaikaisesti kohdat
- ja näet mitä tapahtuu */
- b->bitmap[yy*xsize+xx]=
- bitmapbuffer[ (buffery+yy) * bufferxs + (bufferx+xx) ];
- }
- }
- }
- bload on itseasassa täysin sama kuin ensimmäinenkin funktio, mutta
- alussa meillä on pari alustusta jotta BITMAP-rakenne saadaan halutuksi
- (muistinvarausta, sijainnin nollausta, koon alustus...). Vain
- piirtofunktio on korvattu versiolla, joka ei piirrä ruudulle, vaan
- lataa ruudulta (bitmapbuffer tässä tapauksessa, jottei tarvi oikeaa
- kaksoispuskuria välttämättä käyttää) pikselit. Ei se loppujenlopuksi
- ole sen vaikeampi.
- Nyt kun lisäämme kaikki yhteen kirjastoomme BITMAP.H ja teemme lopuksi
- vielä pienen esimerkkiohjelman, joka liikuttelee palloa
- ruudulla. Koska kirjastomme ei kykene estämään ruudun yli menemisiä,
- niin meidän pitää kääntää liikkuvan pallon suuntaa ennenkuin alareuna
- osuu ruudun alareunaan ja menee sitten siitä yli (eli jos bittikartan
- koko, sijainti ja nopeus yhteenlaskettuna on yli ruudun koon, tai
- bittikartan sijainti ja nopeus yhteenlaskettuna on pienempi kuin
- 0). Eli kun jompikumpi edellisistä ehdoista täyttyy niin käännetään
- pallon suuntaa ja saadaan pallo "pomppimaan" reunoista.
- Mutta, olemme taas puhuneet ihan tarpeeksi. Menkäämme nyt esimerkkiohjel-
- mamme pariin (BITMAP1.C). Siinä lataamme bittikartan tiedostosta BITMAP.PCX
- ja tausta tiedostosta BITBACK.PCX. Näin näemme läpinäkyvyyden toiminnassa
- (muutenhan pallo olisi neliönmuotoinen). Lisäksi tietenkin käytämme jo va-
- kioiksi muuttuneita palettifunktiota ohjelmamme koristukseksi:
- #include <go32.h>
- #include <sys/movedata.h>
- #include <conio.h>
- #include <stdio.h>
- #include <dos.h>
- #include <stdlib.h>
- char *doublebuffer;
- #include "palette.h"
- #include "pcx.h"
- #include "bitmap.h"
- #define flip(c) _dosmemputl(c, 64000/4, 0xA0000)
- int main() {
- char palette[768];
- BITMAP bitmap;
- doublebuffer=(char *)malloc(64000);
- if(doublebuffer==NULL) {
- printf("Ei tarpeeksi muistia kaksoipuskurin varaukseen!\n");
- return 1;
- }
- textmode(0x13);
- loadpcx("BITMAP.PCX", doublebuffer);
- loadpal("BITMAP.PCX", palette);
- setpal(palette);
- bload(&bitmap, 160, 100, 1, 1, 16, 16, doublebuffer, 1, 1, 320);
- loadpcx("BITBACK.PCX", doublebuffer);
- /* Lataus vasta kun bittikartta on otettu edellisestä tiedostosta.
- Ei ladata palettia koska se on sama kuin edellisessä PCX:ssä. */
- while(!kbhit()) {
- bdraw(&bitmap);
- waitsync();
- flip(doublebuffer);
- bhide(&bitmap);
- bmove(&bitmap);
- if((bitmap.x+bitmap.xsize+bitmap.xspeed)>320 ||
- bitmap.x+bitmap.xspeed<0)
- bitmap.xspeed= -bitmap.xspeed;
- if((bitmap.y+bitmap.ysize+bitmap.yspeed)>200 ||
- bitmap.y+bitmap.yspeed<0)
- bitmap.yspeed= -bitmap.yspeed;
- }
- getch();
- fadetoblack(palette);
- textmode(0x3);
- return 0;
- }
- Varaa kunnolla aikaa ja tutki lähdekoodeja, mieti teoriaa ja kokeile kaikkea
- käytännössä mitä mieleen tulee. Kun luulet keksineesi idean niin palaa
- takaisin dokumentin ääreen, ja siirrymme seuraavaan aiheesemme. Menehän
- siitä! Jos vieläkin tuntui siltä ettet tajunnut niin ota yhteyttä ja
- kysy mikä jäi mietityttämään, niin tarkennan sitten vielä tätä.
- 4.2 Animaatiot
- --------------
- Tämänkertainen aiheemme on pieni parannus koodiin, joka on paljon näy-
- töllä ja jonka jälkeen on tämän tutoriaalin bittikarttarutiinit lähes kä-
- sitelty. Tulemme kyllä hyväksikäyttämään edellisen kappaleen koodia
- tehdessämme fonttiengineä, sekä parantelemme koodia tehdessämme törmäys-
- tarkistuksen, mutta itse animointi- ja bittikarttateoria käsitellään
- kokonaan tässä ja edellisessä kappaleessa.
- Eli tänään tutustumme ensimmäisenä animaatiohin. Mitä animaatiot sitten
- ovat? No itseasiasas animaatio on vain sarja kuvia, joita vaihdellaan
- ja saadaan kuva liikkeestä. Animaatiota voidaan käyttä lähes kaikkeen
- pelissä. Sillä voidaan tehdä pyörivä alusanimaatio, jonka jokainen
- kuva on yksi aluksen suunta. Jokaisella suunnalla voisi olla vielä oma
- animaationsa, joka saa vaikka rakettimoottorit hehkumaan ja laserit
- aiheuttamaan välähdyksiä aluksen pinnassa. Pienellä mielikuvituksella
- ja taitavalla graafikolla päästään ihmeisiin. Tässä kappaleessa esi-
- telty kirjasto ei varmaankaan käy suoraan moneen tarkoitukseen tai ole
- tarpeeksi nopea peliin, mutta enginen onkin vain tarkoitus näyttää
- pääperiaatteita animoinnin ja muiden olennaisien asioiden takana.
- Eli animaatio on kuvasarja, jotka näytetään tietyssä järjestyksessä. Miten
- sitten toteutamme tämän. Tässä on tapa jolla minä olen sen tehnyt. Meillähän
- on täysin toimivat rutiinit yhden kuvan näyttämiseen. Tehkäämme vain
- animointikoodi, joka vaihtaa pointterin bitmap osoittamaan seuraavaan
- kuvaa, eli frameen. Tätä täytyy kutsua silloin kun spriteä, joksi kutsumme
- animoivaa bittikarttaamme tästälähin ei ole piirretty puskuriin. Jälleen
- voit kokeilla siirtää animointikoodin kutsun kohtaan jossa esine on piir-
- rettynä, mutta se ei tule näyttämään hyvältä (jos objektin peittämän alueen
- muoto muuttuu). Eli siis tarvitsemme uuden rakenteen, joka voi säilöä
- useita kuvia, koodin joka vaihtaa bitmap-pointterin osoittamaan seuraavaan
- kuvaan, laskurin joka kertoo monennessako kuvassa mennään ja toisen muuttu-
- jan joka kertoo montako kuvaa meillä on animaatiossa, sekä lopulta uuden
- latausfunktion, joka osaa ladata useita kuvia käsittävän animaation.
- Tähän kaikkeen voimme kopioida vanhaa koodiamme ja lisäillä sinne tar-
- peellisia osia. Eli teemme nyt uuden rakenteen, jossa voi olla maksimis-
- saan MAXFRAME määrä frameja, eli kuvia (tämä toteutuksen helpottamiseksi):
- #define MAXFRAME 64
- typedef struct {
- char *frame[MAXFRAME];
- int curfrm;
- int frames;
- char *bitmap;
- char *background;
- int x;
- int y;
- int xsize;
- int ysize;
- int xspeed;
- int yspeed;
- } SPRITE;
- Se olikin helppoa. Nämä rutiinit tulevat kirjastoon SPRITE.H, josta löydät
- myös joukon vanhoja tuttujamme uudelleennimettynä ja vähän
- muunneltuina (sdraw, shide...). Seuraavaksi sitten animointirutiini:
- void sanimate(SPRITE *s) {
- s->curfrm++;
- if(s->curfrm >= s->frames)
- s->curfrm=0;
- s->bitmap=s->frame[s->curfrm];
- }
- Radikaaleja muutoksia tarvinnee myös latausrutiinimme. Tärkeimmät muutok-
- set siinä on, että se lukee framet rivistä. Katso SPRITE.PCX esimerkkinä
- tällaisesta animaatiosta. Jos ihmettelet outoja kertolaskuja joissain
- kohdin se johtuu siitä, että jokaisen framen jälkeen hypätään 1 pikseli
- yli, sillä teemme rajat animaatioiden väliin selvennykseksi. Eli tässä
- olisi latauskoodimme, uusi parametri on animaatioiden määrä:
- void sload(SPRITE *s, int x, int y, int xspeed, int yspeed, int xsize,
- int ysize, char *bitmapbuffer, int bufferx, int buffery,
- int bufferxs, int frames) {
- int yy, xx, current;
- ssetlocation(s, x, y);
- ssetspeed(s, xspeed, yspeed);
- s->xsize=xsize;
- s->ysize=ysize;
- s->curfrm=0;
- s->frames=frames;
- for(current=0; current<frames; current++) {
- s->frame[current]=(char *)malloc(xsize*ysize);
- if(s->frame[current]==NULL) {
- printf("Ei tarpeeksi muistia sprite-puskureille!\n");
- exit(1);
- }
- }
- s->background=(char *)malloc(xsize*ysize);
- s->bitmap=s->frame[s->curfrm];
- if(s->background==NULL) {
- printf("Ei tarpeeksi muistia sprite-puskureille!\n");
- exit(1);
- }
- /* Eli loopataan koko suorakulman kokoinen alue. bitmap-
- puskurissahan lasketaan sijainti seuraavasti:
- y * s->xsize + x. Uloimpana looppina on uutena framelooppi,
- joka on lisätty koska meidän pitää ladata usea kuva. */
- for(current=0; current<frames; current++)
- for(yy=0; yy<ysize; yy++) {
- for(xx=0; xx<xsize; xx++) {
- /* doublebuffer muuttuja osoittaa kaksoispuskuriin. Huomaa, että
- yläkulma on y*320+x, mutta koska haluamme vielä piirtää useita
- rivejä, lisäämme yy-looppimme y-arvoon, kutenn myös xx-looppi
- x-arvoon. Jos et ymmärtänyt niin poista väliaikaisesti kohdat
- ja näet mitä tapahtuu */
- s->frame[current][yy*xsize+xx]=
- bitmapbuffer[ (buffery+yy) * bufferxs + (bufferx+xx) +
- (xsize+1)*current ];
- }
- }
- }
- Kirjastoon SPRITE.H lisätään vielä bdraw, bhide, bmove, bsetlocation ja
- bsetspeed nimettynä nimillä sdraw, shide, smove, ssetlocation ja ssetspeed
- funktioiden erottamiseksi bitmap-rutiineista (jos vaikka halutaan käyttää
- molempia). Muitakin pikkumuutoksia on tehty. Huomaat ne helposti
- kurkkaamalla kirjaston sisään. Nyt meillä onkin animaatiot taitava engine,
- jota meidän täytyy tietenkin heti kokeilla. Tässä on esimerkkiohjelmamme
- SPRITE1.C, joka havainnoi funktioiden käyttöä:
- #include <go32.h>
- #include <sys/movedata.h>
- #include <conio.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <dos.h>
- char *doublebuffer;
- #include "palette.h"
- #include "pcx.h"
- #include "sprite.h"
- #define flip(c) _dosmemputl(c, 64000/4, 0xA0000)
- int main() {
- char palette[768];
- SPRITE sprite;
- doublebuffer=(char *)malloc(64000);
- if(doublebuffer==NULL) {
- printf("Ei tarpeeksi muistia kaksoipuskurin varaukseen!\n");
- return 1;
- }
- textmode(0x13);
- loadpcx("SPRITE.PCX", doublebuffer);
- loadpal("SPRITE.PCX", palette);
- setpal(palette);
- sload(&sprite, 160, 100, 1, 1, 16, 16, doublebuffer, 1, 1, 320, 8);
- loadpcx("BITBACK.PCX", doublebuffer);
- /* Lataus vasta kun bittikartta on otettu edellisestä tiedostosta.
- Ei ladata palettia koska se on sama kuin edellisessä PCX:ssä. */
- while(!kbhit()) {
- sdraw(&sprite);
- waitsync();
- waitsync();
- flip(doublebuffer);
- shide(&sprite);
- smove(&sprite);
- sanimate(&sprite);
- if((sprite.x+sprite.xsize+sprite.xspeed)>320 ||
- sprite.x+sprite.xspeed<0)
- sprite.xspeed= -sprite.xspeed;
- if((sprite.y+sprite.ysize+sprite.yspeed)>200 ||
- sprite.y+sprite.yspeed<0)
- sprite.yspeed= -sprite.yspeed;
- }
- getch();
- fadetoblack(palette);
- textmode(0x3);
- return 0;
- }
- Luultavasti huomaat nykimistä, sillä täysin optimoimaton sprite-enginemme
- ei aivan pysty 70 frameen sekunnissa. Siksi laitoin ohjelmamme odottamaan
- kahta vertical retracea, jotta nykiminen ei olisi niin häiritsevää
- (P75:lläni kahdella waitilla meno näyttää paljon tasaisemmalta, eikä yhden
- framen hyppy näy läheskään niin selvästi). Jos kuitenkin sinulla on hidas
- kone niin poista toinen tai kummatkin odotuksista, se nopeuttaa koodia
- paljon, mutta voit joutua laittamaan delay-komennolla viivettä säätääksesi
- pyörimistä tasaisemmaksi. Pienellä optimoinnilla olisimme toki saaneet
- moninkertaisesti lisää nopeutta, mutta koodi olisi menettänyt luettavuut-
- taan, joka esimerkkiohjelmien tarkoitus on. Tietenkin kun alat tekemään
- omaa peliäsi teet uudet ja paremmin tarkoitukseesi sopivat rutiinit ke-
- räämiesi tietojen pohjalta.
- Nyt onkin tämän kappaleen aika loppua ja sinun on aika paneutua uuden
- asian pariin. Seuraavassa luvussamme käsitelläänkin sitten viimeistä
- kysymystä spritejen parissa, monen spriten käyttöä, niiden törmäyksiä
- ja ylitseliukumisia. Mutta nyt jätän sinut rauhaan. Näemme seuraavassa
- luvussa!
- 4.3 Pitääkö spriten törmätä? Entä coca-colan?
- ---------------------------------------------
- Nyt pääsemmekin vihoviimeiseen vaiheeseen teoriassamme ja ryyditämme sitä
- pienin, tai ehkä niinkään pienin muutoksin SPRITE.H-kirjastoomme. Nimit-
- täin jokainen vähänkään vakavasti pelintekoa harkinnut tarvitsee useampia
- kuin yhden spriten. Mutta mitä tapahtuu kun ne ovat menossa päällekäin?
- Jos teet vain loopin, joka piirtää spriten ja toisen, joka pyyhkii ne
- samassa järjestyksessä olet varmaan huomannut, että se ei aiheuta toivot-
- tuja tuloksia. Muutos mitä tarvitaan on pieni ja yksinkertainen, mutta
- ajatellaanpas esimerkkiämme.
- Ajatellaan, että sinulla on kolme pikseliä. Punainen, sininen ja keltainen.
- Haluat laittaa ne samaan kohtaan ruudulle. Laitat ne edellä olevassa
- järjestyksessä mustalle ruudulle ja laitat lapulle muistiin punaisen koh-
- dalle, että sen alla oli musta, sinisen kohdalle, että sen alla oli
- punainen ja keltaisen kohdalle, että sen alla oli sininen.
- Nyt haluat poistaa ne. Ottaisitko ne nyt samassa järjestyksessä, eli ensin
- punainen, sitten sininen ja lopuksi keltainen? Et, sillä jos ottaisit lopuksi
- keltaisen, katsoisit lapustasi sen alla olleen sinisen värin ja ruutu
- muuttuisikin siniseksi. Tässä meidän täytyykin mennä käänteisesti, eli
- keltainen, sininen ja sitten vasta punainen, jonka tilalle laitat lopulta
- mustan ja kaikki on hyvin.
- Eli jos sinulla olisi 10 bittikarttaa taulukossa SPRITE s[10], niin niiden
- piirto ja pyyhkiminen tapahtuisi seuraavasti:
- for(c=0; c<10; c++) sdraw(s[c]);
- flip(doublebuffer);
- for(c=10; c>=0; c--) shide(s[c]);
- Ja ei enää toimimattomia koodinpätkiä, vaan hienosti toistensa ylitse
- liukuvat spritet.
- Mutta aina ei haluta kaikkien vain liukuvan toistensa ylitse. Miltä
- näyttäisi matopeli, jossa madot kiltisti liukuvat toistensa ylitse?
- Ei kovin oikealta, sanoisin. Meidän täytyy siis tehdä rutiini, joka
- tarkistaa törmäyksen kahden spriten välillä. Olkoon sen kutsutapa
- seuraava: scollision(SPRITE *a, SPRITE *b) ja se palauttaa arvon
- 1 jos törmäys on tapahtunut, muuten se palauttaa nollan. Jos siis
- haluat tehdä törmäyksen tultua jotakin, niin koodi menisi suurinpiirtein
- näin:
- if(scollision(sprite[0], sprite[1]))
- tee_jotain_kun_tulee_pamahdus();
- Mutta, miten toimii tämä salaperäinen funktiomme? Itseasiassa minä en
- saanut siitä mitään selvää luettuani sen aikoinani Mikrobitin grafiikka-
- ohjelmointikurssin toisesta osasta, mutta luulisin nyt pystyväni teke-
- mään samanlaisen, ja jos onnistumme pystynen selittämäänkin toimintaperi-
- aatteen.
- int scollision(SPRITE *a, SPRITE *b) {
- /* Lasketaan spritejen yläkulmien väliset etäisyydet. Huomaa, että tässä
- lasketaan mukaan nopeudet, eli palautusarvo 1 kertoo spritejen
- törmäävän ENSI vuorolla. Näin ehditään päällekkäin meneminen estää
- ajoissa. */
- int xdistance= (a->x+a->xspeed) - (b->x+b->xspeed);
- int ydistance= (a->y+a->yspeed) - (b->y+b->yspeed);
- int xx, yy;
- /* Jos x- tai y-etäisyys on suurempi kuin suuremman leveys eivät
- spritet voi mitenkään olla toistensa päällä. */
- if(xdistance>a->xsize && xdistance>b->xsize) return 0;
- if(ydistance>a->ysize && ydistance>b->ysize) return 0;
- for(xx=0; xx< a->xsize; xx++)
- for(yy=0; yy< a->ysize; yy++)
- if(xx+xdistance < b->xsize && xx+xdistance>=0 &&
- yy+ydistance < b->ysize && yy+ydistance>=0)
- if(a->bitmap[ yy * a->xsize + xx ] &&
- b->bitmap[ (yy+ydistance) * b->xsize + (xx+xdistance) ])
- return 1;
- return 0;
- }
- Loopissa ideana on se, että laskuilla saadaan b-spriten vastaava koordinaatti
- selville ja jos se on siis positiivinen ja spriten b rajoissa (pienempi
- kuin leveys tai y-koordinaatin ollessa kyseessä korkeus). Tarkemmin en
- ala selittämään. Jos välttämättä haluat saada selville miten pätkä toimii
- niin piirrä pari tilannetta paperilla ja katso miten niiden kanssa tapah-
- tuu. Nyt meillä onkin käsiteltynä kaikki tärkein spriteistä ja voimme
- mennä viimeiseen pelkästään spritejä käyttävään ohjelmaamme. Tämä ohjelma
- on pienimuotoinen peli, jossa liikutaan edellisen esimerkin palikoilla. Pe-
- laajia on 2 ja tarkoitus on leikkiä hippaa. Eli toinen yrittää pakoon ja
- toinen yrittää ottaa kiinni. Peli loppuu kun pelaajat törmäävät. Kontrol-
- lit ovat pelaajalla 1 wsad ja pelaajalla 2 ujhk. Tämä on vain pieni esi-
- merkki siitä mitä näillä taidoilla voisi tehdä. Lisäksi nappeina on
- + ja - nopeuden säätöön (nyt ei odoteta waitsyncillä) sekä ESC lopetuk-
- seen kesken. Eli SPRITE2.C:
- #include <go32.h>
- #include <sys/movedata.h>
- #include <conio.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <dos.h>
- char *doublebuffer;
- #include "palette.h"
- #include "pcx.h"
- #include "sprite.h"
- #define flip(c) _dosmemputl(c, 64000/4, 0xA0000)
- int main() {
- char palette[768];
- SPRITE pl1, pl2;
- int quit=0, waittime=0;
- doublebuffer=(char *)malloc(64000);
- if(doublebuffer==NULL) {
- printf("Ei tarpeeksi muistia kaksoipuskurin varaukseen!\n");
- return 1;
- }
- textmode(0x13);
- loadpcx("SPRITE.PCX", doublebuffer);
- loadpal("SPRITE.PCX", palette);
- setpal(palette);
- sload(&pl1, 100, 100, 0, 0, 16, 16, doublebuffer, 1, 1, 320, 8);
- sload(&pl2, 220, 100, 0, 0, 16, 16, doublebuffer, 1, 1, 320, 8);
- loadpcx("BITBACK.PCX", doublebuffer);
- while(!quit) {
- sdraw(&pl1);
- sdraw(&pl2);
- flip(doublebuffer);
- shide(&pl1);
- shide(&pl2);
- smove(&pl2);
- smove(&pl1);
- sanimate(&pl1);
- sanimate(&pl2);
- if((pl1.x+pl1.xsize+pl1.xspeed)>320 ||
- pl1.x+pl1.xspeed<0)
- pl1.xspeed= -pl1.xspeed;
- if((pl1.y+pl1.ysize+pl1.yspeed)>200 ||
- pl1.y+pl1.yspeed<0)
- pl1.yspeed= -pl1.yspeed;
- if((pl2.x+pl2.xsize+pl2.xspeed)>320 ||
- pl2.x+pl2.xspeed<0)
- pl2.xspeed= -pl2.xspeed;
- if((pl2.y+pl2.ysize+pl2.yspeed)>200 ||
- pl2.y+pl2.yspeed<0)
- pl2.yspeed= -pl2.yspeed;
- if(scollision(&pl1, &pl2))
- quit=2; /* 2 tarkoittaa, että toinen saatiin kiinni */
- while(kbhit()) { /* tyhjennetään näppispuskuri */
- switch(getch()) {
- case 'w': pl1.yspeed=-1; pl1.xspeed=0; break;
- case 's': pl1.yspeed=1; pl1.xspeed=0; break;
- case 'a': pl1.xspeed=-1; pl1.yspeed=0; break;
- case 'd': pl1.xspeed=1; pl1.yspeed=0; break;
- case 'u': pl2.yspeed=-1; pl2.xspeed=0; break;
- case 'j': pl2.yspeed=1; pl2.xspeed=0; break;
- case 'h': pl2.xspeed=-1; pl2.yspeed=0; break;
- case 'k': pl2.xspeed=1; pl2.yspeed=0; break;
- case '+': if(waittime) waittime--; break;
- case '-': waittime++; break;
- case 27: quit=1; break;
- }
- }
- delay(waittime);
- }
- if(quit==2) { /* jos kiinni, niin feidataan ensin valkoiseen (räjähdys) */
- fadetowhite(palette);
- for(waittime=0; waittime<256*3; waittime++)
- palette[waittime]=63;
- }
- fadetoblack(palette);
- textmode(0x3);
- return 0;
- }
- Tässä oli sitten sellainen lähdekoodi, jota kukaan vähänkään omanarvontuntoa
- omaava peliohjelmoija, taikka muukaan ohjelmoija EI TEE. Jos pelistä to-
- della halutaan selvä ja helposti laajennettava ei tehdä jokaiselle pelaa-
- jalle eri spriteä eri nimellä, vaan kaikki pelaajaspritet ovat
- taulukossa. Ja muutenkin esimerkkikoodi ainoastaan demonstroi mahdolli-
- suuksia oppimiemme asioiden käyttämiseen, ei suinkaan minkälainen pelin
- runko pitäisi olla. Siihen me palaamme myöhemmin. Mutta meneppäs pelaamaan
- ja näytä kavereillesi minkälaisia pelejä osaisit jo tehdä. =) Äläkä
- palaa takaisin ennenkuin tämän kappaleen asiat ovat hallussa. Sillä niiden
- osaamista luultavasti tullaan vaatimaan seuraavissakin luvuissa. Mutta jos
- olet malttamaton, niin on tietenkin mahdollista palata takaisin opettelemaan,
- mutta turhauttavaa se on.
- Jälkikäteen kaiken sprite, animaatio ja bittikarttanäpräilyn jälkeen totean,
- että kaikissa kohdissahan ei käytetty täsmälleen oikeita termejä. Bittikart-
- tahan on käytännössä vain kuvadata ja mahdollisesti hieman lisätietoa, ani-
- maatio on yleensä peräkkäisiä bittikarttoja osaksi yhteisellä datalla,
- olio on yleensä sitten se mikä osaa pyyhkiä itsensä ja joka tietää mitkä
- bittikartat ja muut vastaavat sille kuuluvat, joka voi pyyhkiä itsensä ja
- tehdä monia muitakin kivoja asioita. Sprite on sitten jotain siellä jossain
- välillä tai päässä, en tiedä kovin tarkasti mutta käytin nyt tätä nimitystä
- täysin toimivasta oliosta joka kykenee itsensä käsittelyyn.
- 4.4 Maskatut spritet
- --------------------
- Vähän aikaa sitten kerroin PC-Ohjelmointi -alueella tämän kurssin sisällöstä
- ja eikös vain joku mennyt kysymään minulta selittikö tutoriaali maskatut
- vai maskaamattomat spritet. Minähän en ollut edes kuullut moisesta asiasta
- ja utelin ideaa sen takana. Sainkin kuulla se ja tein sen pohjalta assemb-
- lerilla nopean rutiinin. Pienellä nopeuskokeella se osoittautui 11 kertaa
- nopeammaksi kuin muutama luku sitten tekemämme rutiini. Aion nyt selittää
- idean tämän tekniikan takana, joten kiinnittäkää turvavyönne ja valmistau-
- tukaa!
- Maskatuiden spritejen ideana on se, että niiden piirrossa ei tarvita pikse-
- likohtaisia vertailulauseita lainkaan, jolloin voidaan käyttää assembleril-
- la neljän tavun kanssa operoivia funktioita. Mutta miten sitten kierrämme
- vertailulausekkeet säilyttäen silti läpinäkyvyyden nollavärin kanssa?
- Idea perustuu bittioperaattoreihin.
- Jokaiselle spriten framelle tehdään etukäteen maski, joka on nolla kohdissa
- joissa on pikseli ja 255 läpinäkyvissä kohdissa. Nyt sitten vain suoritamme
- kaksoispuskurin pikselille loogisen AND-operaation:
- Maski spritelle FF 00 FF FF
- Näyttö 4F 3C 93 5A
- ----------------------------
- Tulos 4F 00 93 5A
- Kuten huomaatte, jäävät läpinäkyvät kohdat (FF) jäljelle. Sitten vain
- käytämme OR-operaattoria sytyttämään spriten pikselit, sillä ne kohdat
- ovat juuri äsken nollautuneet, joten looginen OR asettaa juuri oikeat
- bitit:
- Sprite 00 46 00 00
- Maskattu näyttö 4F 00 93 5A
- ----------------------------
- Tulos 4F 46 93 5A
- Lopun saat toteuttaa aivan itse. Huomattavaa tässä on se, että jos haluat
- käyttää tehokkaita 4 tavun (dword) operaatioita on bittikartan leveyden
- oltava jaollinen neljällä. Huipputehoon tarvitset assembleria, sillä C:llä
- on vaikea kontrolloida edellä mainittuja asioita. Jos et vielä osaa assemb-
- leria, varsinkaan DJGPP:n AT&T syntaksia, suosittelen seuraavia tiedostoja:
- ASSYT.ZIP Assemblerin alkeet suomeksi.
- PCGPE10.ZIP PCGPE sisältää kaiken muun lisäksi assemblytutoriaalin.
- DJTUT2_4.ZIP Jos osaat Intel-syntaksin, muttet AT&T-syntaksia
- (movd %eax, %ebx). Sisältää myös muuta kiinnostavaa
- materiaalia, jota tässäkin tutoriaalissa on sivuttu.
- NASM095B.ZIP Tällä voit tehdä Intel-syntaksin assemblerilla DJGPP:n
- COFF-muotoisia objektitiedostoja. Tiivistettynä TASM joka
- osaa myöskin DJGPP:n objektiformaatin. Huomaa, että uusin
- versio voi olla muutakin kuin 0.95 (NASM095B.ZIP).
- Lisäksi voisi olla hyvä idea lainata kirjastosta kirja 486-ohjelmointi,
- joka on suomenkielinen assembler-ohjelmointia käsittelevä kirja ja kaiken
- lisäksi hyvä sellainen!
- Loppulisäyksenä jälleen kiva vinkki Pekka Nurmiselta. Kaksoispuskuri
- kannattaa tarvittaessa tehdä sen verran leveämmäksi, että jos spitea
- ei saada katki juuri neljän tavun kohdalta ei tuo tule toisesta reunasta
- vastaan. Eli jättää sinne neljä tavua ruudun reunoihin, jota ei vain
- sitten kopioida näytälle. Näin kaksoispuskurin kooksi tulisi 328x200.
- 5.1 Näppäimistön käsittely - ja nyt meillä on hauskaa
- -----------------------------------------------------
- Jos pelasit ahkerasti esimerkkipeliämme, niin ehkä huomasit, että painaessasi
- useita nappia ilmenee myös useita ongelmia. Näihin voivat kuulua näppäimis-
- tön jumiutuminen, nappien huomiotta jättäminen jne. Tarvitsemme siis ru-
- tiinin joka päästäisi meidät pälkähästä. Tarvitsemme näppishandlerin!
- Tämä perustuu siihen, että joka kerta kun nappia painetaan kutsutaan
- keskeytystä 9, joka lukee merkin näppäimistöltä portista 60h (0x60) ja
- muuntaa sen ASCII:ksi ja laittaa näppäimistöpuskuriin. Mutta mepäs ohi-
- tammekin tämän ja teemme oman handlerin, joka ei muutakaan mitään miksi-
- kään ASCII:ksi, vaan laittaa näppäimistötaulukon vastaavan kohdan arvoon
- 1, josta peli voi sitten sen tarkistaa. Ja kun nappi päästetään tulee
- myös keskeytys, tällä kertaa tulee napin arvo + 128, joten vähennämme
- luetusta arvosta 128 ja nollaamme vastaavan kohdan taulukosta. Ja millainen
- on tämä taulukko?
- Taulukossa on 128 alkiota, yksi jokaiselle SCAN KOODILLE, jollaisia näppäi-
- mistö syytää. Olen tehnyt näistä numeroista kirjaston, jossa esimerkiksi
- ESC-näppäimen scan koodi on nimellä SxESC ja sen arvo on 1. Jos siis haluat
- pelissäsi tietää onko ESC painettuna, osoitat näppäimistöpuskuriin:
- if(keybuffer[SxESC]==1) printf("ESC painettu!\n");
- Kirjasto on nimellä D_SCAN.H. Ja sitten tarvitsemme siis koodia, joka lukee
- tavun portista 60h ja jos se on alle 128 se laittaa vastaavan kohdan
- taulukosta ykköseksi ja jos se on yli tai yhtäsuuri kuin 128, niin laitamme
- alkion tavu-128 nollaksi. Lopuksi lähetämme signaalin PIC:ille, että kes-
- keytyksemme on valmis, eli outtaamme tavun 20h porttiin 20h. Tällainen on
- siis handlerimme (KEYBOARD.H):
- void keyhandler() {
- register unsigned char tavu=inportb(0x60);
- if(tavu<128) keybuffer[tavu]=1;
- else keybuffer[tavu-128]=0;
- outportb(0x20, 0x20);
- }
- Tämä onkin oikeastaan helpoin osa tehtäväämme. Vaikeampi (joskin esimerkki-
- koodin takia helppo) on koukuttaa tarvitsemamme näppäimistökeskeytys ja
- palauttaa se kun tarvitaan näppäimistörutiineja (gets, getch...) tai pois-
- tutaan ohjelmasta. Lisäksi tarvitsemme joukon apumuuttujia, jotka ovat
- tässä:
- volatile unsigned char keybuffer[128], installed;
- _go32_dpmi_seginfo info, original;
- Keybuffer säilöö näppäinten tilat, installed kertoo onko tämä handleri a-
- sennettuna ja estää samalla uudelleenasentamisen. Kaksi viimeistä muuttujaa
- info ja original ovat koukuttamiseen ja koukutuksen (hooking) poistamiseen
- tarvittavia rakenteita, joista infoa käytetään oman asentamiseen ja origi-
- naliin säilötään alkup. handlerin osoite ja muut tarpeelliset tiedot.
- Tässä on koukutukseen ja palautukseen tarvittava koodi, johon emme perehdy
- kovinkaan tarkasti, lisäinfoa asiasta saat vaikka DJGPP:n FAQ:sta hakusanalla
- handler:
- int setkeyhandler() {
- int c;
- for(c=0; c<0x80; c++)
- keybuffer[c]=0; /* nollataan napit */
- if(!installed) {
- _go32_dpmi_get_protected_mode_interrupt_vector(0x0009, &original);
- info.pm_offset=(unsigned long int)keyhandler;
- info.pm_selector=_my_cs();
- _go32_dpmi_allocate_iret_wrapper(&info);
- _go32_dpmi_set_protected_mode_interrupt_vector(0x0009, &info);
- installed=1;
- return 1;
- } else return 0;
- }
- int resetkeyhandler() {
- if(installed) {
- _go32_dpmi_set_protected_mode_interrupt_vector(0x0009, &original);
- installed=0;
- return 1;
- } else return 0;
- }
- Lisäämme kaikki kolme funktiota ja globaalit muuttujamme tiedostoon
- KEYBOARD.H. Nyt meillä on tarpeen vaatiessa täydellisen toimiva näppäimis-
- töhandleri (jota ehkä myöhemmin tulemme käyttämään).
- 5.2 Fixed point matematiikka
- ----------------------------
- Alamme pikkuhiljaa lähestyä kurssimme loppua (tai ken tietää, todellista
- alkua?), joten käsittelen tässä hieman pelin optimointiin vaikuttavia
- tekijöitä ja parannuksia aiemmin esittelemiimme kirjastoihin (omaan peliin
- kun kannattaa kuitenkin tehdä osa kirjastoista uusiksi). Selitän fixed-
- pointin, lookupin idean ja pari muuta nopeuttavaa temppua sekä mainitsen
- pullonkauloja joita nopeuttamalla saadaan aikaan dramaattisia muutoksia.
- Siis fixed point, mitä se on? Kuten tiedät, C:n int-tyyppi on kokonaisluku,
- eli sillä ei voi ilmoittaa desimaalilukuja. Monesti desimaaliluvu olisivat
- tarpeellisia, esimerkiksi sprite-enginessä, jos halutaan että eri spritet
- liikkuvat eri nopeuksilla. Näyttää nimittäin todella typerältä jos ohjus
- pomppii kymmenen pikseliä eteenpäin, koska se on 10 kertaa nopeampi kuin
- pelin hitain sprite. Tarvitsemme siis nopeudeksi desimaaliluvun, jolloin
- ohjuksen nopeus voisi olla 1 ja kilpikonnan 0.1 (jolloin se liikkuisi yhden
- pikselin joka 10. frame). Valitettavasti float-tyyppisten muuttujien kä-
- sittely on moninkertaisesti hitaampaa (tosin pentium-optimoitu peli voi
- niitä käyttää, ainakin assemblerilla voidaan pentiumin matematiikkapro-
- sessoria käyttää täysipainoisesti ja peliä nopeuttaa). Niinpä meidän täy-
- tyisi pystyä esittämään kokonaisluvuilla desimaalilukuja. Onko tämä mahdol-
- listakaan?
- Kyllä se on, katsokaamme hieman toisella tavalla normaaleja lukujamme.
- Meidän luvuissamme on kokonaislukuosa ja desimaaliosa sekä välissä piste.
- Kokonaislukuosalla voidaan ilmaista 10^<numeroja> lukua, eli jos
- kokonaislukuosassa on 3 numeroa niin voimme ilmaista sillä 10^3=1000
- erilaista lukua, välillä 0-999. Pisteen toisella puolella on kaikki muuten
- samalla tavalla, mutta meidän täytyy ajatella käänteisesti. Voimme ilmaista
- desimaaliosalla desimaalin, joka on yksi 10^<numeroja>:sosa. Tämä näyttää
- sekavalta, mutta oletetaan että meillä on 2-numeroinen desimaaliosa, niin
- pienin desimaali on 1/10^2, eli yksi SADASOSA. Seuraava kaavio varmaan sel-
- ventää asiaa:
- 1234.123 = 1234 + 123/10^3 = 1234 + 123/1000 = 1234.123
- Nyt menemme vähän pidemmälle. Oletetaan, että meillä olisi luvussa pilkku
- AINA samalla kohdalla ja desimaalia esittäviä lukuja 3. Takaisin voisimme
- sen palauttaa vain jakamalla kokonaisluku tuhannella (kolme desimaalinumeroa,
- eli siis 10^3=1000):
- 1234123 = 1234123/1000 = 1234.123
- Kuten huomaat pilkku voidaan ajatella sinne nelosen ja ykkösen väliin.
- Nyt kysyt ehkä että mitä hyötyä tästä on. Siitä on seuraava hyöty: Meillä
- on kaksi lukua, 0.1 ja 5.4, jotka haluamme laskea yhteen. Muunnetaanpa ne
- oikeaan muotoon: 0.1*1000=100 ja 5.4*1000=5400. Haluamme laskea ne yhteen:
- 100+5400 = 5500. Nyt muuntakaamme takaisin:
- 5500/1000 = 5.5 = 5.5 (5.4 + 0.1 = 5.5).
- Eli meillä on sama tulos! Vähennyslasku toimii ihan yhtä hyvin. Voimme las-
- kea desimaalilukuja kokonaisluvuilla. Mutta tarvitsemme vielä kaksi laskua,
- kerto- ja jakolaskun. Koska lukumme ovat kummatkin 1000-kertaisia todelli-
- suuteen nähden niin ne kertomalla saamme 1000000-kertaisen tuloksen, joten
- lopuksi meidän täytyy jakaa tulos tuhannella. Eli:
- 5400*100 = 540000 => 540000/1000 = 540 => 540/1000 = 0.54
- (5.4 * 0.1 = 0.54)
- Ja tadaa! Meillä onkin oikea tulos. Vielä jakolasku, siinähän jaamme vain
- numerot toisillamme, mutta tässä häviää meiltä desimaaliosa, eli meidän pi-
- täisi kertoa tulos lopuksi tuhannella. Tarkemman tuloksen saamme kun
- kerromme ensin jaettavan tuhannella ja sitten vasta jaamme:
- (5400*1000) / 100 = 54000 => 54000/1000 = 54 (5.4 / 0.1 = 54).
- Nyt meidän täytyy sitten syventyä siihen miten toteutamme nopeasti edelliset
- asiat tietokoneen binäärijärjestelmällä. Se on erittäin helppoa. Teemme
- vaikka 32-bittisen luonnollisen (unsigned int), josta 16 alinta bittiä on
- varattu desimaaliosalle. Koska binäärijärjestelmä on 2-kantainen, niin
- meidän täytyy vain muuttaa pikku laskumme kahden potensseilla leikkimisiksi.
- Tällaisella luvulla voimme siis esittää 16-bittisen kokonaislukuosan,
- maksimissaan 2^16=65536 ja 16-bittisen desimaaliosan, joten pienin desimaali
- n 1/2^16 = 1/65536 = n. 0.000015228.
- Entiset laskumme toimivat ihan hyvin, muunnamme vain luvut kertomalla ne
- 65536:llä ja palautamme jakamalla 65536:llä. Nopeuttamisessa apuna ovat
- vielä bittisiirrot, joiden avulla voimme kertoa nopeasti 65536:lla
- siirtämällä bittejä 16 vasemmalle ja jakaa siirtämällä niitä oikealle.
- Tässä on pieni esimerkkiohjelma, joka demonstroi fixedin käyttöä:
- #include <stdio.h>
- int main() {
- unsigned int a, b, tulos;
- a=(unsigned int)(5.4 * 65536.0);
- b=(unsigned int)(0.1 * 65536.0);
- tulos=a+b;
- printf("A+B=%f\n", tulos/65536.0);
- tulos=a-b;
- printf("A-B=%f\n", tulos/65536.0);
- tulos=(a*b)/65536;
- printf("A*B=%f\n", tulos/65536.0);
- tulos=(a/b)*65536;
- printf("A/B=%f\n", tulos/65536.0);
- return 0;
- }
- Mieti nyt kaikkea ihan rauhassa. Jos luulet ymmärtäneesi edes jotain niin
- hyvä, jos et ymmärtänyt mitään niin lue uudelleen ja uudelleen ja kokeile
- paperilla. Jos et siltikään ymmärtänyt niin lue jostain toisesta dokumentis-
- ta! Fixed-pointissa on huomattava pari asiaa:
- 1) Luvut voivat mennä yli ja tulee ihmeellisiä tuloksia. Jakolaskuesimerkis-
- säni en voinut kertoa a:ta ensin 65536:lla, sillä muuten olisi luku men-
- nyt ympäri. Kannattaa aina varmistaa ettei luku voi mennä ympäri.
- 2) Käytä bittioperaatioita aina kuin mahdollista. 32-bittisestä
- 16.16-fixedistä (tarkoittaa, 16 bittiä kokonais- ja 16 bittiä desimaali-
- osalle) saat desimaaliosan halutessasi AND-funktiolla maskin 0xFFFF
- kanssa. Voit käyttää kaikkia nerokkaita optimointikikkoja jos vain kek-
- sit niitä. Myös pyörähdystä voi käyttää hyväksi (jotenkin).
- 3) Signed luvut toimivat samoin, mutta ylin bitti merkkaakin etumerkkiä,
- eli 16.16-luku int-tyyppinä onkin oikeasti 15.16.
- 4) Valitse itse pilkun paikka. Mitä enemmän bittejä desimaaleille sitä tar-
- kempia lukuja. Mitä enemmän bittejä kokonaisluvuille sitä suurempia ja
- epätarkempia lukuja.
- 5.3 Lookup-tablet ja muita optimointivinkkejä
- ---------------------------------------------
- Lookup-tableissa, eli lookupeissa ei ole oikeastaan muuta selittämistä, kuin
- että niissä toistuvia, vain yhtä (tai joskus kahtakin) muuttujaa käyttävis-
- sä monimutkaisissa laskutoimituksissa (tai muuten vain hidastavissa)
- lasketaan tulokset etukäteen taulukkoon käyttäen indeksinä sitä lukua joka
- oli muuttuvana laskutoimituksessa. Tähän käy esimerkkinä sinin laskeminen
- taulukkoon. Sin-funktio on hidas laskea ja siinä pitää aina suorittaa pitkä
- konversio asteista radiaaneiksi (3.14*2*aste/256, 256:n ollessa suurin
- kulma + 1, 360-asteisella ympyrällä luku olisi 360 ja suurin kulma 359) ja
- lopuksi vielä ottaa siitä sini. Nyt laskemmekin kaikki 256 arvoa taulukkoon
- (fixed-point-sellaiseen, muoto 1.14, 16-bittinen signed, muuntoluku 16384):
- for(c=0; c<256; c++)
- sin_table[c] = (short)(sin(3.141592654*2*c/256.0)*16384);
- Nyt jos haluamme kulman 15 sinin, niin osoitamme vain sin_table[15], emmekä
- (short)(sin(3.141592654*2* 15 /256.0)*16384).
- Sitten sekalaisia optimointivinkkejä:
- 1) Suuria määriä dataa käsittelevät loopit assemblerilla. Lisää tietoa
- inline-assemblerin käytöstä DJGPP:llä tiedostosta DJTUT*.ZIP,
- vaikka MBnetistä, tai tämän tutoriaalin Nasmia käsittelevästä
- luvusta.
- 2) Kaikki muuttumattomat vertailulausekkeet loopin ulkopuolelle:
- for(c=0; c<1000000; c++) if(a==b) puskuri[c]=0; onkin:
- if(a==b) for(c=0; c<1000000; c++) puskuri[c]=0;
- Vähennämme näin 1000000 vertailua.
- 3) Älä tuhlaa aikaasi optimoimalla suuria määriä logiikkaa, ellei siitä
- todella ole hyötyä. Esimerkkinä vaikka kaksoispuskurin tyhjennyksen
- tekeminen inlinenä memsetin sijaan säästää kyllä aikaa, mutta kun
- ajansäästö funktiokutsun jäämisessä pois on jotain 1/10000 siitä
- mitä aikaa memsetissä menee joka tapauksessa, on hyödyttömyys
- varsin ilmeistä.
- 4) Käytä fixediä floatin tilalla aina kuin mahdollista.
- 5) Laske kaikki toistuva konemainen laskenta taulukkoihin.
- 6) Käytä DJGPP:n käännösvalitsinta -O2, tai jopa -O3 (joka kyllä suurentaa
- ohjelmaasi reilusti).
- Yleensäkin kannattaa uhrata paljon aikaa grafiikkakirjastojen ja äänikirjas-
- tojen optimointiin ja pitää itse runko selkeänä C-kielisenä kutsujen joukko-
- na. Tämä ei paljoa hidasta ja selventää uskomattomasti koodia ja nopeuttaa
- kehitystä.
- 5.4 Väliaikatulokset ja fontteja
- --------------------------------
- Tässä vaiheessa osaat nyt kaikki tärkeimmät niksit mitä peliohjelmointiin
- tarvitaan. Tästä luvusta lähtien alan tietoisesti vähentämään, ellen
- jopa joissain kohdissa poistamaan esimerkkiohjelmia. Mitä tästä lähtien
- tarvitset on maalaisjärkeä ja kykyä osata soveltaa oppimiasi asioita.
- Eli tänään meillä on siis jotain, mitä kutsutaan nimellä fontit? Idea fon-
- tienginen teossa on tehdä tavallaan karsittu bittikarttaengine. Fontti-
- enginen voit tehdä esimerkiksi poistamalla sprite-koodistamme pyyhkimisen
- (halutessasi voit myös poistaa läpinäkyvyyden tai jättää pyyhkimisen jos
- tarvitset sitä, sinun pitää siinä tapauksessa vain tehdä erikoisjärjeste-
- lyjä) ja käyttää animaationa kuvasarjaa jossa on piirrettynä merkit a-z,
- A-Z, 0-9 ja sitten joitakin mahdollisesti tarvittavia välimerkkejä, kuten
- .!?,;:'" ja muut vastaavat. Sitten vain teet funktion, joka vaihtaa framek-
- si oikean kuvan ja piirtää sen, jonka jälkeen se korottaa x-arvoa merkin
- leveydellä (plus jonkin verran väliä seuraavan merkin ja viimeisen välille)
- ja ottaa käsittelyyn seuraavan merkkijonon merkin.
- Koodi voisi näyttää vaikka tältä:
- void printString(char *string, int x, int y) {
- int c;
- for(c=0; c<strlen(string); c++ {
- if(string[c]>'a' && string[c]<'z') {
- setframe(string[c]-'a'); /* a olisi frame 0 */
- drawchar(x+c*9, y); /* merkin leveys 8 + 1 pikseli erottamaan */
- } else if(string[c]>'A' && string[c]<'Z') {
- setframe(string[c]-'A' + 'z'-'a' + 1);
- /* eli suomeksi A-kirjaimella olisi paikka heti viimeisen pienen
- kirjaimen jälkeen, joka on 'z'-'a' */
- drawchar(x+c*9, y);
- } else if(string[c]>'0' && string[c]<'9') {
- setframe(string[c]-'0' + 'z'-'a' + 1 + 'Z'-'A' + 1);
- /* tämä taas tulee pienien JA isojen kirjaimien jälkeen */
- drawchar(x+c*9, y);
- } else if(c == '.') { /* jos c on erikoismerkki */
- setframe('9'-'0' + 1 + 'z'-'a' + 1 + 'Z'-'A' + 1);
- drawchar(x+c*9, y);
- /* ideana siis, että piste tulee kaikkien kirjainten ja
- numeroiden jälkeen */
- }
- ...
- }
- }
- Kuten ehkä huomasit tuli koodista aivan kammottavaa sekasotkua ja on ihme
- jos sait siitä jotain selvää. Lisäksi koodi ei ole erityisen nopeaakaan,
- saati sitten että se edes välttämättä toimii. Mutta miten voisimme nopeuttaa
- tätä? Vastaus on lookup-tablet. Sillä mehän tiedämme, että C:llä kirjain on
- vain numero välillä 0-255. Niinpä teemme taulukon jonka jokainen alkio
- osoittaa indeksin mukaisen ASCII-kirjaimen framenumeroon. Jos et ymmärtänyt
- niin tässä on esimerkki taulukon käytöstä:
- frame = asciitaulukko['a'];
- Asciitaulukon alkio 'a' (numerona 97) olisi 0, joten framenumeroksi tulisi
- näinollen tämä luku. Sitten vain framenvaihto: "setframe(frame)".
- Tietenkin tuo kannattaisi käyttää näin: "setframe(asciitaulukko['a'])"...
- Mutta miten sitten taulukko alustetaan? Tapoja on monia, jotkin ovat seka-
- vampia ja jotkin vähän selvempiä, mutta annan sinun itsesi päättää mikä on
- paras. Mahdollisuutena olisi ensin täyttää taulukko nollalla (joka olisi
- tyhjä frame) ja sitten loopata aakkoset a-z täyttäen taulukon kohdat 'a'-'z'
- oikeilla framearvoilla (1...26), sitten loopataan 'A'-'Z' täyttäen ne alkiol-
- la 27...52 jne. Myös lataaminen kannattaa automatisoida.
- Muista lisäksi huomioonottaa erikoismerkit enginessäsi. Tarpeellisia voivat
- olla välilyönti (32), rivinvaihto (\n), tabulaattori (\t) jne. Ja lisäksi
- saat aivan vapaasti päättää onko fontin väri mahdollista vaihtaa vai käytät-
- kö aina samanlaisia fontteja, joka mahdollistaa vähän hienommat, vaikka moni-
- väriset fontit.
- 5.5 Hiirulainen, jokanörtin oma lemmikki
- ----------------------------------------
- Tänään, tytöt ja pojat, setä puhuu hieman kotieläimistä. Ne ovat sellaisia
- pieniä valkoisia ötököitä, joilla on häntä ja jotka viipottavat matolla.
- Sen lisäksi niitä voi myös painella. Ei, nyt ei ole kyse mistään karvaisesta,
- vaan ihan aidosta tietokoneen lisälaitteesta, jota hiireksikin kutsutaan.
- Tällä karvattomalla ystävällämme on säädyttömän monia haaroja sukupuussaan.
- Löytyy Logitechia, Microsoftia, Targaa ja ties mitä vimputinta ja kaiken
- kukkuraksi rautatasolla käskyttäminenkin on suorastaan säädyttömän
- epästandardia. Onneksi hätiin rientää kymmenisen vuotta vanha apu nimel-
- tään _hiirikeskeytys_, kiinnostavemmin ilmaistuna keskeytys 33h. Tätä
- keskeytystä käyttäen saadaan kaikkien hiireen tungettujen vimpainten, kuten
- nappien ja pohjassa (yleensä) pyörivän pallukan tila. Nämä tiedot ovat helpon
- saatavuuden lisäksi myös naurettavan helppokäyttöisiä, kunhan vain tietää
- miten niitä käyttää.
- Jos et vielä tiedä miten keskeytyksiä käytetään tulee tässä tiivistettynä
- niiden käyttö DJGPP:llä. Keskeytykselle annetaan parametrit rekistereissä
- ja ne saadaan rekistereissä. Jos DJGPP oli yhtä huoleton kuin Borland
- Turbo-kääntäjineen olisi meilläkin rekisteri ax nimellä _AX jne. Mutta koska
- kaikki on tehty rakkaalla kääntäjällämme hipun vaikeammaksi teemme sen
- standardilla tavalla. Alhaalla näet tarvittavat askeleen keskeytyksen kut-
- sumiseksi ja rekisterien näpläykseksi. Esimerkki käyttää yhtä kymmenistä kes-
- keytyksen aiheuttavista funktiosta int86(...) kirjastosta dos.h:
- 1) Tarvitset rekisterit muuttujinaan sisältävän unionin, int86:n tapauksessa
- unioni on nimeltään REGS ja sen sisällä on pari structia joihin
- tutustut vaikka selaamalla ko. kirjastoa. En ala perehtymään syvemmin
- näihin x, d ja w-rakenteisiin. Tässä kuitenkin käytämme viimeistä, joka
- on 16-bittiset rekisterit.
- union REGS rekisterit;
- 2) Tunge kaikki parametrit uuteen muuttujaasi.
- rekisterit.w.ax=jotain;
- rekisterit.w.di=muuta;
- rekisterit.w.cs=kivaa;
- 3) Kutsu funktiota int86(vektori, inputti rekisterit, outputti rekisterit)
- int86( keskeytys, &rekisterit, &rekisterit );
- 4) Kaivele esiin muuttuneet rekisterisi ja tallenna ne muuttujiin.
- ihan=rekisterit.w.bx;
- helppo=rekisterit.w.ds;
- homma=rekisterit.w.cx;
- Tehdessäsi hiiriohjattua ohjelmaa sinun pitää tietysti hiiren koordinaattien
- ja nappien käsittelyn lisäksi piirtää kursori ruudulle, ellet sitten halua
- käyttää (amatöörimäisen näköistä) kursoria, jonka ajuri piirtelee ruudullesi.
- Grafiikkatilassa tämä onnistuu vaikka tekemällä hiirestä yksi spriteistä ja
- liikuttelemalla sitä. Antaa paljon paremman kuvan ohjelman tekijästäkin!
- Tekstitilassa vaihdat vaikka ko. kohdan väriä. Tähän ihmeelliseen tilaan
- tutustumme kohtapuolin, eli jatka lukemistasi jos haluat tehdä tekstitila-
- ohjelman, joka käyttää kursoria...
- Tässä nyt olisivat nämä kaikkein käytännöllisimmät ja alkuun auttavat funk-
- tiot. Lisää löydät vaikkapas Ralph Brownin interruptilistasta tai kenties
- jopa HelpPC:stä. RB:n lista on MBnetissä nimellä INTERxxy.ZIP, jossa xx on
- versionumero (kai 48 tarkoittaen 4.8:aa) ja y paketin numero, itse listassa
- A-E tjsp. ja muitakin kirjaimia on sisältäen muunmuassa selailuohjelman,
- konvertoinnin Windowsin help-muotoon jne.. Mutta, kuten lupasin:
- Funktio 0 - Hiiren alustus
- Parametrit: AX=0
- Palauttaa: AX=0 jos ajuria ei ole installoitu, FFFFh jos on installoitu.
- Funktio 1 - Näytä kursori (se kauhea siis)
- Parametrit: AX=1
- Palauttaa: -
- Funktio 2 - Piilota kursori (se kauhea siis)
- Parametrit: AX=2
- Palauttaa: -
- Funktio 3 - Anna koordinaatit ja nappien tila
- Parametrit: AX=3
- Palauttaa: CX=x-koordinaatti (0...639)
- DX=y-koordinaatti (0...199)
- BX=nappien tila (bitti 0 vasen nappi, bitti 1 oikea ja
- bitti 2 keskimmäinen nappi)
- Funktio 4 - Aseta kursorin koordinaatit
- Parametrit: AX=4, CX=x-koordinaatti, DX=y-koordinaatti
- Palauttaa: -
- Funktio 5 - Nappien painallukset
- Parametrit: AX=5,
- BX=mikä nappi (0 vasen, 1 oikea ja 2 keskimmäinen)
- Palauttaa: Muuten kuten funktio 3, mutta koordinaatit kertovat kursorin
- sijainnin viime painalluksella ja BX kertoo ko. napin painal-
- luksien määrän sitten viime kutsun.
- Funktio 6 - Nappien vapautukset
- Parametrit: AX=6,
- BX=mikä nappi (0 vasen, 1 oikea ja 2 keskimmäinen)
- Palauttaa: Muuten kuten funktio 5, mutta vapautuksen tiedot.
- Funktio 7 - Vaakarajoitukset
- Parametrit: AX=7,
- CX=pienin sallittu X-sijainti,
- DX=suurin sallittu X-sijainti
- Palauttaa: -
- Funktio 8 - Pystyrajoitukset
- Parametrit: AX=8,
- CX=pienin sallittu Y-sijainti,
- DX=suurin sallittu Y-sijainti
- Palauttaa: -
- Funktio B - Liikemäärä
- Parametrit: AX=B
- Palauttaa: CX=vaakamikkien määrä
- DX=pystymikkien määrä
- Funktio F - Mikkejä pikseliä kohden
- Parametrit: AX=F
- CX=vaakamikkien määrä
- DX=pystymikkien määrä
- Palauttaa: -
- Lisäksi on vielä ainakin funktio C, joka asettaa oman käsittelijän, mutta
- koska se ei luultavasti kiinnosta kovin monta (rm-osoitetta odottava käsit-
- telijä ei ehkä oikein toimi PM:ssä kunnolla jne...) jätän sen tässä väliin.
- Sitten vain tekemään kaiken maailman testiohjelmia. Esimerkkejä ei tule
- tässä lainkaan, sillä oletan jokaisen pystyvän edellisten ohjeiden perusteel-
- la kyhäämään itseään tyydyttävän ohjelman.
- Jos homma ei kuitenkaan ota luonnistuakseen tai tässä kappaleessa oli muita
- epäselvyyksiä niin otahan yhteyttä niin kaivelen lisää tietoa aiheesta.
- Erityiskiitos tämän kappaleen teon auttamisesta kuuluu nyt kyllä MB:n numerol-
- le 4/96 josta katsoin nopeasti tiivistelmän hiirifunktioista.
- Ja ensi kappaleessa onkin uudet kujeet, näyttäisi olevan tekstitilan hallinta
- seuraavana edessä...
- 5.6 Tekstitilan käsittely suoraan
- ---------------------------------
- Tästä kappaleesta tulee tulemaan äärimmäisen lyhyt. Ainoa meitä kiinnostava
- seikkahan on tekstimuistin osoite (tila 3, 80x25, myös muut voivat toimia)
- ja rakenne. Osoite on perusmuistin segmentti B800h, eli lineearinen osoite
- selektorin _dos_ds osoittamassa muistissa olisi C:llä 0xB8000. Rakenne
- on myös naurettavan yksinkertainen. Erona VGA:han (ks. kappale
- "Grafiikkaa - mitä se on?" jos et muista) on vain se, että yksi alkio
- koostuu kahdesta tavusta (joista ensimmäinen on merkin ASCII ja toinen
- merkin väri) ja ruudun leveys on 80 merkkiä. Jos ei mennyt päähän niin
- tutustu vielä kerran VGA:ta käsittelevään kappaleeseen ja tutkaile seuraavia
- makroja:
- #define putchar(x, y, c) _farpokeb(_dos_ds, 0xB8000+(y*80+x)*2, c);
- #define putcolor(x, y, c) _farpokeb(_dos_ds, 0xB8000+(y*80+x)*2+1, c);
- Vielä jos olit kiinnostunut hiiren kursorin tekemisestä tekstitilaan voisi
- seuraava funktio olla sinulle omiaan:
- void inline addcolor(int x, int y, char c) {
- int originalc=_farpeekb(_dos_ds, 0xB8000+(y*80+x)*2+1);
- putcolor(x, y, originalc+c);
- }
- Sitten vain "piirrät" kursorin lisäämällä väriarvoon - sanotaan vaikka 17
- ja pyyhit kursorin lisäämällä siihen saman arvon vastaluvun (-17), eli
- toisinsanoen vähennät siitä 17:
- #define CShow(x, y, c) addcolor(x, y, c)
- #define CHide(x, y, c) addcolor(x, y, -c)
- Makrojen käyttö sitten komennoilla "CShow(17)" ja "CHide(17)"...
- Lopuksi vielä sananen merkin värin muodosta. Se on XYYYZZZZ, jossa jokainen
- kirjain edustaa yhtä bittiä väritavussa. X ilmaisee vilkkuuko merkki (1).
- YYY ilmaisee taustan värin (0-7) ja ZZZZ ilmaisee tekstin värin (0-15).
- Tässä vielä pikkuruinen makro, joka voi osoittautua hyödylliseksi:
- #define BuildC(blink, fore, back) ( (blink<<7) + (back<<4) + (fore) )
- Sitten vain vaikka komento "putcolor(x, y, BuildC(0,15,1))", joka aiheuttaisi
- välkkymättömän valkoisen tekstin sinisellä pohjalla (31).
- Sellaista tällä kertaa. Nyt painun suihkuun ja katsomaan X-Filesia. Jatketaan
- taas vaikka huomenna!
- 6.1 Projektien hallinta - useat tiedostot
- -----------------------------------------
- Nyt seuraakin sitten jakso lukuja (tai yksi luku, katsotaan nyt),
- joissa käsitellään kaikkea tärkeää mitä pelejä ohjelmoidessa pitää
- osata sen hardwaren tuntemuksen lisäksi. Tarkoituksena on käydä läpi
- useiden c-tiedostojen käyttö, headerien teko, Rhiden projektit,
- makefileet, ulkoisen assyn ja assyn yleensäkin käyttö, engineiden
- teko, kirjastojen luonti. Kaikki suhteellisen kevyttä kamaa kun ne
- vain kerran opettelee, joten aloitamme.
- Tähän asti olen opettanut teille huonoja tapoja joita itselläni oli
- tapana käyttää vielä puolitoista vuotta sitten (ja vasta viime aikoina
- olen päässyt lopullesesti niistä eroon). Olen nimittäin laittanut
- koodia noihin .h-tiedoistoihin ja tehnyt niistä kirjastoja, joiden
- rutiineja on sitten helppo käyttää. Laajempien projektien ja miksei
- hieman suppeampienkin kanssa alkaa kuitenkin ennenpitkää esiintyä
- suorastaan ärsyttävän hidasta kääntämistä. Ajattele seuraavaa
- tapausta:
- Peliprojektissa on ääniengine sound.h (yksinkertainen, vain vähän alle
- 3000 riviä), sprite-engine sprite.hh (minimaalinen toiminta, hieman
- inline-assyä, 800 riviä), sekalaisia hardware-rutiineja
- (kellokeskeytys, näppishandleri jne. 1000 riviä) sekä itse pelin
- koodia 2000 riviä. Näin joka kerta käännämme vähän alle 7000 riviä
- C-koodia. Mutta miksi kääntää kaikki joka kerta kun vain yksi muuttuu
- yleensä kerrallaan? Muuttakaamme hieman lähestymistapaa löytääksemme
- parempi keino.
- Keinoa kutsutaan projekteiksi, usean C-tiedoston käytöksi ja ties
- miksi. Ideana on, että jokainen looginen kokonaisuus on jaettu omaan
- .c-tiedostoonsa ja .h-tiedostoonsa. Tällaisia voisivat olla
- näppishandleri, timerhandleri, sprite-rutiinit, modien lataus,
- äänienginen ohjelmointirajapinta, sb-osa koodista, gus-osa koodista
- jne.. Jokaiselle tiedostolle olisi sitten oma .h-tiedostonsa, jossa
- määritellään kaikki c-tiedoston funktiot ja globaalit muuttujat (jos
- niitä tarvitaan). Sitten toiset c-tiedostot jotka tarvitsevat tuon
- tiedoston funktiota tai muuttujia ottaisivat vain includella
- h-tiedoston mukaan ja kääntäjän linkkeri huolehtisi siitä, että
- ohjelmakutsut menevät oikeisiin osoitteisiinsa.
- Katsotaanpas pientä esimerkki h-tiedostoa ja c-tiedostoa. En väitä
- tämän olevan ainoa oikea tapa, tämä on vain yksi tapa hoitaa homma:
- ESIM.H:
- #ifndef __ESIM_H
- #define __ESIM_H
- #include <stdio.h>
- #define ESIMTEKSTI "Moikka, olen esimerkki!"
- void Esimteksti();
- extern int Kutsukertoja;
- #endif
- ESIM.C:
- #include "esim.h"
- int Kutsukertoja=0;
- int Oma=666;
- void Esimteksti() {
- puts(ESIMTEKSTI);
- Kutsukertoja++;
- }
- Lähdetäänpäs askeltamaan ESIM.H-tiedostoamme lävitse. Ensimmäisenä
- rivi #ifndef __ESIM_H, joka ilmoittaa C-koodin esikäsittelijälle, että
- jos __ESIM_H ei ole määritelty (IF Not DEFined, IFNDEF) niin osio
- #ifndef:in ja #endif:in välissä tulee ottaa mukaan. Sen jälkeen
- määritellään tuo kyseinen muuttuja, jotta H-tiedostoa ei pureta
- kahteen kertaan (voi sattua kaikkea hassua jos vaikka h-tiedostot
- kutsuvat toisiaan). Sitten tulee tämän C-tiedoston tarvitsemien
- funktioiden kirjastot ja #definet (kirjastot voitaisiin sijoittaa myös
- C-tiedostoon, mutta joskus tästä tulee ongelmia, jos käytetään makroja
- tai muuta vastaavaa).
- Sitten tulevat muuttujat ja funktiot. Muuttujien eteen TULEE laittaa
- extern-määre, joka kertoo että ne on oikeasti määritelty jossain
- muualla, jottei kääntäjä varaa muistia näille joka H-tiedoston
- includettamisen kohdalla, jolloin linkatessa useissa C-tiedostoissa on
- varattu muistia samannimiselle globaalille muuttujalle -> ongelmia.
- Funktioiden edessä extern ei ole pakollinen ja sen voikin jättää pois
- ja lisätä extern-määreen jos ko. funktio on ulkoisessa
- assembler-tiedostossa.
- Funktion parametrien nimet voi halutessa jättää määrittelyistä pois,
- mutta se ei ole suositeltavaa. Muista myös, että globaalit muuttujat
- esitellään ja alustetaan VAIN ja AINOASTAAN C-tiedostossa, ei
- H-tiedossa!
- C-tiedosto sisältää vastaavat H-tiedostossa "luvatut" funktiot ja
- muuttujat. Jos haluat tehdä globaaleja muuttujia jotka eivät näy
- muihin C-tiedostoihin, niin jätät sen esittelyn H-tiedostosta pois,
- jolloin headerin sisällyttävät muut C-tiedostot eivät tiedä mitään
- ko. muuttujan olemassaolosta eikä vahingossa tule virheitä. Tällainen
- on esimerkki C-tiedoston muuttuja Oma.
- Useita C-tiedostoja käyttäessäsi teet siis jokaisesta loogisesta
- kokonaisuudesta oman "paketin", joka sisältää C-tiedoston, joka on
- toimiva kokonaisuutensa ja H-tiedoston, joka tarjoaa muille
- C-tiedostoille mahdollisuuden käyttää tämän paketin rutiineja.
- Muista, että käyttäessäsi includea tuollaisen tiedoston kohdalla
- käytetään heittomerkkejä normaalin <>-parin sijasta, jottei kääntäjä
- lähde hakemaan ESIM.H:ta omasta include-hakemistostaan, vaan jotta se
- hakisi tiedoston senhetkisestä työskentelyhakemistosta.
- Mieti nyt nämä asiat selviksi, jotta ymmärrät miten tehdään useita
- tiedostoja ja käytetään ilman ongelmia, niin voit sen jälkeen jatkaa
- seuraavaan lukuun, jossa kerrotaan miten niistä muodostetaan ajettavia
- ohjelmia, kirjastoja ja objektitiedostoja.
- 6.2 Useiden tiedostojen projektit - kääntäminen ja hallinta
- -----------------------------------------------------------
- No niin, osaat nyt tehdä C-tiedostoja ja H-tiedostoja, mutta sillä ei
- varmaankaan pitkälle pötkitä. Lähdemme nyt tutkimaan hieman
- kääntäjämme, GCC:n sielunelämää ja tutustumme muutamaan elintärkeään
- tietoon joita ilman ei voi edes elää. Nimittäin janoamme tietoa
- formaateista.
- Tiedostot joiden kanssa pyörimme DJGPP:n kanssa voidaan jakaa helposti
- pelkistäen neljään (4) kategoriaan. Tässä ne ovat:
- 1. Lähdekooditiedostot (c, cc, s, asm). Kääntäjä muuttaa koodin
- konekieleksi ja tekee muut tarvittavat tehtävät tuottaen
- objektitiedoston.
- 2. Objektitiedosto (O). Sisältää koodin ja symboleja (eli funktioiden
- ja muuttujien nimiä) ja kaikkea muuta kivaa infoa jotka liittyvät
- olennaisesti rutiinien käskyihin ja dataan. Linkkeri linkkaa kaikki
- objektitiedostot yhteen ja lisää tarvittavaa käynnistyskoodia sun
- muuta luodakseen ajettavan tiedoston. Nämä ovat eräänlaisia
- rakennuspalikoita, joissa kaikki on jo binäärimuodossa.
- 3. Archive (A). Tätä voidaan halutessa käyttää useiden objektien
- säilömiseen, eli paketoidaan monta objektitiedostoa yhteen kasaan
- jotka voidaan liittää sitten yhtenä pakettina
- kääntäjälle. Objekteista siis kootaan nippu jota voidaan käsitellä
- yhtenä kokonaisuutena.
- 4. Ajettava tiedosto. Sisältää objektitiedostoista tehdyn EXE:n, jossa
- on lisäksi tarvittava koodi ohjelman käynnistämiseen.
- GCC:n toimintaperiaate EXE:n käännössä on seuraava: Lähdetään
- kääntämällä lähdekooditiedostot objektitiedostoiksi. Tässä vaiheessa
- siis laajennetaan makrot, includet ja esikäsittelijän komennot (kaikki
- #ifndef-rakenteet sun muut). Sitten käännetään koodi konekielelle ja
- tehdään objektitiedostot.
- Seuraavaksi kutsutaan linkkeri joka liittää objektitiedostot yhteen ja
- lisää tarvittavat kirjastot (LIBC.A tulee EXE:en aina mukaan ja
- lisäksi muut -l<nimi> parametreillä annetut kirjastot) sekä
- aloituskoodin, joka kutsuu main-funktiota, jonka oletetaan löytyvän
- jostain O-tiedostosta.
- Itseasiassa tuo ei mene aivan noin yksinkertaisesti, mutta tärkeintä
- on ymmärtää, että lähdekoodista tehdään rakennuspalikoita,
- objektitiedostoja joista voidaan myöhemmin koota ajettavia tiedostoja.
- Jos meillä siis olisi C-tiedostot main.c ja apu.c (mahdollisesti
- vastaavine H-tiedostoineen), joista main.c sisältäisi main-funktion ja
- pääkoodin ja apu.c kaikkia tarpeellisia rutiineja, niin voisimme
- kääntää ne objektitiedostoiksi ja aina kun jompaakumpaa muunnetaan,
- niin kääntäisimme tämän lähdekooditiedoston uudelleen. EXE
- muodostettaisiin erikseen toisella komennolla jolloin muutos toisessa
- tiedostossa vähentäisi käännettävän koodin määrää (tosin linkkaustyö
- pysyisi ennallaan).
- Miten sitten näitä erilaisia tiedostoja tehdään? Hyvä kysymys. Alla
- näette kaikkein komentoja objektitiedostojen, EXE:jen ja archivejen
- luontiin, lähdekoodit osaatte varmaan jo. =)
- Objektitiedosto GCC:llä:
- gcc -c koodi.c -o objekti.o (halutessa lähdetiedostoja voi olla useampia)
- Archive-tiedosto objektitiedostoista:
- ar rs archive.a objekti1.o ... (kaikki halutut objektit vain perään)
- Ajettava tiedosto archive-, objekti- ja lähdekooditiedostoista (GCC
- osaa käsitellä ne päätteiden mukaan):
- gcc <tiedostot> -o tulos.exe <parametrit>
- Lisää infoa sitä haluaville löytyy englanninkielisenä komennolla
- INFO. Sitä löytyy aika paljon enkä todellakaan halua tästä
- tutoriaalista mitään DJGPP:n komentoriviparametrien selitystä. =)
- Eli kerrataan vielä vaiheet joita käytätte "oikeaoppisen" projektin
- tekoon:
- 1. Luo C- ja H-tiedostot ja muu tarvittava lähdekoodi
- 2. Käännä ne O-tiedostoiksi (tyyliin gcc -c koodi.c -o objekti.o)
- 3. Jos haluat tehdä kirjastoja, niin tee objektitiedostoista ar:llä
- niitä. Esimerkiksi grafiikkaenginen objektitiedostot voisi liittää
- yhteen ja nimetä libgraf.a:ksi ja siirtää DJGPP:n LIB-hakemistoon.
- Myöhemmin nuo enginen objektit olisi helppo lisätä EXE:een pelkällä
- -lgraf -parametrilla.
- 4. Käännä ajettava ohjelma objektitiedostoista ja archive-tiedostoista
- (gcc <tiedostot> -o tulos.exe <parametrit>). Archive-tiedoston
- nimen voi antaa joko tiedostojen mukana tai parametrinä -l<nimi>
- JOS archive on DJGPP:n LIB-hakemmistossa nimellä lib<nimi>.a.
- Grafiikkaenginekin voi olla projekti, jolloin jätätte EXE:ksi
- kääntämisen kokonaan pois, ja teette vain archive-tiedoston. Tai jos
- tarvit vain yhden .o -tiedoston, niin mikäs siinä, valinta on vapaa.
- Nyt sinun pitäisi osata tehdä objektitiedostoja lähdekoodista,
- kirjastotiedostoja objekteista ja ajettava ohjelma objekteista (ja
- mahdollisesti myös kirjastoista). Kun hallitset nämä asiat jatkamme
- jälleen taivaltamme.
- 6.3 Hieman automaatiota - tapaus Rhide
- --------------------------------------
- No tällä hetkellä me osaamme kaikki tarvittavat taidot komentoriviltä,
- mutta uusien tiedostojen nimien muistaminen ei aina ole kivaa ja
- komentorivillä vääntäminen sopii vain perusteiden harjoitteluun. Rhide
- on tapa päästä koko roskasta helpolla ilman perusteita edes
- objektitiedostoista, mutta koska teillä tulee olemaan niin paljon
- helpompaa kun ne osaatte niin olen katsonut tarpeelliseksi ne myös
- neuvoa. (sillä Rhidenkin kanssa kunnon projekteilla tarvitaan tuota
- osaamista).
- Ainahan pääsee helpolla, mutta valitettava tosiasia on, että se joka
- hyppäsi edelliset kappaleet ylitse onkin sormi suussa kun tulee
- ongelma eteen. Mikään ei korvaa tietoa ja kokemusta, ei edes hyvä
- ohjelmointiväline.
- Eli tämän kappaleen tarjoama informaatio käsittelee Rhideä ja sen
- projekteja projektien hallinnassa. Jos teitä ei Rhide kiinnosta niin
- voitte hypätä yli, lupaan että seuraava kappale kiinnostaa teitä,
- sillä makefilejen käyttö on vaihtoehtoinen (ja gurumpi, elegantimpi ja
- yleisempikin) tapa automatisoida projektien kääntäminen. Mutta te
- joita kiinnostaa yksi tämän hetken parhaimmista DOS-ympäristön
- IDE-ohjelmista pysykää kappaleessa, tosin asia voi olla joillekin jo
- vanhaa leipää.
- Eli Rhiden sisältää makefileiden kaltaisen järjestelmän projektien
- hallintaan, mutta toisin kuin make se sisältää tekoälyä, joka osaa
- projektille valitusta kohteesta päätellä millainen tulos halutaan ja
- projektin tiedostojen päätteistä minkätyyppinen tiedosto on kyseessä
- ja miten se pitää kääntää. Koska Rhide on aika yksinkertainen
- järjestelmä käsittelen vain lyhyesti sen perusasiat, eli projektien
- teon, availun, käsittelyn, Rhiden kustomoinnin ja kohteiden
- määräämisen.
- Eli aloittakaamme tekemällä oletusprojekti Rhidelle. Ensimmäinen
- tehtäväsi lienee installoida Rhide, joka yleensä koostuu purkamisesta
- DJGPP-hakemistoon ja ohjelman käynnistämisestä kokeeksi. Dokumenttien
- lukeminenkaan ei ole pahasta, mutta kyllä ilmankin voi pärjätä, tosin
- vaikeuksien sattuessa ne ovat usein korvaamattomia. Rhiden jotkin
- versiot ovat olleet enemmän tai vähemmän bugisia, mutta ainakin
- versiot 1.1 (bugikorjattuna!), 1.2 ja 1.3 ovat toimineet minulla hyvin,
- joten joko Altavistaan hakusanalla Rhide, MBnettiin tai MB:n
- H&H-rompulle.
- Sitten kun Rhide toimii niin menette DJGPP:n BIN-hakemistoon ja
- kirjoitatte "rhide rhide". Tämä tarkoitus on luoda/muuttaa
- BIN-hakemistossa olevaa rhide-nimistä projektia, jonka asetukset
- ladataan AINA kun rhide käynnistetään ilman projektia ja jotka
- toimivat uusien projektien oletusasetuksina. Muuttele rhide-projektia
- niin paljon kuin haluat/uskallat/viitsit ja lopeta sen jälkeen
- rhide. Voit kokeilla vielä asetusten toimivuutta menemällä jonnekin
- hakemistoon missä on jokin muu määrä kuin yksi projekteja (jos niitä
- on vain yksi niin se ladataan automaattisesti) ja käynnistämällä
- Rhiden.
- Nyt pitäisi kaiken olla valmista uuden projektin teolle. Ota
- Project-valikosta Open project ja kirjoita avautuvan ikkunan
- Name-sarakkeeseen haluamasi projektin nimi. Ruudun alalaitaan avautuu
- ikkuna joka kertoo projektin tiedostot. Aktivoimalla tämän ikkunan ja
- painamalla insert-nappia (tai Project-valikosta Add item) saat
- lisättyä uusia tiedostoja. Kun olet valmis paina Cancel-nappia.
- Tällä tavalla lisäät haluamasi tiedostot (lähdekooditiedostot, tosin
- jos ehdottomasti haluat voit laittaa jonkin valmiiksi käännetynkin O-
- tai A-tiedoston mukaan) projektiin.
- Mukaan lisättäviä kirjastoja voit määrittää Options-valikon
- Libraries-kohdasta. Muista, että tämä hakee kirjastoja VAIN DJGPP:n
- LIB-hakemistosta, ja että kirjaston nimeen lisätään aina kääntäjän
- toimesta eteen LIB ja loppuun .A, eli älä kirjoita koko kirjaston
- nimeä tyyliin LIBJOKIN.A, vaan JOKIN. Sellainen erikoisuus kyllä
- kääntäjästä löytyy, että ylipitkät (yli 5 merkkiä) kirjaston nimet
- katkaistaan, joten IOSTREAM antaa tiedoston LIBIOSTR.A, eikä
- virheellistä LIBIOSTREAM.A:ta (joka olisi siis liian pitkä).
- Kun olet tyytyväinen kaikkeen muuhun niin ota vielä Project-valikosta
- main targetname ja määritä kohteen nimi. Jos olet tekemässä
- ääniengineä, niin sinulla on äänienginen C-tiedostot projektissasi ja
- kohteena (esim.) LIBSND.A. Jos taas teet C++ EXE:ä, niin sinulla on
- C-tiedostot joita käytetään, kohteena (esim.) PLUSPLUS.EXE ja
- mahdollisesti kirjastossa IOSTR ja jotain muuta. .A-päätteestä Rhide
- osaa automaattisesti kääntää archive-muotoisen tiedoston ja
- .EXE-päätteestä ajettavan. Muutkin voivat toimia (O ainakin), mutten
- ole kokeillut koskaan, sillä siihen ei yleensä ole tarvetta.
- Projektin kääntäminen onnistuu napilla F9, jolloin Rhide osaa
- automaattisesti katsoa tiedoston päiväyksistä mitkä tiedostot ovat
- muuttuneita (lähteen päivämäärä uudempi kuin kohteen) ja kääntää näin
- vain tarpeellisen. Aikaa säästyy ja hermoja samoin. Kääntämisen
- jälkeen hakemistostasi löytyy luultavasti kasa objektitiedostoja,
- joita voidaan käyttää myöhemmin linkkauksessa (jos vastaava
- lähdekooditiedosto ei ole muuttunut).
- Sellaista tällä kertaa. Aika perusasiaa ja itsekin pääteltävissä,
- mutta joskus vain käy siten ettei jotain perusasiaa itse hoksaa, tai
- ainakin säästää aikaa kun ei tarvitse kaikkea kokeilla. Nyt hallussa
- pitäisi olla projektien teko Rhidellä ja niiden toimimaan saaminen, ei
- sen kummempaa tällä kertaa. Voit jatkaa halutessasi seuraavaan jos
- tuntuu että osaat tämänkin kappaleen materiaalin.
- 6.4 Todellista guruutta - salaperäinen make
- -------------------------------------------
- Make on kuin suoraan Unix-maailmasta tullut. Jos pelkkä vilkaisu sen
- info-sivuille (INFO MAKE) saa aloittelijan vapisemaan horkassa. Mutta
- ei hätää, minä kävin siellä ja selvisin elossa - tosin en ole enää
- ollut sama itseni sen jälkeen. Olen nimittäin huomattavasti gurumpi
- jälleen sillä voin käännellä projektini halutessani hienosti
- komentoriviltä automatisoituna. Ja se onnistuu maken
- makefileillä. Tässä luvussa kerron miten niitä tehdään, tosin en
- mitään monimutkaisempaa valota kun mitään ihmekonsteja harvemmin
- normaalissa perustyöskentelyssä tarvitsee.
- Eli ensimmäisenä tehtävänä on jälleen kaivaa make jostain, paikat ja
- keinot ovat samat kuin Rhiden kohdalla, mutta toisin kuin Rhide maken
- pitäisi toimia ilman manuaaliin vilkaisua (koska se on huomattavasti
- yksinkertaisempi systeemi). Ideana on tehdä projektille ns. makefile,
- jonka make osaa tulkita ja tehdä sen mukaan tiedostossa käsketyt
- asiat.
- Mutta tehdäksemme oikeanlaisia makefilejä meidän täytyy ensin hieman
- ymmärtää filosofiaa maken takana.
- Normaali makefile koostuu yleensä alussa olevasta kasasta
- muuttujamäärittelyjä, joita myöhemmin käytetään kääntämisessä. Sen
- jälkeen on kasa ohjeita, jotka koostuvat muutamasta
- komponentista. Tässä on ohjeen muoto ja esimerkki yhdestä:
- kohde: riippuvuudet
- komento kohteen tekoon
- esim.
- ohjelma.exe: ohjelma.o
- gcc ohjelma.o -o ohjelma.exe -s -Wall -v -O2
- Eli ensimmäisenä on kohde joka kertoo makelle, että tässä on ohje
- miten teet tämän. Sitten on riippuvuudet, joka kertoo, että näiden
- pitää olla kunnossa ennenkuin tätä ohjetta aletaan
- toteuttamaan. Seuraavalla rivillä on yksi TAB:in painallus ja komento
- jolla kohde tehdään (komentoja voi olla useampiakin, jokainen omalla
- rivillään alkaen TAB:illa). Huomaa, että tarvitsemme EHDOTTOMASTI
- oikean TAB:in, emme mitääs MSDOS EDIT:in lelutabbeja, jotka eivät
- itseasiassa ole kuin määrätty määrä välilyöntejä. Eli pitää olla
- jonkinlainen editori, joka osaa käyttää aitoja TAB-merkkejä.
- En taida alkaa miettimään syvällisemmin maken toimintaa, mutta ideana
- on, että esittelet ensin pääkohteen ja sen riippuvuudet ja sen jälkeen
- esittelet nämä uudet riippuvuudet ja niiden riippuvuudet jatkaen
- pohjalle asti kunnes lopulta sinulla on kohteena objektitiedosto ja
- lähteenä lähdekooditiedosto ja alla komento tämän kääntämiseksi,
- jolloin make katsoo päivämäärän mukaan tarvitseeko tämä kohde
- päivittämistä. Jos lähde on uudempi kuin kohde niin käsky suoritetaan
- mutta jos kohde on uudempi niin se on täydytty kääntää lähteen
- muuttamisen jälkeen eikä kääntöä tarvita. Tällä tavalla vain
- muuttuneiden tiedostojen aiheuttamat käännöstarpeet hoidetaan eikä
- ylimääräistä työtä tehdä.
- Yleensä makefilessä on ensin kohde all, jossa riippuvuuksina on kaikki
- mitä makefilen tulee saada tuloksena valmiiksi (EXE:t, kirjastot),
- sitten on näiden tuloksien ohjeet riippuvuuksina objekti- ja
- archive-tiedostot, sitten archive-tiedostot riippuvuuksina
- objektitiedostot ja lopuksi objektitiedostot riippuvuuksina
- lähdekooditiedostot. Tässä on esimerkki joka varmaan valaisee aika
- sekavaa selitystäni. =) Huomaa myös makrot, jotka määritellään alussa
- ja joita muuttamalla on helppo vaihtaa käännöksessä tarvittavia
- parametrejä ja kääntäjien nimiä:
- CC=gcc
- CFLAGS=-s -Wall
- AR=ar
- ARFLAGS=rs
- all: esim.exe libx.a
- esim.exe: esim.o libx.a
- $(CC) $(CFLAGS) esim.o libx.a -o esim.exe
- libx.a: x1.o x2.o
- $(AR) $(ARFLAGS) libx.a x1.o x2.o
- esim.o: esim.c
- $(CC) $(CFLAGS) -c esim.c -o esim.o
- x1.o: x1.c
- $(CC) $(CFLAGS) -c x1.c -o x1.o
- x2.o: x2.c
- $(CC) $(CFLAGS) -c x2.c -o x2.o
- Kun tämän tiedoston tallentaa nimelle makefile tarvitsee sinun vain
- antaa komento make niin ohjelma osaa automaattisesti kääntää kaikki
- makefilessä määritellyt tiedostot. Käyttääksesi muita makefilen nimiä
- pitää maken komentoriville antaa parametri -f<makefile>.
- Esimerkki oli hyvin yksinkertaistettu ja vältin käyttämästä paria
- hauskaa kikkaa jotka tekevät makefilestä paljon lyhyemmän (ja
- sotkuisemman näköisen). Jos kuitenkin toiminta on epävarmaa, niin
- selostetaan se tässä vielä kertaalleen:
- 1. Make aloittaa lausekkeesta all (komentorivillä voit halutessasi
- määrätä mikä ohje tulee tehdä, esim make libx.a ei koskisi esim.*
- -tiedostoihin) ja etenee tekemään esim.exe:ä.
- 2. Esim.exe:n teko tarvitsee ensin esim.o:n, siirrytään siihen.
- 3. Esim.o tarvitsee esim.c:n, mutta sille ei löydy ohjetta, joten
- suoritetaan ensimmäinen käännös. Makrot CC ja CFLAGS puretaan
- komentoriville ja se suoritetaan ja kaiutetaan näytölle. Jatketaan
- esim.exe:n riippuvuuksien tutkimista.
- 4. Esim.exe:n teko tyssää kun siihenkin pitää tehdä libx.a, joten
- siirrytään tekemään sitä.
- 3. Libx.a:han pitää olla x1.o ja x2.o, joten siirrytään niihin.
- 4. Riippuvuudelle x1.c ei ole ohjetta, joten suoritetaan x1.o:n
- komento (näissä kohtaa olisi päivämäärätarkistus, mutta koska
- noita objektitiedostoja ei vielä ole olemassa niin...) ja palataan
- takaisin.
- 5. x2.o tehdään samaan tapaan kuin edellinen ja palataan libx.a:n
- pariin
- 6. Riippuvuudet kunnossa, tehdään kirjasto libx.a, palataan esim.exe:n
- kimppuun.
- 7. Esim.exe:n riippuvuudetkin ovat hanskassa, joten tehdään se ja
- palataan kohtaan all.
- 8. Libx:kin on tehty juuri, joten kaikki on valmista, poistutaan.
- No niin, kyllä toiminta varmaankin selvisi, ja jos ei niin paljon
- pidemmät ja selvemmät tekstit löytää englanniksi komennolla info make
- (no selvemmistä en itseasiassa tiedä :).
- Mutta make ei vielä ole ohitse, en uskalla päästää teitä kappaleesta
- ennenkuin osaatte tehdä ohjeita jotka tekevät vaikka 30
- objektitiedostoa kerralla, ne kun ovat kovin mukavia systeemejä
- verrattuna siihen että joutuisit kirjoittamaan jokaista varten oman
- ohjeen.
- Ideana tässä on eräänlainen nimentäydennys. Make osaa poistaa päätteen
- nimestä ja korvata sen toisella, jota ominaisuutta käytetään juuri
- tähän useiden samankaltaisten tiedostojen tekoon kerralla. Jos siis
- sinulla on 10 objektitiedostoa ja jokainen käännetään
- vastaavannimisestä lähdekooditiedostosta (o1.o ja o1.c, o2.o ja o2.c
- jne.), niin niiden kääntö onnistuu seuraavalla tyylillä (aika maken
- infoista pöllittyä ja suoraan käännettyä tavaraa mutta who cares?-):
- KOHTEET: KOHDE-PATTERN: RIIPPUVUUS-PATTERN ...
- OBJECTS=object0.o object1.o object2.o object3.o object4.o object5.o
- object6.o object7.o object8.o object9.o
- $(OBJECTS): %.o: %.c
- $(CC) $(CFLAGS) -c $< -o $@
- Eli ensimmäisenä tulee lista (OBJECTS) tehtävistä kohteista, sitten
- tulee %-merkki, joka esiintyy kohde-patternissa vain kerran, ja
- maken infosivut käyttävät siitä nimeä "stem". Tämä vastaa mitä tahansa
- kohtaa yhden kohteen nimestä, kaikki muut kohteen nimessä (.o tässä
- tapauksessa) täytyy vastata täysin.
- Jos siis kohteena olisi foo.o ja kohde-pattern olisi %.o, niin "stem"
- (anteeksi minulla ei ole sanakirjaa käsillä ;) saisi arvon foo. Jos
- riippuvuus-pattern olisi %.c niin riippuvuus tälle tiedostolle olisi
- foo.c. Ei mitään sen vaikeampaa, % on kuin DOS-maailman * ja
- ensimmäisenä tulee lista tiedostoista (kuten hakemistolistaus), sitten
- stemillä varustettu patterni ja lopuksi riippuvuudet jotka
- täydennetään sillä mitä stem vastaa.
- Lisäksi täytyy kiinnittää huomio merkkisarjoihin $< ja $@, joista
- ensimmäinen korvataan riippuvuudella (tai riippuviiksilla jos niitä on
- useampia) ja toinen kohteen nimellä. Myös muita vastaanvankaltaisia
- löytyy, mutta ne eivät ole läheskään niin hyödyllisiä kuin nämä kaksi.
- Näillä eväillä ainakin pitäisi onnistua makefileiden teko aika
- pitkälle. Hyviä esimerkkejä löytyy lukemattomista DJGPP-paketeista,
- joissa kääntäminen hoidetaan makefileillä. Makefilet ovat muutenkin
- yleisin tapa levittää lähdekoodin kanssa softaa, harvemmin olen nähnyt
- kirjaston käännöstä automatisoitavan Rhiden projekteilla. :)
- 6.5 Ammattimaista meininkiä - enginen teko
- ------------------------------------------
- Tämä luku kertoo hieman niistä vähäisistä kokemuksista mitä minulla on
- ollut projektien kanssa, tai oikeammin kertoo mitä kannattaisi ottaa
- huomioon enginen teossa, jotta se toimisi myös huomenna ja jotta siitä
- jälkeenpäin saisi jotain selvääkin.
- Näppärä tapa pääohjelman yksinkertaistamiseksi on tehdä tietyn
- tehtävän suorittavista tiedostoista yksi paketti, kirjasto jonka
- headerin koodiin sisällyttämällä voi kyseisen tehtävän hoitaa
- kirjaston tarjoamilla rutiineilla.
- Sen lisäksi että tapa yksinkertaistaa koodia se myös parantaa sen
- ylläpidettävyyttä huomattavasti ja myöskin muunneltavuus on aivan eri
- luokkaa kuin "kaikki-yhdessä-kasassa" -ohjelmilla. Lisäksi kun engine
- on kerran valmis voi sitä käyttää uudelleen ja uudelleen - yleensä
- pienillä muutoksilla tai parhaimmillaan muuttamattomanakin.
- Mutta tällaisenkin teossa kannattaa huomioida joitakin asioita, jottei
- jälkeenpäin paljastuisi että olet tehnyt turhaa työtä koko
- ajan. Nimittäin ensin on tarkoin otettava selvää mitä engineltä
- vaaditaan ennenkuin sellaista alkaa tekemään. Hyvä tapa on miettiä
- millaista peliä on tekemässä ja millaisia ominaisuuksia engineltä
- vaaditaan. Matopelin teossa ei välttämättä tarvita kovin kummoisia
- järjestelmiä, sillä ne eivät useastikaan vaadi kovinkaan monimutkaista
- toimintaa hyvän jäljen aikaansaamiseksi. Toisin on vaikka sivultapäin
- kuvatussa ammuskelupelissä, jossa spritejen piirron pitää olla
- äärimmäisen nopeaa ja turhaa piirtelyä tulee välttää. Skrollaus vaatii
- myös tällaisissa peleissä tehoja ja muuttujia spriteihin tulee
- huomattavasti enemmän kuin matopelissä.
- Mikään ei voita kunnon suunnittelua kun koodausta sitten aletaan
- tekemään. Hyvällä onnella koko enginen teko on suoraviivaista koodin
- kirjoittamista jos tärkeimpiä algoritmejä on jo hahmoteltu paperilla
- ja mielessä on kunkin funktion toiminta ja tarvittavat muuttujat
- kuhunkin tehtävään.
- Kun tarpeet ovat vihdoin paperilla ja koodin kirjoitus edessä voi olla
- hyvä vielä etukäteen nimetä enginen lohkot ja nimetä ne. Näppärä tapa
- jolla pääsee suoraan toimeen on käynnistää vaikka Rhide ja lähteä
- lisäilemään uuteen projektiin tiedostojen nimiä. Tiedostoja ei
- tarvitse edes olla olemassa vaan riittää että hahmotat mitä
- järjestelmän pitää tehdä ja minkälaisiin osiin se pitäisi
- jakaa. Kaikkein kevyimmät enginet eivät edes paljoa tiedostoja tarvi,
- näppishandleri ja timerhandleri, hiirirutiinit ja yksinkertaisemmat
- grafiikkaenginet menevät ainakin tähän kastiin. Äänienginet,
- playerit ja 3D-enginet sekä raskaammat grafiikkaenginet taas voivat
- hyvinkin viedä toistakymmentäkin tiedostoa.
- Hyviä jakotapoja on monia ja järki varmaan sanoo, että hyvä jakotapa
- ei ole aakkosjärjestys taikka pituusjärjestys. Hyvä jakotapa voi olla
- vaikka äänienginen teossa päätiedosto sisältäen käynnistys- ja
- lopetusfunktiot ja jonka .h-tiedostosta löytyvät keskeiset
- datarakenteet, latausrutiinit sisältävä tiedosto, universaali
- efektinsoittorajapinta ja eri tiedostot jokaiselle äänikortille,
- modien lataus, modien soittorutiinit sisältävä tiedosto jne.. Aivoja
- saa, pitää ja kannattaa käyttää.
- Tärkeitä suunnittelun kohteita on myös se miten ohjelma säilöö datansa
- sekä muistissa että kovalevyllä. Jo alussa fiksusti ja
- laajennettavasti tehty rakenne on monta kertaa käyttökelpoisempi kuin
- senhetkiseen tarpeeseen väsätty kyhäelmä. Myös tallennus- ja
- latausrutiinit kannattaa tehdä erikseen eikä pyrkiä tekemään mitään
- purkkaviritelmiä jotka kaatuvat vähintäänkin kun haluat lisätä uuden
- ominaisuuden.
- Hyvä idea on myös tehdä universaalit rutiinit virheistä
- ilmoittamiseen, muistin varaukseen ja vaikka tiedostojenkin
- lukuun. Yleensäkin enginen suurin osa tulisi sijoittaa keskivälille
- muutaman kriittisten low-level -rutiinien jäädessä alapuolelle ja
- yläpuolelle tuleva rajapinta ohjelmalle mahdollistaa enginen
- muuttumisen radikaalistikin ilman muutoksia pääohjelmaan. Low-level
- -rutiinien siirto toisille nimille jo pelkillä #define-lausekkeilla
- (tyyliin "#define OmaFopen(a,b) fopen(a,b)") auttaa sen verran, että
- kun haluatkin muuttaa kaikki tiedostorutiinit pakattuja datatiedostoja
- käyttäviksi ei tarvitse muuttaa kuin pari kohtaa kaiken muun jäädessä
- samanlaiseksi.
- Kommentointi on elintärkeää engineä tehdessä, sillä hyvä engine voi
- olla käytössä pitkänkin aikaa ja sitten kun se lopulta jää ahtaaksi
- voi huonosti kommentoineen kooderin periä hukka muuntelun
- osoittautuessa mahdottomaksi yksinkertaisesti siitä syystä ettei edes
- tekijällä ole enää mitään aavistusta mitä hänen koodinsa tekee. Hyvä
- ohjelmoija tekee sen verran lyhyitä funktioita, että niistä saa selvää
- vähän tutkailemalla ja nimeää muuttujat ja funktiot kuvainnollisesti
- säästelemättä turhaan nimen pituudessa (järkevällä tasolla kuitenkin,
- mutta saa se nyt enemmän olla kuin Jdrwsprt()). Kun epäselvemmät
- kohdat vielä kommentoi koodista pitäisikin saada huomattavasti
- paremmin selvää.
- Yksi hyödyllinen asia voisi olla tiedostoja editoidessa kirjoittaa
- tietty headeri jokaisen tiedoston alkuun. Hyviä voisi olla
- copyright-ilmoitukset (joilla ei kyllä omassa käytössä tee mitään),
- luontipäivämäärä, viimeisen muutoksen päivämäärä ja muutoshistoria,
- jonne kirjataan muutokset koodiin. Jälkeenpäin ja bugeja etsiessä
- tuollaisesta on kummasti hyötyä, kun miettii mitä onkaan tullut
- lähiaikoina muunneltua.
- Viimeinen asia mikä koodissa pitää vielä huomioida on ne funktiot,
- jotka tarjoavat rajapinnan, "käyttöliittymän" engineen. Nämä funktiot
- ovat siis ne jotka tarjotaan engineä käyttävälle ohjelmalle enginen
- käyttöön. Näiden tulee olla tarpeelliksi kattavat jotta kaikkia
- enginen ominaisuuksia voidaan halutessa käyttää hyväksi. Hyödyllistä
- on tehdä Init- ja Deinit-funktiot, joita kutsutaan pääohjelmasta
- ohjelman käynnistyessä ja siitä poistuttaessa.
- Myös funktioiden nimeäminen erottamiseksi muista mahdollisista
- samankaltaisista funktioista voi olla hyödyllistä. Kirjaston
- funktioille ja globaaleille muuttujille voisi antaa jonkin etuliitteen
- erottamaan ne muista ja huolehtimaan siitä ettei kahdella funktiolla
- ole samaa nimeä. Omassa grafiikkakirjastossani käytän JG-etuliitettä,
- jolloin funktioiden nimet ovat tyyliin JG_Draw, JG_Hide jne.. Myös
- mahdollinen versionumero kirjastolle on kätevä jos sitä aikoo todella
- kehittää kunnolla.
- Sitten vain huolehtimaan siitä että enginestä ei löydy
- pullonkauloja. Helpointa lienee tehdä enginen eniten tehoa vaativat
- osat mahdollisimman nopeiksi, jolloin pääohjelma on helppo tehdä
- korkean tason koodilla. Assembler-optimointikin voisi olla ihan kiva,
- joten seuraavassa luvussa luulen että selitän hieman sen lisäilystä
- DJGPP:n koodiin.
- Tämä luku ei nyt varsinaisesti opettanut mitään, mutta ainakin jotain
- evästä pitäisi nyt löytyä ensimmäisen enginen tekoon. Katsotaan mitäs
- tähän nyt keksisikään seuraavaksi. =)
- 7.1 Vauhtia peliin - ulkoisen assyn käyttö
- ------------------------------------------
- No niin, assembler, tuo kielistä jaloin näyttää olevan tämänkertaisen
- kiinnostukseemme kohteena. Vaan mikä on tuo salaperäinen kieli ja
- miten sitä käytetään. Se jää ihan sinun itsesi selvitettäväksi, mutta
- voin kuitenkin antaa jonkinlaisia ohjeita jotta löytäisit tiedon
- lähteille. Ensihätään kannattaa hakea koneelleen ainakin seuraavat
- opukset vaikkapa MBnetin ohjelmointialueen kautta:
- ASSYT.ZIP:
- Cyberdune (tjsp.) magazinen assykurssit kaikki samassa kasassa,
- suomeksi opettaa assemblerin perusasiat.
- HELPPC21.ZIP + HPC21_P5.ZIP:
- HelpPC referenssiteos ja Pentium-update sisältäen mm. kaikki
- x86-prosessorikäskyt, matikkaprossukäskyt ja Pentiumin omat käskyt
- (kuten CMPXCHG8B tai jotain).
- PCGPE10.ZIP:
- Assytutoriaali löytyy täältäkin, tosin englanniksi.
- 3DICA*.ZIP:
- Sisältää Henri Tuhkasen mainion assembler-optimointitutoriaalin.
- Ehdoton ensihankinta optimoinnista kiinnostuneelle.
- Lisäksi todella hyvä kirja assyn opetteluun (ja ainoita suomeksi) on
- kirja nimeltään 486-ohjelmointi. Tuota kaikki aina suosittelevat enkä
- itsekään voi kirjaa haukkua. Kirjastosta tuon saa vielä kaiken lisäksi
- ilmaiseksi, vähintään kaukolainauksella.
- Jos sinua ei assembler kiinnosta yhtään niin voit tietenkin hypätä
- tämän kappaleen yli, mutta varoituksen sana sitä ennen: Jos aiot tehdä
- joskus nopean toimintapelin (lähiaikoina ainakin), niin tulet hyvin
- luultavasti kaipaamaan assembler-osaamista. No tietenkin jos odottaa
- tarpeeksi niin voi tehdä kaiken vaikka Visual Basicin kasiversiolla,
- mutta en minäkään takaa että pysyn myöhemmin tutoriaalissa pelkässä
- C:ssä. <grin>
- Mutta sen jälkeen kun osaat assyn, niin alahan lukemaan pidemmälle,
- sillä käsittelen hieman C-kielisestä ohjelmasta kutsuttavien
- funktioiden tekoa assyllä. En aio selittää sinulle mikä on pino, sillä
- assyoppaista löytyy tuokin tieto. Muistiasi virkistääkseni mainitsen
- kuitenkin, että tulee muistaa pinon kasvavan alaspäin, eli jos haluat
- varata pinosta 16 tavua niin sinun tulee vähentää esp:stä (extended
- stack pointer) 16 tavua, ei lisätä! Palautus taas hoituu lisäämällä.
- Eli hieman tietoa siitä miten C-kielinen ohjelma kutsuu funktiota ja
- mitä se tekee sinun palattuasi. Eli kutsuessaan funktiota C-kielinen
- ohjelma ensin pushaa parametrit pinoon lähtien parametrilistan
- oikeasta laidasta päätyen lopulta ensimmäiseen parametriin ja sitten
- se heittää ebp:nsä pinoon, kopioi ebp:n esp:hen ja lisää siihen itse
- käyttämänsä muistin määrän (eli itseasiassa vain varmistaa että esp
- osoittaa pinon päälle) ja kutsuu funktiota käyttäen call-komentoa,
- joka vielä kaiken huippuna heittää senhetkisen eip:n (extended
- instruction pointer) pinoon.
- Huomaamme, että kun suoritus alkaa omasta funktiostamme on asioiden
- laita seuraava:
- Pino sisältää indeksissä 0 pinon huipun, eli tällä hetkellä kutsuneen
- ohjelman eip:n. Sen jälkeen on ensimmäinen parametri, sitten toinen
- parametri jne.. Mutta koska meidän täytyy aluksi tallentaa ebp pinon
- päälle pushaamalla se huipulle, jolloin tiedämme, että parametrit ovat
- kahden kaksoissanan (ebp ja eip), eli 8 tavun päässä. Tässä funktion
- tarvitsema alustuskoodi:
- push ebp
- mov ebp, esp
- Lisäksi on mahdollista varata pinosta muistia haluttu määrä
- vähentämällä esp:tä,jolloin siihen jää aukko jonka alussa ebp
- on. Muista kuitenkin vapauttaa muisti korottamalla esp:tä. Muista
- lisäksi, että koska pino menee alaspäin, niin varattu muisti sijaitsee
- myös esp:stä alaspäin, eli negatiivisissä offseteissa.
- Sen jälkeen vain osoitellaan parametrejä. Ensimmäinen parametri on
- siis nyt kohdassa ebp+8 (koska kopioimme ebp:hen esp:n, jossa pino
- oli), ja parametrit seuraavat järjestyksessä 4 tavun välein
- riippumatta parametrin koosta, DJGPP näet sijoittelee myös nuo
- mahdollisimman hyvin, toisin kuin aiemmin luulin.
- Koko roska on itseasiassa hemmetin vaikea ymmärtää ja olen tunnin ajan
- loikkinut ympäri kovalevyäni etsimässä tarkennuksia pinon toimintaan
- ja miten C-funktiota itse asiassa kutsutaan, sillä en ole koskaan
- ottanut viimeisen päälle selvää kääntäjän sielunelämästä.
- Piirrän nyt pikkaisen kaavion siitä mitä tietääkseni muistista löytyy
- sen jälkeen, kun funktiota void func(short,long) on kutsuttu, ebp on
- pushattu ja esp siirretty siihen ja pinosta varattu muistia 2 tavua:
- C B A
- ----------------------------------------------------------------
- RR RR RR MM MM BP BP BP BP IP IP IP IP 11 11 -- -- 22 22 22 22
- ----------------------------------------------------------------
- A) Ohjelmaan tullessa ESP osoittaa tähän
- B) Kun EBP on pushattu niin ESP osoittaa tähän, samoin EBP kun ESP on
- ensin siirretty myös EBP:hen. Huomaa EBP:n ja EIP:n sijainti
- kohdasta B nähden ja parametrin 1 sijainti offsetissa 8 (viivat
- ovat käyttämättömiä palasia), sekä
- parametrin 2 sijainti offsetissa 10 (parametrin 1 koko on short,
- eli 2 tavua!)
- C) Kun ESP:tä vähennetään kahdella jotta pinosta saadaan ohjelmalle 2
- tavua muistia on meillä nyt kaksi tavua muistia käytössä alkaen
- offsetista EBP-2. ESP osoittaa tämän muistin alkuun, mutta
- pushailun sattuessa se lähtee vaeltelemaan yhä kauemmas vasemmalle.
- Palautuksessa poppaillaan kaikki, jolloin ESP on taas kohdassa C. Sen
- jälkeen vapautetaan pino vähentämällä ESP:tä kahdella, jolloin ESP ja
- EBP ovat jälleen samoja, eli kohdassa B molemmat. Nyt vielä popataan
- EBP, jolloin EBP on alkuperäisessä tilassaan, samoin kuin ESP, joka
- osoittaa EIP:n kohdalle. Nyt vain ret, joka ottaa EIP:n pinosta ja
- palaa tähän osoitteeseen.
- JES! TEIN SEN! (anteeksi tunteenpurkaus mutten uskonut saavani tätä
- itsekään selville ilman kenenkään apua ;)
- Huomaa, että on aina kutsuvan ohjelman vastuulla pitää rekistereistään
- huolta ja puhdistaa parametrit pinosta, jotka sinne on pitänyt
- pushailla ennen ohjelman kutsua (niitä ohjelma ei palauta).
- Tässä nyt tämä lopullinen assyosuus, joka pitää olla alussa ja
- lopussa:
- push ebp
- mov ebp, esp
- sub esp, <pinon koko>
- <koodia>
- add esp, <pinon koko>
- pop ebp
- ret
- Toisen C-funktion kutsu taas onnistuu seuraavasti, otetaan esimerkkinä
- vaikka foo(int,short,char,int):
- push <int>
- push <char>
- push <short>
- push <int>
- call _foo
- add esp, 11
- Nuo <int>-hommat siis tarkoittavat oikeankokoisia rekisterejä tai
- muistialueita. Huomaa myös lopussa esp:n palautus korottamalla sitä
- parametrien yhteenlasketun koon verran. Huomaa myös, että C lisää
- assykoodiin aina yhden alaviivan lisää, eli omien rutiiniesi
- funktionimien edessä pitää ASM-tiedostossa olla aina yksi alaviiva
- enemmän kuin mitä C-kielisessä. Myös C-kirjaston funktioita kutsuessa
- pitää muistaa, eli _printf, _puts jne.. Funktioille joiden nimissä on
- C:lläkin yksi tai useampia alaviivoja suoritetaan vain yhden alaviivan
- eteenlisäys.
- No niin, nyt menee kaikki muu funktioissa, mutta vielä palautus ja
- structit sekä reaaliluvut. No tässä kaikki vähä mitä minä siitä
- tiedän:
- Pointtereiden ja dword (4 tavua siis) kokoisten kokonaislukujen
- palautus EAX:ssä. Sanojen (2 tavua, word) palautus AX:ssä ja tavujen
- palautus AL:ssä. Reaaliluvut matikkarekisterissä ST[0]. Structeista
- minulla ei ole aavistusta, sillä olen käyttänyt helpompaa ja yleensä
- hyödyllisempää tapaa välittää ne vain structin osoitteina.
- Reaaliluvut annetaan parametreinä tietääkseni ihan samoin kuin muutkin
- parametrit.
- No mutta. Kaikki tietävät nyt miten varata muistia, kutsua funktioita,
- palauttaa tietoja, käyttää parametreja. Mutta tärkein puuttuu, sillä
- kukaan ei osaa tehdä tiedostoja jotka voisi linkata DJGPP-ohjelman
- mukaan. Siispä töihin!
- Jotta objektitiedoston voisi linkata mukaan DJGPP-ohjelmaan täyty sen
- olla oikeaa formaattia. DJGPP:n hyväksymä formaatti tunnetaan nimellä
- COFF (ei kaljaa!), eli common object file format. Ainoat
- käyttämistäni assembler-kääntäjostä jotka tuota tukevat ovat as ja
- NASM. As on GNU assembler ja sisältää TODELLA kryptisen näköistä AT&T
- assembleria kääntävän yksikön. Mutta kerron jo etukäteen, että
- AT&T-formaatti, jota DJGPP käyttää itse sen Unix-taustan takia on
- aivan toisen näköistä kuin Intel-syntaksin assy, joten suosittelen,
- että ette käytä sitä (halukkaat imuroivat tiedoston DJTUT255.ZIP)!
- Paljon parempi kääntäjä on nimeltään Netwide Assembler, lyhyesti NASM,
- jonka löytää ainakin MBnetistä ja tietenkin Internetistä. Nimi on
- NASM094B.ZIP, mutta voi kyllä olla että uudempiakin on
- ilmestnyt. Jokatapauksessa kääntäjä on aivan loistava ja sen käyttökin
- on suhteellisen yksinkertaista. Kaikkein parhaiten sen käytön oppii
- lukemalla NASM.DOC läpi ja tutkailemalla esimerkkikoodeja (etenkin
- AOUTTEST.ASM!) hakemistosta TEST. Mutta niille jotka eivät mielellään
- lue englantia on ihan pikkuinen esimerkkisorsa, jolla pääsee nyt
- ainakin alkuun siihen asti, että kunnon sanakirja tai tulkkaava kaveri
- löytyy:
- TEST.ASM:
- BITS 32
- EXTERN _cfunktio
- EXTERN _cmuuttuja
- GLOBAL _asmmuuttuja
- GLOBAL _asmfunktio
- SECTION .text
- ; int asmfunktio(int)
- _asmfunktio:
- push ebp
- mov ebp, esp
- mov eax, [ebp+8]
- add [_asmmuuttuja], eax
- push eax
- call _cfunktio
- add esp, 4
- mov eax, [_asmmuuttuja]
- pop ebp
- ret
- SECTION .data
- _asmmuuttuja DD 0
- TEST.H
- extern int asmfunktio(int);
- void cfunktio(int);
- int cmuuttuja;
- TEST.C
- #include <stdio.h>
- void cfunktio(int luku) {
- puts("kutsuttiin C-funktiota parametrilla %d\n", luku);
- }
- int main() {
- printf("asmfunktio(10) palautti arvon %d\n", asmfunktio(10));
- printf("asmfunktio(20) palautti arvon %d\n", asmfunktio(20));
- printf("asmfunktio(5) palautti arvon %d\n", asmfunktio(5));
- printf("asmfunktio(2) palautti arvon %d\n", asmfunktio(2));
- return 0;
- }
- H-tiedoston ja C-tiedoston varmaan ymmärrätte, mutta selvennyksenä
- vielä assysuudesta, että ensin asetetaan NASM 32-bittiseen
- koodinkääntötilaan, sitten määritellään ulkoiset muuttujat _cmuuttuja
- (kaksoisasna) ja _cfunktio (kaksoissana sisältäen rutiinin
- osoitteen). Sitten koodisegmentissä (.text) on _asmfunktio, joka tekee
- kuten aiemmin neuvottiin, eli tallettaa ebp:n ja kopioi esp:n
- ebp:hen. Sen jälkeen se korottaa _asmmuuttuja -muuttujaa parametrillä
- ja kutsuu vielä _cfunktio -funktiota parametrillä palauttaen lopuksi
- _asmmuttuja:n arvon. Datasegmentissä on varattu _asmmuuttuja
- -muuttujalle tilaa kaksoissanan verran ja alustettu se nollaksi.
- Sitten vain tutkimaan antaako ohjelma oikean tulosteen. En minäkään
- tiedä mutta menen katsomaan. =) Toimi ainakin minulla. Jaa että se
- kääntäminen NASM:illa?-) No se on tietenkin komennolla:
- nasm -o jokin.o -f coff jokin.asm
- No niin, nyt sinun pitäisi hallita assemblerin käyttö C:n kanssa
- jotakuinkin välttäen ja nasmilla kääntelykin pitäisi onnistua, sekä
- nasm-tiedostojen tekokin ainakin rajoitetusti. Pahoittelen että
- tarkempia ohjeita ei annettu, sillä ne olisivat olleet niin pitkät,
- että katsoin oppimisen onnistuvan ilman tarkempia ohjeita. Mutta jos
- kuitenkin tuntuu, että tämän kappaleen taso leijui kilometritolkulla
- tajuntasi yläpuolella niin pyydän ottamaan yhteyttä, sillä en
- ihmettele vaikka tämä olisikin vaikein osa tähän asti ja kaikki apu
- sen suhteen miten tätä pitäisi parantaa on tarpeen.
- Mutta toisaalta jos et assyä muuten osaa etkä ole kaikkea
- dokumentaatiota kaivanut esiin mitä löydät voi olla että asia on
- paljon selkeämpi jo muutaman päivän päästä. Jos ei kuitenkaan helpota
- niin heitä viestiä tännekin päin. Mutta nyt jatkan taas kohti uutta
- tuntematonta.
- Phew, tämähän käy työstä kun koko päivän kirjoittaa!
- 7.2 PIT - aikaa ja purkkaa
- --------------------------
- Hiphei taipaleemme jatkuu edelleen, vaikka kello osoitteleekin
- kirjoitushetkellä melkein kahtatoista. Myös ihmeellisestä tekstistä
- voinee sen päätellä etten ole välttämättä aivan parhaimmillani ja
- terävillimmilläni (villimmilläni?) tähän aikaan päivästä. No, tehän
- siitä vain kärsitte, en minä, joten jatkakaamme! ;)
- Eli ihmeellinen lyhenne PIT? Mistä se tulee? No tietenkin sanoista
- Programmable Interval Timer, eli ohjelmoitava keskeytysajastin. Tämä
- on tällainen hauska piiri PC:llä, joka kykenee generoimaan ties millä
- tavalla keskeytyksiä. Kiinnostavaa ja tarkkaa tietoa löytyy PCGPE:stä
- (PCGPE10.ZIP) tiedostosta PIT.TXT, mutta me keskitymme vain
- olennaiseen, nimittäin systeemin omaan kelloon, keskeytykseen
- 8. Kerron kuitenkin hieman millä tavalla piiri laskee milloin pitää
- generoida keskeytys 8, ennenkuin pääsemme hauskaan tavaraan (eli
- esimerkkikoodiin ;).
- Eli PIT tikittää 1193181Hz:n taajuudella, eli suomeksi 1193181 kertaa
- sekunnissa. Joka kerta se esim. vähentää kanavan 0 laskuria yhdellä ja
- jos se on 0 niin se generoi keskeytyksen ja asettaa uudelleen laskurin
- haluttuun arvoon ja lähtee laskemaan alaspäin. Laskuri on kahden
- tavun, eli yhden sanan mittainen ja kykenee näinollen vastaanottamaan
- luvun väliltä 0-65335. Mutta erikoisuutena on se, että jos laskurin
- alustusarvo 0 ei tarkoitakaan että keskeytystä kutsutaan jatkuvalla
- syötöllä, vaan että sitä kutsutaan 65536:n "tikahduksen" (ei näin
- myöhään oikein sanat muistu mieleen) jälkeen. Normaali systeemikello
- on asetettu tähän kutsuntatiheyteen, eli sitä kutsutaan
- 1193181/65536=n. 18.2 kertaa sekunnissa.
- Jos siis koukutamme tämän keskeytyksen kuten olemme aiemmin tehneet
- näppiskeskeytyksellekin tulee alkuperäistä kutsua tähän tahtiin, sillä
- toisin kuin näppiskeskeytys, kellokeskeytys on huomattavasti
- tärkeämmässä asemassa eikä sitä voi hypätä noin vain yli (ainakin
- DOS:in kello pysähtyy koko ajaksi =). Jos me siis koukutamme
- keskeytyksen tulee sen olla tämäntyylinen:
- funktio kellokeskeytys
- <tee jotain>
- laskuri = laskuri + tikkejä_per_kutsu;
- jos (laskuri on suurempi tai yhtäsuuri kuin 65536)
- laskuri = laskuri - 65536
- kutsu_vanhaa();
- muuten
- kuittaa_keskeytys();
- end jos
- end funktio
- Tikkejä_per_kutsu on siis uusi määrä tarvittavia tikkejä jokaisen
- keskeytyksen välissä. Jos vaikka haluaisimme että omaa kelloamme
- kutsutaan 100 kertaa sekunnissa, niin meidän pitäisi asettaa PIT:ille
- laskurin alustusluvuksi 1193181 / 100 = n. 11931. Sitten vain joka
- kutsulla lisätään laskuria sen mukaan montako tikkiä on kulunut
- edellisestä vanhan kellon kutsusta ja jos se on alkuperäinen 65536 tai
- suurempi, niin vähennetään siitä tämä luku ja kutsutaan vanhaa
- keskeytystä. Jos se on vielä alle 65536, niin lähetetään tuttuun
- tapaan tavu 0x20 porttiin 0x20.
- Kellokeskeytyksen <tee jotain> -kohdan voi ja kannattaakin yleensä
- korvata laskurilla, jota korotetaan jatkuvasti. Tätä voi käyttää
- vaikka ajanottoon tai muuhun hyödylliseen, kuten näemme myöhemmin.
- Kaikki tuntuisi olevan toteutusta vailla - MUTTA.
- Ongelmaksi muodostuu vanhan kutsuminen. Kun keskeytys generoidaan niin
- senhetkinen koodisegmentti ja -osoitin (eli CS+EIP) kipataan pinoon,
- samoin kuin liput ja kutsutaan käsittelijää. Vastaavasti iret
- keskeytyskäsittelijän lopussa ne otetaan sieltä pois ja niiden avulla
- palataan jatkamaan keskeytynyttä ohjelman suoritusta samasta tilasta.
- Mutta kun kutsumme vanhaakin käsittelijää välissä, niin pinosta pois
- otto tapahtuu kahdesti, mikä eteen? Selvää on, että ohjelma kaatuu jos
- ei tätä ongelmaa korjata. Mutta hätiin saapuu Kaj Björklund uljaalla
- inline assembler-ratsullaan pelastaen meidät pulasta! Meidän tarvitsee
- vain kellokeskeytystä asetettaessa ottaa talteen alkup. handlerin
- koodiselektori ja offsetti sekä tallentaa ne 64-bittiseen muuttujaan
- (long long). Sitten vain käytetään seuraavanlaista inline-pätkää:
- __asm__ __volatile(
- "pushfl
- lcall %0
- "
- :
- : "g" (oldhandler));
- Edellinen koodinpätkä tekee samat temput ennen funktion kutsumista
- kuin mitä sanoin normaalisti tehtävän, eli heittää liput pinoon ja
- lcall pistää sinne CS:n ja EIP:nkin, joten iret vanhassa
- timer-rutiinissa palaakin omaan koodiimme ja kaikki toimii hienosti,
- kun if...else huolehtii siitä ettei outata kahdesti porttiin 0x20!
- Hienoa! Nyt meillä onkin oikeastaan kaikki tarvittava tieto handlerin
- tekoon:
- #include <dos.h>
- #include <dpmi.h>
- #include <go32.h>
- #include <stdlib.h>
- #include <stdio.h>
- #include <sys/nearptr.h>
- _go32_dpmi_seginfo info;
- _go32_dpmi_seginfo original;
- volatile long long OldTimerHandler;
- volatile int TicksPerCall, OriginalTicks, Counter;
- static volatile void TimerStart() {}
- void TimerHandler() {
- Counter++;
- OriginalTicks+=TicksPerCall;
- if(OriginalTicks>=65536) {
- OriginalTicks-=65536;
- __asm__ __volatile__ ("
- pushfl
- lcall %0
- "
- :
- : "g" (OldTimerHandler));
- } else {
- outportb(0x20, 0x20);
- }
- }
- static volatile void TimerEnd() {}
- void SetTimerRate(unsigned short ticks) {
- outportb(0x43, 0x34);
- outportb(0x40, ( ticks & 0x00FF ) );
- outportb(0x40, ( ( ticks >> 8 ) & 0x00FF ) );
- }
- void InitTimer(int tickspersecond) {
- __dpmi_meminfo lock;
- lock.address = __djgpp_base_address + (unsigned) &TimerStart;
- lock.size = ((unsigned)&TimerEnd - (unsigned)&TimerStart);
- __dpmi_lock_linear_region(&lock);
- Counter=0;
- OriginalTicks=0;
- TicksPerCall=1193181/((unsigned short)tickspersecond);
- disable();
- _go32_dpmi_get_protected_mode_interrupt_vector(0x0008, &original);
- OldTimerHandler=((unsigned long long)original.pm_offset) +
- (((unsigned long long)original.pm_selector)<<32);
- info.pm_offset=(unsigned long int)TimerHandler;
- info.pm_selector=_my_cs();
- _go32_dpmi_allocate_iret_wrapper(&info);
- SetTimerRate(TicksPerCall);
- _go32_dpmi_set_protected_mode_interrupt_vector(0x0008, &info);
- enable();
- }
- void DeinitTimer() {
- disable();
- SetTimerRate(0);
- _go32_dpmi_set_protected_mode_interrupt_vector(0x0008, &original);
- enable();
- }
- Muu mennee ihan hyvin tajunnan perälle asti, mutta InitTimer-rutiinin
- alku voi hyvinkin tuottaa ihmettelyä, samoin kuin kaksi tyhjää
- funktiota kummallakin puolella TimerHandler-rutiinia. No minäpäs
- kerron mistä on kyse. Kyse on muistin lukitsemisesta, kuten ehkä
- komentojen nimistä voi päätellä. Normaalisti DPMI-palvelin (jos se
- siihen kykenee) voi swapata levylle koodia ja dataa jos siltä tuntuu,
- mutta kun muistialue lukitaan niin sitä ei swappaillakaan
- minnekään. Älä huoli jos epäilet ettet olisi osannut noita tehdä itse,
- sillä minäkin varas- käytin apunani libc:n lähdekoodeista löytyvää
- koodinpätkää ja Kaj Björklundin esimerkkikoodia.
- No nyt vain sitten esimerkkiohjelma, joka näyttää hieman mihin
- timer-rutiini pystyy:
- #include <stdio.h>
- #include <conio.h>
- extern void InitTimer(int);
- extern void DeinitTimer();
- extern volatile int Counter;
- int main() {
- InitTimer(100);
- while(!kbhit()) {
- printf("Counter=%d\r", Counter);
- fflush(stdout);
- }
- getch();
- DeinitTimer();
- return 0;
- }
- Näin. Seuraavassa luvussa esittelen ennen nukkumaanmenoani (ellei joku
- tule ajamaan minua unten maille ennen kuin ehdin kirjoittaa seuraavan
- luvun =) kiinnostavaa käyttöäkin tälle, joten pysykää kanavalla!
- 7.3 Miten peli toimii yhtä nopeasti kaikilla koneilla
- -----------------------------------------------------
- No tähän on useita tapoja, mutta lähes kaikissa tarvitaan ajanottoa ja
- näinollen edellisen luvun ajastinrutiini pohjusti varsin mukavasti
- tämän luvun aihetta (josta tulee luultavasti todella lyhyt). Idea on
- siis se, että jokaisella koneella peli pyörisi yhtä nopeasti. No
- helpommin sanottu kuin tehty.
- Varmastikin käytetyin ja toimivin on menetelmä, jota kutsutaan
- hienosti termillä "frameskip", eli kuvien yli hyppiminen. Ilkka
- Pelkonen käytti siitä brutaalia termiä harppominen, mutta koska
- minulle tulee siitä mieleen vain pitkäjalkaiset laihat
- kumisaapasjalkaiset miehet niin käytän englanninkielistä termiä
- (Ilkka, kyllä minä käyttäisin edes "loikkimista", siitä tulee edes
- kengurut mieleen ;).
- Eli idea on, että kaikki muu tehdään joka framelle, mutta piirtäminen
- jätetään väliin jos ollaan "aikataulusta jäljessä". Niinpä kun meillä
- on nyt ajastinrutiini voimme käyttää tällaista systeemiä:
- päälooppi
- käsitteleframe
- vähennä timerlaskuria
- jos timerlaskuri = 0
- piirrä
- tai jos timerlaskuri < 0
- odota kunnes timerlaskuri >= 0
- end päälooppi
- Eli itseasiassa edellisen luvun laskuria vähennetään itse pelissä koko
- ajan pyrkien pitämään se nollassa, mutta jos piirron aikana on ehtinyt
- mennä useampi frame sivu suun niin käsitellään framea ja vähennetään
- timerlaskuria niin kauan että ollaan taas saatu "kiinni" oikea tahti
- ja voidaan päivittää seuraava ruutu. Myös toinen mahdollisuus, eli
- "ylinopea" kone täytyy huomioida odottelemalla jos pyyhkäistään jo
- aikataulusta ohitse.
- Valitettavasti tällä alle 18.2:n framen nopeudet eivät toimi, joten
- sellaisiin tapauksiin pitää kehitellä erikoisratkaisuja (fixed-point
- -laskuri esimerkiksi, joka korottuu vain puolella joka vuoro tms.).
- On myös muita mahdollisuuksia toteuttaa frameskip, kuten siirtämällä
- käsitteleframe -funktio suoraan timeriin, joka ei tosin mielestäni ole
- hyvä ratkaisu, mutta joka toisaalta on tietyllä tavalla selvä
- laskurien jäädessä pois. Mutta mitä teetkään kun kesken ruudulle
- piirron päivitetään aluksien paikkaa? Ei ole enää kenestäkään,
- (varsinkaan pelaajasta) kivaa siinä vaiheessa.
- Toinen paljon toimivampi vaihtoehto on käyttää kulunutta aikaa
- ikäänkuin kertoimena tehtävissä. Eli jos vaikka joka vuorolla pitää
- siirtää spriteä 1 eteenpäin, niin siirretään joka framella spriteä
- 1*kulunut aika verran eteenpäin. Tämä valitettavasti vaatii paljon
- tiheämmän ajastimen kutsun kuin sellaiset 70 kertaa sekunnissa
- toimiakseen hyvin ja lisäksi fixed-point -matikka on yleensä aika
- välttämätön tämänkaltaisessa toteutuksessa.
- Mutta, aihe ei ole vaikea ja varmasti osaat päättää minkälaisen
- toteutuksen teet itse peliisi. Minä häivyn nukkumaan ja jätän sinut
- oman onnesi nojaan. Öitä!
- 7.4 Yleistä asiaa pelin levityksestä
- ------------------------------------
- Tässä luvussa olisi tarkoitus hieman valaista pelinteon toista puolta,
- eli sen joka ei sisällä ohjelmointia, vaan dokumentaation kirjoitusta,
- grafiikan piirtoa, musiikkia, pelin levitystä ja ties mitä. Taidan
- kyllä olla aika turha tästä kauheasti puhumaan, sillä valmiiksi emme
- ole saaneet kuin vasta yhden pelin ja toinen on kovasti tekeillä, kunhan
- laiska kooderimme (minä) löytäisi jostain aikaa kirjoittaa koodia.
- Ensimmäinen homma pelin teossa olisi varmaan päättää minkä tyyppisen
- pelin tekee. Parasta on valita sellainen pelityyppi, jonka uskoo
- pystyvänsä toteuttamaan. Ensimmäisenä projektina kannattaa varmaan
- tehdä jokin yksinkertainen kaksiulotteisen toimintapelin, vaikkapa
- sitten sen iänikuisen matopelin. Sitten vasta pikkuhiljaa kun kokemusta
- kertyy niin kannattaa jatkaa vaikeammilla projekteilla.
- Nimi lienee toinen huolenaihe kun pelityyppi ja sen pääpiirteet ovat
- tiedossa. Älä mielellään nimeä ohjelmaa samannimiseksi kuin jokin
- olemassaoleva tuote. Esimerkiksi matopeli, jonka nimi on Windows voi
- aiheuttaa lievää närää jos se leviää laajemmalle (tosin yleensä
- ensimmäinen peliprojekti ei leviä kauhean laajalle, mutta mistä sitä
- koskaan kuitenkaan tietää).
- Sen jälkeen olisi varmaan parasta alkaa pelin teko. Sen lisäksi, että
- pelin engine täytyy saada kuntoon olisi myös hyvä tehdä siihen
- grafiikkaa. Musiikki ja ääniefektitkin olisivat varsin mukava idea,
- jos kunnianhimoa löytyy tarpeeksi. Levityksessä on useita
- äänikirjastoja, jotka tarjoavat enemmän tai vähemmän toimivan
- ratkaisun ääniongelmiin. Enginen lisenssit kannattaa varmaan kuitenkin
- tarkistaa hieman tavallista tarkemmin, kun joidenkin mukana tuppaa
- olevan varsin kirjavia käyttöehtoja (suurin osa kieltää kaupallisen
- käytön).
- Jos grafiikka tai musiikki ei itseltä suju on tietenkin mahdollista
- hankkia joku kaveri tai vaikka aivan tuntematonkin mukaan projektiin
- tekemään grafiikkaa ja säveltämään musiikkia. Pelin ollessa sitten
- muiden osien osalta kasassa alkaakin kannattaa miettimään levitystä
- ja dokumentointia, jotka ovat muun kokonaisuuden kanssa myös tärkeitä.
- Normaalisti käytettyjä levitystyyppejä on kolme, PD, FW ja SW (ja
- täysin kaupallinen levitys, mutta tätä tutoriaalia ei kyllä sellaisen
- tekijöille ole tarkoitettu). PD (Public Domain) tarkoittaa, että
- luovut kaikista oikeuksistasi ohjelman suhteen, eli muut saavat
- tehdä ohjelmallasi mitä ikinä keksivät, vaihtaa nimen ja levittää tai
- myydä miten haluavat.
- Hieman rajoitetumpi muoto on FW (FreeWare), jossa pidät
- tekijänoikeutesi tuotokseesi ja saat itse sanella ehdot miten sitä
- levitetään. FreeWare -tuotteista ei kuitenkaan saa periä mitään (sillä
- se on termi jota käytetään ilmaisesta tuotteesta). SW (ShareWare) taas
- on levitystyyppi, jossa käyttäjä saa kokeilla ohjelmaa tietyn ajan ja
- sitten vasta päättää mitä tekee ohjelman kanssa.
- Ensimmäinen tehtävä päättäessäsi minkätyyppinen ohjelmastasi tulee
- on miettiä mihin ohjelmasi pystyy. Jos tuotos on ensimmäinen pelisi
- ja harjoitustyö voi hyvinkin olla järkevää antaa koko ohjelma
- lähdekoodeineen muiden levitykseen. Tällaiset julkistukset ovat aina
- harvinaisia ja ohjelmasi ehkä leviää tällä tavoin paremmin. Jos
- olet kuitenkin sitä mieltä, että et halua muiden käyttävän peliäsi
- miten haluavat kannattanee levitysmuodoksi laittaa FW.
- Sharewarena tuotetta kannattaa levittää vasta jos todella olet panos-
- tanut siihen vain siinä mielessä, että saat siitä rahaa tai jos olet
- sitä mieltä, että ohjelmasi on merkittävästi parempi kuin kilpailevat,
- kaupalliset tai SW-tuotteet. Shareware-ohjelmaksi ei kuitenkaan
- kannata laittaa sitä ensimmäistä matopeliä tai jotain bugista
- viritelmää, jos haluaa säilyttää maineensa. =)
- Sharewareakin on kolmea tyyppiä, nimittäin tiukka aikarajoitettu
- shareware, aikarajoitettu shareware ja rajoittamaton shareware. Tiukka
- aikarajoitettu SW on tyypillisesti kuten kaikki mahdolliset Windows-
- viritellyt HTML-editorit, joissa 99% on jokin viritelmä, joka terminoi
- ohjelman ennemmin tai myöhemmin (yleensä ennemmin). Löysemmästi
- aikarajoitetut ohjelmat ovat siitä kiitollisia, että niiden toimivuus
- säilyy aikarajan jälkeenkin. Rajoittamattomat ovat sitten tietenkin
- ne kaikkein mukavimmat ja niiden toimintaperiaate ei enää yleensä
- olekaan antaa käyttäjän kokeilla ohjelmaa, vaan rahaa pyydetään siitä,
- että käyttäjä ottaa käyttöönsä kaikki ohjelman toiminnot.
- Jos peli on aivan ehdottoman huippu niin voi yrittää levittää sitä
- täysin kaupallisesti, mutta se vaatiikin sitten yleensä aika lailla
- kokemusta ja tietenkin hieman hyvää tuuria.
- Sama minkä tyypin levitykseen sitten päätyy, niin kannattaa varmaan
- kirjoittaa hieman tekstiä, jossa kerrot miten haluat ohjelmaasi
- levitettävän. PD-tyypillä et tarvitse ehkä kuin tekstitiedoston, jossa
- ilmoitat luopuvasi kaikista oikeuksista ja kaikesta vastuusta ohjelman
- suhteen. SW:n ja FW:n kanssa kannattaakin sitten panostaa
- lainopilliseen puoleen hieman enemmän.
- Tärkein on ilmoittaa selvästi pelissä, että tekijänoikeudet kuuluvat
- sinulle tai useammalle henkilölle ja kertoa ehdot joiden rajoissa
- ohjelmaa saa levittää. Tärkein rivi lienee tämänkin dokumentin alusta
- löytyvä:
- Copyright (C) Joonas Pihlajamaa 1997. All rights reserved.
- Tekijänoikeuksien merkki, (C) ei toistu oikein tietokoneella, sillä
- sen pitäisi itseasiassa olla ympyrän keskellä oleva C. Näinollen
- Copyright-teksti alussa voi olla varsin hyödyllinen. Sen jälkeen tulee
- tekijän nimi ja loppuun yleensä vuodet joiden aikana olet tuotteen
- tekijänoikeuksia pitänyt hallussasi (eli käytännössä minä aikana olet
- peliä tehnyt). Jos tekijöitä on useita kannattaa varmaan pelata varman
- päälle ja selittää tarkemmin ketkä henkilöt ovat tehneet mitäkin.
- Sitten vain perään kaikki ehdot, joita haluat peliäsi levitettäessä
- noudatettavan. Suhteellisen kattava vastaava löytynee suomeksi
- tämänkin dokumentin alusta (porsaanrei'istä saa kyllä vapaasti
- ilmoittaa ;), sekä kotisivujeni levitysehdoista, joiden parissa vietin
- runsaasti aikaa pyrkien saada siitä niin vaikean kuin mahdollista.
- Jos noiden tekeminen tuntuu turhalta, niin kannattaa muistaa, että jos
- joskus satut joutumaan kahnauksiin ohjelmasi väärinkäytösten tai sen
- aiheuttamien ongelmien kanssa, niin tuo teksti saattaa olla ainoa
- apusi. Ilman tekstiä on paljon hankalampaa sanoa oikeudessa, ettet ole
- vastuussa ohjelman aiheuttamasta sydämentahdistimen pysähtymisestä,
- toisin kuin jos olisit kirjoittanut ehtoihin, että et ole vastuussa
- moisista vahingoista.
- Mitä sinun tulisi ilmoituksessa mainita olisivat seuraavat:
- 0. Mihin kaikkeen ehdot ilmoituksessa ulottuvat
- 1. Miten ohjelmaa ja sen tiedostoja saa käyttää
- 2. Mistä olet vastuussa
- 3. Mitä tehdä jos ei suostu ehtoihin ja milloin katsotaan käyttäjän
- suostuneen niihin
- 4. Miten ohjelmaa saa levittää
- 5. Missä ohjelmaa saa levittää
- Kun lainopillinen puoli ja itse peli on kunnossa lienee jäljellä vain
- levityspuoli. Se onkin suhteellisen helppoa. Lähetys pariin suosittuun
- purkkiin (MBnettiin =) ja kenties Internetiinkin lisää varmasti
- leviämistä aivan toisin kuin kavereille antaminen. Mainostustakin voi
- harrastaa, mutta kannattanee pitää se kohtuullisissa rajoissa, ettei
- ohjelmasi saa negatiivista julkisuutta häiritsevästä mainonnasta. :)
- Jos ohjelmasi on SW-tuote, niin lienee vielä yksi kohta, nimittäin
- rekisteröinnit ja päivitykset, sekä mahdollisien lisäominaisuuksien
- "vapautus" (enabling, tätä se on kun lukee liikaa englanninkielistä
- materiaalia). Rekisteröimätön versio kannattaa pitää niin paljon
- ominaisuuksia sisältävänä, että siitä todella on jotain iloa, mutta
- pitää niin paljon hyviä ominaisuuksia rekisteröidyssä versiossa, että
- rekisteröimätön käyttäjä näkee saavansa rekisteröintirahoilleen
- vastinetta rekatessaan pelin. Myös mahdolliset ilmaiset/alennetut
- päivitykset tai muut vastaavat etuisuudet tulevaisuudessa voivat
- jonkin verran avittaa, mutta muista, että suurin osa käyttäjistä etsii
- välitöntä hyötyä, eikä paljoa välitä tulevien pelien
- rekisteröintihintojen alentumisista.
- Kannattaa myös harkita millä tavalla hoidat rekisteröinnit. Maksaminen
- pitää tehdä helpoksi (ja mielellään halvaksi), sillä suurin osa
- rekisteröijistä on kuitenkin laiskaa porukkaa ja mahdollisuus
- rekisteröityä tuoliltaan nousematta voi olla hyvinkin suuri
- etu. Maksutapoina kannattaa ainakin huomioida suoran käteisen lisäksi
- pankkisiirrot, jotka ovat viitteiden kanssa varsin näppärä tapa
- rekisteröidä. Myös postiennakko on hyvä tapa, vaikka sillä on
- suhteellisen korkeat kustannukset se on kuitenkin näppärä keino
- varsinkin vähän tyyriimmille ohjelmille (20 markan
- rekisteröintihintaan saman verran lisää voi pelottaa ostajia).
- Rekisteröidyn version lähetykseenkin on useita mahdollisuuksia. Itse
- olen miettinyt näitä ja tässä on muutama, mistä valita, osa helppoja
- toteuttaa ja osa vaikeita:
- 1. Rekisteröintiavain
- + Pieni, näppärä lähettää vaikka sähköpostilla
- - Todella helppo kopioida
- - Helppo murtaa
- 2. Rekisteröity EXE
- + Suhteellisen pieni, mennee suurempiin sähköpostilaatikkoihin
- + Varma, vaikea murtaa
- - Lähes yhtä helppo kopioida
- 3. Rekisteröity versio
- + Helppo toteuttaa
- + Ihan pikkuisen vaikeampi kopioida
- + Varma, vaikea murtaa
- 4. Rekisteröity versio ja avain
- + Varmin menetelmistä, vaikea kopioida, helppo toteuttaa
- Toisaalta kannattaa muistaa, että jos sinä jaksat laittaa sen
- disketeillä postissa ei se ole kenellekään ongelma laittaa
- viidellekymmenelle koneelle ja vaikka valmistaa diskcopyllä rekatusta
- versioista piraattiversioita jatkolevitykseen, eli kopiointi on aina
- aika helppoa. Toisaalta kopiointia haittaa ainakin hieman erillisenä
- annettava avain tai installointiohjelmassa rekisterijöijän nimen ja
- tunnuksen pyytäminen jne.
- Myös voi pitää mielessä, että mitä enemmän turvatoimia sitä hankalampi
- se on rekisteröijälle. Kohtuus kaikessa niin pysyvät rekkaajatkin
- tyytyväisinä.
- Siinä lienevät ne tärkeimmät asiat, joita kannattaa pitää mielessä
- peliä tehdessä. Lisäksi tietenkin löytyy kokonaisia kirjoja pelinteon
- taiteesta ja niiden suunnittelusta, mutta tämän luvun päätarkoitus on
- ollut valaista pelinteon käytännöllisempiä puolia. Nyt tämä
- tutorialisti lähtee lukemaan ruotsin kokeisiin!
- 7.5 Interpolointi ja viivoja
- ----------------------------
- Ilja Bräysy taisi tässä kuukausi sitten patistaa minua neuvomaan miten
- DJGPP:llä piirretään viivoja. No, pääsin pälkähästä lupaamalla
- kirjoittaa siitä jutun sitten Laamatuttiin. No minkä taakseen jättää
- sen edestään löytää, eikä tämäkään kerta näytä olevan mikään poikkeus.
- Kuitenkin tällä kertaa selitän muutakin kuin sen viivanpiirron, nimittäin
- selitän mitä tarkoittaa interpolointi, sekä miten ja mihin sitä voi
- tietokoneella käyttää.
- Eli termimme on interpolointi. Inter voisi latinassa tai jossain muussa
- kielessä hyvinkin tarkoittaa välissä, ainakin interpolointi tarkoittaa
- jotain tähän hyvin liittyvää. Interpolointi on nimittäin sitä, että kun
- tiedossamme on kaksi pistettä, niin voimme "arvata" sinne keskelle ääret-
- tömästi uusia pisteitä, jotka kaikki kuuluvat samalle suoralle. Tämä
- on ns. lineaarista interpolointia, eli interpoloidaan pisteitä samalle
- suoralle. Tällaisesta toimenpiteestä hyvä esimerkki voisi hyvinkin
- olla viivanpiirto, sillä siinähän meillä on kaksi pistettä, ja meidän
- täytyy saada niiden välillä tarpeellinen määrä pisteitä viivan
- esittämiseksi.
- No niin, tiedämme siis mitä on interpolointi. Se on siis pisteiden
- lisäämistä kahden tunnetun pisteen välille. Vaan miten noiden pisteiden
- sijainti sitten pitäisi laskea? No, miettikäämme tilannetta, jossa meillä
- on kaksi pistettä, a ja b, joiden koordinaatit ovat vastaavasti (ax,ay)
- ja (bx,by). Nyt me laskemme näiden välillä yhden pisteen. Ensimmäinen
- tehtävä lienee laskea, kuinka pitkästi meillä on matkaa x- ja y-suunnassa.
- Näitä lukuja nimitetään yleisesti delta-arvoiksi. Ne lasketaan
- seuraavasti:
- delta_x = | bx - ax |
- delta_y = | by - ay |
- Missä merkit "|" tarkoittavat itseisarvoa, siis "| a |" luetaan
- "a:n itseisarvo". C:llä funktio on abs, tai fabs, jos käytämme
- floatteja.
- No niin, tiedämme kuinka kaukana pisteet ovat toisistaan, mutta mitä
- ihmettä sitten oikein teemme tällä uudella, kiinnostavalla tiedolla?
- No jatketaanpas hieman viivanpiirron kehittelyä. Jos haluamme
- katkeamattoman viivan, niin meillä pitää olla yhtä monta pikseliä kuin
- viivan pidemmän akselin pituus on. Eli jos delta_x on suurempi kuin
- delta_y, niin tarvitsemme delta_x:n verran pikseleitä. Tilaanteen
- ollessa päinvastainen on tarvittavien pikselien määrä tietenkin
- vastaavasti delta_y.
- Sitten pidemmälle toteutukseen. Kun nyt tiedämme montako pikseliä
- tarvitsemme ja kummassa suunnassa, niin voimmekin suunnitella
- seuraavanlaisen piirtorakenteen:
- jos delta_x >= delta_y niin
- y = ay
- y_korotus = delta_y / delta_x
- looppaa x välillä ax...bx
- piste ( x, y, väri )
- y = y + y_korotus
- end looppi
- muutoin
- x = ax
- x_korotus = delta_x / delta_y
- looppaa y välillä ay...by
- piste ( x, y, väri )
- x = x + x_korotus
- end looppi
- end jos
- Nyt te tietenkin kysytte: "Mitä tuo tekee?" No, olen ilkeä ja kerron
- teille. Koska meidän täytyy piirtää pidemmän akselin verran
- pikseleitä, niin se tarkoittaa, että piirtosilmukan täytyy korottaa
- pidemmän akselin koordinaattia yhdellä ja lyhemmän jollain pienemmällä
- kuin yhdellä. Jos alkaisimme piirtelemään lyhyemmän akselin mukaan,
- niin viivan toinen akseli harppoisi yli 1 pikselin askelia ja viivaan
- jäisi reikiä.
- Eli jos...muutoin -rakenne valitsee pidemmän akselin. Sitten
- alustetaan lyhyemmän akselin aloituskoordinaatti ja korotus jo
- valmiiksi. Koska tiedämme, että looppi korottuessaan yhdellä tulee
- toistamaan sen sisällä olevan koodin yhtä monta kertaa kuin pidemmälle
- akselille tulee pikseleitä (jos delta_x on pidempi akseli, niin
- delta_x kertaa) niin voimme helposti laskea paljonko lyhyemmällä
- akselilla täytyy liikkua yhden kierroksen aikana. Tämä korotus
- saadaan siis jakamalla lyhyen akselin pituus pidemmän akselin
- pituudella.
- Ette varmaan ymmärtäneet mitään, joten parasta ottaa esimerkki. Meillä
- on viiva pisteestä (10, 10) (eli siis ax=10 ja ay=10) pisteeseen (30,
- 20) (eli taas bx=30 ja by=20).
- delta_x = | bx - ax | = | 30 - 10 | = | 20 | = 20
- delta_y = | by - ay | = | 20 - 10 | = | 10 | = 10
- Huomaamme, että delta_x on pidempi ja meidän täytyy piirtää delta_x
- kappaletta pikseleitä saadaksemme yhtenäisen viivan. Valitsemme siis
- pseudo-koodistamme jos-osaa seuraavan pätkän, sillä lause
- 'delta_x >= delta_y' on tosi.
- y = ay = 10
- y_korotus = delta_y / delta_x = 10 / 20 = 0.5
- Nyt kun siis looppaamme x:n välillä 20...30, niin joka x:n korotusta
- yhdellä seuraa y:n korotus 0.5:llä. Näin siis x ja y menevät:
- x | y
- ---------
- 10 | 10
- 11 | 10.5
- 12 | 11
- .. | ..
- 29 | 19.5
- 30 | 20
- Huomaa, että koska piirrossa pitää käyttää kokonaislukuja, niin nuo
- desimaaliosan sisältävät y-koordinaatit pyöristyvät aina alaspäin,
- jolloin piirtokoordinaatit ovat:
- x | y
- --------- -- (10, 10)
- 10 | 10 --
- 11 | 10 --
- 12 | 11 --
- 13 | 11 --
- 14 | 12 --
- 15 | 12 --
- .. | .. --
- 27 | 18 - (30, 20)
- 28 | 19
- 29 | 19
- 30 | 20
- Vasemmalla siis taulukko loopissa kiertäessä x- ja y-arvoista ja
- sitten oikealla viiva, jonka näköinen tuosta suurinpiistein tulee.
- Mutta, hienoa muuten, mutta pari ongelmaa on
- ratkaisematta. Selvitettyämme pidemmän akselin ja laskettuamme
- lyhyemmälle akselille tarvittavan koordinaatin korotuksen voimme kyllä
- piirtää viivan noin, mutta ongelmia seuraa heti, jos ensimmäinen piste
- on toisen pisteen oikealla- tai alapuolella. Sillä korotus on aina
- positiivinen, kun sekä jaettava että jakaja ovat
- positiivisia. Ongelmia aiheuttaa myös se, että jos pidemmän akselin
- ensimmäinen koordinaatti on suurempi kuin jälkimmäinen, niin
- korotuksen tilallahan pitäisi olla vähennys!
- No, hieman lisälogiikkaa ja hyvin menee. Teemme nimittäin sillä
- tavalla, että järjestämme pidemmän akselin ensimmäisen koordinaatin
- aina pienemmäksi kuin toisen. Eli jos ensimmäinen piste onkin toisen
- oikealla-/alapuolella, niin funktiomme vaihtaa pisteiden
- paikkoja. Sama viiva se on silti, mutta loopissa ei tarvitse miettiä
- onko se ensimmäinen pienempi tai suurempi, sillä se on aina pienempi.
- Ja kun vielä poistamme pyöristykset alusta, niin jos lyhyemmän akselin
- pituus on negatiivinen, niin sen jako pidemmän akselin pituudella
- tuottaa negatiivisen korotuksen (y_korotus ja x_korotus). Ja jo
- ala-asteellahan on opetettu, että negatiivisen luvun lisäys on sama
- kuin vastaluvun vähennys. (eli suomeksi: 10 + (-10) = 10 - 10)
- Eli upea pseudorutiinimme kokonaisuudessaan:
- funktio viiva( int ax, int ay, int bx, int by, char väri )
- float x, y, x_korotus, y_korotus, delta_x, delta_y
- delta_x = bx-ax
- delta_y = by-ay
- jos |delta_x| >= |delta_y| niin
- jos delta_x < 0 niin
- vaihda( ax, bx )
- vaihda( ay, by )
- end jos
- y = ay
- jos delta_y == 0 niin
- y_korotus = 0
- muutoin
- y_korotus = delta_y / delta_x
- end jos
- looppaa x välillä ax...bx
- piste( (int)x, (int)y, väri )
- y = y + y_korotus
- end looppaa
- muutoin
- jos delta_y < 0 niin
- vaihda( ax, bx )
- vaihda( ay, by )
- end jos
- x = ax
- jos delta_x == 0 niin
- x_korotus = 0
- muutoin
- x_korotus = delta_x / delta_y
- end jos
- looppaa y välillä ay...by
- piste( (int)x, (int)y, väri )
- x = x + x_korotus
- end looppaa
- end jos
- end funktio
- Tuo tarkistus nollasta pidemmän akselin kohdalla ('jos delta_x == 0'
- sekä 'jos delta_y == 0') siksi, että pystyviivan kanssa pitää korotus
- olla 0, eikä jako nollalla tule kysymykseen muutenkaan, sillä se
- kaataa ohjelman. Itseisarvot vertailussa 'jos |delta_x|>=|delta_y|'
- pitää olla siksi, että emme käyttäneet niitä aiemmin lainkaan.
- No juu, voin kyllä lyödä vetoa, ettei se toimi, mutta kirjoitetaanpas
- silti kauniilla C-kielellä puhtaaksi:
- void vaihda( int *a, int *b ) {
- int temp;
- temp=*a;
- *a=*b;
- *b=temp;
- }
- void viiva( int ax, int ay, int bx, int by, char vari ) {
- float x, y, x_korotus, y_korotus, delta_x, delta_y;
- delta_x = bx-ax;
- delta_y = by-ay;
- if( fabs(delta_x) >= fabs(delta_y) ) {
- if( delta_x < 0 ) {
- vaihda( &ax, &bx );
- vaihda( &ay, &by );
- }
- y = ay;
- if( delta_y == 0 ) {
- y_korotus = 0;
- } else {
- y_korotus = delta_y / delta_x;
- }
- for( x=ax; x<=bx; x++ ) {
- putpixel( (int)x, (int)y, vari );
- y = y + y_korotus;
- }
- } else {
- if( delta_y < 0 ) {
- vaihda( &ax, &bx );
- vaihda( &ay, &by );
- }
- x = ax;
- if( delta_x == 0 ) {
- x_korotus = 0;
- } else {
- x_korotus = delta_x / delta_y;
- }
- for( y=ay; y<=by; y++ ) {
- putpixel( (int)x, (int)y, vari );
- x = x + x_korotus;
- }
- }
- }
- Tuon testaamiseksi paras on omat silmät ja niinpä yhdistin
- viivanpiirtorutiinin ja hiiriesimerkin pyynnön yhteen
- tiedostoon. Hiirellä pyörivä viivanpiirtäjä löytyy EXAMPLE-hakemiston
- alta tiedostosta LINE.C. Nyt varmaan olisi paras, että selvität
- itsellesi miten interpolointi viivanpiirrossa toimii ja miten rutiini
- yleensäkin toimii. Interpolointi on siis yksinkertaisesti pisteiden
- laskemista kahden pisteen välille ja tietokoneella se tehdään yleensä
- siten, että otetaan koordinaatit ja jaetaan niiden välillä oleva tila
- n kappaleeseen jakolaskulla ja sitten vain loopataan n kertaa
- korottaen koordinaatteja tällä luvulla. Viivanpiirrossa, kuten myös
- monessa muussa hommassa järjestetään asiat siten, että toinen
- korotettavista on 1 ja toinen sitten mitä tarve vaatii.
- Kannattaa myös muistaa se, että esitelty viivanpiirtorutiini on, ellei
- hitain, niin kuitenkin todella takkuinen. Ensimmäisenä voisi aloittaa
- muuttamalla float-muuttujat fixed-pointiksi. Sitten myös omat rutiinit
- pysty- ja vaakasuuntaisille sekä diagonaalisille (45 asteen kulma)
- viivoille. Myös "pitkille" ja "leveille" rutiineille voisi tehdä
- jotain optimointia. Erilliset rutiinit molemmille tai jokin kikka
- millä yhdistää logiikkaa voisi hyvinkin nopeuttaa. Sitten todellisille
- nopeuskiihkoilijoille assyoptimointi tai ehkä mieluummin Bresenhamin
- viivanpiirron opettelu (löytyy PCGPE:stä) voisi olla
- tarpeen. Bresenhamia en ala opettamaan, kun en itsekään rutiinin
- toimintaa ymmärrä. Nopea se on joka tapauksessa.
- Myös tutkiminen paperilla voi auttaa. Jos kuitenkin tuntuu, että jokin
- jäi epäselväksi, niin sitten vain postia. En nimittäin tiedä kuinka
- epätäydellinen selityksestä tuli, kun itse olen asian kanssa takunnut
- niin kauan, että sen osaa etu- ja takaperin. Interpolointi on parasta
- olla hanskassa, sillä sitä tarvitaan myös esim. kaikkeen polygonien
- piirtoon liittyvässä. Mutta minä jatkan seuraavaan aiheeseen, nähdään
- siellä!
- 7.6 Vapaa skrollaus
- -------------------
- Tänään teemme sitä ylös ja alas, sivulle ja toiselle sekä useampia
- yhtä aikaa. Se ei ole mitä luulet, vaan se on vapaasuuntaista
- skrollausta, tarvittaessa vaikka osiin jaetulla ruudulla jokaisessa
- omaan suuntaansa.
- Aihe on helppo. Niin helppo, että minä tein sellaisen ilman mitään
- ongelmia. Ja se on sitten aika helppoa. Mutta jotta ne jotka eivät
- osaa/jaksa itse paneutua ongelmaan paria minuuttia enempää omien
- aivojen voimin saavat tässä tyhjentävän selityksen. Skrollaushan on
- pienen palasen näyttämistä suuremmasta kokonaisuudesta. Ruutu on
- ikäänkuin ikkuna suurempaan ruutuun. Kuten alhaalla näkyy:
- +---------------------------+ Kuvan pisteet vain hahmottavat
- | . . . . . . . . . | näytettävää aluetta. Ne eivät
- | (x,y) . . KOKO. . . .| merkitse mitään. :)
- |. o-----+ . . . . . |
- | . | |. NÄYTETTÄVÄ . |
- | .|RUUTU| . . . . . .|
- |. | | .ALUE . . . |
- | . +-----+. . . . . . |
- | . . . . . . . . .|
- +---------------------------+
- Ruutu on yleensä koko näyttöruudun kokoinen, tai sitten jos
- näyttoruutu on jaettu useaan osaan niin sen osan kokoinen, johon
- ikkuna piirretään. Kyse on siis vähän samantapaisesta toiminnasta,
- kuin bittikarttojen kanssa. Bittikartoissa vain piirretään ruutua
- pienempi kuva ruudulle, kun skrollauksessa luetaan ruutua suuremman
- kuvan osa ruudulle. Piirrossa aloitamme näytettävän alueen kohdasta
- (x,y) (merkitty kuvassa kirjaimella 'o' ruudun yläkulmaan). Sitten
- vain kopioimme ruudun leveyden verran pikseleitä näytettävästä
- alueesta ja siirrymme taas seuraavalle riville. Yksinkertaista, mutta
- helppoa! Jatkamme tätä kunnes olemme saaneet ruudun täyteen. Helppoa!
- Skrollaavassa pelissä täytyy ottaa nyt huomioon, että spritejä ja
- muita ei enää piirretä kaksoispuskuriin, vaan tähän näytettävän alueen
- puskuriin. Sen koko voi sitten olla mitä vain maan ja taivaan välillä
- - ainakin lukualueen rajoissa, kuitenkin. Skrollauksessa näyttö onkin
- nyt vain ikkuna liikkuvaan ja elävään pelimaailmaan. Fiksu kooderi
- tietenkin piirtää vain näkyvissä olevat asiat, mutta sellaiset
- hienoudet jäävät ohjelmoijan päätöksen varaan.
- Aika heittää editorisi ruudulle hieman pseudoa:
- char alue[640][400]
- char ruutu[320][200]
- funktio päivitä( int ylä_x, int ylä_y )
- int rivi, sarake
- looppaa rivi välillä 0...199
- looppaa sarake välillä 0...319
- ruutu[ rivi ][ sarake ] = alue[ ylä_y + rivi ][ ylä_x + sarake ]
- end looppaa
- end looppaa
- end funktio
- Näyttääkö vaikealta? Ei pitäisi, ei ainakaan minusta näytä. :)
- Mutta pistetään vähän vaikeammaksi. C-toteutuksessa kun meillä
- kuitenkin on vain yksiuloitteinen taulukko, niin sijoituksessa pitää
- osoite laskea käsin:
- ruutu[ rivi * 320 + sarake ] =
- alue [ (ylä_y + rivi) * 640 + ylä_x + sarake ];
- Toisena tuo on toivottoman hidasta. Kannattanee säästä sisempi looppi
- ja kopioida memcpy:llä koko rivi kerralla:
- memcpy( &ruutu[ rivi * 320 ],
- &alue[ (ylä_y + rivi) * 640 + ylä_x ],
- 320 );
- Näin saamme seuraavanlaisen C-kielisen kyhäelmän:
- char alue[640*400];
- char ruutu[320*200];
- void paivita( int yla_x, int yla_y ) {
- int rivi;
- for(rivi=0; rivi<200; rivi++)
- memcpy( &ruutu[ rivi * 320 ],
- &alue[ (yla_y + rivi) * 640 + yla_x ],
- 320 );
- }
- Eri kokoisten näyttöalueiden/virtuaaliruutujen (tai miksi niitä nyt
- haluatkin sitten kutsua) toteuttaminen ei paljoa vaadi. Puskurin koko
- vain muokkaukseen ja offsetin ((yla_y + rivi) * 640 + yla_x) laskuun
- pikku muutos ja se onkin siinä. Sitten vielä pitää hoitaa niin, että
- piirrettävän alueen alakulma ei mene virtuaaliruudun ulkopuolelle, eli
- tarkoitan tätä:
- +-----------+
- | |
- | VIRTUAALI |
- | RUUTU +--+--+
- | | | |
- +--------+--+ |
- |RUUTU|
- +-----+
- Jos yla_x tai yla_y kasvaa niin suureksi, että yla_x+320 tai yla_y+200
- menisi ruudun yli, niin silloin kaivetaan tavuja varatun muistialueen
- ulkopuolelta aiheuttaen joko ihmeellistä käyttäytymistä tai koneen
- kaatumisen. Joten pidetäänpäs koordinaatit kurissa!
- Mitä tuo onkaan mitä kuulen? (olemattomia?) Totta, taisin luvata
- muutakin kuin koko ruudun skrollausta. No, se ei ole vaikeaa. Kun
- ylemmässä esimerkissä me piirsimme koko ruudulle, niin olisimme
- tietenkin sen sijaan voineet aloittaa ruudultakin jostain muualta kuin
- oikeasta yläkulmasta ja ikkunan koko olisi voinut olla vaikka 100x100.
- Kun ikkunan koko on vähemmän kuin koko näyttöruudun koko se tarkoittaa
- myös sitä, että ikkunoita mahtuu ruudulle tarvettaessa
- useampia. Tällainen onnistuu funktiolla, joka ottaa parametreinään
- virtuaaliruudun aloituspisteen lisäksi myös aloituspisteen
- näyttöruudulla ja ikkunan korkeuden ja leveyden.
- Tosi kooderi osaa tietenkin toteuttaa tuollaisen pienellä
- miettimisellä. Ja koska minäkin olen sellainen, niin olen tehnyt
- yhdistetyn näppäinesimerkin ja skrollausesimerkin joka löytyy myös
- EXAMPLE-hakemiston alta tiedostosta, tällä kertaa nimen SCROLL.C alta.
- Tässä vaiheessa täytyy vielä varoittaa siitä, että memcpy on syntisen
- hidas tapa kopioida muistia. Optimointi assyllä tai jopa C:llä voi
- nopeuttaa toimintaa, jos muistia heitellään 4 tavun palasissa. Mittaa
- kuitenkin mahdollinen nopeushyöty, ettet vahingossa laita hitaampaa
- korvaavaa rutiinia! Sitten vain sisäistämään luvun asiaa, olikos se
- nyt niin vaikeaa? Viiden sekunnin sormienvenyttelytaun jälkeen onkin
- sitten vuorossa sinit ja kosinitit, sekä plasmaa.
- 7.7 Sinit ja kosinit sekä plasmaa
- ---------------------------------
- No nyt hieman kertausta yhdeksännen luokan matematiikasta. Sinit ja
- kosinit. Mitä ne sitten ovat ja mitä niillä tehdään? Ennenkuin
- vastaan, niin tutustukaamme Suorakulmaiseen Kolmioon:
- o a = kateetti
- |\
- | \ b = kateetti
- a | \ c
- | \ c = hypotenuusa
- | \
- | \
- | /\ * = tässä nurkassa on kulma alpha
- o-------*
- b
- No kaikki varmaan osaavat jo pythagoraan lauseen c^2 = a^2 + b^2.
- Mutta sinit ja kosinit ovatkin jotain ihan uutta, ainakin
- 8-luokkalaisille ja ysinsä aloittaneille, kenties:
- sin alpha = a/c
- cos alpha = b/c
- tan alpha = a/b
- Vain kaksi ensimmäistä oikeastaan kiinnostavat meitä, sillä niitä
- yleensä käytetään. Nyt tiedämme siis, että sini jostain kulmasta on
- yhtä kuin kateetin a ja hypotenuusan osamäärä ja kosini vastaavasti
- kateetin b ja hypotenuusan. Vaan mitä h**vettiä oikein teemme tällä
- tiedolla? No KÄYTÄMME HYVÄKSEMME!
- Nimittäin kiinnostavaa on, että jos tiedämme c:n, eli hypotenuusan
- pituuden ja kulman, niin voimme laskea vastaavan suorakulmaisen
- kolmion molempien kateettien pituudet. Pyörittelemällä hieman tavaraa
- puolelta toiselle (kai yhtälön ratkaisu oli jo seiskalla?):
- a = sin alpha * c ; sini
- b = cos alpha * c ; kosini
- Kiinnostavaa on myös, että jos kuviossa tähdellä merkitty kärki on
- ympyrän keskipiste ja c ympyrän säde, niin 'sin alpha * c' antaa
- ympyrältä kulman alpha kohdalla olevan pisteen y-koordinaatin ja
- kosini sitten vastaavasti x-koordinaatin, kas näin:
- --^-- Kuten kaaviosta näkee, niin ympyrän säteestä
- -- | -- muodostuu hypotenuusa ja kun tiedämme kulman
- - | X alpha voimme selvittää kateettien pituudet,
- - | /|- jotka samalla ovat pisteen X koordinaatit
- - | / |- kuviossa. Eli
- - | / | -
- - |/\ | - X = (cos alpha * radius, sin alpha * radius)
- <------o----+->
- - | - Viivanpiirron kehittely tästä ei olisi vaikeaa,
- - | - tarvitaan vain looppaus vaikka 360 astetta
- - | - ja joka kerralla lasketaan pisteen koordinaatit
- - | - laskurin osoittamalle kulmalle (0...359), jolloin
- - | - ruudulle piirtyy kaunis ympyrä säteeltään
- -- | -- <radius>.
- --v--
- Olet myös varmaan pelannut autopeliä nimeltään Slicks, tai jotakin
- luolalentelyä (esim. Auts, V-Wing, Kops, Rocket Chase, Kaboom,
- Spruits, Wings, PP, Turboraketti, A-Wing, ...). Tällaisissa peleissä,
- joissa täytyy pystyä liikkumaan muuallekin kuin ylös, alas, oikealle
- ja vasemmalle täytyy myös pystyä liikkumaan muihin ilmansuuntiin.
- Jos ajattelemme tietokoneen koordinaatistioa, niin aste 0 osoittaa
- oikealle, 90 alas, 180 vasemmalle ja 270 ylös. Nyt jos haluamme tietää
- paljonko pitää alusta siirtää x-suunnassa ja paljonko y-suunnassa
- nopeuden ollessa vaikkapa 5 vasemmalle alaviistoon (siis 90+45=135
- astetta) saamme seuraavan lausekkeen:
- x_nopeus = (cos 135)*5
- y_nopeus = (sin 135)*5
- Kaikki näyttää helpolta. Osaamme piirtää ympyrän, laskea tarvittavan
- x- ja y-nopeuden tiettyyn suuntaan kohdistuvalle liikkeelle ja vaikka
- tehdä pyörivän tähden viivanpiirtorutiinien avulla. Vaan vielä hieman
- pitää pinnistää päästäksemme tavoitteeseemme C:llä. Alla muutamia
- totuuksia sinistä ja kosinista:
- 1) Ne palauttavat 99.99% tilanteista arvon joka on yli -1 ja alle 1,
- joten jos leikitään kokonaisluvuilla saadaan tulokseksi 0
- 2) Koska arvoalue on niin pieni, täytyy aina käyttää joko liukulukuja
- (float) taikka fixed point -lukuja. Fixedeinäkin sietää käyttää
- monta bittiä desimaaliosalle, jottei tule karkeita muotoja.
- 3) Kuten muitakin matikkatavaroita käyttävät funktiot, myös sinit ja
- kosinit vaativat math.h:n ja libm.a:n (käännösoptio -lm) mukaansa
- toimiakseen.
- 4) Parametrina funktioille sin() ja cos() annetaan luku RADIAANEINA,
- muunto tapahtuu seuraavasti:
- radiaanit = 3.1415 * 2 * kulma / MAX_KULMA
- MAX_KULMA on sama kuin suurin_mahdollinen_kulma+1. Eli
- normaalistihan se on 360, mutta tietokoneella käytetään usein
- 256-, 512- ja jopa 1024-asteisia kulmia, sillä ne ovat huomattavan
- helppoja laskea. Etenkin 256-asteinen on näppärä, sillä kun
- suurinta kulman arvoa 255:ttä korotetaan ja laskuri on tyyppiä
- unsigned char, niin se pyörähtää automaattisesti ympäri, takaisin
- nollaan. Huomaa myös, että 360-asteisillakin ympyröillä maksimi
- kulma on 359!
- Nyt kun tiedät kaiken tärkeän, niin olet valmis käyttämään taitojasi
- käytännössä. Mutta vielä yksi asia: Taulukointi. Sini ja kosini ovat
- molemmat luvattoman hitaita suorittaa, joten parasta on tehdä
- lookup-taulukko niille. Eli teemme 256-alkioiset taulukot sekä sinille
- ja kosinille ja laskemme niihin molempiin valmiiksi arvot sinistä ja
- kosinista kulmille 0...255 (käytämme siis 256-asteista ympyrää):
- int loop;
- float sintab[256], costab[256];
- for(loop=0; loop<256; loop++) {
- sintab[loop] = sin(3.1415*2*(float)loop/256.0);
- costab[loop] = cos(3.1415*2*(float)loop/256.0);
- }
- Nyt ei sitten tarvita turhia vääntelehtimisiä minkään
- radiaanikonversion kanssa tai muutakaan yhtä epämiellyttävää, vaan
- toiminta on suorasukaista. Laittakaamme aluksemme kohti kaakkoa:
- x_suunta = costab[45];
- y_suunta = sintab[45];
- Jotain oli vielä... aijuu, se plasma! No jotkut ovat ehkä tämän
- tehneet jo ja toiset ovat tehneet ainakin
- paletinpyöritysplasman. Mutteivät vielä sitä aitoa ja oikeaa. Vaan nyt
- tulee asiaan muutos. Liikkuva plasma on oikein mukava olla olemassa ja
- tässä tulee idea lyhykäisesti:
- 1) Tehdään 6 kappaletta eri "korkuisia" sinikäyriä (siis hypotenuusan
- pituus / ympyrän säde / sinin kerroin) ja jotka mielellään alkavat
- eri kohdista ja joissa aallon pituus on eri (eli toinen käyrä on
- kuin 256-asteista ympyrää varten tehty ja toinen taas kuin
- 128-asteista jne.). Myös kosinia kannattaa käyttää.
- Idea on kuitenkin se, että jokainen käyrä tallennetaan omaan
- taulukkoonsa ja että jokaista käyrää voidaan "monistaa" peräkkäin,
- eli jos samaa käyrää piirretään kaksi peräkkäin ei käyrä katkea
- kesken, siis:
- VÄÄRIN:
- \ \ \
- \ \ \
- \ / \ / \ /
- \---/ \---/ \---/
- OIKEIN:
- \ /--- \ /--- \ /---
- \ / \ / \ /
- \---/ \---/ \---/
- Eli koska käyrää joudutaan toistamaan peräkkäin niin jos se ei
- palaa lähtöpaikkaansa loppuun mennessä tulee sahalaitaa. Plasman
- tapauksessa tuloksena on epämiellyttävän näköisiä loikkauksia
- muuten pehmeissä väriliu'uissa.
- Alla esimerkki kuuden erilaisen käyrän alustuksesta ja
- generoinnista:
- float wave1[256], wave2[256], wave3[256],
- wave4[256], wave5[256], wave6[256];
- int loop;
- for(loop=0; loop<256; loop++) {
- wave1[loop]=cos(3.1415*2* (float) loop /256) * 25.0 + 25.0;
- wave2[loop]=cos(3.1415*2* (float) (loop%128) /128) * 15.0 + 15.0;
- wave3[loop]=cos(3.1415*2* (float) (255-loop) /256) * 17.5 + 17.5;
- wave4[loop]=sin(3.1415*2* (float) (loop%64) /64) * 22.5 + 22.5;
- wave5[loop]=cos(3.1415*2* (float) ((128-loop)%128) /128) * 20.0 + 20.0;
- wave6[loop]=sin(3.1415*2* (float) loop /256) * 25.0 + 25.0;
- }
- Koska sin ja cos palauttavat myös negatiivisia arvoja, täytyy
- niihin lisätä sama luku kuin kerroinkin, jotta ne olisivat aina
- positiivisia. Näinollen kaikkien aaltojen summa on pahimmillaan
- kerrointen summa * 2, suomeksi maksimissaan 250.
- 2) Kun aallot ovat tallessa aletaan liikuttamaan niitä eri
- suuntiin. Käytännössä tämä hoituu käyttämällä yhtä laskuria
- jokaista aaltoa kohti, joka kertoo senhetkisen aallon alun
- sijainnin.
- Sitten vain aletaan piirtämään. Kolme ensimmäistä aaltoa ovat
- vaaka-aaltoja ja kolme viimeistä pystyaaltoa. Tämä tarkoittaa
- sitä, että kolmen ensimmäisen aallon alkion numero riippuu
- väritettävän pikselin x-koordinaatista ja kolmen viimeisen indeksi
- on riippuvainen y:stä. Kun indeksi vielä typistetään välille
- 0...255 and-funktion maskilla 0xFF niin voimme laskea jokaiselle
- aallolle oikein indeksin:
- wave1[ ( ind1 + x ) & 0xFF ]
- wave2[ ( ind2 + x ) & 0xFF ]
- wave3[ ( ind3 + x ) & 0xFF ]
- wave4[ ( ind4 + y ) & 0xFF ]
- wave5[ ( ind5 + y ) & 0xFF ]
- wave6[ ( ind6 + y ) & 0xFF ]
- Sitten vain ynnätään jokaiselle pikselille aallon senhetkiset
- alkiot yhteen ja saatu luku tungetaan ruudulle pikselin
- väriarvona. Koska x- ja y-koordinaatit liikuttavat tasaisesti
- aaltojen indeksiä eteenpäin syntyy ynnäämällä tasainen kumpuileva
- värimaasto. Eli piirtolooppi:
- for(x=0; x<320; x++) for(y=0; y<200; y++) {
- dblbuf[y*320+x] =
- wave1[ ( ind1 + x ) & 0xFF ]+
- wave2[ ( ind2 + x ) & 0xFF ]+
- wave3[ ( ind3 + x ) & 0xFF ]+
- wave4[ ( ind4 + y ) & 0xFF ]+
- wave5[ ( ind5 + y ) & 0xFF ]+
- wave6[ ( ind6 + y ) & 0xFF ];
- }
- Jonka ulkopuolella korotetaan ja vähennetään aaltojen
- aloituskohtia (ind1-ind6) ja tätä toimintaa toistetaan niin kauan
- kunnes painetaan nappia. Täydet sorsat EXAMPLE-hakemistosta
- tiedostosta PLASMA.C.
- Siinä olikin tämän kappaleen asia. Nyt taidan katsoa läksyni ja tehdä
- esimerkit loppuun. Vielä pitäisi paletin kvantisointi saada tehtyä
- ennen joulua, katsotaan ehdinkö ajoissa, pakko kai kyllä varmaan on. :)
- No niin, nyt vain piirtelemään ihme käyriä ja kuvioita paperille ja
- ruudulle, jota sinin ja kosinin syvin olemus selviää täydellisesti.
- 7.8 Paletin kvantisointi - Median cut
- -------------------------------------
- Nyt ollaankin sitten kyynärpäitä myöden mudassa. Paletin kvantisointi
- lienee sellainen temppu, jota eivät kaikki kokeneemmatkaan osaa, ja
- kaiken lisäksi se on suhteellisen vaikea homma. Joten kiinnittäkää
- turvavyönne ja valmistautukaa yritykseeni selventää hieman asiaa
- tuntemistani kvantisointitavoista helpomman, tai ainakin nopeamman,
- osalta.
- Eli mistä nyt sitten puhumme? Paletin kvantisointi on sitä, että kun
- sinulla on vaikkapa 6 erilaista PCX-kuvaa, joissa on yhteensä 1321
- erilaista väriä, niin esittääksesi nämä 1321 erilaista väriä ruudulla
- täytyy sinun KVANTISOIDA palettisi. Kvantisointi on siis värimäärän
- tiputusta. Ja nyt seuraa se, miten sen teemme.
- Ensin hieman funktioiden ideasta. Kuvittelemme värit kuutiona, jossa
- x-y-z -akseliston tilalla onkin r, g ja b. Meillä on siis
- ns. rgb-avaruus, jossa värin sijainnin kuutiossa kertoo punaisen,
- vihreän ja sinisen komponentin määrä. Jos jokainen koordinaatti on
- välillä 0...63 (kuten normaaleissa PCX-kuvissa), niin meillä on
- 64*64*64 pikseliä sisältävä kuutio, jonka särmän pituus siis on 64
- pituusyksikköä.
- Tämän monimutkaisen ajatusrakennelman pohjalle perustuu
- algoritmimme. Kun teemme taulukon, joka sisältää kaikki erilaiset
- värit on taulukon jokainen alkio (rgb-tripletti) piste
- kuutiossa. Funktiomme etsii sen akselin (siis r, g tai b), joka on
- "pisin", eli suomeksi katsotaan jokaisen värikomponentin pienin ja
- suurin esiintyvä arvo. Sitten funktio jakaa värikuution kahtia
- täsmälleen siten, että puolet pisteistä/väreistä jää toiselle ja
- puolet toiselle puolelle. Ei siis suurimman ja pienimmän väriarvon
- puolesta välistä!
- Kun koko kuutio on jaettu, niin kutsumme vain samaa jakofunktiota
- kummallekin pienemmälle kuutiolle, jossa molemmissa on nyt siis yhtä
- monta väriä. Nämä funktiot etsivät oman palasensa pisimmän
- värikomponenttien välin ja jakavat kuution kahtia kutsuen itseään
- molemmille kuutioille.
- Funktio, joka kutsuu itseään on ns. rekursiivinen funktio ja se on
- näppärä monessa asiassa. Jos piirrät paperille yhden laatikon ja siitä
- lähtemään kaksi laatikkoa, joista kummastakin lähtee kaksi laatikkoa,
- joista jokaisesta lähtee kaksi laatikkoa jne., niin saat huomaat, että
- joka rekursiotasolla funktioiden "määrä" kaksinkertaistuu. Jos siis
- asetamme rekursiorajaksi 3, niin värit jakautuvat 2^3:n, eli
- kahdeksaan pienempään kuutioon. Kvantisointi 256:een väriin vaatii
- siis kahdeksan rekursiotasoa, jotta saataisiin kuutio 256:een osaan.
- Nyt te, jotka saitte ahaa-elämyksen (jokaisen pitäisi saada ;) menette
- tekemään oman funktionne. Tyhmemmille ja kenties niille, jotka
- haluavat saada vielä hieman varmennusta tulee kuitenkin vielä
- teknisempi selostus, jonka teossa apuna on käytetty skenelehti
- Imphobian osan 10 sisältämää informaatiota. Kiitoksia Fakerille.
- Ensimmäiseksi teemme iiison taulukon, jonka koko on kaikkien
- mahdollisten värien yhteenlaskettu määrä. Jos valitsemme 64 sävyä
- jokaiselle värikomponentille saamme siis kooltaan 64x64x64 kokoisen
- värikuution. Varaamme tälle muistia:
- unsigned char *PaletteArray=(unsigned char *)calloc(64*64*64, 1);
- (huomaa kaikkien alkioiden nollaus alussa) Kun haluamme lisätä värin
- kvantisoitavien joukkoon, merkkaamme yksinkertaisesti tämän kuution
- vastaavan pikselin ykköseksi. Näin meillä on kvantisoinnin alkaessa
- kuutiossa ykköstä käytettyjen värien kohdalla ja voimme koostaa niistä
- näppärästi värilistan sisältäen kaikki kvantisoitavat
- värit. Koordinaatin kuutiossahan voimme laskea vaikka kaavasta:
- r*64*64 + g*64 + b
- No niin, sitten itse kvantisointirutiiniin. Funktion tehtävä on siis
- ottaa kaikki pikselit tietyltä värikuution osakuutiolta ja katsoa mikä
- värikomponentti vaihtelee eniten (eli tummimman ja vaaleimman värisävyn
- ero on suurin). Osakuution kuvailemiseksi tarvitsemme tietenkin rajat
- kuutiolle, eli tummimman ja vaaleimman mukaan otettavan sävyn kustakin
- värikomponentista, eli:
- int RedSubspaceBottom;
- int GreenSubspaceBottom;
- int BlueSubspaceBottom;
- int RedSubspaceTop;
- int GreenSubspaceTop;
- int BlueSubspaceTop;
- Nämä funktiot ovat siis punaisen, vihreän ja sinisen alimmat sallitut
- pitoisuudet ja vastaavasti kolme viimeistä korkeimmat sallitut. Hyvä
- idea on laittaa tällaisen kuution tiedot yhteen rakenteeseen. Sekaan
- laitamme vielä tilaa funktion laskemille kunkin värisävyn
- optimiarvoille, joka siis lasketaan sitten, että jos kuutio
- halkaistaan tämän sävyn kohdalta, jää molemmille kuution puolikkaille
- yhtä monta väriä:
- int OptimalRed;
- int OptimalGreen;
- int OptimalBlue;
- Nämä kaikki on esimerkissä laitettu structiin BORDERS. Nyt kun meillä
- on pätevä rakenne kuution määrittelemiseksi, niin voimmekin alkaa
- pohtimaan käytännön toimia, mitä rekursiivisen kuutionjakajamme tulee
- toteuttaa. Idea on seuraava:
- 1) Tyhjennetään punaisten, vihreiden ja sinisten värikomponenttien
- laskurit (RedCount[64], GreenCount[64] ja BlueCount[64]).
- 2) Lasketaan kuution rajojen sisällä jokaisen värikomponentin sävyn
- määrä looppaamalla kaikki kvantisoitavat värit läpi ja katsomalla,
- ovatko värin rgb-arvot parametrina annetun Borders (tyyppiä
- BORDERS) sisällä ja jos ovat, niin korotetaan vastaavia punaisen,
- vihreän ja sinisen laskureita:
- RedCount[red]++;
- BlueCount[blue]++;
- GreenCount[green]++;
- Lisäksi täytyy pitää yllä tietoa pienimmästä ja suurimmasta mukaan
- otetusta värikomponentin sävystä, eli tyyliin:
- jos red < PieninPunainen
- PieninPunainen = red
- tai jos red > SuurinPunainen
- SuurinPunainen = red
- 3) Nyt kun sävyt on laskettu, seuraakin jännittävä vaihe. Muutamme
- kunkin värisävyn määrät sisältävän taulukon juoksevaksi laskuriksi,
- eli tässä näette muutoksen:
- Indeksi 0 1 2 3 4 5 6 7 8
- Aluksi 0 0 3 1 0 2 2 0 1
- Nyt 0 0 3 4 4 6 8 8 9
- Tämäntyyppinen rutiini toimii:
- for(loop=1; loop<64; loop++)
- RedCount[loop]+=RedCount[loop-1];
- Nyt värisävytaulukossa on siis tietyn indeksin kohdalla, ei
- suinkaan sen sävyn määrä, vaan siihen värisävyyn 'mennessä'
- olleiden värien määrä. Nyt vielä etsitään se 'optimaalinen'
- katkaisukohta kulkemalla kohti taulukon loppua, kunnes olemme
- ohittaneet (noin) puolet pikseleistä, eli kun laskuri on suurempi
- kuin RedCount[63]/2 (joka on siis kaikkien mukana olevien värien
- määrä jaettuna kahdella). Onnistuu esim. seuraavasti:
- for(loop=0; loop<63; loop++) {
- if(RedCount[loop+1]>(RedCount[63]/2)) {
- Borders.OptimalRed=loop;
- break;
- }
- }
- Älkää ihmeessä kysykö miksi se on tuollainen. Minulla oli aiemmin
- jotain ongelmia toisenlaisen lähestymistavan kanssa ja tein
- tuollaisen idioottivarman systeemin.
- Tämä toistetaan tietenkin kaikille värisävyille.
- 4) Nyt vasta kivaa tuleekin. Funktiolle parametrina annettu
- rekursiotason laskuri tarkistetaan ja toimitaan sen mukaan. Jos
- taso on 0, niin olemme siinä pisteessä, että kuutioita ei enää
- jaeta. Voimmekin kirjoittaa Borders-rakenteen optimaaliset
- värisävyt (OptimalRed, OptimalGreen, OptimalBlue) lopullista
- paletinmuodostusta odottamaan.
- Esimerkissä funktio saa parametrinaan osoittimen
- BORDERS-taulukkoon, sekä laskurin, joka kertoo montako ollaan jo
- täytetty. Niinpä tallennus onnistuu varsin vaivattomasti:
- memcpy(&BorderTable[TablesUsed[0]], &Borders, sizeof(BORDERS));
- TablesUsed[0]++;
- Jos on kuitenkin niin onnettomasti, ettei vielä olla lopussa niin
- tehtävämme on silti helppo. Etsimme pisimmän akselin vähentämällä
- alussa keräämämme suurimman ja pienimmän väriarvon sisältävät
- muuttujat toisistaan:
- red=SuurinPunainen-PieninPunainen;
- green=SuurinVihreä-PieninVihreä;
- blue=SuurinSininen-PieninSininen;
- Esimerkissä nämä muuttujat tottelevat lyhyempiä nimi sr, br, sg,
- bg, sb ja bb.
- Sitten vain katsotaan mikä on pisin akseli ja tehdään uudet
- pikkukuutiot näppärästi kahteen pienempään ja jaetaan kuutioiden
- väriavaruudet siten, että toisen ylärajaksi tulee optimiväri-1 ja
- toisen alarajaksi optimiväri. Tämä ylä- ja alarajojen muuttaminen
- siis _vain_ pisimmän väriakselin arvojen kohdalta. Esimerkistä
- löydät koodin miten tämä on toteutettu. Sitten vain kutsumme
- itseämme molemmille pienemmille kuutioille, yhtä matalemmalla
- rekursiotasolla ja annamme logiikan hoita loput.
- Tästä puuttuu vielä tarkistus, josko kuutioon kuuluu enää vain 1 väri,
- jolloin tehdään siitä suoraan paletin väri ja palataan rekursiossa
- ylöspäin (ks. esimerkkiohjelma).
- Kun itse rekursiivinen funktio on valmis, täytyy vielä hieman laittaa
- lihaa ympärille. Tarvitsemme ohjelman, joka muuttaa alussa neuvotulla
- tavalla varatun värikuution värivaraukset (eli ykköset värin kohdalla)
- normaaliksi rgb-triplettitaulukoksi, varaa muistia
- BORDERS-rakenteille, joihin optimaaliset värit tallennetaan, laskee
- tarvittavan rekursiotason ja lopuksi hoitaa alussa ykköstä ja nollaa
- sisältäneen värikuution sisältämään vastaavan sijainnin kvantisoidun
- värin.
- Viimeksimainittuun voisimmekin perehtyä hieman tarkemmin. Kun oikein
- kutsuttu rekursiivinen funktio loppuu ja palaamme takaisin, on meillä
- siististi koko kuutiomme jaettu 256:een (yleensä) pienempään
- värikuutioon. Emme kuitenkaan vielä tiedä mikä väri tarkoittaa
- milläkin välillä olevia sävyjä, joten teemme vielä yhden
- homman.
- Looppaamme jokaisen BORDERS-rakenteen läpi ja laitamme looppimuuttujan
- mukaisen arvon alussa varattuun PaletteArray-muuttujaan kaikkiin
- rakenteen ilmoittamiin pikseleihin. Eli piirrämme kuution sisään
- SubspaceBottom ja SubspaceTop -muuttujien rajoittamalle alueelle
- pienemmän kuution värillä, jonka rakenteen indeksi taulukossa
- ilmoittaa ja talletamme rakenteen Optimal-tripletin palettiin indeksin
- kohdalle.
- Kuten aiemmin mainittiin, joissakin tapauksissa paletti menee siten,
- että ennen rekursiotason 0 saavuttamista on jäljellä vain 1
- väri. Tässä tapauksessa BORDERS-rakenteisiin ei talletetakaan täyttä
- 256:tta väriä optimisävyineen, joka taas täytyy ottaa huomioon
- palettia tehtäessä. Eli ei mitään looppia välillä 0..256, vaan välillä
- 0..N, jossa N on se laskuri, jota korotetaan aina kun rekursiivinen
- funktio täyttää yhden BORDERS-rakenteen. Esimerkkiohjelmassa
- 'TablesUsed'.
- Pseudona se menisi jotenkin näin:
- looppaa loop välillä 0...TablesUsed
- looppaa r välillä border[loop].RedSubspaceBottom ..
- border[loop].RedSubspaceTop
- looppaa g välillä border[loop].GreenSubspaceBottom ..
- border[loop].GreenSubspaceTop
- looppaa b välillä border[loop].BlueSubspaceBottom ..
- border[loop].BlueSubspaceTop
- PaletteArray[r*64*64+g*64+b]=loop;
- end looppaa
- end looppaa
- end looppaa
- paletti[index].red=border[loop].OptimalRed;
- paletti[index].green=border[loop].OptimalGreen;
- paletti[index].blue=border[loop].OptimalBlue;
- end looppaa
- Sitten vain palauttamaan syntynyt paletti. Alustusfunktiomme on
- muuttanut paletinvaraustaulukon taulukoksi, josta voidaan rgb-arvojen
- avulla hakea oikea väri (colortorgb = PaletteArray[r*64*64+g*64+b]) ja
- palauttanut tarvittavan paletin, jotta väri myös näyttää joltakin.
- Paljon mainostettu Esimerkkiohjelma löytyy EXAMPLE-hakemistosta
- nimellä QUANTIZ.C. Koodi on kieltämättä vähintään viisi kertaa
- vaikeampaa kuin aiemmat esimerkit, mutta kyllä täytyy myöntää, että
- kvantisointi asianakaan ei ole läheskään niin helppoa kuin
- viivanpiirto.
- Kvantisoinnin hyödyistä voidaan olla monta mieltä, mutta yksi asia on
- varma. Jos ei kunnollista värimäärää omaavaa näyttötilaa ole
- saatavilla, niin kyllä kvantisoitu paletti aina päihittää kotikutoisen
- 2-3-2 -järjestelmän (2 bittiä punaiselle ja siniselle ja 3 vihreälle).
- Lisäksi kvantisoinnin tuloksena syntyvän kuution avulla voi tehdä
- monta kivaa asiaa, kuten esimerkiksi motion blurin (väri on uuden ja
- vanhan pikselin rgb-arvojen sekoitus) tai jotain muuta yhtä
- hyödyllistä.
- Yritä sisäistää asia. Jos ei mene kaaliin sitten millään (= mieti
- kauemmin kuin 15 minuuttia), niin ilmoittele hämäristä kohdista. Asia
- ON vaikea, mutta mielestäni selitin sen melkein ymmärrettävästi. Ja
- ne, jotka ymmärsivät idean ja tekivät oman rutiinin (vain hullut
- käyttävät esimerkkiohjelman koodia ;) saavat vain kiristää niitä
- turvavöitään, sillä ensi luvussa hieman vaikeampaa kvantisointia!
- Silti jo tämä tapa, etenkin nopeutensa ja suhteellisen hyvän
- tuloksensa ansiosta on varsin hyvä.
- 7.9 Lisää paletin kvantisointia - Local K Mean
- ----------------------------------------------
- Niille, jotka nauroivat itsensä ulos edellisen luvun
- esimerkkiohjelmasta lyödään nyt luu kerralla kurkkuun. Tätä lähemmäksi
- täydellisyyttä ette pääse - ainakaan tässä luvussa. Tämä algoritmi on
- niin hidas, että edellinen versio on tähän verrattuna kuin rasvaamaton
- salama. Myös 3Dicassa on selostettu pääpiirteittäin tämä tekniikka ja
- kumarrankin kohti Sampsa Lehtosta, sillä muokkailen hänen selostustaan
- hieman.
- Perusidea tämän takana, toisin kuin kuutioihin jakavassa
- rekursiivisessa versiossa, on pallomainen
- ajattelutapa. Värikuutiossamme onkin nyt Palloja, joiden sijainti on,
- kuten edellisessäkin, värin rgb-arvo. Koko taasen määräytyy sen
- mukaan, kuinka monta tämän väristä pikseliä löytyy kvantisoitavasta
- kuvasta. Jos et käytä kuvia tai et jostain syystä halua laskea mukaan
- pallojen vetovoimaa, johon niiden koko vaikuttaa, niin värin määrä
- kuvassa on aina 1, jolloin asialla ei kaavoissa ole merkitystä.
- Näiden väripallojen seassa liikkuu sitten paletin verran
- palettipalloja, eli yleensä 256 kappaletta. Näillä palloilla ei ole
- kokoa. Väripallot vetävät puoleensa näitä kelluvia palloja sen mukaan,
- kuinka suuria ne ovat ja nämä paletin värejä esittävät pallukat
- liikkuvat sitten näiden mukana.
- Vitsinä on se, että värit ovat kuin palloja vetäviä kappaleita ja
- palettipallot pyrkivät sijoittumaan optimaaliseen paikkaan
- väripallojen väliin. Koska jokaisella kerralla pallot liikkuvat vain
- hieman, tulee kertoja luultavasti aika useita, ennenkuin palettipallot
- ovat saavuttaneet optimaalisen sijaintinsa, joista tulee sitten
- kvantisoidun paletin rgb-arvot. Pallojen yhteenlaskettua liikettä
- käytetäänkin laskemaan sitä, milloin pallot ovat tarpeeksi lähellä
- parhaita sijaintipaikkojaan (=liike edelliseen pientä). Mitä pienempi
- liikkeen pitää vuorolla olla loppumisen tapahtumiseksi, sitä kauemmin
- homma kestää ja sitä parempi tulos tulee.
- Koska väripallot vetävät vain lähintä palettipalloa, niin jokin pallo
- voi jäädä ilman vetovoimaa. Tässä tapauksessa pallo heitetään jonkin
- värin lähelle tai kohdalle, jotta tämäkin väärälle tielle eksynyt väri
- saadaan käyttöön.
- Ja kuten Ilkan editoima selostuskin tekee, menemme sitten teknisempään
- puoleen. Niin tein minäkin tätä opetellessani, joten älkää hävetkö
- lukea tätä ennenkuin yritätte tehdä oman versionne rutiinista.
- Kvantisoinnin aluksi teemme histogrammin, eli käyrän, joka ilmoittaa
- kunkin värin määrän kuvassa. Esimerkissä käytämme sanan kokoista
- laskuria 15-bittisille pikseleille (5 bittiä jokaiselle
- värikomponentille), jolloin taulukon koko on 2^15 * 2 = 65536 tavua.
- Nollaamme sen aluksi ja sitten korotamme jokaista tietyn värin
- esiintymää kohti histogrammin tätä kohtaa yhdellä. Tietyn
- rgb-tripletin sijaintihan on taas r*32*32 + g*32 + b.
- Seuraavana sitten teemme taulukon niistä väreistä, joita todella
- kuvassa on. Tallentaa täytyy rgb-tripletin lisäksi jokaisen värin
- määrän, jonka saamme nyt histogrammista, joka taasen on 0 jos ei
- tiettyä väriä ole lainkaan. Lehtonen suosittelee seuraavanlaista
- rakennetta:
- typedef struct {
- unsigned char R; /* väriarvo */
- unsigned char G; /* väriarvo */
- unsigned char B; /* väriarvo */
- unsigned long count; /* Värimäärä kuvassa */
- } colorListStruct;
- colorListStruct colorList[32768];
- Muistia säästää tietenkin myös jos laskee värit ja varaa sitten
- staattisesti muistia systeemille:
- colorListStruct colorList=
- (colorListStruct *)malloc(sizeof(colorListStruct)*colors);
- Lisäksi täytyy vielä tallettaa kaikkien eri värien määrä kuvassa,
- vaikka muuttujaan colorListCount. Sitten seuraavana peruspaletti:
- unsigned long palette[256][3]; /* 3 = R,G & B */
- Ja muuttujien lisääminen vain lisääntyy... Teemme vielä
- värilaskuritaulukon, johon summaamme palettipalloa kutsuneiden värien
- rgb-arvot kerrottuna värin määrällä. Tarvitsemme siis suht' suuren
- lukualueen. Ja sitten vielä laskuri värien yhteismäärälle.
- unsigned long colorSum[256][3]; /* 256 väriä, 3 = R,G & B */
- unsigned long colorCount[256]; /* Voidaan yhdistää kyllä
- colorSummiinkin */
- Ja lopuksi vielä pisteenä i:n päälle läiskäisemme laskurin, joka
- laskee paletin muutoksen edelliseen.
- unsigned long variance;
- Sitten vain kvantisoimaan. Jälleen rankasti kopioituna Sampsalta
- tarvittavat askeleet. Mitäs teki niin hyvän jutun tästä. :) Eli itse
- kvantisointirutiini:
- 1) colorSum ja colorCount -laskurien nollaus ja paletin täytto
- colorList:in ensimmäisillä (256:lla) värillä.
- 2) Läpikäydään colorList:in värit. Värien määrähän löytyi
- muuttujasta colorListCount, kuten aiemmin kerrottiin.
- Loopataan c välillä 0 .. colorListCount-1
- a) Otetaan colorList:istä väri c
- b) Etsitään lähin väri palette-muuttujasta. Tuloksena numero
- välillä 0..256. Etäisyys avaruudessahan on r- g- ja
- b-etäisyyksien neliöiden summan neliöjuuri. Eli
- delta_r = abs( r2-r1 )
- delta_g = abs( g2-g1 )
- delta_b = abs( b2-b1 )
- sqrt( delta_r^2 + delta_g^2 + delta_b^2 )
- Meidän täytyy loopata joka väri ja laskea tämä etäisyys ja
- verrata sitä siihen mennessä löytyneeseen lyhimpään
- etäisyyteen ja jos uusi väri on lähempänä tallennamme tämän
- numeron ja etäisyyden ja jatkamme.
- Optimointikikkoina se, että koska toinen potenssi on aina
- positiivinen, putoaa itseisarvo (abs) pois. Ja koska
- a^2 < b^2 <=> a < b
- Niin neliöjuuriakaan ei tarvita. Esimerkkiohjelman
- Dist-funktion ydin on seuraava:
- for(loop=0; loop<Wanted; loop++) {
- dist=Dist(r, g, b,
- Palette[loop][0], Palette[loop][1], Palette[loop][2]);
- if(dist<shortest) {
- shortest=dist;
- sl=loop;
- }
- }
- Jossa dist vain palauttaa tuon delta_r^2 + delta_g^2 + delta_b^2.
- c) Lisätään lähimmän värin colorSum-taulukkoon väripallon
- rgb-arvot kerrottuna väripallon pikseleiden määrällä (count
- kappaletta tätä värisävyä). (x=lähin väri, c=looppi)
- colorSum[x][0] += colorList[c].R * colorList[c].count;
- colorSum[x][1] += colorList[c].G * colorList[c].count;
- colorSum[x][2] += colorList[c].B * colorList[c].count;
- d) Sitten täytyy vielä tallettaa montako kertaa niitä rgb-arvoja
- sinne ynnättiinkään.
- colorCount[x]+=colorList[c].count;
- 3) Nollataan liikelaskurimuuttujamme variance.
- 4) Käydään läpi kaikki värit peruspaletista (c = 0..255)
- a) Jos värin värilaskuri colorCount on suurempi kuin nolla, niin
- väri oli lähinnä ainakin yhtä väripalloa. Nyt vain laskemme
- keskiarvon kaikista kutsuneista väreistä ottamalla keskiarvon
- niistä. Koska colorSum sisältää aina n kappaletta värin c
- rgb-arvoja ja colorCount sisältää tämän luvun n niin
- keskiarvo saadaan yksinkertaisesti:
- palette[c][0] = colorSum[c][0] / colorCount[c];
- palette[c][1] = colorSum[c][1] / colorCount[c];
- palette[c][2] = colorSum[c][2] / colorCount[c];
- Jos et oikein ymmärtänyt ideaa, niin otetaan
- esimerkki. Paletin väri 5 on lähinnä kahta väripalloa:
- palloja A = (10, 20, 0) ja B = (10, 20, 5). Väriä A on
- kvantisoitavassa kuvassa 100 ja väriä B 200.
- Rutiinimmehän ensin lisää colorSum:iin värin A rgb-arvot
- kerrottuna värin A määrällä kuvassa:
- colorSum[5][0] = A.R * A.count = 10 * 100 = 1000
- colorSum[5][1] = A.G * A.count = 20 * 100 = 2000
- colorSum[5][2] = A.B * A.count = 0 * 100 = 0
- colorCount[5] = 100
- Nyt meillä on siis värin A rgb-arvot 100-kertaisena
- tallessa. Koska väriä B on kaksinkertaisesti, saamme sen
- 200-kertaisena summataulukkoomme:
- colorSum[5][0] = 1000 + 10*200 = 3000
- colorSum[5][1] = 2000 + 20*200 = 6000
- colorSum[5][2] = 0 + 5*200 = 1000
- colorCount[5] = 100 + 200 = 300
- Nyt laskemme sitten lopullisen värin paletille:
- palette[5][0] = 3000 / 300 = 10
- palette[5][1] = 6000 / 300 = 20
- palette[5][2] = 1000 / 300 = 3.33...
- Huomaamme, että koska väriä B oli kaksi kertaa enemmän, on
- rgb-arvokin lähempänä väriä B. Näin siis se väri, jota on
- kaikkein eniten, vaikuttaa suurimpana uuden värin
- rgb-arvoihin. Helppoa, eikö totta?
- b) Mittaamme paletin muutoksen alkup. väriin:
- temp = 0;
- temp += abs( R - palette[c][0] );
- temp += abs( G - palette[c][1] );
- temp += abs( B - palette[c][2] );
- variance += temp;
- c) Kirjataan väri lopullisesti uuteen palettiin.
- palette[c][0] = R;
- palette[c][1] = G;
- palette[c][2] = B;
- 5) Nyt uusi paletti on taas generoitu ja muutos muuttujassa
- variance. Nollaamme nyt colorSum- ja colorCount -taulukot.
- 6) Jos variance-muuttujan kertoma paletin muutos on vielä yli
- sallitun rajan, niin hyppäämme kohtaan 2. Jos taas olemme jo
- sallitun rajan alla, niin lopetamme. Täten mitä pienempi
- MAX_VARIANCE on, sitä useammin pyörimme looppia ja sitä kauemmin
- tämä kestää.
- Siinäpä se. Vaan ei kuitenkaan. Hitain osuus nimittäin alkoi
- nyt. Kvantisoitu paletti on kiva, vaan missä onkaan toivottu
- värikuutio? Tai edes oikeat indeksit kullekin histogrammin värille? Ei
- missään. Paletti on optimaalinen, mutta ei kerro lainkaan, mitkä värit
- sitä ovat lähinnä. Jos haluamme tietää jollekin rgb-arvolle lähimmän
- värin täytyy meidän yksinkertaisesti loopata lävitse paletin kaikki
- 256 väriä ja selvittää mihin etäisyys on lyhin. Ja koska haluamme
- värikuution outoihin tarkoituksiimme, merkitsee se tässä tapauksessa
- 256:n värin läpikäyntiä 32768 kertaa...
- Siitä vain laskemaan pojat. :) Fiksu idea voisikin olla tallentaa 64
- kilon tulos tiedostoon, ettei sitä tarvitse pelin käynnistyessä
- laskea. Säästätte hermojanne kummasti. Tosin, itse käytin kuutta
- bittiä värille, jolloin sain huimaavan 262144-tavuisen värikuution. =)
- No nyt jokainen varmasti osaa tehdä tuosta toimivan
- kvantisointisysteemin. Ja esimerkkisorsa löytyy sitten
- tiedätte-kyllä-mistä hakemistosta nimellä QUANT2.C. Ja hyvää joulua ja
- onnellista uutta vuotta vain kaikille, sillä tässä kohtaa saan
- kakkosversion valmiiksi, juuri tapaninpäivänä!
- 7.10 VESA 2.0, rakenteet
- ------------------------
- Ohhoh, vaikuttaa taas siltä että on aika alkaa puurtamaan kun
- kesäloman viimein alettua minunkin osaltani (neljä ekaa viikkoa
- kesätöissä), en enää keksi hyviä selityksiä myöhästymiselle. Eli
- homman nimi on SVGA-ohjelmointi ja tavan nimi VESA 2.0. Lähdekoodiakin
- tulee juuri sopivasti jotta kauan toivottu asia, 640x480 -kokoisten
- PCX-kuvien lataus ja näyttäminen ruudulla onnistuu. Ja jotta homma
- olisi hieman haastavampi, muutamme 256-värisen paletillisen PCX-kuvan
- vielä 16-bittiseksi ennen näyttämistä.
- Vaan ensin tongimme hieman menneisyyttä. Ihmisten alkaessa hiljalleen
- kypsymään 640x480 16-väriseen tilaan alkoivat näytönohjainvalmistajat
- tekemään näyttökortteja, jotka tukivat 640x480-tilaa 256
- värillä. Sitten pikkuhiljaa jokaiselta valmistajalta alkoi tippumaan
- uusia kortteja, jotka pystyivät yhä parempiin näyttötiloihin ja olivat
- mahdollisimman toimimattomia toistensa kanssa. Samoin on asian laita
- nykyäänkin, suoraan korttia ohjelmoimalla päästään hyviin nopeuksiin,
- mutta valitettavasti ei ole kovin mukavaa jos ohjelmasi toimii esim 2%
- maailman SVGA:n alla toimivasta konekannasta, toistaiseksi.
- Tilanteen muuttuessa yhä sekavammaksi markkinoilla hätiin riensi
- VESA-niminen standardointijärjestö (Video Electronics Standards
- Assocation tjsp.) ja luotiin yhtenäinen rajapinta jota käyttäen
- voitaisiin ohjelmoida kaikkia sitä tukevia kortteja. Ja kehityksen
- kehittyessä VESA on muodostunut varsin suosituksi tavaksi käyttää
- korkeampia näyttötiloja. Etenkin versio 2.0 tarjoaa lähes lyömättömän
- tavan tehdä näyttäviä graafisia sovelluksia. Ja miten tämä meihin
- liittyy? No me tietenkin opettelemme käyttämään tätä rajapintaa!
- Heti alkuun totean, että nämä kappaleet käsittelevät VESA 2.0:llaa
- erittäin puutteellisesti. Oletan että näytönohjaimesi on VESA
- 2.0-yhteensopiva ja tukee juuri määrättyä tilaa, 640x480 16-bittisillä
- väreillä. Jätän 90% standardin funktioista ja mahdollisuuksista
- käyttämättä. Täydellisen, lähes 1400-rivisen englanninkielisen
- dokumentaation löytää esim. MBnetistä nimellä VBE20.ZIP tai Scitech
- softwaren kotisivuilta, joka dokumenttia tietääkseni levittääkin,
- osoitteessa www.scitechsoft.com. Scitech Display Doctor, eli entinen
- UniVBE on myös SE ohjelma jos näytönohjaimesi VESA 2.0-tuki on
- vajaavainen (esim. oman korttini 20 VESA 2.0-näyttötilaa laajenevat
- tuon myötä 58:aan).
- Toisaalta kun VESA 2.0:llan info-rakenteen osaa ja ymmärtää
- moodi-infon rakenteen ja tietää miten VESA-yhteensopiva tila
- asetetaan, pystyy tekemään lähes millaisen ohjelman tahansa, uupumaan
- jää vain mahdollinen hardware-tason tuki skrollaukselle ja muulle
- vastaavalle (jota todellinen guru ei tietenkään tarvitse).
- Tämä luku kattaa kahden olennaisimman VESA 2.0:llaan liittyvän
- rakenteen määritelmän ja kaikkien kenttien selostuksen. Myöhemmissä
- luvuissa tutkimme hieman käyttöä. Tässä ensimmäinen, ns. VESA
- information struct, suoraan VESA-enginestäni revittynä:
- /* DJGPP laajentaa esim. tavun kokoiset rakenteen kentät 4-tavuisiksi
- nopeuden takia ilman tätä määrettä, jos tämä puuttuu
- palautettavista rakenteista on tieto täysin päin seiniä. Voit
- kokeilla. */
- #define PACKED __attribute__ ((packed))
- typedef unsigned long int dword; /* Kaksoissana 4 tavua */
- typedef unsigned short int word; /* Sana 2 tavua */
- typedef signed long int s_dword; /* Etumerkillinen kaksoissana 4 tavua */
- typedef signed short int s_word; /* Etumerkillinen sana 2 tavua */
- typedef unsigned char byte; /* Tavu */
- typedef unsigned bit; /* Bitti */
- typedef struct {
- byte sign[4] PACKED;
- word version PACKED;
- dword OEMstring PACKED;
- bit fixedDAC : 1 PACKED;
- bit VGAcompatible : 1 PACKED;
- bit RAMDACtype : 1 PACKED;
- bit reserved1 : 29 PACKED;
- dword videomodeptr PACKED;
- word totalmemory PACKED;
- word OEMsoftwarerev PACKED;
- dword OEMvendornameptr PACKED;
- dword OEMproductnameptr PACKED;
- dword OEMproductrevptr PACKED;
- byte reserved2[222] PACKED;
- byte OEMdata[256] PACKED;
- } VesaInformation;
- sign täytetään merkkijonolla "VESA", jos näytönohjaimesi tukee
- VESA-tiloja. Jos halutaan ns. laajennettu tietokenttä, tämä tulee
- ennen kutsua asettaa olemaan "VBE2", jolloin VESA 2.0-toteutus
- ymmärtää täyttää uudet VESA 2.0:llan mukanaan tuomat kentät.
- version taas on BCD-muotoinen versionumero (käytetään vain heksoja
- 0h-9h, eli versio voi olla 0000h - 9999h). Ylemmät kaksi heksaa ovat
- suurempi versionumero (haluamme sen olevan väh. 02h) ja alempi
- pienempi (edellisessä versiossa 1.2 olisi luku 0102h).
- OEMstring on reaalitilan (seg:off, eli segmentti ja offsetti)
- osoitin NULL-päätteiseen valmistajan määrittelemään merkkijonoon.
- OEMsoftwarerev kertoo version-kenttää vastaavasti versiotietoa, ja
- OEMvendornameptr, OEMproductnameptr ja OEMproductrevptr taasen
- ovat reaalitilan osoittimia valmistajan ja tuotteen nimeen sekä
- tuotteen versionumeroon. VESA 2.0-yhteensopivien toteutusten tulisi
- laittaa merkkijonot OEMdata-alueelle (ei ROM-muistiin tai muuallekaan
- infoblokin 512 tavun ulkopuolelle), jolloin kopioidessasi infoblokin
- ohjelman omaan datasegmenttiin DOS-muistialueelta voidaan nämä
- pointteritkin muuttaa toimiviksi suojatun tilan osoitteiksi näytölle
- tulostamista varten.
- fixedDAC on 0 jos DAC on aina kuusi bittiä per värikomponentti, ja 1
- jos se on vaihdettavissa kahdeksaan bittiin komponenttia kohden. DAC
- hoitaa palettia.
- VGAcompatible on 0 jos kontrolleri on VGA-yhteensopiva ja 1 jos
- ei. Eli VGA-portit toimivat ja normaalit videotilat myös. Varmaan
- jokaisessa PC-näytönohjaimessa 0.
- RAMDACtype on 0 jos RAMDAC on "normaali", 1 tarkoittaa että
- VESA-keskeytyksessä 09h tulee asettaa blank bitti. Käytännössä tämä ja
- fixedDAC koskevat sinua vain jos käytät paletti-tilaa (256 väriä),
- johon en tässä luvussa ainakaan puutu. Blank-bitti palettia
- asetettaessa estää "lumisateen" ruudulla ajoittamalla paletin
- muutokset muualle kuin piirron ajalle.
- videomodeptr on kaikkein tärkein info version-kentän jälkeen, sillä se
- osoittaa tilojen numerot sisältävään listaan, joka lopetetaan -1:llä
- (0xFFFF). Ohjelman tehtävä on tarkistaa onko jokin moodi todella
- olemassa, esimerkiksi UniVBE asettaa minulla tuonne tiloja joita ei
- ole olemassa. Lisää virheentarkastuksesta alempana.
- totalmemory kertoo käytössä olevan muistin määrän 64kilon palasissa,
- eli 1 mega esimerkiksi on 16.
- No nyt vielä se toinen tärkeä rakenne, moodi-info, jota voi pyytää
- halutulle tilalle jos vain tietää sen numeron. Ja ne numerothan
- löytyivät jo info-blokista, joten nyt vain rakennetta tutkimaan. Tämä
- rakenne on hirviö:
- typedef struct {
- bit modesupported : 1 PACKED;
- bit reserved : 1 PACKED;
- bit TTYsupported : 1 PACKED;
- bit colormode : 1 PACKED;
- bit graphicsmode : 1 PACKED;
- bit notVGAcompatible : 1 PACKED;
- bit notVGAwindowmemory : 1 PACKED;
- bit linearmodeavailable : 1 PACKED;
- bit reserved1 : 8 PACKED;
- bit Arelocatablewindows : 1 PACKED;
- bit Areadablewindow : 1 PACKED;
- bit Awritablewindow : 1 PACKED;
- bit reserved2 : 5 PACKED;
- bit Brelocatablewindows : 1 PACKED;
- bit Breadablewindow : 1 PACKED;
- bit Bwritablewindow : 1 PACKED;
- bit reserved3 : 5 PACKED;
- word windowgranularity PACKED;
- word windowsize PACKED;
- word windowAsegment PACKED;
- word windowBsegment PACKED;
- dword windowfunctionptr PACKED;
- word bytesperscanline PACKED;
- word horizontalresolution PACKED;
- word verticalresolution PACKED;
- byte characterwidth PACKED;
- byte characterheight PACKED;
- byte planes PACKED;
- byte bitsperpixel PACKED;
- byte banks PACKED;
- byte memorymodel PACKED;
- byte banksize PACKED;
- byte imagepages PACKED;
- byte reserved4 PACKED;
- byte redbits PACKED;
- byte redshift PACKED;
- byte greenbits PACKED;
- byte greenshift PACKED;
- byte bluebits PACKED;
- byte blueshift PACKED;
- byte reservedbits PACKED;
- byte reservedshift PACKED;
- bit programmablecolorramp : 1 PACKED;
- bit reservedbitsusable : 1 PACKED;
- bit reserved5 : 6 PACKED;
- dword physicalbasepointer PACKED;
- dword offscreenmemoryoffset PACKED;
- word offscreenmemorysize PACKED;
- byte reserved6[206] PACKED;
- } VesaModeInformation PACKED;
- modesupported kertoo onko tila edes tuettu. (1 = tosi, 0 = epätosi)
- TTYsupported kertoo tukeeko toteutus tekstin tulostusfunktioita
- colormode on 1 jos tila on väritila, 0 jos mustavalkoinen
- graphicsmode on 1 jos tila on grafiikkatila, 0 jos tekstitila
- notVGAcompatible on 1 jos tila ei tue VGA-rekistereitä ja IO-portteja
- notVGAwindowmemory on 0 jos banked-tilat ovat mahdollisia, 1 jos eivät
- linearmodeavailable on 1 jos LFB on, 0 jos ei. Lisää termeistä alempana
- A- ja B-alkuiset kolme kenttää, relocatablewindows, readablewindow ja
- writeable window kertovat, voiko ikkunaa A tai B (riippuen nimen
- alkukirjaimesta) siirtää, voiko sitä lukea ja voiko siihen
- kirjoittaa. Nämä tiedot ovat ns. banked-tiloille, jossa videomuistia
- katsotaan yleensä reaalitilassa olevista "ikkunoista", joita voidaan
- sitten siirrellä. windowgranularity kertoo kilotavuina, kuinka
- pienissä askelissa ikkunaa voidaan siirtää näyttömuistissa ja
- windowsize kuinka suuri ikkuna on. windowAsegment ja B-vastaava
- kertovat CPU-osoiteavaruudessa ikkunoiden osoitteet, A000h:ta
- näytti minulla, eli ihan normaali VGA-muistisegmenttihän tuo yleensä
- on (muista vain että segmentti kerrotaan kuudellatoista jotta saadaan
- suojatun tilan offsetti, ja muista että selektori on _dos_ds, eli
- pitää käyttää movedata, _farpoke* tai dosmemput-komentoja).
- En puutu banked-tiloihin tässä luvussa, vaan oikaisen ja kerron
- linear-tiloista. Banked-tiloissa tarvitaan paljon logiikkaa jotta
- voidaan selvittää millaisissa pätkissä pitää näyttömuistissa liikkua,
- kumpaa ikkunaa A vai B voi liikuttaa vai voiko molempia, saako niihin
- kirjoittaa jne. Banked-tilassa kaksoispuskurin kopiointi ruudulle
- tehdään siten, että siirretään ikkuna muistin alkuun, kirjoitetaan
- ikkunan pituuden verran kaksoispuskurista, siirretään ikkunaa
- eteenpäin granularity-muuttujan sallimissa rajoissa pitkin
- näyttömuistia (esim. 64 kiloa kerrallaan), kirjoitetaan seuraava pala
- jne.
- Tarvittavalla älykkyysosamäärällä ja ehkä hitusella englannin kielen
- taitoa varustettu yksilö kyllä pystyy tekemään banked-tuen
- halutessaan. windowfunctionptr on reaalitilan osoitin (seg:off)
- rutiiniin, joka vaihtaa nopeasti ikkunan sijaintia. Valitettavasti
- tällaisen funktion kutsumiseen tehtävät valmistelut ovat sen verran
- massiivista luokkaa (lue: en jaksa alkaa perehtymään asiaan), että en
- ala niitä tässä esittelemään.
- bytesperscanline on hyödyllinen tieto myös LFB:tä käytettäessä. Se
- kertoo montako tavua yksi rivi näyttötilassa vie. Yleensä on
- turvallista olettaa että se on suoraan vaakaresoluutio kerrottuna
- tavuilla per pikseli, mutta joillakin korteilla, esim. Matroxilla
- kuulemma nuo joskus ovat jotain ihan muuta. Tämä poistaa sinulta
- mahdollisuuden käyttää koko kaksoispuskurin kopioimisen kerrallaan
- näyttöpuskuriin, mikä tietenkin hidastaa ohjelmaa. Kannattaa ehkä
- tehdä kaksi piirtofunktiota, joista vain toinen ottaa huomioon tämän
- seikan.
- horizontalresolution ja verticalresolution kertovat tilan vaaka- ja
- pystyresoluution. Tekstitilassa nämä arvot ovat riveinä,
- characterwidth ja characterheight -muuttujien ilmoittaessa merkin
- leveyden ja korkeyden pikseleissä.
- planes kertoo muistitasojen määrän tässä tilassa. Käytännössä näillä
- on väliä vain 16-värisissä tiloissa ja mode-x:ää vastaavissa
- VESA-tiloissa. Normaalisti tämä on 1.
- bitsperpixel kertoo montako bittiä pikselille tässä tilassa on
- varattu. 16-värisessä tämä on 4, 256-värisessä 8, 16 tai 15
- highcolor-tiloissa ja 24 tai 32 truecolor -tiloissa. Jos käytät
- tasoja, on bittien määrä / taso yleensä suoraan bitit/tasojen_määrä,
- eli 4-tasoinen 256-värinen tila olisi 2 bittiä per taso.
- banks kertoo montako scanline-bankkia kussakin tilassa
- on. Esim. CGA:ssa kaksi ja Herculeksella neljä. Yleensä tämä on 1
- memorymodel kertoo minkä tyyppinen muistimalli tilassa
- on. Muistimalleja on 8:
- 00h Tekstitila
- 01h CGA
- 02h Hercules
- 03h Plane-tyyppinen
- 04h Pakattu pikseli (yleinen 256-värinen)
- 05h Non-chain 4, 256-värinen
- 06h Suora värimalli (jota me tulemme käyttämään)
- 07h YUV (värimalli, tyyliin RGB)
- 08h-0Fh = Varattuja VESA:n määriteltäviksi
- 10h-FFh = Varattuja valmistajan määriteltäviksi
- banksize kertoo aiemmin kuvattujen scanline-bankkien määrän. Jos
- jollakulla on jotain aavistusta näistä scanline-bankeista, tasoista ja
- plane- pakatun pikselin, non-chain ja YUV-muistimalleista, olen
- kiinnostunut tietämään, tässä moodi-info -rakenteessa on minulle
- ainakin lähes outoja kenttiä ihan tarpeeksi.
- imagepages kertoo montako ylimääräistä ruutua näyttömuistiin mahtuu,
- eli ohjelma voi ladata useampia ruudullisia muistiin ja vaihdella
- näiden välillä.
- Sitten tuleekin varmaan meitä eniten kiinnostava osa. Suoran
- värimallin tiloissa (memorymodel = 06h) 15-, 16-, 24- ja 32-bittisissä
- tiloissa käytetään aina tietty määrä bittejä ilmaisemaan kutakin
- värikomponentteja. Esimerkiksi 16-bittisessä tilassa yleensä on 5
- bittiä punaiselle, sitten 6 vihreälle ja vielä 5 siniselle. Näin
- meillä on 2^5 = 32 sävyä punaiselle ja siniselle ja 2^6 vihreälle
- (jonka eri sävyjä silmä parhaiten erottaa). Jotta voimme koota ne
- meidän täytyy vielä shiftata bittejä oikeille paikoilleen:
- #define RGB16(r,g,b) ((r<<11) + (g<<5) + b)
- Shiftaukset ja bittien määrä värikomponenttia kohden vaihtelevat ja
- moodi-infossa jokaiselle värikomponentille on määritetty bittien määrä
- ja shiftaus (redbits, redshift, bluebits...). Jos ohjelma ottaisi
- kaikki mahdolliset yhdistelmät huomioon, olisi homma luultavasti
- tuskallisen hidasta. Onneksi kuitenkin on varsin turvallista olettaa
- seuraavia asioita:
- 15-bittinen värimalli on 5:5:5, eli kaava on (r<<10)+(g<<5)+b
- 16-bittinen värimalli on 5:6:5, eli kaava on (r<<11)+(g<<5)+b
- 24-bittinen värimalli on 8:8:8, eli kaava on (r<<16)+(g<<8)+b
- 32-bittinen värimalli on sama kuin 24, mutta 8 ylintä bittiä jäävät
- käyttämättä ja näin ollen kaikkien pikselien muistiosoitteet ovat
- neljällä jaollisia, paljon helpompaa kuin 24-bittisten tilojen
- kolmella jaolliset.
- Joskus tietenkin poikkeuksia voi olla ja on parasta tarkistaa ennen
- moodin asettamista, täsmäävätkö shiftaukset ja bitit oletuksiin ja
- tulostaa vaikka virheilmoitus, jos näin ei käy. Jos haluaa että
- ohjelma varmasti toimii kaikilla korteilla, voi tehdä yleisluontoisen,
- mutta kylläkin älyttömän hitaan muuttujia käyttävän systeemin.
- Tiivistelmänä shift- ja bits-kentistä, että tee päätös bitsperpixelin
- mukaan ja tarkista ennen moodiin siirtymistä vielä että shiftaukset
- ovat oikeita moodi-infossa ja jos ne eivät täsmää, älä asetakaan
- tilaa.
- programmablecolorramp kertoo voiko väriramppia ohjelmoida. Jos arvo on
- 0 ei sitä voi muuttaa, mutta jos se on ohjelmoitava, voit säätää
- punaiselle, vihreälle ja siniselle haluamasi muotoisen
- värirampin. Voit esim. tehdä siten, että jos punaiselle on 64 sävyä
- niin sen sijaan että 0 on ei yhtään ja 64 on maksimi, saavutetaan
- maksimi jo 32:ssa ja loppu on tasaisen punaista. Tästä on hyötyä
- käytännössä vain gamma-korjauksessa ja joissakin
- erikoisefekteissä. Jos asia kiinnostaa kannattanee tutustua
- VBE20.TXT:n keskeytykseen 09h.
- reservedbitsusable on 1 jos yli jäävät (esim 32-bittisessä tilassa
- yleensä 8 ylimmäistä) bitit ovat käytettävissä.
- physicalbasepointer on LFB:n aloitusoffset fyysisessä
- muistiavaruudessa (eli alkaen muistin alusta aina maksimiin 4
- gigaan). Tämä on tärkeimpiä kenttiä jos et käytä banked-tiloja.
- Jos LFB ei ollut tuettu tämä kenttä on 0.
- offscreenmemoryoffset kertoo kuinka pitkällä näyttömuistin alusta on
- ohjelman käytettävissä oleva ylimääräinen näyttömuisti ja
- offscreenmemorysize kertoo montako kilotavua siinä on.
- Huhhuh. Tämä käy työstä. Pahoittelen jos näissä kenttien selostuksissa
- on jotain epäselvyyksiä, ilmoitathan asioista joita et ymmärrä
- minulle, en tiedä kuinka selvää tämä on sellaiselle joka ei vielä
- VESA-tiloja ole ohjelmoinut.
- 7.11 VESA 2.0, ensimmäiset keskeytykset
- ---------------------------------------
- No niin, struktit ovat siis hallinnassa? Sitten hommiin. VESA 2.0:llan
- käyttö on tiivistettynä seuraavanlaista:
- 1) Otetaan talteen info-structi ja tarkistetaan että kortti on
- VESA 2.0-yhteensopiva (versionumerosta)
- 2) Luetaan structin lopusta tuettujen VESA-tilojen numeroiden lista
- 3) Tutkitaan mikä tuetuista numeroista on se tila jonka haluamme (eli
- mennään yksitellen lävitse kaikki, pyydetään moodi-info, tutkitaan
- ja jos ei ole oikea, jatketaan eteenpäin).
- 4) Löydettyämme oikean asetetaan tila.
- Sitten hieman VESA-rajapinnan toiminnasta. VESA 2.0 on, kuten
- edeltäjänsä, vanhan video-keskeytyksen 10h alle tehty joukko
- keskeytyksiä. Tunnus VESA-keskeytykselle on arvo 4Fh
- ah-rekisterissä. Al-rekisteriin asetetaan halutun toiminnon numero
- (joista kolme ensimmäistä, 00h-02h selitetään tässä). Systeemi toimii
- myös 16-bittisissä sovelluksissa, mikä tarkoittaa käytännössä sitä,
- että kun keskeytykselle annetaan muistialueen osoite, jonne
- informaatiorakenne tulee kirjoittaa, täytyy sen sijaita
- perusmuistissa. Keskeytyksen kutsun jälkeen ax:ssä palautetaan
- seuraavanlainen palautusarvo:
- AL == 0x4F: Funktio on tuettu
- AL != 0x4F: Funktio ei ole tuettu
- AH == 0x00: Funktiokutsu onnistunut
- AH == 0x01: Funktiokutsu epäonnistui
- AH == 0x02: Softa tukee funktiota, mutta rauta ei
- AH == 0x03: Funktiokutsu virheellinen nykyisessä näyttötilassa
- Kaikki AH:n arvot muut kuin 0 täytyy tulkita yleisinä virhetiloina,
- sillä myöhemmissä VESA:n versioissa virhemäärittelyjä saattaa tulla
- lisää. Jotta voisimme käyttää normaalien rekisterien lisäksi
- tarvittavia segmenttirekisterejä, käytämme __dpmi_int:iä ja rakennetta
- __dpmi_regs. Alla esimerkkimalli VesaInt-komennosta, jolle annetaan
- vain toiminnon numero.
- int VesaInt(byte function, __dpmi_regs *regs) {
- regs->h.ah=0x4F;
- regs->h.al=function;
- __dpmi_int(0x10, regs);
- if(regs->h.al != 0x4F) {
- puts("Funktio ei tuettu");
- return -1;
- }
- switch(regs->h.ah) {
- case 0x00:
- break;
- case 0x01:
- puts("Funktiokutsu epäonnistui!");
- return 1;
- case 0x02:
- puts("Softa tukee funktiota, mutta rauta ei!");
- return 2;
- case 0x03:
- puts("Funktiokutsu virheellinen nykyisessä videotilassa!");
- return 3;
- default:
- puts("Tuntematon virhe!");
- return 4;
- }
- return 0;
- }
- __dpmi_regs-rakenteen h-kentän alta löytyy tavun kokoiset palat ja
- x-osasta 16-bittiset rekisterit (ainakin). Muita emme tarvikaan.
- Nyt olemme tarpeeksi evästettyjä kutsumaan funktiota 0h, joka
- palauttaa VESA-infoblokin. Ah täytetään 4Fh:lla, al asetetaan nollaksi
- ja es:di asetetaan osoittamaan puskuriin minne infoblokki sijoitetaan.
- Jotta saisimme info-struktuurin talteen, täytyy meidän ensin varata
- muistia megan alapuolelta tarvittavat 512 tavua (keskeytys jolla info
- palautetaan haluaa reaalitilan osoitteen ja tämän takia meidän täytyy
- varata DOS-muistia). Sekä ohjelman omalta muistialueelta saman
- verran tilaa, emme halua käsitellä tietoja dos-muistissa jonkin
- farpeekb:n avulla. Lisäksi sign täytyy asettaa VBE2:ksi, jotta
- keskeytys tietää että haluamme version 2 mukaista tietoa.
- Alla esimerkkikoodista pala, joka varaa dos-muistin, asettaa
- tarvittavat asiat ja kutsuu keskeytystä:
- int VesaInit() {
- __dpmi_regs regs;
- dosbuffer=(dword)__dpmi_allocate_dos_memory(64, (int *)&dosselector);
- if(dosbuffer==-1) {
- puts("Ei tarpeeksi perusmuistia VESA-infoblokille!");
- return 1;
- }
- dosbuffer*=16; /* muutetaan lineaariseksi osoitteeksi (seg*16) */
- vesainfo=(VesaInformation *)malloc(sizeof(VesaInformation));
- memcpy(vesainfo->sign, "VBE2", 4);
- dosmemput(vesainfo, sizeof(VesaInformation), dosbuffer);
- regs.x.es=dosbuffer/16;
- regs.x.di=0;
- if(VesaInt(0x00, ®s)) {
- puts("Virhe VESA-keskeytyksessä!");
- return 1;
- }
- dosmemget(dosbuffer, sizeof(VesaInformation), vesainfo);
- if(strnicmp(vesainfo->sign, "VESA", 4)!=0 || vesainfo->version<0x0200) {
- puts("not found!");
- return 1;
- }
- puts("found!");
- return 0;
- }
- void VesaDeinit() {
- __dpmi_free_dos_memory(dosselector);
- }
- Kuten selvästi näkyy, homma on varsin helppoa. Varataan muistit,
- laitetaan "VBE2"-pala, kopioidaan DOS-muistiin, asetetaan rekisterit,
- keskeytys, kopioidaan takaisin omaan muistiin ja se on siinä. Tutkimme
- palautusarvon ja jos onnistuimme voimme jatkaa moodi-infojen
- tiirailuun.
- Moodi-infon lukemiseksi vain matkaamme lävitse halutun
- alueen. Kaikkein varmin tapa tutkimiseen on hakea tieto
- perusmuistista, jos jostain syystä lista ei olisikaan infoblokin
- alueella, vaan jossain muualla. Käytämme vain dosmemget:iä niin
- monesti että vastaan tulee -1 ja joka arvolle katsomme moodi-infon.
- Allaoleva esimerkki etsii 640x480-tilan 16-bittisillä väreillä ja
- asettaa tilan, varaa kaksoispuskurin ja palauttaa sen osoitteen tai
- NULL jos ilmaantui virhe. Varsinainen monitoimityökalu, siis.
- Ennen kuitenkin tutustumme käsitteeseen LFB, sillä se on se mitä
- käytämme. Edellisissä versioissa käytettiin VGA-muistia, joka osoitti
- aina haluttuun palaan videomuistia. Muistiin täytyi siis käydä käsiksi
- 64 kilon palasissa, mikä oli varsin tuskallista touhua. Versio 2.0 toi
- kuitenkin mukanaan suojatun tilan käyttäjille uuden asian,
- LFB:n. Systeemi on sellainen, että videomuisti sijoitetaan jonnekin
- osaan muistiavaruutta. Homma on siis sama kuin osoitteen 0xA0000
- kanssa, mutta nyt paikka on yleensä jossain 300 megan paikkeilla tai
- kauempana ja kokoa on 64 kilon sijasta näyttömuistin verran, omalla
- koneellani 4 megaa.
- Osoitteen saimmekin jo infoblokissa, mutta jotta voisimme käyttää tätä
- osoitetta, täytyy muistisuojauksista päästä eroon. Tarvitsemme siis
- selektorin joka osoittaa halutun muistiosoitteen alkuun ja joka on
- asetettu toimivaksi tarvittavan pitkälle matkalle, jottemme saa
- segmentation faultia muistialueen ohi kirjoittamisen takia
- kopioidessamme kaksoispuskuria ruudulle. Alla suoraan jostain pöllitty
- funktio (kiitoksia tekijälle) mappaamiseen ja mappauksen poistoon:
- /* Funktio ottaa fyysisen osoitteen muistiavaruudessa (physaddr) sekä
- koon tavuissa (size) ja palauttaa linear-muuttujassa varatun alueen
- lineaarisen offsetin (linear), sekä selektorin jota käytetään kun
- halutaan käsitellä muistialuetta (segment, tätä käytettäessä offset
- aina 0). Funktio palauttaa 0 jos onnistui, 1 jos ei */
- int VesaMapPhysical(dword *linear, s_dword *segment,
- dword physaddr, dword size) {
- __dpmi_meminfo meminfo;
- meminfo.address = physaddr;
- meminfo.size = size;
- if(__dpmi_physical_address_mapping(&meminfo) != 0)
- return 1;
- linear[0]=meminfo.address;
- __dpmi_lock_linear_region(&meminfo);
- segment[0]=__dpmi_allocate_ldt_descriptors(1);
- if(segment[0]<0) {
- segment[0]=0;
- __dpmi_free_physical_address_mapping(&meminfo);
- return 1;
- }
- __dpmi_set_segment_base_address(segment[0], linear[0]);
- __dpmi_set_segment_limit(segment[0], size-1);
- return 0;
- }
- Eli käytännössä tarvitaan vain palautettua segmenttiä, offset segmentin
- alla on suoraan (y*leveys+x)*tavuja_per_pikseli, eli mitään lukujen
- lisäyksiä ei tule, kuten asian laita VGA-tilojen kanssa on (0xA0000).
- Sitten tietenkin vapautus loppuun:
- /* Tämä taasen vapauttaa muistin käsittelyyn varatut kahvat, kutsutaan
- kun palataan VESA-tilasta. */
- void VesaUnmapPhysical(dword *linear, s_dword *segment) {
- __dpmi_meminfo meminfo;
- if(segment[0]) {
- __dpmi_free_ldt_descriptor(segment[0]);
- segment[0]=0;
- }
- if(linear[0]) {
- meminfo.address=linear[0];
- __dpmi_free_physical_address_mapping(&meminfo);
- linear[0]=0;
- }
- }
- Hieno homma, vaan mitenkäs näitä käytetään? No näemme kohta senkin,
- hieman vain kärsivällisyyttä. Ensin tutkimme funktiot 01h ja 02h.
- 01h palauttaa cx-rekisterissä annettavan moodin tiedot, puskurin
- ollessa jälleen es:di. Voimme käyttää mainiosti alustusfunktiossa
- varattua muistialuetta dosbuffer. Käyttö on naurettavan helppoa:
- /* Palauttaa 1 jos moodi ei ole olemassa */
- int VesaGetModeInfo(word mode, VesaModeInformation *info) {
- __dpmi_regs regs;
- regs.x.cx=mode;
- regs.x.es=dosbuffer>>4;
- regs.x.di=0;
- if(VesaInt(0x01, ®s))
- return 1;
- dosmemget(dosbuffer, sizeof(VesaModeInformation), info);
- return 0;
- }
- Sitten vain tutkimme halutut arvot moodi-infosta ja jos oikea on
- kohdalla, asetetaan tila. Keskeytyksen numero on 02h ja bx:ssä
- annetaan tarvittava tieto moodista. Mukaan pakataan tieto haluammeko
- lineaarisen tilan vain banked-tilan ja josko näyttömuisti tulee
- tyhjentää ennen vaihtoa. Bitit on järjestelty näin:
- 0-8 Moodin numero
- 9-13 Nollaa (säästetty tulevaisuutta varten)
- 14 0 jos käytetään banked-tilaa, 1 jos lineaarinen, eli LFB-tila
- 15 0 jos tyhjennetään näyttömuisti, 1 jos ei
- eli vaikkapa:
- #define MODEFLAG_BANKED 0x0000
- #define MODEFLAG_LINEAR 0x4000
- #define MODEFLAG_CLEAR 0x0000
- #define MODEFLAG_PRESERVE 0x8000
- /* Jälleen ei-nolla arvo tarkoittaa virhettä */
- int VesaSetMode(int mode) {
- __dpmi_regs regs;
- regs.x.bx = mode | MODEFLAG_LINEAR | MODEFLAG_CLEAR;
- if(VesaInt(0x02, ®s))
- return 1;
- return VesaMapPhysical(&vesalfb_linear, &vesalfb_segment,
- vesamodeinfo[modenum].physicalbasepointer,
- vesamodeinfo[modenum].bytesperscanline*
- vesamodeinfo[modenum].verticalresolution);
- }
- No niin, mitäs tässä enään on jäljellä. No ihan oikeassa olet, eipä
- kai mitään. Vai häh? Ai mikä? Esimerkki?!? No kai se nyt vielä tähän
- mahtuu. Täydelliset sorsat ja määrittelyt voit kaivaa tiedostosta (kai
- nyt flipin osaa tehdä kuka tahansa kun tietää selektorin ja
- näyttömuistin koon?) VESA20.C. Ja sitten miten homma todella hoidetaan
- voit lukea seuraavasta luvusta. Mutta se moodin asetus:
- /* Palauttaa 0 jos onnistui */
- word * VesaSet640x480_16() {
- VesaModeInformation modeinfo;
- s_word mode=0;
- dword addr=vesainfo->videomodeptr;
- while(mode!=-1) {
- dosmemget(addr, 2, &mode);
- addr+=2;
- if(mode!=-1) {
- /* Jos virhe tulee jatketaan seuraavaan */
- if(VesaGetModeInfo(mode, &modeinfo))
- continue;
- if(modeinfo.linearmodeavailable &&
- modeinfo.horizontalresolution==640 &&
- modeinfo.verticalresolution==480 &&
- modeinfo.bitsperpixel==16) {
- if(VesaSetMode(mode, &modeinfo))
- return NULL;
- vesascreen = (word *)malloc(640*480*sizeof(word));
- return vesascreen;
- }
- }
- }
- return NULL;
- }
- Ja vielä se deinitti taitaapi puuttua.
- void VesaReset() {
- textmode(0x03);
- VesaUnmapPhysical(vesalfb_linear, vesalfb_segment);
- free(vesascreen);
- }
- Sitten vain niputetaan kaikki mitä on vastaan tullut, lisätään hieman
- suolaa ja nautitaan PCX-kuvan kera. Hyvää ruokahalua!
- 7.12 Miten se todella pitäisi tehdä
- -----------------------------------
- Aiemmat kaksi lukua vain raapaisivat pintaa VESA-ohjelmoinnin
- saralla. Tärkeimmät funktiot kuitenkin on selostettu ja niiden
- pohjalta on jo varsin helppoa tehdä oma engine. Esimerkkikoodia ei
- kannata suoraan käyttää, sillä se on käytännössä vain omasta
- enginestäni kokoon parsittu kevytversio, joka sisältää tarpeeksi
- esimerkkejä eri asioiden teosta, jotta oman systeemin teko
- helpottuisi. Tässä luvussa hieman siitä miten järjestelmän voisi
- toteuttaa.
- Ensimmäiseksi kannattaa erotella VESA-rutiinit järkeviin
- palasiin. Itselläni esimerkiksi yhdessä tiedostossa on Vesa-rutiinit
- inforakenteiden lukemiseksi muistiin ja olennaisimmat funktiot, kuten
- VesaInt. Toinen osa sitten hoitaa graafisen puolen, eli asettaa
- halutun näyttötilan, ja hoita näytönpäivityksen. Luonnollisesti
- funktioiden määrittelyt ja rakenteet ovat omissa .h-tiedostoissaan ja
- koodi ja muuttujat taas .c-osissa. Ei ole yhtään tyhmä idea tehdä
- kirjastosta yhtä pakettia, esim libvesa.a, jonka DJGPP:n
- lib-hakemistoon sijoittamisen jälkeen voi sisällyttää johonkin
- ohjelmaan pelkästään parilla #include-lauseella ja -lvesa
- -parametrilla.
- Toiseksi erittäin tärkeä asia on tehdä systeemistä tarpeeksi joustava,
- jotta siitä olisi todella jotain hyötyä. Nykyisellään näytönohjainten
- kirjo ja resoluutioiden määrä on niin suuri, että jo tästä syystä
- VESA-esimerkki lienee ensimmäisiä tutoriaalin ohjelmia, joka ei tule
- koskaan toimimaan kaikilla koneilla. Hyvä järjestelmä hoitaa asiat
- siten, että kutsuva ohjelma on tyystin tietämätön siitä mitä raudassa
- on. Unelmasysteemi on sellainen, että initialisoit moottorin alussa ja
- deinitialisoit lopussa. Käytön aikana sinulla on puskuri jonne voit
- laittaa grafiikan ja käsky jolla tavara heitetään näytölle. Ja
- systeemin tulisi toimia näin vaikka alla ei edes olisi todellista
- VESA-yhteensopivaa rautaa.
- Miten tämän pystyy sitten saavuttamaan? No initit ja deinitit on
- helppo hoitaa, mutta että vielä universaali piirtotapa, vaikka alla
- olisi ihan toinen resoluutio ja värimäärä kuin mitä ohjelma luulee,
- onko tämä mahdollista? Vastaus on myöntävä. Eikä ratkaisu edes ole
- kovin vaikea. Taikasana: funktio-osoittimet (C++:ssalla
- virtuaalimetodit ja eri resoluutioiden periyttäminen perusluokasta).
- Oma systeemini sisältää tällaisen muuttujan:
- void VESAREFRESH (*VesaScreenRefresh)()=NULL;
- Käytännössä VESAREFRESH on vain määritelty tyhjäksi (#define
- VESAREFRESH), mutta yllä esitetty systeemi osoittautuu aika
- käytännölliseksi kun se haluttu tila ei löydykään. Systeemi toimii
- näin:
- Oletetaan että pelini on tarkoitus toimia 320x200-resoluutiossa
- 32-bittisillä väreillä. Systeemi on unelma, koska yksi pikseli on
- dwordin kokoinen (=nopeaa) ja jokainen värikomponentti on tavun verran
- ja ylimmäisen tavun jäädessä tyhjäksi. Vaan, ongelmana on, että vain
- hyvin harvalla on 32-bittinen halutun resoluution näyttötila. No,
- ongelma on helposti ratkaistu:
- Yhden flipin sijasta tehdäänkin _useita_ päivitysrutiineja. Yksi
- muuntaa värit 24-bittisiksi lennossa (käytännössä yhtä nopea kuin aito
- 32-bittinen tilakin), toinen muuttaa ne 16-bittisiksi, yksi voi jopa
- käyttää korkean resoluution 32-bittistä tilaa emuloimaan joistakin
- korteista puuttuvia 320x200-kokoisia korkeavärisiä tiloja (oma
- Matroxini esim. tukee normaalisti tiloja vain resoluutiosta 640x480
- ylöspäin). Varalle voidaan vielä tehdä kvantisoitua tilaa tai
- harmaasävyjä käyttävä, 100% VGA-yhteensopiva flippi, joka käyttää
- 256-väristä tilaa. Initin aikana vain asetetaan VesaScreenRefresh
- osoittamaan siihen päivitysfunktioon mitä asetettu näyttötila vastaa.
- Ja kun flippi hoitaa kaksoispuskurin muuntamisen sellaiseen muotoon
- että se on näytettävissä sillä hetkellä käytössä olevalla parhaiten
- oikeaa vastaavalla näyttötilalla, ei ohjelman tarvitse kuin piirtää
- tavara vesascreen-puskuriin, joka on aina saman suuruinen ja jossa on
- aina sama värimäärä, sekä kutsua VesaScreenRefresh-funktiota. Näin
- funktion ollessa oikea flippi tulee tavara ruudulle vaikka käyttäjällä
- ei sattuisikaan olemaan 320x200 32bit -tilaa, vaan esim. 320x200
- 24bit. Ihanaa. Ja tässä pätkä omasta koodistani:
- int VesaLowresInit(int flags) {
- int loop;
- if(!(vesaflag & VESA_INITIALIZED))
- VesaError("Vesa low-resolution mode init", "Engine not initialized!");
- vesascreen=(pointer)Jmalloc(320*200*sizeof(dword));
- memset(vesascreen, 0, 320*200*sizeof(dword));
- for(loop=0; loop<vesamodes; loop++)
- if(vesamodeinfo[loop].linearmodeavailable &&
- vesamodeinfo[loop].horizontalresolution==320 &&
- vesamodeinfo[loop].verticalresolution==200 &&
- vesamodeinfo[loop].bitsperpixel==32) {
- printf("Initializing mode 320x200 32-bit colors...\n");
- vesacurmodeinfo=&vesamodeinfo[loop];
- VesaScreenRefresh=VesaLowres_320x200x32;
- VesaSetMode(loop);
- vesamode=VESAMODE_320x200x32;
- return vesamode;
- }
- for(loop=0; loop<vesamodes; loop++)
- if(vesamodeinfo[loop].linearmodeavailable &&
- vesamodeinfo[loop].horizontalresolution==640 &&
- vesamodeinfo[loop].verticalresolution==400 &&
- vesamodeinfo[loop].bitsperpixel==32) {
- printf("Initializing 640x400 to 320x200 emulation"
- " with 32-bit colors...\n");
- vesacurmodeinfo=&vesamodeinfo[loop];
- VesaScreenRefresh=VesaLowres_640x400x32;
- VesaSetMode(loop);
- vesamode=VESAMODE_640x400x32;
- return vesamode;
- }
- for(loop=0; loop<vesamodes; loop++)
- if(vesamodeinfo[loop].linearmodeavailable &&
- vesamodeinfo[loop].horizontalresolution==640 &&
- vesamodeinfo[loop].verticalresolution==480 &&
- vesamodeinfo[loop].bitsperpixel==32) {
- printf("Initializing 640x480 to 320x200 emulation"
- " with 32-bit colors...\n");
- vesacurmodeinfo=&vesamodeinfo[loop];
- VesaScreenRefresh=VesaLowres_640x480x32;
- VesaSetMode(loop);
- vesamode=VESAMODE_640x480x32;
- return vesamode;
- }
- for(loop=0; loop<vesamodes; loop++)
- if(vesamodeinfo[loop].linearmodeavailable &&
- vesamodeinfo[loop].horizontalresolution==320 &&
- vesamodeinfo[loop].verticalresolution==200 &&
- vesamodeinfo[loop].bitsperpixel==24) {
- printf("Initializing mode 320x200 24-bit colors...\n");
- vesacurmodeinfo=&vesamodeinfo[loop];
- VesaScreenRefresh=VesaLowres_320x200x24;
- VesaSetMode(loop);
- vesamode=VESAMODE_320x200x24;
- return vesamode;
- }
- printf("Initializing mode 320x200 with greyscale palette...\n");
- VesaScreenRefresh=VesaLowres_320x200x8;
- textmode(0x13);
- VesaGreyscalePalette();
- vesamode=VESAMODE_320x200x8;
- return vesamode;
- }
- Ja jokainen voi arvata kuinka monelta harmaalta hiukselta systeemi on
- minut säästänyt, kun toinen demokooderimme omistaa näytönohjaimen,
- jossa on vain 24-bittisiä tiloja ja minulla on näyttis, joka taasen
- tukee vain 32-bittisiä.
- Vihjeenä nopeaan greyscale-flippiin on, että kun kerran komponentteja
- on kolme ja jakaminen muilla kuin kahden potensseilla on tuhottoman
- hidasta, käytä paletista vain ensimmäiset 192 väriä musta-valkoinen
- liukuun (asteen muutos kolmen värin jälkeen), ja jaa komponenttien
- summa kolmen sijasta neljällä. Voi ehkä olla hyödyllistä vääntää
- nousukäyrää hieman siten, että vaaleampiin sävyihin päästään
- nopeammin.
- Sen lisäksi että moottori tukee useita eri näyttötiloja saman puskurin
- esittämiseen, olisi virheensietokyvyn olla niin hyvä kuin se voi
- olla. Esimerkkiohjelman sietokyky on jo aika korkea, mutta parempikin
- se vielä voisi olla. Ongelma on myös se milloin ei enää voida
- jatkaa. Jokin pelin alkulogon näyttäminen korkeammassa resoluutiossa
- ei vielä exit:tiä vaadi, mutta jos koko peli vaatii paljon värejä ja
- tarkkuutta, ei pelkkä virheilmoitus riitä.
- VESA-enginen seuraksi voi olla hyvä idea kerätä myös mittava joukko
- sekalaisia apufunktioita, kuten spritejen piirrot, motion blur ja
- muuta sellaista pientä kivaa. Ehkä jopa kuvatiedostojen saumaton
- integrointi voisi olla hyödyllistä, sillä sekalaisten PCX-laturien ja
- piirtorutiinien kaapiminen kovalevyn uumenista alkaa viimeistään
- silloin olla tuskastuttavaa, kun käytät 32-bittisiä tiloja joskus,
- toisinaan 16-bittisiä ja välillä vain korkean resoluution 256-värisiä
- tiloja. Hyvä grafiikkamoottori säästää vaivalta ja sinne muutokset on
- helppo tehdä keskitetysti, jonkin nerokkaan optimoinnin lisääminen on
- helpompi tehdä yhteen kirjastoon kuin jokaiseen optimoitua funktiota
- käyttävään ohjelmaan.
- En minä taida enempää porista, lähettäkääpäs ihmiset kommentteja tästä
- SVGA-osiosta, sillä jälleen kirjoittelen tätä side silmillä, en minä
- tiedä ymmärrättekö tästä mitään, minähän vain teen tätä. ;-D Nyt
- taidankin siirtyä aloittelemaan "Asioiden taustaa"-osiota. Lykkyä tykö
- grafiikkasysteemien tekoon.
- 8.9 Polygoneista ja niiden fillauksesta
- ---------------------------------------
- 8.8 Lisää kivaa - zoomaus
- -------------------------
- 8.7 Prekalkattuja pintoja - ja tunneli
- --------------------------------------
- 8.6 Plasma tekee comebackin - wobblerit
- ---------------------------------------
- 8.5 Musiikkijärjestelmistä
- --------------------------
- 8.4 Vektorit pelimaailmassa
- ---------------------------
- 8.3 Motion blur - sumeeta menoa
- -------------------------------
- 8.2 Läpinäkyvyys ja sen vaihtoehto - shadebobit
- -----------------------------------------------
- 8.1 Datatiedostot - miten?
- --------------------------
- No niin, on aika siis viimein aloittaa se mitä pitkän aikaa jo olen
- suunnitellut. Eli lukijoiden pakottaminen koodaamaan rutiininsa
- varmasti itse. :) Tästä lähtien ei koodia heru yhtä tai kahta riviä
- enempää, joudutte tekemään tuttavuutta libc:n dokumentaation kanssa
- enemmänkin (komento 'info libc').
- Eli homman nimi on datatiedostot. Ja helppoahan tämä. Idea on luoda
- oma pieni "levyjärjestelmä" ja laittaa tiedostot sen
- alaisuuteen. Käytännössä FAT:in tyyppistä varausyksikkö-systeemiä ei
- kannattane luoda, ellei aio myös kirjoittaa paljon tiedostoja ja
- lisätä datatiedostossa oleviin tiedostoihin uutta tavaraa. Yleensä
- riittää pelkkä read-only järjestelmä, johon vain pakataan senhetkiset
- erilliset tiedostot haluttaessa.
- Kaikkein yksinkertaisin on tehdä headeri, joka sisältää tiedoston
- nimen, sen koon ja sijainnin datatiedostossa. Datatiedoston rakenne on
- vain sellainen, että ensin tulee tiedostojen määrä datatiedostossa, ja
- sitä seuraa määrän verran mainitun kaltaisia headereita, jotka luetaan
- muistiin. Sitten vain kun ohjelma haluaa lukea jonkinnimisen tiedoston
- datatiedostosta, etsitään tiedostonimeä vastaava headeri ja mennään
- fseekillä headerin kertomaan sijaintiin ja luetaan tiedot. Luku voi
- olla puhdas "kaikki-tai-ei-mitään", eli että tiedosto luettaisiin vain
- kokonaisuudessaan puskuriin joka palautetaan (tyyliin
- bufferi=GetFile("dummy.fil")).
- Toinen mahdollisuus on luoda normaaleja f*-funktioita vastaavat
- funktiot datatiedostossa seikkailemiseen (minulla esim. on jfopen,
- jfclose, jfread, jfgetc ja jfseek). Jos aiotaan tukea usean tiedoston
- yhtäaikaista aukipitoa ja liikkumista yhden tiedoston sisällä
- muihinkin suuntaan kuin eteenpäin täytyy myös toteuttaa
- tiedostokahva-järjestelmä. Tämäkin on helppoa, kahvahan on käytännössä
- vain rakenne, joka kertoo missä kohdassa tiedostoa ollaan ja kuinka
- pitkä tiedosto on jne. Yksinkertaisimmillaan systeemissäsi kahva
- sisältää tiedostoheaderin numeron, josta voidaan noutaa pituus ja
- nimi, sekä senhetkisen sijainnin. Lukufunktiot ottavat sitten huomioon
- sen mistä kohdasta nyt pitäisi lukea.
- Jos koodaat kahvat siten, että käytät fseekiä joka lukukerralla
- siirtyäksesi oikeaan paikkaan ja päivität sitten kahvan sijaintia
- luetun palan koon verran eteenpäin, käy tavupohjaisille lukijoille
- kalpaten. Normaalisti fgetc nimittäin lukee hieman eteenpäin ja
- seuraavalla kutsukerralla luettava tavu on jo puskurissa tallessa ja
- se tulee nopeasti muistista. Vaan fseekin jälkeen puskuri tyhjätään ja
- päädyt todella lukemaan tiedostoa tavu kerrallaan, etkä esim. 256
- tavua kerrallaan, kuten asian laita normaalisti puskuroituna
- olisi. Vaikutus näkyy esim. demossamme Bleedissä - 20 kertaa normaalia
- hitaampi PCX-laturi.
- Nopeuden vuoksi kannattaa joko toteuttaa itse puskurointi, eli kahvaan
- myös pieni, esim. 20-tavuinen puskuri, joka kertoo seuraavien tavujen
- sisällön. Näin todellinen levyhaku tapahtuu 20 kertaa harvemmin. Tai
- ainakin tarkistaa ettei fseekata ellei oikean datatiedostoon
- osoittavan kahvan sijainti ole muuttunut.
- Hyvä idea edellisten estämiseksi voi myös olla yksinkertainen
- järjestelmä, jossa fopen korvataan funktiolla, joka avaa uuden
- FILE-kahvan datatiedostoon ja siirtää sen osoittamaan tiedoston
- aloituspaikkaan datatiedostossa. Nyt lukufunktioihin tarvitaan vain
- tarkistus, ettei lueta tiedoston pituutta pidemmälle eteenpäin
- (LueKirjain vain lisäisi sijaintilaskuria yhdellä, palauttaisi EOF:in
- jos oltaisiin tiedoston lopussa ja muussa tapauksessa palauttaisi
- suoraan fgetc(handle):n). Fseek-funktiokin vain muuttaisi sijainnin
- tiedostossa sijainniksi datatiedostossa (lisäisi alkuoffsetin jne.).
- EXE-tiedostoon tallettaminenkin on helppoa. Tunget vain datan EXE:n
- loppuun, sitten headerit ja lopuksi tiedostojen määrän. Eli käännät
- normaalin datatiedoston toisinpäin ja lätkäiset EXE:n perään. Lukiessa
- sitten käytät aluksi fseek(handle, -4, SEEK_END), jolloin pääset
- käsiksi neljään viimeiseen tavuun jotka sisältävät headereiden määrän,
- sitten seekkaat taas taaksepäin päästäksesi headerien alkuun ja luet
- itsesi loppuun (tai siis, neljä tavua vaille, headerit loppuvat ja
- viimeisenä tulisi vielä niiden määrä).
- Tiedostojen lisääminen fiksusti datatiedostoon on itseasiassa astetta
- vaikeampi homma kuin lukeminen (jos siis teet sen helpolla tavalla, et
- käytä puskurointeja tai monimutkaista logiikkaa). Järkevää voi olla
- tehdä ohjelma, jolle annetaan esim. tiedoston nimi jossa on lista
- mukaan otettavista tiedostoista. Sen jälkeen ohjelma lukee jokaisen
- tiedoston nimen headeriin ja pituuden, sekä laskee offsetit.
- Ensimmäisen tiedoston sijainti datatiedostossa on tiedostojen määrän
- kertovan muuttujan ja headerien jälkeen, eli laskennallisesti:
- sizeof(headerien_määrä)+sizeof(header)*headerien_määrä
- Seuraava sijaitsee sitten edellisen koon verran edellisen jälkeen,
- kolmas on toisen koon verran toisen jälkeen jne. Kun headeriin on näin
- laskettu koon lisäksi sijainti, niin voidaan kirjoittaa tiedostoon
- headereiden määrä, sitten headerit ja lopuksi lisätään tiedostot
- samassa järjestyksessä kuin niiden headerit tiedoston
- jatkoksi. EXE:een lisätessä tapa tietenkin olisi käänteinen, ensin
- tiedostot loppuun, sitten headerit ja lopuksi tiedostojen määrä. Tai
- toinen mahdollisuus EXE-pohjaisessa olisi vain lisätä datatiedosto
- EXE:n loppuun ja viimeiseksi vielä offsetti siihen kohtaan missä EXE
- aiemmin loppui ja josta data nyt alkaa headerin määrineen ja
- headereineen.
- No sepä oli siinä, ei tässä oikeastaan mitään vaikeaa pitäisi olla,
- kun vain ei anna tämän luvun sekoittaa päätä.
- 8.9 Polygoneista ja niiden fillauksesta
- ---------------------------------------
- 8.8 Lisää kivaa - zoomaus
- -------------------------
- 8.7 Prekalkattuja pintoja - ja tunneli
- --------------------------------------
- 8.6 Plasma tekee comebackin - wobblerit
- ---------------------------------------
- 8.5 Musiikkijärjestelmistä
- --------------------------
- 8.4 Vektorit pelimaailmassa
- ---------------------------
- 8.3 Motion blur - sumeeta menoa
- -------------------------------
- 8.2 Läpinäkyvyys ja sen vaihtoehto - shadebobit
- -----------------------------------------------
- Läpinäkyvyys on hieno efekti. Puoliksi läpi näkyvää esinettä
- piirrettäessä piirretään puoliksi esine ja jätetään puolet taustasta
- jäljelle. Todellinen läpinäkyvyys muodostetaan siten, että
- valon läpäisemättömyys ilmoitetaan arvolla välillä 0..1 (jossa 0 on
- näkymätön ja 1 täysin näkyvä), ja "pikselin" väri lasketaan kaavasta:
- transparency * object_pixel + ( 1 - transparency ) * background_pixel
- Valitettavasti täytyy todeta, että SVGA-tiloissa, joissa tätä
- käytetään, täytyy jokainen värikomponentti ensinnäkin laskea erikseen
- ja toisekseen että kertolasku ei ole kovin nopeaa hommaa. Vaikka
- kuinka optimoit, jää käteen joka tapauksessa yksi kertolasku, yksi
- addi ja shiftaus oikeaan (ja tämä vain jos trans*obj voidaan laskea
- etukäteen, eli objekti ja läpinäkyvyys ei muutu).
- Tosin jos muistia kulutetaan kaikille käytetyille läpinäkyvyyksille ja
- prekalkataan taustatkin päästään yhdellä yhdeenlaskulla, joka on
- nopeampaa kuin alhaalla esitettävä ratkaisu. Tämäkin hidastuu siinä
- tapauksessa että objektin läpinäkyvyys voi vaihdella eri kohdissa,
- jolloin joudutaan vaihtamaan lähdepikselin puskuria koko ajan. Ja
- tietenkään päällekkäiset läpinäkyvät esineet eivät onnistu ellet laske
- joka objektia kaikkiin mahdollisiin läpinäkyvyyksiin. Joka taas on
- hidasta.
- Mutta kuten nimeltä mainitsemattomassa pelissä Stan sanoo "But wait,
- there is more!". Ja sen 'enemmän' nimi on shadebobit. Nämä 90-luvun
- alkupuolen demoista vakioefektinä löytyvät vekkulit ovat varsin halpa
- tapa toteuttaa maittavia läpinäkyvyysefektejä minimimäärällä
- kertolaskuja, eli ilman ainoatakaan mullia. Ja mitenkä tämä onnistuu?
- Shadebob eroaa normaalista spritestä vain siten, että sitä ei piirretä
- päälle, vaan se lisätään. Jos sinulla on puhtaan sininen tausta ja
- lennätät päällä punaista shadebobbia, niin bobin kohdalle syntyy
- violettia, sillä (255,0,0) + (0,0,255) = (255,0,255)
- (rgb-triplettejä). Esim. flaret ja muut toteutetaan usein
- shadebobeilla.
- Yksi ongelma kyllä on tässäkin tekniikassa. Ylivuodot. Joku teistä
- varmaan on nähnyt efektin joka näyttää siltä että ruutu
- "palaa". Käytännössä tämä on shadebobbi, joka on liian kauan samassa
- paikassa, väriarvot kohoavat viimein 255:een ja kun ne menevät yli ne
- pyörähtävät ympäri, ollaan takaisin nollassa ja jälki on
- karua. Valkoisen kirkkaan täplän keskellä epämääräinen musta roso ei
- oikein ole hienoa. Nopean shadebob-piirturin teko C:llä on jo taidetta
- ja assylläkin hommaa on ihan liikaa. Ehtolause on periaatteessa
- tällainen:
- jos shadebob + tausta > yläraja
- tausta = yläraja
- muuten
- tausta += shadebob
- Assyllä voidaan käyttää hyväksi carry-flagia, joka menee päälle luvun
- pyörähtäessä ympäri. Jnc hyppää jos carryä ei ole asetettu, jc taas
- jos on. Myös adc ja sbc voivat olla mukavia, ne kun lisäävät
- lisättävään/vähennettävään carry-flagin. Esimerkkinä varsin nopeasta
- shadebob-rutiinista:
- add al, bl
- jnc .eiyli
- mov al, 255
- .eiyli:
- Käytännössä tuo vie yhden kellon ja niissä tapauksissa kuin palamista
- syntyy siltäkin vältytään toisen kellon menetyksellä. Ongelmallista
- tosin on, että truecolor-tiloissa (24 ja 32) täytyy jokainen
- komponentti lisätä erikseen, toisin kuin sopivaa palettia käyttävässä
- 256-värisessä tilassa. Ja 15- ja 16-bittisissä tiloissa homma alkaa jo
- muistuttamaan masokismiä.
- Joka tapauksessa läpinäkyvyyden kaksi vaihtoehtoista menetelmää ovat
- molemmat varsin hyödyllisiä oikein käytettynä. Shadebob-systeemissä on
- vain se vika, että puoliksi läpinäkyvää valkoista ei ole nähtykään, ja
- todellisuudessa vain punaista lävitseen päästävä lasi sinisen taustan
- päällä on mustaa. Siksi shadebobit sopivat parhaiten valoefektien
- tekoon. Tosin läpinäkyvyyskin onnistuu tekemällä alpha-kanava (joka on
- kuten bittikartan maski, mutta kertookin kunkin pikselin
- läpinäkyvyyden). Piirrossa sitten taustasta vähennetään alpha-kanava
- (valkoisella tausta on aina musta) ja bittikartasta 255-alpha
- (mustalla ei muutu, valkoisella muuttuu "läpinäkyväksi", eli nollaksi)
- ja sitten vasta lisätään bittikarttaan. Ja tarkistuksia ei enää
- tarvita kun alpha-laskut ovat varmistaneet, että taustan ja
- bittikartan summa ei voi olla yli 255. Tässäkin bittikartan voi
- etukäteen laskea oikealle läpinäkyvyydelle alpha-kanavan suhteen, jos
- se ei muutu.
- Jännittäviä hetkiä tämänkin parissa, shadebobbeja ja läpinäkyvyyttä
- käyttämällä voi kehittää vaikka millaisia viritelmiä, jos ette usko
- niin katsokaa vaikka Orangen Mr. Black, arvatkaa kahdesti onko pyörivä
- lonkero-mömmö muuta kuin taustakuva joka lonkeroiden kohdalta näkyy
- läpi, tummentuen lonkeron reunoja kohti. Shadebobbeja tai
- läpinäkyvyyttä, luultavasti ensimmäisiä. Oikaiskaa jos olet väärässä.
- 8.9 Polygoneista ja niiden fillauksesta
- ---------------------------------------
- 8.8 Lisää kivaa - zoomaus
- -------------------------
- 8.7 Prekalkattuja pintoja - ja tunneli
- --------------------------------------
- 8.6 Plasma tekee comebackin - wobblerit
- ---------------------------------------
- 8.5 Musiikkijärjestelmistä
- --------------------------
- 8.4 Vektorit pelimaailmassa
- ---------------------------
- 8.3 Motion blur - sumeeta menoa
- -------------------------------
- Motion blur ei ehkä peleissä kovin hyödylliseksi muodostu, mutta
- demoissa se on varsin suosittu, ja kyllä sitä voi vaikka
- autopelissäkin käyttää. Joka tapauksessa idea tulee nyt, joten turha
- pyristellä vastaan. Tämä ei satu kuin hetken.
- Motion blur eli liikesumennus kuten joku sen voisi suomentaa
- tarkoittaa sitä, että liikkuvista tavaroista jää jäljet ruutuun ja ne
- häviävät vasta pikkuhiljaa. Efekti on tuttu Dubiuksen ja muiden
- demojen lisäksi vaikkapa Jyrkistä, joissa ainakin aikoinaan valoista
- jäi hirmuiset raidat ruutuun.
- Tekniikkakin on helppo, käytännössä motion blur on melkein sama kuin
- läpinäkyvyys, sillä toteutus sattuu olemaan sellainen, että tietty osa
- uudesta ruudusta muodostuu juuri piirretystä informaatiosta ja tietty
- osa edellisestä ruudusta (jossa taas oli jonkin verran sitä edellistä
- jne. Näin syntyy pikkuhiljaa häipyvä efekti). Siinä se. Sekoitat
- vanhaa ja uutta halutussa suhteessa ja olet valmis. Shadebobit eivät
- tähän käy kauhean hyvin (taino, jos vähennät edellisestä aina
- esim. 100 ja piirrät uuden framen skaalalla 0-155 ja lisäät ne, niin
- mikäs siinä, itseasiassa voisi tämäkin toimia).
- Yleisemmin käytetään kuitenkin aitoa läpinäkyvyyttä ja yksinkertaisia
- perusjakoja, jotka menevät ilman kertolaskuja. Käytännössä tämä on 1:1
- ja pienellä kikkailulla vaikka 1:3 ja 1:7 suhteet onnistuvat varsin
- helposti. Lupasin olla antamatta sorsaa, joten vihjeenä, että
- truecolor-tiloissa shiftaamalla ja poistamalla sopivalla and-maskilla
- toisten komponenttien alueelle mahdollisesti eksyneet bitit saa
- 1:1-sekoituksen muutamassa kellossa. 1:3 onnistuu mukavasti
- shiftaamalla neljällä molemmat ja kertomalla toisen lea-käskyä
- käyttäen kolmella on homma vauhdikasta.
- Jos käytät yhä jotain ankeaa kvantisoitua tilaa, tai jotain
- muuta palettitilaa, niin voit olla onnellinen, voit laskea helposti
- etukäteen 256x256-kokoisen taulukon, jonka jokainen alkio kertoo
- pysty- ja vaakarivin mukaisten värien optimaalisen sekoituksen. Ja kun
- taulukko prekalkataan voit valita sekoitussuhteen ihan
- vapaasti. Rutiinikin on yksinkertainen, jokaista taulukon alkiota
- kohden otat paletista pysty- ja vaakarivin mukaiset värit, sekoitat
- halutussa suhteessa aidon läpinäkyvyyden mukaisesti ja etsit
- tulokselle kvantisointikuutiosta (tai käyttäen lähimmän sopivan värin
- etsintää, selostettu vektoreiden ohessa seuraavassa luvussa) lähimmän
- sopivan värin.
- Tämä oikeastaan tässä olikin. En tiedä kumpi on nopeampaa, blurraus
- flipin sisällä (käytetään näyttöpuskuria toisena), vaiko nopeamman
- keskusmuistin käyttö ja sitten vasta flippi ruudulle. Kokemuksia
- otetaan vastaan.
- 8.9 Polygoneista ja niiden fillauksesta
- ---------------------------------------
- 8.8 Lisää kivaa - zoomaus
- -------------------------
- 8.7 Prekalkattuja pintoja - ja tunneli
- --------------------------------------
- 8.6 Plasma tekee comebackin - wobblerit
- ---------------------------------------
- 8.5 Musiikkijärjestelmistä
- --------------------------
- 8.4 Vektorit pelimaailmassa
- ---------------------------
- Vektorit ovat sen verran vekkuleja juttuja, että käsittelen niitä
- lyhyesti ja vähemmän teoreettisesti tässä. Kärsivälliset odottavat
- lukion kursseja, ja kärsimättömät mutta tiedonhaluiset kaivavat
- syvemmän teorian vaikka lukion matikankirjasta tai 3Dicasta, sieltä
- löytyy pistetulo, ristitulo ja muutkin tärkeät tiedot. Me keskitymme
- vain peruskäsitteeseen ja vektorien muodostamiseen, skaalaamiseen ja
- yhteenlaskuun.
- Olet varmaan jo käyttänyt vektoreita pariin otteeseen. Vektorit ovat
- vain tapa ajatella muita kuin skalaarisia suureita
- (suuruudellisia). Hyvä esimerkki on jonkin esineen sijainti
- ruudulla. Aiemmin olet ajatellut että sijainti koostuu x- ja
- y-koordinaateista. Mutta voit ajatella sijaintia myös vektorina, jonka
- x-komponentti on x-koordinaatti ja y-komponentti ja y-koordinaatti.
- Vektori on nuoli. Ja kuten nuoli, vektori voi osoittaa mihin suuntaan
- tahansa ja olla minä pituinen tahansa. 2d-peleissä käytät varmaan
- yleensä 2d-vektoreita, sillä paperillekaan ei voi piirtää nuolia kuin
- tasossa. 3-ulotteisessa avaruudessa taas nuoli voi osoittaa pysty- ja
- vaakasuunnan lisäksi myös sisään ja ulos näytöstä ja kaikkialle näiden
- välillä. Vektorit ovat itseasiassa vain niputettuja koordinaatteja ja
- yleensä riittää että keskitytään origokeskeisiin vektoreihin, jotka
- alkavat koordinaatiston keskipisteestä. Ajattele ruutupaperia, jossa
- on koordinaatisto. Origokeskeinen vektori on nuoli, joka lähtee
- keskipisteestä ja jonka kärki on missä tahansa koordinaatistossa. Sama
- millaisen vektorin piirrät koordinaatistoosi on helppoa huomata että
- origokeskeinen vektori voidaan aina ilmoittaa kahdella luvulla, x- ja
- y-koordinaatilla mihin nuolen kärki sitten osuukin. Myös
- tietokonemaailmassa vektori ilmoitetaan kahdella luvulla, ihan kuten
- ennen teit x- ja y-koordinaattiesi kanssa.
- Määrittely onnistuu lukutaulukkona, tai rakenteena. Suosittelen
- taulukkoa, eroa structiin ei kuitenkaan nopeudessa ole:
- float v[3]; /* 3-ulotteinen vektori, v[0] on x-komponentti, v[1] on y
- ja z tietenkin v[2] */
- Vektoreita voi muodostaa kaikkien pisteiden välille, muodostat vain
- molemmista pisteistä vektorin (käytännössä ajattelet koordinaatteja
- vektorin komponentteina) ja vähennät ne toisistaan ja tuloksesta
- tulee vektori, joka on yhtä pitkä kuin mitä pisteiden välillä lyhin
- matka (tätä voi käyttää esimerkiksi kvantisoinnissa, kahden värin
- etäisyys voidaan selvittää muodostamalla niiden välille vektori ja
- laskemalla sen pituus). Vektorien yhteen- ja vähennyslasku on helppoa,
- jokainen komponentti vain käsitellään erikseen. Eli jos vektorin a
- (päällä pitäisi olla viiva, mutta...) x- ja y-komponentit ovat 5 ja 2
- ja vektorin b vastaavasti 3 ja 4, ovat niiden summavektorin
- komponentit vastaavasti 5+3=8 ja 2+4=6. Yleensä vektori ilmoitetaan
- kuten koordinaatti, eli a + b = (8,6) ja a - b = (2,-2). Vektorin
- pituus lasketaan kaavasta sqrt(x*x + y*y). Vektorin pituutta voidaan
- skaalata jollain luvulla kertomalla jokainen komponentti erikseen
- luvulla. Tällöin pituus kasvaa <luku>-kertaiseksi, mutta suunta pysyy
- ennallaan.
- Kolmiulotteisessa erona on vain se, että mukaan tulee
- z-komponentti. Pituuden kaavaan lisätään + z*z ja laskuissa myös tämä
- komponentti pitää muistaa käsitellä. Helppoa kun sen osaa.
- No nyt tiedät mikä on vektori. Vaan mitä sillä tekee? Paras vastaus
- on, että ihan mitä tahansa. Vaikka autopelin. Tai Quaken. Vektori on
- vain kätevä tapa niputtaa n-ulotteisen avaruuden koordinaatit yhteen
- pakettiin.
- Joku kysyi minulta vähän aikaa sitten miten autopelissä tai
- luolalentelyssä tehdään liikkuminen. Olisi ollut todella helppoa
- selittää asia jos olisin ollut varma että kysyjä ymmärtää mikä on
- vektori, mutta kun jouduin selittämään asian x- ja y-koordinaateilla,
- hommassa oli paljon enemmän tekemistä. Selitänpä nyt miten
- luolalentely voitaisiin toteuttaa vektoreilla:
- Aluksen sijainti on normaali 2-ulotteinen vektori. Lisäksi aluksella
- on nopeusvektori ja kiihtyvyysvektori. Joka framella sijaintivektoriin
- lisätään nopeusvektori. Nopeusvektorin suunta vastaa aluksen
- kulkusuuntaa (eli ihan kuten x-nopeus ja y-nopeus, vain niputettuna)
- ja pituus nopeutta. Voit hahmottaa liikettä piirtämällä esimerkiksi
- sijaintivektorin ja sen kärjestä lähtien nopeusvektorin. Joka
- kierroksella nopeusvektori lisätään sijaintiin, ja tulos on juuri se
- mitä saat kun piirrät nopeusvektorin suuntaisen ja pituisen jatkeen
- sijaintivektorille. Kokeile vaikka alkusijaintia (5,7) ja nopeutta
- (2,-1) ja lisää edelliseen jälkimmäinen ja piirrä tämä uusi vektori
- (9,6). Huomaat että uuden ja vanhan sijaintivektorin väli on juuri
- nopeusvektorin suuntainen ja pituinen.
- Lisäksi meillä on vielä kiihtyvyysvektori, joka ilmoittaa mihin
- suuntaan alus on kiihtymässä. Tämä vektori kertoo mihin moottori
- milläkin hetkellä alusta työntää (suunta) ja kuinka nopeasti
- (pituus). Rakettimoottoreilla kiihtyvyys on aina vakio, joten
- kiihtyvyyden määrittäminen onnistuu luomalla sinillä ja kosinilla
- yksikkövektori (nimitys jota käytetään vektoreista joiden pituus on 1,
- joka pätee kaikkiin vektoreihin joiden x-komponentti on kosini ja
- y-komponentti sini, se on näiden trigonometristen funktioiden
- perusluonne) ja skaalaamalla se rakettimoottorin teholla, eli esim:
- kiihtyvyys[0] = cos(kulma_rad);
- kiihtyvyys[1] = sin(kulma_rad);
- kiihtyvyys[0] *= TEHO;
- kiihtyvyys[1] *= TEHO;
- Ja kiihtyvyys lisätään tietenkin joka vuorolla nopeuteen, eli kun
- moottori ponnistelee kulkusuunnan mukaisesti vauhti kiihtyy ja jos
- käännät aluksen nokan vastakkaiseen suuntaan ja painat kaasun pohjaan
- (onko napeissa muka muita asentoja ?-) vauhti alkaa
- hidastumaan. Täydellistä.
- Ja koska vektorit ovat suoraan fysiikkaa varten luotuja, on
- painovoiman, aseen rekyylin, törmäysten ja muiden lisääminen lasten
- leikkiä. Painovoima on vain vakiosuuntainen (alas) kiihtyvyys. Kitka
- pinnasta (ei luolalentelyissä, autopeleissä kylläkin) on tietyn
- prosenttimäärän (1-kitkakerroin) mukainen vauhdin hidastuminen,
- rekyyli ja törmäykset perustuvat siihen, että jos ammut panoksen
- tiettyyn suuntaan (vektori), niin aluksen suunnanmuutos on
- vastakkainen ja suoraan suhteessa massojen eroon. Esim. jos panos
- painaa 1/1000 aluksesta, lisätään nopeuteen ammuksen suuntavektori
- käännettynä (miinusmerkki joka komponentin eteen ja nuoli osoittaa
- vastakkaiseen suuntaan) ja skaalattuna yhteen tuhannesosaan, eli
- kerrottuna 0.001:llä. Jos olet perfektionisti voit pitää lukua
- panoksista ja vähentää ne massasta. :) Täydellinen törmäys (molemmat
- kappaleet jatkavat samaan suuntaan, esim. luodit) on ihan sama, mutta
- käänteisesti, eli ei tarvitse kääntää ammuksen suuntavektoria, vaan
- lisätään se vain massojen suhteessa. Alusten ja seinän väliset
- törmäykset ovat hankalampia, niissä kun pitäisi molempien kolahtaa eri
- suuntiin. Tähän saat vapaasti kehitellä omasi, luolalentelyiden
- tekijät ovat tyytyneet yleensä pysäyttämään aluksen seinään osuessa ja
- antavat alusten läpäistä toisensa.
- Siinäpä kaikki tärkein vektoreista. Jos ymmärrät mitä ne ovat, miten
- ne jakautuvat x-, y- ja z-komponentteihin ja tajuat että skaalaamalla
- muutetaan niiden pituutta, ja osaat kaiken lisäksi lisätä, vähentää ja
- skaalata niitä, olet vahvoilla. Oppitunti on päättynyt.
- 8.9 Polygoneista ja niiden fillauksesta
- ---------------------------------------
- 8.8 Lisää kivaa - zoomaus
- -------------------------
- 8.7 Prekalkattuja pintoja - ja tunneli
- --------------------------------------
- 8.6 Plasma tekee comebackin - wobblerit
- ---------------------------------------
- 8.5 Musiikkijärjestelmistä
- --------------------------
- Ne jotka haukkovat kotikatsomoissaan henkeä jo kuin kalat, saavat
- aloittaa hengittämisen jälleen. Tiedossa ei vielä ole äänikorttien
- saloja, eikä edes vaivaisia miksauksen perusteita, ne taidan laittaa
- vasta 3.0-versioon, jos sellaista edes kannattaa siinä vaiheessa alkaa
- tekemään. Nyt kuitenkin esittelen lyhyesti NE soittosysteemit, jotka
- tällä hetkellä minun henk. koht. mielipiteeni mukaan ovat hyviä
- vaihtoehtoja kun musiikkia pitää alkaa kuulumaan, muusikkojen tai
- kohdeyleisön vaatimuksesta.
- Alkusanoina totean, että kaikkein paras on tietenkin tehdä oma
- systeemi. Mutta se suurin kompastuskivi on siinä, että levityksessä
- olevien tasoisten (paraskaan ei soita IT- ja XM-kappaleita kaikkia
- oikein) "playereiden" teko vie yhdestä viiteen vuotta. Tervetuloa
- todellisuuteen. Vaatimukset nimittäin ovat hurjat, mitään yhtenäistä
- standardia kun ei Windows Sound Systemiä lukuunottamatta DOS:in
- puolella. Tai no, SB on vahva sana, Pro-mallia tukemalla tuet
- luultavasti 99% tavoiteyleisösi äänikorteista.
- Mutta edes yhden kortin koodaukseen vaadittava tieto- ja taitomäärä on
- sen verran suuri, että jos viikossa saa sellaisen systeemin tehtyä,
- että sillä voi soittaa looppaavia ja looppaamattomia sampleja,
- katkottomasti, ilman naksahduksia ja vapaasti säädeltävällä vauhdilla
- ja voluumilla, niin voi ajatella että kymmenesosa hommasta on jo
- tehty. Sen jälkeen hyökätään FMODDOC:in kimppuun ja vietetään seuraava
- viikko kyhäten jonkin formaatin moduuliloaderi (jos kyseessä on XM tai
- IT suosittelen varaamaan pari viikkoa ja purkin Buranaa). Sen jälkeen
- vielä pari kuukautta efektitukea väännellen (XM on dokumentaation
- saatavuudessa suorastaan kuninkuusluokkaa, herrat FT2:n tekijät kun
- ovat sitä mieltä että heidän trackerinsahan on melkein
- itsedokumentoiva - tiedossa siis ainakin S3M- tai MOD-formaatin
- dokumenttien luku ja hauskoja hetkiä niin heksaeditorin kuin FT2:nkin
- parissa).
- No nyt olette varmaan niin kauhuissanne että tämän luvun todellinen
- asiasisältö menee kuin kuumille kiville. ;-D No ei, ei se
- moduuliplayerin teko niin hirveää hommaa ole, täytyy vain omata
- itsepäisyyttä, taito tehdä asiat tarpeeksi hyvin kerrasta (lukemalla
- fmoddocin kerran läpi ennen aloitusta voi tähän syntyä kummasti
- kiinnostusta) ja paljon paljon kärsivällisyyttä. Optimointitaitokaan
- ei olisi pahitteeksi.
- Jos kuitenkin lykkäät moduuliplayeriasi hieman kauemmaksi
- tulevaisuuteen ja kokeilet ensin jotain muuta kuin kotikutoista
- ratkaisua, kannattaa ensimmäiseksi kurkata Housemarquen sivuille
- (www.housemarque.com/fi) ja imuroida Midaksen uusin versio. Midas on
- moduuliplayereiden ehdoton Rolls Royce, jopa IT-tuki taisi löytyä, ja
- XM:tkin soivat vain osaksi pieleen. Funktioita riittää vaikka muille
- jakaa (tosin subrow-tarkkuista laskuria moduulin sijainnista ei saa
- käyttöönsä :), timerista lähtien vga-tilojen asetukseen ja
- näyttösynkronointeihin. DirectX-tuki löytyy niinikään.
- Ainoa Midaksen ongelma on se, että sitä ei EHDOTTOMASTI saa käyttää
- SW- tai muuhun kaupalliseen levitykseen. Ilmaisohjelmat ovat ok,
- kunhan vain muistaa mainita käyttäneensä midasta, mutta jos otat siitä
- rahaa, otat Midaksen myös pois ohjelmastasi. Kaupallisia lisenssejä
- tosin on mahdollista hankkia, joten postia vain housemarquelle
- sähköisessä muodossa. Aiemmin muistaakseni rekisteröintihinta oli
- $500, mutta ehkä se on tippunut. :)
- Jos SW kiinnostaa, tai haluat vaihtoehtoisen, DJGPP-optimoidun
- systeemin peliisi, on Humppa (entinen Hubrmod), eli HUbris Module
- Player PAckage tarkistamisen arvoinen, lähetät vaikka pelisi
- lähdekoodit Kaikalle ja saat alkaa myymään peliä. :) Kuulemani mukaan
- XM-tukikin on jo varsin hyvä ja sormeni syyhyävät päästä kokeilemaan
- systeemiä, vaan en ole vielä ehtinyt.
- Toinen hieman kalliimpi, mutta pitkät perinteet moduuliplayerien
- saralla omaava vaihtoehto on MikMod, josta löytyy Midaksen tapaan tuki
- melkein joka laitealustalle, mukaan lukien Linux. Rekisteröintihinta
- oli varsin halpa, muistaakseni parikymmentä dollaria, ja sekin vain
- siinä tapauksessa että haluat käyttää playeria kaupallisiin
- tarkoituksiin, ilmaislevittäjät saavat käyttää softaakin
- ilmaiseksi. Ja kuten Humpassa, myös MikModissa lähdekoodi tulee
- mukana, joten mahdollisuudet omiin viritelmiin kohoavat huimasti.
- Kaikkien kolmen mukana tulee varsin laadukas dokumentaatio, tai jos ei
- sellaista löydy, niin esimerkkikoodia löytyy jokaisesta. Itse olen
- kokeillut neljää tai viittä eri playeria ja jok'ikisen toimimaan
- saanti ei ole vaatinut muuta kuin sopivan esimerkin räätälöimistä
- omaan käyttöön sopivaksi. Uskaltakaa hyvät ihmiset kokeilla niitä, ja
- jos ette kerta kaikkiaan ymmärrä niitä englanninkielisiä kommentteja
- niin sanakirja tai taitava kaveri varmaan auttaa mielellään. :)
- Tässä kaikki tältä erää, minulle saa postittaa ilmoituksia jos jokin
- ehdottoman mahtava DJGPP-playeri puuttui (ei, se J. Hunterin DJGPP:lle
- tehty SB Library tai mikä olikaan ei käy - edes modit eivät soi siinä
- oikein).
- 8.9 Polygoneista ja niiden fillauksesta
- ---------------------------------------
- 8.8 Lisää kivaa - zoomaus
- -------------------------
- 8.7 Prekalkattuja pintoja - ja tunneli
- --------------------------------------
- 8.6 Plasma tekee comebackin - wobblerit
- ---------------------------------------
- Yksi varsin hulvaton demoefekti ja peleissäkin kenties
- hyödynnettävissä oleva vekkuli on nimeltään wobbler ja selostan
- toiminnan lyhyesti tässä. Sen sijaan että ottaisin x:n mukaan parista
- aallosta värin ja y:n mukaan parista aallosta, lisäätkin x-arvon
- mukaisesti siniaallolla y-koordinaattia ja toisinpäin. Tuloksena
- syntyy ihanasti vellova efekti, jota voi käyttää miten mieli sitten
- tekeekään. Hyvää idea on käyttää 256x256-kokoista tekstuuria, jolloin
- y- ja x-arvot on helppo saada menemään ympäri (y&255 ja assyllä
- suoraan tavurekistereillä) ja oikeanlaisilla kartoilla ei reinoja
- huomaa. (mikä olisi hyvä termi englanninkielessä käytetylle
- tilingille? tiiliytyminen? tileytyminen?)
- Voit myös kokeilla y:n lisäämistä y:n mukaan jolloin syntyy
- venytystä. En myöskään tiedä millainen on tulos, jos et lisää näitä x-
- ja y-arvoihin, vaan laitat ne sellaisenaan (eli ei x + ..., vaan vain
- ...).
- 8.9 Polygoneista ja niiden fillauksesta
- ---------------------------------------
- 8.8 Lisää kivaa - zoomaus
- -------------------------
- 8.7 Prekalkattuja pintoja - ja tunneli
- --------------------------------------
- Jälleen uutta pikkukivaa efektien saralla. Tunneli. Idea on sellainen,
- että sinulla on kaksi puskuria, samaa kokoa kuin ruutukin, sekä
- tekstuuri (esim 256x256). Puskurista 1 otetaan samasta kohdasta kuin
- ruudulle laitettava pikselikin ensin y-arvo, ja sitten puskurista 2
- x-arvo. Tunnelin tapauksessa puskuriin 1 piirretään kuvio, joka on
- säteittäisiä viivoja keskipisteestä ja kuvio lasketaan siten, että
- mennään joka pikseli läpi, muodostetaan vektori pikselin ja
- keskipisteen välille (x-komponentti on x-160 ja y-komponentti y-100)
- ja selvitetään välillä oleva kulma. Tämä hoituu joko tangentilla,
- jolloin homma on nopeampaa, tai jos et sitä osaa, teet sen siten, että
- piirrät keskipisteestä tarpeeksi tiheään ympyröitä tähän tyyliin:
- for(radius = 0; radius < 140; radius ++) {
- for(angle = 0; angle < 2048; angle++) {
- x = (int)(cos(3.1415*(float)angle/180.0)*radius);
- y = (int)(sin(3.1415*(float)angle/180.0)*radius);
- buffer1[y*320+x] = angle/8; // 0..255
- }
- }
- Ja toinen taas on sarja ympyröitä keskipisteestä, värin ollessa
- etäisyys keskipisteestä. Tämä on helppo ratkaista neliöjuurella. Eli:
- for(y=0; y<200; y++) {
- for(x=0; x<320; x++) {
- tx = x-160;
- ty = y-100;
- buffer2[y*320+x] = (int)sqrt(tx*tx+ty*ty);
- }
- }
- Sitten vain piirretään tunneli:
- for(loop=0; loop<64000; loop++) {
- screen[loop] = texture[ buffer2[loop] * 256 + buffer1[loop] ];
- }
- Ainiin, perspektiivinkin voi lisätä vaihtamalla y-arvon (buffer2)
- laskuun sqrt:n tilalle 256/sqrt:n. Tällöin pitää tosin varmistaa ettei
- sqrt ole alle 1, sillä muuten käy todella huonosti. Ja jos etäisyys ei
- tuollaisena tyydytä sen voi kertoa halutun suuruisella luvulla. Niin
- ja tunnelin saa liikkeelle kun lisää piirron aikana x- ja y-arvoihin
- jotain. Tässä pitää kuitenkin huolehtia ettei kumpikaan mene yli 255:n
- (eli käytännössä ((buffer2[loop]+yoff) & 255) + ...).
- Muitakin kuvioita joissa tekstuuri liikkuu "pintaa" pitkin, voi
- helposti luoda. Wormhole on yksi tällainen, eikä edes hirvittävän
- vaikea, mietippäs vain. Ja Trauman Mindtrapissa luultavasti käytettiin
- samaa tekniikkaa siinä pyörivän pallon kohdassa jossa ympärillä
- pyöritään toiseen suuntaan.
- 8.9 Polygoneista ja niiden fillauksesta
- ---------------------------------------
- 8.8 Lisää kivaa - zoomaus
- -------------------------
- Jälleen sarjassa "helppo nakki kun käytät aivoja"-efektejä. Tällä
- kertaa vuorossa vanha tuttumme reaaliaikainen suurennos. Ja mikäs sen
- helpompaa.
- Normaalissa bittikartan piirrossahan korotat ruudun offsettia yhdellä
- ja bittikartan offsettia yhdellä. Entäs jos korottaisit bittikartan
- offsettia kahdella? Bittikartta "loppuisi" puolet nopeammin, ja
- tuloksena piirtäisit sen puoleen aiemmasta tilasta, sekä x- että
- y-suunnassa. Pienensit juuri kuvaasi kahdella. Onnea. No entä
- suurennos sitten? Helppoa, korotat ruudun offsettia kahdella? Totta,
- mutta tuloksena syntyy hieman reikiä (aika kiva räjähdysefekti silti),
- joten ehkä käytämme jotain muuta. No varmaan kaikki arvasivatkin jo -
- korotetaan bittikartan offsettia puolella ja päästään samaan
- tulokseen.
- Vastaavalla tavalla pystyt suurentamaan ja pienentämään bittikarttoja
- kaikilla kahden potensseilla. Mutta pystyt kyllä parempaankin ja
- tiedät sen aivan varmasti. Nappaa käyttöön fixed-point luvut tai
- vaikka floatit, niin yhtäkkiä voitkin tehdä sama minkä kokoisia
- zoomauksia, portaattoman näköisiä vieläpä! (taino, siinä syntyy
- sellaistä ärsyttävää pyöristysvirhekuviota, kokeile vaikka suurentaa
- kuvaa pikkuhiljaa pienentämällä askelta mahdollisimman vähän framejen
- välissä). Helppoa kun sen osaa.
- Ainoa häiritsevä piirre tulee olemaan se, että karttasi lentelevät
- ruudun ylitse tai loppuvat kesken. Niinpä piirrossa täytyy normaalin
- for(... ; bitmap_x < x_size; bitmap_x++) -systeemin sijaan tarkistaa
- pyörityksen aikana sekä ruudun että bittikartan x- ja
- y-koordinaatit. Tai sitten klippaat ennen looppia, eli jos kartta
- menee ruudun ylitse siirrät piirtoa alkamaan hieman myöhemmin kuin
- ensimmäisen pikselin kohdalta. Jälleen tässä tulee tehdä sen verran
- tarkka järjestelmä, ettei reunoilta jää satunnaisesti 2-5 kappaletta
- pikseleitä tyhjäksi.
- 8.9 Polygoneista ja niiden fillauksesta
- ---------------------------------------
- Minua on pitkän aikaa ruinattu tekemään 3D:stä juttua ja aina olen
- käännyttänyt kysyjät 3Dican puoleen. Tai ainakin polygonijutuista. No
- niin, samoin käy tällä kertaa. :) Tai melkein, tässä luvusta niin
- lyhyesti ja ytimekkäästi polygonien fillaus ilman klippejä ja muita
- kuin vain mahdollista. Ennen kuin taaperrat luvun läpi, hakkaa päähäsi
- tieto miten piirretään muitakin kuin suoria viivoja. Eli lue se
- interpolointi-juttu lävitse.
- Polygonien täyttäminen on varsin helppoa. Ensin käymme lävitse
- kolmioiden täyttämisen. Joka on itseasiassa _niin_ helppoa, että
- keksin lineaarisen interpoloinnin ja kolmion täyttämisen idean ihan
- itse, ilman kenenkään apua. Hienoa, lohduttiko?-) No joka tapauksessa,
- asiaan, eli tarkemmin sanoen flat-polyfilleriin.
- Tasaisella värillä täytetyn kolmion piirto on varsin helppoa. Kun
- piirtelet vaikka 27 kappaletta kolmioita paperille, niin huomaat että
- jos vedät kolmion pystytasossa katsottuna keskimmäisen pisteen kautta
- kulkevalla vaakaviivalla halki, saat kaksi pienempää kolmiota (tai
- erikoistapauksissa vain yhden, jos sinulla on jo tasapohjainen
- kolmio), joista toisessa on tasainen pohja ja toisessa tasainen
- "katto". Jos vielä ajattelet syntyneiden kolmioiden kylkiä viivoina ja
- kuvittelet piirtäväsi ne ylhäältä alaspäin interpoloiden x:ää, huomaat
- että olisi varsin helppoa aloittaa huipulta ja lisätä y:tä yhdellä,
- interpoloida hieman alku- ja loppu- x-koordinaatteja ja piirtää
- x-koordinaattien välille vaakaviiva. Itseasiassa jos mietit vielä
- hetken huomaat, että se olisi enemmän kuin helppoa.
- Siitä vain tekemään, ensimmäinen flat-fillerisi valmistui juuri. Eli
- sorttaa pisteet y:n mukaiseen järjestykseen (menee kolmella
- if-lauseella, bubblesorttia) siten että ensimmäisenä on ylin ruudulla,
- sitten keskimmäinen ja lopuksi alin. Nyt lasket pisimmälle viivalle,
- eli sille joka ulottuu ylimmästä alimpaan, x-askeleen
- (kaava: (x3-x1)/(y3-y1)). Sitten tarkistat että y1 ja y2 eivät ole
- samoja ja jos eivät, lasket vastaavan x-askeleen x1:n ja x2:n välille
- ja menet loopilla välin y1-y2. Jos y1 oli sama kuin y2 niin et tee
- tuota ja jatkat suoraan vastaavaan tarkistukseen y2:n ja y3:n kanssa,
- ja jälleen jos ne ovat erisuuria jatkat pidemmän viivan piirtämistä
- siitä mihin se jäi ja lasket x-askeleen vielä y2:n ja y3:n väliselle
- matkalle. Koko ajan piirrät vaakaviivoja pisimmän viivan
- x-koordinaatin ja ylemmän tai alemman kolmion viivan x-koordinaatin
- välille, sille korkeudelle missä y-looppisi meneekään.
- Vaakaviivassa voisi olla hyvä tarkistaa että x1 on pienempi kuin x2
- (jos näin ei ole, vaihda pisteet keskenään) ja onko viiva edes
- ruudulla (x2 >= 0 && x1 <= 319 && y>=0 && y<=199) ja ettet ala
- piirtämään ruudun ulkopuolelta (siirrä x1 nollaan jos se on alle ja
- laske x2 319:ään jos se on yli). Sitten vain helpolla for-loopilla
- viiva täyteen väriä. Muista, että se se on for(x=x1; x<=x2; x++), eli
- <=, eikä <!
- Gouraud-kolmio on ihan samanlainen, mutta väriä täytyy interpoloida
- samoin kuin x-koordinaattia. Tekstuurikolmiossa interpoloidaan värin
- sijaan tekstuurin x- ja y-koordinaatteja. Huomioitavaa on, että jos
- viivanpiirrossa siirrät x1-koordinaattia -5:stä 0:aan jottet piirtäisi
- ruudun ohitse, täytyy myös tekstuurien/gouraud-väriarvojen
- alkupisteitä siirtää viidellä askeleella eteenpäin. Ja texture ja
- gouraud-fillereissä hlinessäkin pitää interpoloida samalla tavalla
- viivaa piirrettäessä kuin tehdään kolmion sivujen kohdalla.
- Sossiinä, mie meen saunaan.
- 8.10 Pari kivaa feikkimoodia
- ----------------------------
- Ihmissilmää on helppo huijata, se on todistettu useampaan kertaan. Ja
- huijauksen kohteeksi joutuu puolueellisen tutkimuksen mukaan varsin
- useassa VESA-tiloja käyttämättömässä demossa. Ja mikäs siinä. Mode-X
- -tiloja en käsittele, kun en jaksanut twiikata itsekään kuin 320x400
- -tilan toimimaan, nykyään ne alkavat olla hieman turhia. Mutta, demoja
- tehdessä etenkin arvostaa kunnon temppuja jolla saa ruudulle enemmän
- värejä kuin mitä siellä oikeastaan voisi yhtä aikaa olla.
- Ensimmäinen tapa onkin kaluttu jo lävitse, ja sen nimi on kvantisoidut
- tilat. Niissä ongelmana tahtoo vain olla, että homma ei ole kovin
- vauhdikasta. Demoihin asian kyllä voi hoitaa siten, että tekee
- erikoisflipin, joka laskee kaikki käytetyt värit ja tekee niistä
- värikuution ja paletin etukäteen, mutta erityisen värikkäillä
- efekteillä alkaa homma maistua puulta, tai paletti näyttämään yhä
- enemmän siltä perinteiseltä 3:3:2-ratkaisulta, eli käytetään
- 256-värisen tilan pikselin kolmea ylimmäistä bittia punaiseen, kolmea
- keskimmäistä vihreään ja kahta alimmaista siniseen. Voit hyvin
- kuvitella miltä väriliu'ut näyttävät. Täytyy kuitenkin todeta, että
- onhan se kuitenkin kahdeksan/neljä eri värikomponenttia, toisin kuin
- 16-väristen tilojen värikomponenttien sekoitus, välivärit ja
- tumma/vaalea -valinta. Argh. Perfektionistit sanovat ei väreille ja
- tekevät 256-värisen tilansa tietenkin puhtailla harmaasävyillä, mutta
- me (vai pitäisikö sanoa te? ;) muut ihmiset voimme käyttää erilaisia
- pieniä kikkoja laadun parantamiseen.
- Eräs hieno ja usein käytetty tapa on jakaa 256-värinen paletti
- pehmeään sinisen, punaisen ja vihreän liukuun (á 64 sävyä) ja
- muodostaa väri kolmesta vierekkäisestä pikselistä. Laadussa ei
- todellakaan ole hurraamista, mutta välttämättä asiaa ei ihan ensi
- silmäyksellä huomaa (minua ainakin on muutaman kerran vedetty
- höplästä). Valkoinen väri on aika karu, mutta jääähän paletista 64
- sävyä yli ja ne voi aina käyttää harmaasävyihin. Tehokasta jälkeä
- tulee ainakin, jos twiikkaat käyttöösi jonkin 320x400-tyyppisen tilan,
- jolla normaali tilan 13h pikselit tuplaantuvat ja päällekkäisiä
- pikseleitä on suorastaan ihana käyttää tämäntyyppisiin
- sekoituksiin. Jo pelkästään käyttämällä kahta 3:3:2-pikseliä
- approksimoimaan värilänttiä saadaan teoriassa 4:4:3-tila, ja
- kummallisemmilla värijaotteluilla (1 bitti ilmoittamaan onko ylä- vai
- alapikselin paletti ja loput 7 bittiä molemmissa erikseen käyttöön,
- jolloin saadaan 14-bittinen tila, teoriassa, ainakin) päästään jo
- todella hyviin tuloksiin.
- On myös olemassa todellisten virittelytilojen maineeseen päässyjä
- moodeja, joissa käytetään kahta kuvaa joita välkytetään nopeasti ja
- kaiken maailman muita virityksiä, mutta vauhti on yleensä huono
- verrattuna lähes suoraan raudalla toteutettaviin illuusioihin ja
- toimivuus joidenkin tilojen kohdalla satunnainen, riippuen koneen
- näyttökortista ja Uranuksen kallistumiskulmasta (joka tietenkin on
- vakio). Näihin en ole erityisemmin perehtynyt, mutta jos joku on, niin
- minua voi vapaasti pommittaa informaatiolla. Joka tapauksessa hus vain
- keksimään erikoisia tapoja saada värien määrää näennäisesti
- korotettua!
- 9.1 Saatteeksi
- --------------
- Yli-guruiksi itsensä tuntevat, skip it.
- Nyt et ole enää laama, vaan hatarasti pääasiat osaava, toivottavasti
- innokas peliohjelmoijan alku. Tai kenties jopa vielä enemmän, jos
- kaikki dokumentin asiat ovat hanskassa. Tämän dokumentin tarkoituksena
- on ollut saattaa sinut vain alkuun. Ensimmäinen neuvoni aloittelevalle
- peliohjelmoijalle, eli sinulle on, että älä jätä lukemistasi
- tähän. Mikään ei korvaa tuhansia tunteja tutoriaalien ja kokiksen
- ääressä vietettyjä tunteja. Samaa asiaa voi opetella useasta eri
- lähteestä, jolloin tajuaa asiat paremmin, selkeämmin ja syvemmin kuin
- yhden tutoriaalin luvulla.
- Toisena on se, että englannin kieli on pakko opetella. Sen oppii
- parhaiten sanakirjan kanssa tutoriaaleja kahlailemalla. Jos aiot
- pärjätä hyvin peliohjelmoinnissa täytyy englantia osata. Jos et vielä
- sitä osaa, niin työskentele oppiaksesi.
- Kolmanneksi kaikkein tärkeimpänä ovat oman järjen käyttö, rautainen
- tahto ja sammumaton tiedonhalu, sekä ahkeruus. Maailma on täynnä
- laamoja, jotka eivät osaa mitään siksi koska eivät ole tosissaan
- yrittäneet. Minä aloitin C-ohjelmoinnin alle kolme vuotta sitten ja
- pelkällä kovalla yrittämisellä ja innostuneisuudellani opettelin
- koodaamaan. Olen lukenut tuhansia ja tuhansia rivejä ohjelmointiasiaa,
- enkä ole vielä sitä joutunut katumaan.
- MBnetistä löytyy valtava määrä lähdekoodia ja tutoriaaleja lähes
- kaikkiin ohjelmoinnin haaroihin. Netistä puhumattakaan. Ennenkuin
- menetät toivosi tai menet kysymään mistään kahlaile sieltä kaikki
- tarpeelliset alueet ("C/C++" ja "muut") läpi. Melkein kaikkeen pitäisi
- vastaus noista dokumenteista löytyä (mistä muualta henkilöt joilta
- kysytään olisivat ne saaneet selville kuin dokumenteista).
- Lisäksi löytyy kymmeniä ohjelmointikirjastoja, joissa tulee lähdekoodi
- mukana. Ja muista, että itsekin voi tehdä päätelmiä ja kokeiluja. Kyllä
- aina jostain tarvittu tieto löytyy!
- 9.2 Hieman koodereiden jargonia
- -------------------------------
- Alta löytyy muutamia kivoja tietokoneslangin termejä, joiden merkitystä
- on hieman valotettu lyhyellä selityksellä. Huomaa, että tässä on vain
- tietokoneslangia, sanat optimaalinen, ideaalinen ja muu vastaava pitää
- yhä hakea Suomen Sivistyssanakirjasta:
- PIKSELI Yksi kuvaruudun piste. Piste voi olla mustavalkoinen,
- harmaasävyinen tai värillinen riippuen näyttötilasta ja sen
- muistinvienti vaihtelee 1 bitin ja 4 tavun välillä.
- KAKSOISPUSKURI näyttöpuskurin (ks. NÄYTTÖPUSKURI) kokoinen puskuri, jonne
- kaikki piirretään ennen ruudulle kopioimista, jolloin saadaan tavara
- mahdollisimman "tiivissä" (ei hajanaisia kirjoituksia silloin tällöin)
- paketissa hitaaseen näyttömuistiin.
- SEGMENTTI reaalitilassa muisti on jaettu 64 kilon kokoisin segmentteihin.
- Itseasiassa segmentti on 16 ylintä tavua 20-bittisessä (max. 1 mega
- osoitettavaa muistiavaruutta) osoitteessa ja tähän lisätään sitten
- vielä offsetti (ks. OFFSET). Muodostuskaava on seg*16+off.
- Myös lähdekoodista käännettävissä objektitiedostoissa (ks. OBJEKTI)
- on segmenttejä, mutta ne tarkoittavat tietojen säilömispalasia,
- kuten koodille varattu segmentti, datalle varattu segmentti ja
- numeromuuttujille varattu segmentti. Ks. myös SEGMENTTIREKISTERIT.
- OFFSET reaalitilan 20-bittisen muistiosoitteen 16 alinta bittiä. Huomaa,
- että segmenttirekisterin ollessa 16 ylintä on osa osoitteista
- "päällekkäin" Näin ollen on useita tapoja osoittaa samaan kohtaan
- muistia. Katso alhaalla olevaa esimerkkiä:
- Huomaa, että segmentti on yhden heksan, eli 4 bittiä enemmän vasemmalla,
- sillä 4 bitin bittisiirtohan vastaa kertomista 16:sta (2^4=16).
- Offsetti: 1234 6784
- Segmentti: + 5678 + 5123
- ------- -------
- 20-bittinen: 579B4 579B4
- SELEKTORI Suojatussa tilassa prosessori jakaa todellisen fyysisen muistin
- halutun kokoisiin loogisiin paloihin. Selektori kertoo prosessori mihin
- loogiseen alueeseen halutaan osoittaa. Tähän liittyy hieman monimutkai-
- sempaakin asiaa, mutta sen hyödyllisyys jää kyseenalaiseksi normaalissa
- peliohjelmoinnissa. Loogisilla muistialueilla on myös koko tallessa ja
- jos osoitellaan tätä pidemmälle muistialueella ohjelma kaatuu ja
- tulostaa virheilmoituksen. (SIGSEGV muistaakseni) Selektoreja käytetään
- usein kuten segmenttirekisterejäkin (ks. SEGMENTTIREKISTERIT), eli
- ds:ssä on dataselektori, cs:ssä koodiselektori jne.
- SEGMENTTIREKISTERIT Prosessorilla on muutama 16-bittinen segmenttirekisteri,
- jota täytyy käyttää kun halutaan osoittaa tiettyyn segmenttiin tai
- suojatussa tilassa selektoriin (ks. SELEKTORI). Näitä ovat cs, ds, es,
- fs, gs ja ss. Rekisteri cs säilöö koodin segmenttiä, eli koodia
- luettaessa prosessori katsoo aina segmentistä 'cs' tavaraa. Cs:n pari
- on ip-rekisteri, joka on 16-bittinen ja osoittaa koodin offsetin (ks.
- OFFSET). Lähes kaikilla segmenttirekistereillä on tällainen "pari",
- joka hoitaa offset-puolen.
- 386-prosessorista lähtien näissä on myös extended osa, joka on myös
- 16-bittinen ja käytettäessä tätä extended-osaa lisätään e-kirjain
- rekisterin eteen. Näitä käytetään etenkin suojatussa tilassa, jossa
- 64 kilotavua on hieman liian vähän, kun taas 32-bittinen (16-bittinen
- extended osa normaalin jatkoksi) osoite riittää varsin hyvin, ainakin
- toistaiseksi. Offset-rekisterejä ip, si, di, sp ja bp vastaavat siis
- eip, esi, edi, esp ja ebp.
- Seuraavaksi tulee ds, eli datasegmentti, joka on ns. oletussegmentti,
- jota käytetään jos et määrittele toista segmenttiä assembler-käskyssäsi
- (esim. mov [eax], ebx on sama kuin mov ds:[eax], ebx). Tämän "pari"
- taasen on si/esi. Sitten es, fs ja gs, jotka ovat taas yleissegmentti-
- rekisterejä, joista kaksi viimeistä lisättiin 368-prosessorin mukaan.
- Ainoastaan es:llä on pari, di/edi. Sitten löytyy ss, eli pinosegmentti,
- joka osoittaa siis ohjelman pinon (ks. PINO) segmenttiin ja tämän
- offset-pari, sp/esp. Ylimääräistä bp/ebp -offsetrekisteriä käytetään
- useasti osoittamaan funktion parametreihin ja omaan muistiin.
- PINO Ohjelmalle on varattu pino, josta funktiot voivat varata muistia
- siirtämällä ss:esp -parin osoittamaa osoitetta. Pinossa välitetään
- myös parametrit. Pino toimii LIFO-periaatteella (last in, first out),
- joka tarkoittaa, että viimeiseksi sinne pantu tieto tulee ensimmäisenä
- pois. Pinoon tallennetaan ja sieltä poistetaan tavaraa push ja pop
- käskyillä. Lisää tietoa kannattaa katsoa ulkoista assembleria
- käsittelevästä luvusta.
- OBJEKTI Tällä on käyttöyhteydestä riippuen muutamakin merkitys, mutta tär-
- keimmät lienevät alhaalla. Tietokonemaailmassa merkitys ei ole aivan
- sama kuin tosielämässä, esimerkiksi seksiobjektista puhuttaessa. =)
- Objekti-sanaa käytetään puhuttaessa pelien esineistä, sekä myöskin
- spriteistä ja bittikartoista, eli käytännössä objekti on jokin
- yksittäinen esine (luultavasti pelien käyttämä nimitys on juuri peräisin
- tästä bittikartta-merkityksestä, muistattehan te seikkailupelit?).
- Myös olioille on joskus joissain yhteyksissä käytetty nimitystä
- objekti, vaikkei se nyt olekaan enää kovin yleistä, ainakaan minun
- tietääkseni.
- Objektitiedosto taas on yksittäinen, käännetty lähdekoodi (C, assembler
- tai joku muu), jossa on tiettyjä segmenttejä, jotka sisältävät koodia
- ja lukumuuttujia. Lisäksi tällainen objektitiedosto sisältää paljon
- tietoa funktioiden nimistä ja funktioiden sijainnista tiedostossa,
- joita tarvitaan linkkauksessa (ks. LINKKAUS).
- LINKKAUS Tämä tapahtuma on viimeinen osa ohjelman käännösvaiheessa. Siinä
- konekielelle käännetyt objektitiedostot "linkataan", eli liitetään
- yhteen ajettavaksi tiedostoksi. Omien objektitiedostojesi lisäksi gcc
- linkkaa mukaan standardim C-kirjaston ja muut määrittelemäsi kirjastot,
- sekä ns. stubin (ks. STUB).
- STUB Sijaitsee EXE:n alussa ja käynnistyy ohjelman käynnistyessä. Sen
- tehtävä on raivata itse ohjelmalle sen koodin tarvitsema muistialue
- ja toimittaa muistipalvelut sun muut toimintakuntoon, jotta ohjelman
- ei tarvitse huolehtia näistä. Stub tavallaan hoitaa ohjelman
- suojatun tilan autuuteen sen omalle muistialueelle, jotta itse
- pääohjelmalla on helppoa. Tästä syystä perusmuistin kulutus on suojatun
- tilan ohjelmissa niin pientä, sillä vain stub pitää saada perusmuistiin
- ja sieltä käsin se sitten hinaa pääohjelman jatkettuun muistin, megan
- yläpuolelle.
- REAALITILA PC:n "alkuperäinen" tila, jossa on segmentit ja offsetit ja 1
- megan yläraja muistinkäytöllä. Tätä ollaan kierretty ohjelmilla kuten
- EMM386 ja HIMEM, jotka vastaavasti toimittavat suojatun tilan
- jatkomuistia reaalitilan ohjelmille (ks. EMS ja XMS).
- SUOJATTU TILA Jo 286-prosessorien mukana esitelty tila, mutta paremmin
- toteutettuna vasta 386:ssa (siksi DJGPP ei toimi vasta kuin sillä).
- Suojatussa tilassa on kaikki muisti käytettävissä ja prosessori
- tarjoaa useita palveluja, kuten muistin suojaus ja monta muuta kivaa
- ominaisuutta, jolla yksittäiset ohjelmat eivät pääse niin helposti
- kaatamaan konetta.
- EMS Expanded Memory Services on tapa antaa jatkettua muistia reaalitilan
- ohjelmille. Muistinhallintaohjelma, yleensä EMM386.EXE toimii siten,
- että se käyttää suojattua tilaa hyväkseen mapaten yli megan alueelta
- muistipalasia alle megan alueelle muistin kohtaan, jota kutsutaan
- nimellä PAGE FRAME (katso vaikka DOS:sin helpeistä). Ohjelma voi varata
- EMS-sivuja, joiden koko on 16 kilotavua ja sitten asettaa niistä
- maksimissaan 4 kerralla näkyviksi (siksi page framen koko on yleensä
- 4*16=64kilotavua). Nopea tapa, muttei niin nopea kuin suora muistin
- osoitus.
- XMS Extended Memory Services taas on systeemi, joka perustuu siihen, että
- järjestelmä tarjoaa joukon funktioita, joilla voit varata XMS-muistia
- ja kopioida sitä perusmuisti<->jatkomuisti, ja
- jatkomuisti<->jatkomuisti -alueilla. Ongelmana on se, että reaalitilan
- ohjelma ei voi käsitellä jatkomuistia kuin kopioimalla sen ensin
- perusmuistiin ja sitten takaisin jatkomuistiin, mikä tekee tästä usein
- aika hitaan tavan.
- PALETTI Tämä on näytönohjaimen muistissa oleva taulukko, jossa on
- värinumeroiden (värin 0, värin 1, värin 2 jne.) väriarvot, eli se
- paljonko mikäkin väri sisältää punaista, vihreää ja sinistä. Palettia
- ei käytetä high-color ja true-color tiloissa (ks. HIGHCOLOR ja
- TRUECOLOR), vaan ainoastaan 256- ja 16-värisissä tiloissa. Lisää
- paletin asettamisesta ja lukemisesta palettia käsittelevästä luvusta.
- HIGHCOLOR on väritila, jossa värejä on 65536 tai joissain tapauksissa
- 32768 kappaletta, eli 16-bittinen tai 15-bittinen pikseli
- (ks. PIKSELI). Tämän pikselin värinumero on jaettu yleensä siten,
- että numerosta 5 bittiä on tarkoitettu punaiselle, 6 vihreälle ja
- 5 siniselle (koska ihmisen silmä kai aistii tarkimmin vihreää),
- joten erillistä palettia ei tarvita. 15-bittisessä tilassa
- vastaavasti on vain 5 bittiä / väriarvo. Myös jotain virityksiä
- 14-bittisistä tiloista taitaa olla. Ks. myös PALETTI ja TRUECOLOR.
- TRUECOLOR on väritila, jossa on 16.7 miljoonaa väriä, tarkemmin 2^24
- väriä. Jako on kuten high-color tiloissa (ks. HIGHCOLOR), mutta
- jokaiselle väriarvolle on 8 bittiä, eli 1 tavu punaiselle, vihreälle
- ja siniselle. Ei varmaan tarvitse erikseen mainita, että tällaiset
- tilat ovat ohjelmoijan taivas. Uudempina on myös 32-bittiset tilat,
- joissa yksi tavu käytetään tietääkseni hukkaan. Tämä sen takia, että
- 32-bittinen pikseli (ks. PIKSELI) on paljon helpompi käsitellä, kun
- rekisterit ovat 32-bittisiä, samoin kuin joidenkin assembler-käskyjen
- käyttämät alkioiden koot. 24 tai 32 bittiä voidaan varmaan jakaa useilla
- muillakin tavoilla (ks. CMYK ja RGB) ja tehokkaammin kuin kaksi edellä
- esitettyä, mutta en tiedä kuinka paljon käytännössä käytetään
- toisenlaisia bittien jakotapoja.
- CMYK Cyan, Magenta, Yellow, black. Tämä on yksi tapa jakaa väriavaruus,
- eli käytetään normaalin rgb-tripletin sijasta (ks. RGB) syaania,
- magentaa, keltaista ja mustaa. Myös CMY-tyyppiä on näkynyt, josta siis
- musta puuttuu. Tätä ei käytetä kovin paljoa pelimaailmassa, mutta
- printtereiden ja skannereiden kanssa toiminut on varmaan tästä
- kuullut. Muistaisin, että on vielä pari tapaa jakaa värit, jokin YMK
- tai vastaava oli ainakin, mutta tiedän selittää vain CMYK-, CMY- ja
- RGB-mallit.
- RGB Red, Green, Blue. Tapa jakaa väriavaruus, eli jokainen väri sen puna-
- viher- ja sinikomponentteihin. Vähän samaan tyyliin siis kun sekoitat
- Punaisesta, sinisestä ja keltaisesta vesivärit ja muun vastaavan niin
- tietokoneella ja televisioissa käytetään tätä tapaa. Tiedä sitten
- miksi vihreä, luultavasti se soveltuu paljon helpommin sädeputkelle.
- RGB-AVARUUS Väriavaruus ajatellaan kuutioksi, jossa XYZ-akseliston
- korvaa RGB-akselisto. Kuutio on rajallinen ja rajat asettavat
- värikomponenttien minimi- ja maksimiarvot. Esim. normaalin
- VGA-paletin värit voidaan ajatella pisteiksi kuutiossa, jonka
- alakulma on (0,0) ja vastakkainen kulma (63,63).
- KVANTISOINTI Paletin kvantisointi on tapa optimoida käytössä olevaa
- palettia. Useasti tarvittaisiin enemmän värejä käyttöön kuin mitä
- niitä on käytettävissä ja tähän käytetään paletin värien "optimointia",
- joissa yhdistellään toisiaan lähellä olevia värejä. Kvantisoinnin
- tehtävä on siis lyhyesti etsiä optimaalinen n väriä sisältävä paletti
- jolla voidaan näyttää mahdollisimman alkuperäistä vastaavasti
- m-värinen kuva.
- MOODI Yleisesti käytetty lyhenne näyttötilasta, screen mode.
- HEKSA 16-kantainen, eli HEKSAdesimaalinen luku, käytetään monesti muisti-
- osoitteissa ja porttien numeroissa. Lisää tietoa tiedostosta LUVUT.TXT
- FYYSINEN OSOITE Ks. SELEKTORI
- LOOGINEN OSOITE Ks. SELEKTORI
- SIIRROSOSOITE Ks. OFFSET
- POINTTERI Hiiren kursori tai yleensä ohjelmoinnissa tietoalkio, joka
- sisältää muistiosoitteen. Pointteri näyttömuistiin on siis alkio,
- jonka arvo on näyttömuistin osoite (ks. OFFSET, SEGMENTTI).
- Hyödylliseksi pointterin tekee se, että sitä voidaan indeksoida,
- eli sitä voidaan käyttää kantaosoitteena johonkin muistialueeseen.
- Yhden indeksin osoittaman alkion pituus on pointterityypin pituus.
- Jos pointteri on char-tyyppinen niin sen yksi alkio on yhtä pitkä
- kuin yksi char-alkio, eli 1 tavu. Indeksi 10 olisi siis 10 tavua
- pointterin osoittamasta muistista eteenpäin.
- Tätä käytetään hyväksi esimerkiksi kaksoispuskurissa
- (ks. KAKSOISPUSKURI), jossa pointteri osoittaa sen alkuun (samoin
- kuin indeksi 0) ja sen 600. alkio kaksoispuskurin 600. tavuun,
- tässä tapauksessa tulevan framen (ks. FRAME) 600. pikseliin (ks.
- PIKSELI), olettaen että ollaan 256-värisessä tilassa.
- Lyhesti: Pointteri on muistiosoite, indeksointi on tapa saada indeksin
- määrämä alkio pointterin osoittamalta muistialueelta. Esim. 12. tavu
- pointterin alusta saataisiin pointterin indeksillä 11 (indeksi 0 on
- 1. tavu). Muista, että int-tyyppisen pointterin yhden indeksin
- osoittaman alkion pituus on sizeof(int), eli 4 tavua, jolloin indeksissä
- 0 on 1., 2., 3. ja 4. tavu ja indeksissä 1 vastaavasti 5., 6., 7. ja 8.
- FRAME Tarkoittaa yhtä näyttöruudullista, yhtä näyttöruudun päivityskertaa.
- Jos käytät kaksoispuskuria on frame se, minkä kopioit näyttömuistiin.
- Lyhyesti frame siis on valmis, näytettäväksi tarkoitettu ruudullinen
- kuva-informaatiota. Katso selvennykseksi myös kohta FRAMERATE.
- FRAMERATE On se montako framea (ks. FRAME) jokin ohjelma voi tuottaa
- tietyssä ajassa, yleensä sekunnissa (tätä framea/sekunti kutsutaan
- myös nimellä FPS). Jos matopelisi esimerkiksi pystyisi päivittämään
- madon sijainnin ruudulla, esteet ja muut objektit vaikka 10 kertaa
- sekunnissa niin sen "FPS" olisi näinollen 10.
- FPS, Frames per second. Ks. FRAMERATE.
- VIRKISTYSTAAJUUS Luku ilmoitetaan yleensä hertseinä (herzeinä, hertzeinä?)
- ja se kertoo montako kertaa sekunnissa (hertsi, hZ tarkoittaa
- värähtelyä/sekunti) monitori ja näytönohjain (heikoin lenkki ratkaisee)
- pystyvät päivittämään näyttöruutua. Normaalissa VGA-tilassa luku on 70,
- minkä takia yleensä sanotaan, että hyvän toimintapelin tulisi pyöriä
- 70 fps (ks. FRAMERATE). Tämä ei kuitenkaan estä sitä, että fps ei voisi
- olla suurempi kuin virkistystaajuus, mutta jos ohjelmasi pyörittää
- 300 kuvaa ruudulle sekunnissa (sopiva määrä 3D-enginelle jollain yksin-
- kertaisella varjostuksella, kuten phongilla) niin vain 70 näkyy.
- ANTIALIAS Tämä termi esiintyy yleensä englanninkielisessä materiaalissa
- muodossa antialising, joka tarkoittaa kuvioiden reunojen pehmentämistä
- väreillä. Esimerkiksi kun piirrät vinon viivan vihreällä mustalle poh-
- jalle se näyttää aika rujolta, mutta kun lisäät jokaiseen kulmaan hie-
- man tummanvihreätä näyttää viiva huomattavasti pehmeämmältä. Käytännössä
- tämä hoidetaan laskemalla paljonko viivan "arvio" (se mitä piirretään
- ruudulle) poikkeaa oikean viivan sijainnista ja mitä enemmän se poikkeaa
- sen enemmän reunoille laitetaan samaa väriä (väri siis sekoitetaan siinä
- suhteessa missä viiva sijaitsee minkäkin pikselin päällä.
- CROSSFADE Suomeksi termi voisi olla ehkä ristiliu'utus. Ideana on, että
- toinen kuva ilmestyy toisen takaa pikkuhiljaa, ensin vain haaleana,
- mutta voimistuen hiljalleen toisen häipyessä ja lopuksi ensimmäisestä
- kuvasta onkin tullut jälkimmäinen.
- PALETTE ROTATION Eli kauniimmin paletinpyöritys rullaa palettia ympäri siten,
- että aiemmin värinä 3 toiminut muuttuu väriksi 2, väri 2 muuttuu väriksi
- 1, 1 muuttuu 0:ksi ja nolla menee väriksi 255, väri 255 väriksi 254 jne.
- Eli siirretään koko palettia asken taaksepäin (tai eteenpäin) ja se joka
- ei voi enää mennä edemmäs tai taaemmas laitetaan toiseen päähän
- vapautuneelle paikalle. Myös osia paletista voidaan pyörittää. Tämä
- aiheuttaa varsin kivan näköisiä efektejä, etenkin jos tausta sisältää
- väriliu'utuksia. Tarkempaa tietoa palettia koskevasta kappaleesta ja
- esimerkkiohjelmasta pal3.c.
- RLE Taas uusi jännittävä lyhenne kokoelmaamme. Run Length Encoding
- tarkoittaa käytännössä, että kun meillä on 5 kappaletta N-kirjaimia,
- niin ilmoitamme ne tyyliin 5N. Näin säästämme 3 tavua tilaa jo
- tuossakin. Idea on siis, että useat toistuvat merkit ilmoitetaan
- numerona ja merkkinä. Toisaalta vaihteleva data on ongelma ja tähän
- on useita kiertotapoja, kuten PCX:n lähestymistapa, jossa tavu, jonka
- arvo on yli 192 tarkoittaa että seuraavaa tavua ilmestyy
- <luettutavu>-192 kertaa, tai LBM-tyyli, jossa on yksi tavu, joka kertoo
- joko kuinka monta pakkaamatonta pikseliä edessä on, tai kuinka monta
- pakattua (muistaakseni jos n on alle 128 niin tarkoitetaan montako
- pakkaamatonta edessä ja jos se on yli, niin sitten jotain tyyliin
- n-127).
- HANDLERI Tämä kummajainen on suomeksi sama kuin käsittelijä. Ohjelmassa
- on usein monenlaisia handlereita, kuten näppishandleri, joka käsittelee
- näppäinten painallukset tai esimerkiksi interrupt handleri, joka
- käsittelee toiminnan painaessa CTRL-BREAK tai CTRL-C
- -näppäinyhdistelmiä. Ks. myös KESKEYTYS.
- KESKEYTYS PC-perusrakenteeseen kuuluvat keskeytykset, jotka osa ovat nk.
- software-keskeytyksiä ja osa hardware-keskeytyksiä. Se kummantyyppinen
- keskeytys on riippuu siitä aiheuttaako sen ohjelma itse (esimerkiksi
- videokeskeytys 0x10 jolla voidaan vaihtaa vaikka näyttötilaa) vai
- generoidaanko se laitteiston toimesta (kuten ajastinkeskeytys, joka
- generoidaan halutuin välein). Keskeytyksen satuttua komento siirtyy
- keskeytyskäsittelijään (ks. HANDLERI), joka hoitaa tarvittavat
- toimenpiteet keskeytyksen satuttua. Käyttäjä voi itse koukuttaa
- handlereita (ks. KOUKUTUS) ja näin tarjota keskeytyspalveluja tai
- kutsua itse keskeytyksiä ja pyytää näiltä keskeytyskäsittelijöitä
- palveluksia, kuten edellämainittu videotilan vaihto.
- PC on aika keskeytyspohjainen tietokone ja esimerkiksi kovalevyn
- lukeminen ja muu vastaava tehdään yleensä keskeytysten kautta. Monet
- ajurit koukuttavat laitteen keskeytyksen ja kommunikoivat itse laitteen
- kanssa, jolloin keskeytystä kutsuvan ohjelman ei tarvitse tietää
- tarkasti miten laite toimii. Esimerkiksi hiirikeskeytyksen koukuttaa
- hiiriajuri ja ajuri hoitaa suoran kommunikoinnin hiiren kanssa ohjelman
- tarvitessa vain kutsua keskeytyskäsittelijää generoimalla
- hiirikeskeytys.
- KOUKUTUS (HOOKING) Keskeytyskäsittelijän muistiosoite sijaitsee taulukossa
- aivan muistin alussa (luoja tietää onko se siellä suojatussa tilassa,
- minä en ainakaan tiedä, mutta sillä ei onneksi ole väliä) ja koukutus
- tarkoittaa sitä, että otat talteen alkup. keskeytyskäsittelijän
- osoitteen ja sijoitat omasi sinne osoitteen tilalle, jolloin keskeytystä
- kutsuttaessa käsky siirtyy omalle käsittelijällesi (ks. myös HANDLERI
- ja KESKEYTYS). Voit myös kutsua vanhaa käsittelijää oman toimintasi
- jälkeen.
- LFB Tapaa osoittaa suoraan koko näyttömuistiin. Kuten VGA-segmentti
- A000h, mutta sijaitsee kaukana 1 megan rajan yläpuolella, joten
- kokorajoitus ei enää ole 64 kiloa.
- BANKED-TILAT Toinen tapa päästä käsiksi yli 64 kilon näyttömuistiin on
- tehdä pieni ikkuna (yleensä sama kuin VGA-segmentti ja koko 64
- kiloa) jota liikutellaan pitkin näyttömuistia. Aika tuskainen
- verrattuna LFB:hen (ks. LFB)
- VESA eli Video Electronics Standards Assocation, jonka käsialaa ovat
- mm. näytönohjaimien käsittelyyn yleisesti käytetty
- VESA-standardi. Virallinen nimi standardille lienee kuitenkin VBE
- (ks. VBE)
- VBE eli VESA BIOS Extension on normaalin grafiikkakeskeytyksen 10h
- rinnalle toteutettu joukko laajennuksia joka mahdollistaa
- SVGA-tilojen näytönohjainriippumattoman käsittelyn. 1.2, 2.0 ovat
- suosittuja ja 3.0 on ihan äskettäin saapunut.
- 9.3 Lähteet
- -----------
- Muutamia erityismaininnan ansaitsevia dokumentteja sähköisessä ja
- paperimuodossa sekalaisessa järjestyksessä, joiden sisältämää
- informaatiota on käytetty tämän tutoriaalin tekoon.
- Tiedostot:
- PCGPE10.ZIP
- Jokaisen ohjelmoijan pakkoimurointi. Sekalainen kokoelma valittuja
- paloja. Sisältää 10 ensimmäistä Aphyxian traineria!
- FMODDOC2.ZIP
- Kaikille äänikorteista ja MOD-playereistä kiinnostuineille hieman
- vaikea (äänikortin ohjelmointi ei nimittäin aina ole helppoa)
- tutoriaali sisältäen kaiken tarvittavan tiedon. Löytyy jokaisen
- itseään kunnioittavan TosiKooderin kovalevyltä.
- HELPPC21.ZIP
- Mainio asioiden tarkistamiseen soveltuva lähdeteos.
- HPC21_P5.ZIP
- Päivitys edelliseen sisältäen Pentium-käskyt.
- TUT*.ZIP
- Asphyxian VGA-trainerit. Etsi hakusanalla Asphyxia.
- 3DICA*.ZIP
- 3D ohjelmoija-wannaben sekä kokeneemmankin raamattu. Suomen
- kielellä kaiken lisäksi!
- DJTUT255.ZIP
- Selittää DJGPP:n AT&T-syntaksin ja inline-asseblerin
- englanniksi. Korvaamaton jos haluaa käyttää assembleria
- DJGPP-ohjelmissaan!
- ASSYT.ZIP
- Assemblerin alkeet suomeksi.
- NASM095B.ZIP
- Tällä voit tehdä Intel-syntaksin assemblerilla DJGPP:n
- COFF-muotoisia objektitiedostoja. Tiivistettynä TASM joka osaa
- myöskin DJGPP:n objektiformaatin. Huomaa, että uusin versio voi
- olla muutakin kuin 0.95 (095-osa tiedostonimessä).
- ABEDEMO?.ZIP
- Ruotsalainen demokoulu. Ei onneksi ruotsia, vaan
- englantia. Ensimmäisiä lukemiani tutoriaaleja, joka auttoi minut
- alkuun koodauksessa.
- INTER*.ZIP
- Ralph Brownin keskeytyslista. Sisältää hurjan määrän paketteja ja
- kyllä tietoakin.
- Kirjallisuus:
- Opeta itsellesi C++ -ohjelmointi 21 päivässä
- Jos et vielä osaa C++:ssaa tai C:tä, niin tämä voi olla
- lainaamisen arvoinen teos. Kokeneemmalle ohjelmoijalle
- suositeltavampi voi olla jokin muu, mutta monet on tämä kirja
- auttanut alkuun.
- 486-ohjelmointi
- Aina kun joku on kysynyt assembler-ohjelmointia käsittelevää
- kirjaa, niin tällä hänet on vaiennettu. Omasta mielestänikin kelpo
- kirja.
- Assembler-ohjelmointi
- Vaan joku kuitenkin oli sitä mieltä, että 486-ohjelmointi ei ollut
- paras, vaan että tämä kirja olisi selkeämpi. Itse en ole tätä
- lukenut.
- Computer Graphics: Principles and Practice
- Grafiikkaohjelmoijan raamattu. Sisältää paljon erilaisista
- algoritmeistä sun muusta. Tietääkseni.
- Zen of graphics programming: Second edition
- Grafiikkaohjelmoijan koraani. Tosin nykyään VESA:n ja
- huippumodernien 3d-engineiden aikana osa tiedosta on
- vanhentunutta. Sisältää kuitenkin todella tehokkaita
- optimointikikkoja sun muuta mukavaa.
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement