Raycaster-pelimoottori 4: Spritejen piirtäminen
Tähän asti raycaster-pelimoottorimme grafiikka on koostunut ainoastaan seinistä, mutta pelkillä seinillä ei voida esittää kaikkea sitä, mikä tekee pelistä pelin. Tällaisia asioita ovat esimerkiksi huonekalut, kerättävät esineet, viholliset ja lentävät ammukset. Implementoidaan siis spritejen piirtäminen, minkä avulla voidaan esittää kaikki tuo!
Spritet ovat kaksiulotteisia bittikarttakuvia, joiden kaikkia tekseleitä ei piirretä; niistä voi nähdä siis läpi. Monet vanhojen 3D-pelimoottorien spritet esitetään samanlaisina riippumatta siitä, mistä suunnasta niitä katsoo. Jos siis pelaaja pyörii tällaisten spritejen ympäri, ne ikään kuin pyörivät pelaajan mukana, ja näin tapahtuu meidänkin pelimoottorissamme.
Alkuvalmistelut spritejen laskemiseksi ja piirtämiseksi
Main.c
Muuta main.c-lähdekooditiedoston map-muuttujaa niin, että osa niistä alkioista, joiden arvo on 0, saakin arvon 2. Spritet piirretään siis näihin kohtiin kenttää.
Näistä spriteistä tulee ikään kuin koristeita, joilla ei ole mitään muuta pelillistä tarkoitusta kuin tehdä kentästä mielenkiintoisemman näköinen.
Globals.h
Lisää globals.h-otsikkotiedostoon määrittelyt muuttujille *lampBitmap ja numSprites. Näitä käytetään eri lähdekooditiedostoissa viittaamaan sprite-kuvaan ja spritejen määrään.
Main.c
Lisää sitten main.c-lähdekooditiedostoon *lampBitmap- ja numSprites-muuttujien alustaminen arvoilla NULL ja 0.
Setup.c
Anna setup.c-lähdekooditiedoston SetupProgram-funktion määrittelyssä lampBitmap-muuttujalle arvo, joka on taas load_bitmap-funktiokutsu parametrinaan lamp.bmp.
Piirrettävä sprite voi periaatteessa olla mitä tahansa, mutta tässä tapauksessa se on lamppu. Miksi? No, vaikka pelimaailma on mahdollista tehdä millaiseksi tahansa, se ei tarkoita sitä, että se voi olla millainen tahansa, sillä yksinkertaisimmankin pelin pitää noudattaa fysiikan lakeja niin pitkälle kuin mahdollista. Eli, koska kenttämme on toistaiseksi sisätiloissa, siellä on pakko olla valonlähteitä, jotta pelaaja pystyy näkemään muutakin kuin säkkipimeyttä. Siksi siis lamppu. Oikeastihan lamppu-spritemme ei valaise kenttää mitenkään, mutta leikitään niin.
Teknisesti spritebittikartan korkeus pitää olla 64 pikseliä. Leveys voi olla vähemmänkin, kunhan se on enintään 64 pikseliä.
| Lamp.bmp. |
Tosiaan, spriten kaikkia tekseleitä ei piirretä, vaan niistä näkee läpi. Läpinäkyvyys määritetään käyttämällä paletin ensimmäistä väriä, joka 8-bittisen VGA-kuvatilan oletuspaletissa on musta. Siksi lamp.bmp-bittikartan tausta on musta.
Main.c
Lisää taas main.c-lähdekooditiedoston main-funktioon destroy_bitmap-funktiokutsu, jossa vapautetaan lampBitmap muistista, kun ohjelma suljetaan.
Types.h
Lisää types.h-otsikkotiedostoon allegro.h-kirjaston otsikkotiedoston sisällytys.
Lisää sitten MapObject-rakenne, joka sisältää seuraavat spriteihin liittyvät ominaisuudet:
- sijainti (posX ja posY)
- etäisyys (distance)
- onko sprite aktiivinen vai ei (active)
- voiko pelaaja kulkea spriten läpi (block)
- seuraavat bittikarttamuuttujat, joita tarvitaan spritelle määritetyn bittikartan piirtämisessä:
- (BITMAP* bitmap
- BITMAP* (*getBitmap)(void* object)).
Koodissa spritejä kutsutaan siis nimellä MapObject.
Lisää myös MapObject-taulukko sprites ja anna sen kooksi vaikka 100; kentässä voi siis olla maksimissaan sata spriteä.
Utils.h
Lisää utils.h-otsikkotiedostoon GetObjectBitmap-funktion esittely.
Utils.c
Lisää sitten utils.c-lähdekooditiedostoon GetObjectBitmap-funktion määrittely, joka palauttaa bittikartan.
Setup.h
Lisää setup.h-otsikkotiedostoon allegro.h-kirjaston sekä types.h-otsikkotiedoston sisällytykset sekä InitMapObjects-funktion esittely.
Setup.c
Lisää sitten setup.c-lähdekooditiedostoon utils.h-otsikkotiedoston sisällytys, jotta voimme kutsua GetObjectBitmap-funktiota.
Lisää sitten määrittely funktiolle InitMapObjects, jossa käydään kenttä läpi, ja aina, kun haluttu map-taulukon alkion arvo löydetään (mapValue == mapValueToFind), annetaan arvot MapObject-rakenteen jäsenille posX, posY, distance, active, block, bitmap sekä getBitmap.
Setup.c
Lisää sitten setup.c-lähdekooditiedoston SetupGame-funktioon InitMapObjects-funktion kutsu. Kolmannen parametrin arvo 2 lisää siis spriten kenttään kaikkiin niihin kohtiin, joissa map-taulukon alkion arvoksi on annettu 2.
Utils.h
Lisää utils.h-otsikkotiedostoon esittely Compare-funktiolle, jonka avulla kaksi "päällekkäin" piirrettyä spriteä esitetään oikeassa järjestyksessä niin, että kauempana oleva sprite on lähempänä olevan takana.
Utils.c
Lisää utils.c-lähdekooditiedostoon Compare-funktion määrittely, jossa verrataan kahta spriteä niiden etäisyyden perusteella ja palautetaan niiden välinen etäisyys.
Spritejen laskeminen ja piirtäminen
Draw.h
Lisää draw.h-otsikkotiedostoon HandleMapObjects-funktion esittely.
Draw.c
Lisää draw.c-lähdekooditiedostoon utils.h-otsikkotiedoston sisällytys.
Lisää myös määrittely HandleMapObjects-funktiolle, joka käsittelee ja piirtää pelin spritet näytölle pelaajan perspektiivistä. Se toimii seuraavasti:
- Spritejen alustaminen ja muistinhallinta:
- Funktio allokoi muistia sprite-objekteille ja luo yhdistetyn objektitaulukon. Sprite-objektit kopioidaan tähän taulukkoon.
- Etäisyyksien laskeminen:
- Jokaisen spriten etäisyys pelaajasta lasketaan käyttämällä euklidista etäisyyttä.
- Spritejen järjestäminen:
- Spritet järjestetään etäisyyden mukaan laskevassa järjestyksessä, jotta kaukaisemmat spritet eivät piirry läheisempien päälle.
- Spritejen piirtäminen:
- Jokaisen aktiivisen spriten sijainti suhteessa pelaajaan lasketaan.
- Perspektiivitransformaatio tehdään spriten koordinaateille.
- Lasketaan spriten korkeus ja leveys sekä näytölle piirtämisen aloitus- ja lopetuskohdat.
- Näytölle piirretään spriten pystysuorat viivat (tekselit) tekstuurin avulla. Spriten koordinaatit ja tekstuurit skaalataan sopimaan näytön alueelle.
- Z-bufferia käytetään varmistamaan, että vain lähimmät spritet piirretään kyseiseen kohtaan näytöllä.
- Muistin vapauttaminen:
- Lopuksi vapautetaan objektitaulukkoon varattu muisti.
Lisää vielä draw.c-lähdekooditiedostoon kutsu HandleMapObjects-funktiolle.
Ja näin! Pelimaailmaan piirretään seinien lisäksi myös spritejä!
Spritet piirretään oikein myös toistensa taakse...
...ja silloin, kun ne ovat osittain seinän takana.
Spritejen törmäystunnistus
Seuraavaksi lisätään törmäystunnistus myös spriteille; emmehän halua, että pelaaja pystyy kävelemään niiden läpi.
Utils.h
Muokkaa utils.h-otsikkotiedostossa esiteltyä Collided-funktiota niin, että lisäät sille parametrit MapObject* sprites ja int numSprites.
Utils.c
Muokkaa utils.c-lähdekooditiedoston Collided-funktion määrittelyä niin, että lisäät siihen törmäystunnistuksen myös spriteille.
Lisäys tarkistaa, onko pelaaja (tai jokin toinen ympyrän muotoinen objekti) törmäysalueella minkä tahansa sprite-objektin kanssa. Se toimii seuraavasti:
- Iterointi sprite-objektien läpi:
- Jokainen sprite-objekti käydään yksitellen läpi silmukan avulla.
- Sprite-objektin raja-arvojen määrittäminen:
- Jokaisen objektin koordinaattien ympärille muodostetaan neliön muotoinen alue määrittämällä minimi- ja maksimiarvot x- ja y-akseleilla.
- Lähimmän pisteen löytäminen:
- Pelaajan sijainti suhteessa objektin törmäysalueeseen tarkistetaan, ja pelaajan sijainnin lähin piste objektin rajojen sisällä lasketaan.
- Etäisyyden laskeminen:
- Pelaajan sijainnin ja lähimmän pisteen välinen etäisyys (x- ja y-komponentit) lasketaan.
- Törmäyksen tarkistaminen:
- Jos etäisyyden neliö (distX² + distY²) on pienempi kuin pelaajan säteen neliö (radius²), pelaaja on törmäysalueella.
- Lisäksi tarkistetaan, onko objektin block-ominaisuus aktiivinen, mikä tarkoittaa, että objekti voi estää pelaajan liikkumisen.
- Jos molemmat ehdot täyttyvät, funktio palauttaa arvon 1, joka tarkoittaa törmäystä.
- Törmäyksen puuttuminen:
- Jos yksikään sprite ei täytä törmäysehtoja, funktio jatkaa kaikkien objektien tarkistamista.
Player.c
Muokkaa sitten player.c-lähdekooditiedoston MovePlayer-funktiota niin, että kaikkiin Collided-funktion tarkistuksiin on lisätty parametrit sprites ja numSprites. Huomaa, että vaikka alla on esitetty vain neljä Collided-funktiokutsua uusine parametreineen, lisäys pitää tehdä myös muihin MovePlayer-funktiomäärittelyn Collided-funktiokutsuihin!
Ja nyt, kun lamppuspritejen läpi ei pääse enää kävelemäänkään, spritejen implementointi on valmis!
Itse asiassa koko raycaster-pelimoottorimme on valmis, sillä jo näillä ominaisuuksilla on mahdollista koodata peli. No joo, onhan esimerkiksi Wolfenstein 3-D:n raycaster-moottori kehittyneempi kuin tämä, sillä siinä on seinien ja spritejen lisäksi ovia ja liikkuvia seinäkuutioita, mutta tämä riittää meille paremmin kuin hyvin!
Kommentit
Lähetä kommentti