ÜÜÜÜÜ ÜÜÜÜÜÜÜÜ ÜÜÜÜÜÜÜÜÜ ÜÜÜÜÜÜÜ ÜÜÜÜÜÜÜ ÜÜÜÜÜÜÜÜÜ Û²±° °Û Û²±° °±²±Û Û±° °±²±° Û Û° °±²±°Û Û²±° °±²Û Û °±²±° °±Û Û±° °±Û Û±° °±²±°Û Û° °±²±° °Û Û °±²±° Û Û±° °±²±Û Û°±²±° °±²Û Û° °±²Û Û±° °±²±° °Û Û° °±²±° °±²Û Û°±²±° °±Û±° °±²±°Û Û±²±° °±²±Û Û °±²±Û Û° °±²±° °±Û Û °±²±° °±²±Û Û±²±° °±²±° °±²±° Û Û±²±° °±²±° Û Û°±²±°Û Û °±²ÛÛ °±²Û Û°±²±°ÛÛ±²±°Û Û²±° °±²±° °±²±° °Û Û²±° °ÛÛ±° °Û Û±²±° Û Û°±²±ÛÛ°±²±Û Û±²±° ÛÛ²±° °ÛÛ±° °±Û±° °±Û±° °±Û Û±° °±ÛÛ° °±Û Û²±° °Û Û°±²±°ÛÛ±²±° Û±²±° °ÛÛ±° °±ÛÛ° °±²Û° °±²Û° °±²Û Û° °±²ÛÛ °±²Û Û±° °±ÛÜÜÜÛ±²±° °±²±° °Û²±° °±²±° °±²ÛÛ °±²±Û °±²±Û °±²±ÛÛ° °±²±° °±²±°Û Û° °±²±° °±²±° °±²±° °±²±° °±²±° °±²±° °±²±°ÛÛ±²±ÛÛ°±²±°ÛÛ °±²±° °±²±° Û Û °±²±° °±²±° °±²±° °±²±° °±²±° °±²±° °±²±° ÛÛ²±°ÛÛ±²±° ÛÛ°±²±° °±²±° °Û Û°±²±° °±²±° °±²ÛÛ °±²±° °±²±°ÛÛ±²±° °±²±° °ÛÛ±° ÛÛ²±° °ÛÛ±²±° °ÛÛ±° °±Û ßßßßßßßßßßßßßßß ßßßßßßßßßßßß ßßßßßßßßßßßß ßßß ßßßßßßßßßßßßß ßßßßß Laaman tie DJGPP-peliohjelmointiin versio 2.20. By Jokke of Bad Karma Copyright (C) Joonas Pihlajamaa 1997-1999. 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 alempana tiedostossa). 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 niistä helpoimpaan. 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 /* muistathan, _dos_ds on määritelty täällä! */ #include /* 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 /* _dos_ds ! */ #include /* _farpokeb(selektori, siirros, arvo) */ #include /* 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 #include #include /* 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, vihreä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 #include #include #include #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 väriarvot ja asettaa ne RGB-rakenteeseen . 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 #include #include #include #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 #include #include #include #include #include #include #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(position192) { 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 #include #include #include #include #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 (ei siis BMP-kuva, vaan ihan käsite). 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ä bitmappiimme reikiä, eli teemme sen osittain läpinäkyväksi. Mutta miten tämä kaikki sitten tehdään? Koko asia on, kuten kaikki asiat ohjelmoinnissa lopulta 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; yyysize; yy++) { for(xx=0; xxxsize; 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 - looppaa b välillä 0 - bittikarttasijainti = a * + b ruutusijainti = ( + a ) * 320 + b + 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 , , ja 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; yyysize; yy++) { for(xx=0; xxxsize; 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->bitmap==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; yybitmap[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 #include #include #include #include #include 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; currentframe[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; currentframe[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 #include #include #include #include #include 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 #include #include #include #include #include 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 sen 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. DJTUT*.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. NASM*B.ZIP Tällä voit tehdä Intel-syntaksin assemblerilla DJGPP:n COFF-muotoisia objektitiedostoja. Tiivistettynä TASM joka osaa myöskin DJGPP:n objektiformaatin. En muista mikä se versio on. 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 spriteä 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 desimaaliluvut 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^ 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^: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.000015258. 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 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'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 #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 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 -o tulos.exe 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 -o tulos.exe ). Archive-tiedoston nimen voi antaa joko tiedostojen mukana tai parametrinä -l JOS archive on DJGPP:n LIB-hakemmistossa nimellä lib.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. 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ä. 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 12 (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). Huomaa myös, että C++:ssalla luokan metodeissa ensimmäinen parametri on aina this-parametri, ja sitten tulevat "normaalit" muuttujat. Muutenkin nimien muodostus on jonkin verran hankalaa, joten kannattaa tehdä C-tiedostosta assembly-koodia gcc:n -S -parametrilla ja katsoa minkä niminen funktion tulee olla. Tässä nyt tämä lopullinen assyosuus, joka pitää olla alussa ja lopussa: push ebp mov ebp, esp sub esp, add esp, pop ebp ret Toisen C-funktion kutsu taas onnistuu seuraavasti, otetaan esimerkkinä vaikka foo(int,short,char,int): push push push push call _foo add esp, 11 Nuo -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 void cfunktio(int luku) { printf("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 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 -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 #include #include #include #include #include _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 #include 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 -- | -- . --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; looph.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 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.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.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 -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.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). SEAL on kuulemma hyvä, mutta minä en ole kokeillut. 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.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.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ä 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 -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.