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.

int map[MAP_HEIGHT][MAP_WIDTH] =
{
    {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
    {1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1},
    {1,2,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1},
    {1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,1,1},
    {1,0,0,0,0,0,0,2,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
    {1,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,1},
    {1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,2,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,1},
    {1,0,0,0,1,2,2,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1},
    {1,0,0,0,0,0,2,0,0,0,0,0,0,1,1,0,0,0,0,1,0,0,0,1},
    {1,0,0,0,0,0,1,0,0,0,0,0,1,1,1,0,0,0,0,1,0,0,0,1},
    {1,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1},
    {1,0,0,0,1,2,2,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1},
    {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,1},
    {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
};

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.

extern BITMAP *lampBitmap;

extern int numSprites;

Main.c

Lisää sitten main.c-lähdekooditiedostoon *lampBitmap- ja numSprites-muuttujien alustaminen arvoilla NULL ja 0.

BITMAP *lampBitmap= NULL;

int numSprites = 0;

Setup.c

Anna setup.c-lähdekooditiedoston SetupProgram-funktion määrittelyssä lampBitmap-muuttujalle arvo, joka on taas load_bitmap-funktiokutsu parametrinaan lamp.bmp.

int SetupProgram()
{
    if (allegro_init() != 0)
        return 1;

    install_keyboard();

    install_int_ex(TimerHandler, BPS_TO_TIMER(FRAMERATE));  
    LOCK_FUNCTION(TimerHandler);
    LOCK_VARIABLE(thCounter);

    if (set_gfx_mode(GFX_AUTODETECT, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0) != 0)
    {
        if (set_gfx_mode(GFX_SAFE, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0) != 0)
        {
            set_gfx_mode(GFX_TEXT, 0, 0, 0, 0);
            allegro_message("Unable to set any graphic mode\n%s\n", allegro_error);
            return 1;
        }
    }

    buffer = create_bitmap(SCREEN_WIDTH, SCREEN_HEIGHT);

    textureA = load_bitmap("walla.bmp", NULL);
    textureB = load_bitmap("wallb.bmp", NULL);

    lampBitmap = load_bitmap("lamp.bmp", NULL);

    return 0;
}

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.

Ks. tämä postaus oppiaksi, mitä pitää ottaa huomioon, jotta spritebittikartat piirretään pelissäsi oikein.

Main.c

Lisää taas main.c-lähdekooditiedoston main-funktioon destroy_bitmap-funktiokutsu, jossa vapautetaan lampBitmap muistista, kun ohjelma suljetaan.

int main(void)
{
    if (SetupProgram() != 0)
        return 1;

    SetupGame();

    while(!key[KEY_ESC])
    {
        while (thCounter > 0)
        {
            MovePlayer();
 
            RotatePlayer();

            DrawGame();

            thCounter--;
        }
    }

    destroy_bitmap(textureA);
    destroy_bitmap(textureB);

    destroy_bitmap(lampBitmap);

    destroy_bitmap(buffer);

    remove_timer();

    allegro_exit();

    return 0;
}
END_OF_MAIN()

Types.h

Lisää types.h-otsikkotiedostoon allegro.h-kirjaston otsikkotiedoston sisällytys.

#include <allegro.h>

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ä.

typedef struct {
    double posX, posY;
    double distance;
    int active;
    int block;
    BITMAP* bitmap;
    BITMAP* (*getBitmap)(void* object);
} MapObject;

MapObject sprites[100];

Utils.h

Lisää utils.h-otsikkotiedostoon GetObjectBitmap-funktion esittely.

#ifndef UTILS_H
#define UTILS_H

BITMAP* GetObjectBitmap(void* object);

int Collided(double posX, double posY, double radius);

#endif

Utils.c

Lisää sitten utils.c-lähdekooditiedostoon GetObjectBitmap-funktion määrittely, joka palauttaa bittikartan.

BITMAP* GetObjectBitmap(void* object)
{
    MapObject* obj = (MapObject*)object;
    return obj->bitmap;
}

Setup.h

Lisää setup.h-otsikkotiedostoon allegro.h-kirjaston sekä types.h-otsikkotiedoston sisällytykset sekä InitMapObjects-funktion esittely.

#ifndef SETUP_H
#define SETUP_H

#include <allegro.h>
#include "types.h"

void TimerHandler();

int SetupProgram();

void SetupGame();

void InitMapObjects(MapObject* objectsArray, int* numObjects, int mapValueToFind,
BITMAP* spriteBitmap);

#endif

Setup.c

Lisää sitten setup.c-lähdekooditiedostoon utils.h-otsikkotiedoston sisällytys, jotta voimme kutsua GetObjectBitmap-funktiota.

#include <allegro.h>
#include "globals.h"
#include "setup.h"
#include "utils.h"

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.

void InitMapObjects(MapObject* objectsArray, int* numObjects, int mapValueToFind, BITMAP* spriteBitmap)
{
    int x, y;

    *numObjects = 0;

    for (x = 0; x < MAP_WIDTH; x++)
    {
        for (y = 0; y < MAP_HEIGHT; y++)
        {
            if (map[x][y] == mapValueToFind)
            {
                MapObject* obj = &objectsArray[*numObjects];
                obj->posX = x + 0.5;
                obj->posY = y + 0.5;
                obj->distance = 0;
                obj->active = 1;
                obj->block = 1;
                obj->bitmap = spriteBitmap;
                obj->getBitmap = GetObjectBitmap;
                (*numObjects)++;
            }
        }
    }
}

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.

void SetupGame()
{
    install_mouse();

    InitMapObjects(sprites, &numSprites, 2, lampBitmap);

    player.posX = 22, player.posY = 12;
    player.dirX = -1, player.dirY = 0;
}

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.

#ifndef UTILS_H
#define UTILS_H

BITMAP* GetObjectBitmap(void* object);

int Collided(double posX, double posY, double radius);

int Compare(const void* a, const void* b);

#endif

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.

int Compare(const void* a, const void* b)
{
    MapObject* spriteA = (MapObject*)a;

    MapObject* spriteB = (MapObject*)b;

    return (spriteB->distance - spriteA->distance);
}

Spritejen laskeminen ja piirtäminen

Draw.h

Lisää draw.h-otsikkotiedostoon HandleMapObjects-funktion esittely.

#ifndef DRAW_H
#define DRAW_H

void HandleWalls(double zBuffer[]);

void DrawWallsTextured(int x, int originalDrawStart, int originalDrawEnd, int side,
double perpWallDist, double rayDirX, double rayDirY, int lineHeight, int drawStart);

void DrawWallsAsFlatColors(int x, int mapX, int mapY, int side, int drawStart, int drawEnd);

void HandleMapObjects(MapObject* sprites, int numSprites, double zBuffer[]);

void DrawGame();

#endif

Draw.c

Lisää draw.c-lähdekooditiedostoon utils.h-otsikkotiedoston sisällytys.

#include "utils.h"

Lisää myös määrittely HandleMapObjects-funktiolle, joka käsittelee ja piirtää pelin spritet näytölle pelaajan perspektiivistä. Se toimii seuraavasti:

  1. Spritejen alustaminen ja muistinhallinta:
    • Funktio allokoi muistia sprite-objekteille ja luo yhdistetyn objektitaulukon. Sprite-objektit kopioidaan tähän taulukkoon.
  2. Etäisyyksien laskeminen:
    • Jokaisen spriten etäisyys pelaajasta lasketaan käyttämällä euklidista etäisyyttä.
  3. Spritejen järjestäminen:
    • Spritet järjestetään etäisyyden mukaan laskevassa järjestyksessä, jotta kaukaisemmat spritet eivät piirry läheisempien päälle.
  4. Spritejen piirtäminen:
    1. Jokaisen aktiivisen spriten sijainti suhteessa pelaajaan lasketaan.
    2. Perspektiivitransformaatio tehdään spriten koordinaateille.
    3. Lasketaan spriten korkeus ja leveys sekä näytölle piirtämisen aloitus- ja lopetuskohdat.
    4. Näytölle piirretään spriten pystysuorat viivat (tekselit) tekstuurin avulla. Spriten koordinaatit ja tekstuurit skaalataan sopimaan näytön alueelle.
    5. Z-bufferia käytetään varmistamaan, että vain lähimmät spritet piirretään kyseiseen kohtaan näytöllä.
  5. Muistin vapauttaminen:
    • Lopuksi vapautetaan objektitaulukkoon varattu muisti.

void HandleMapObjects(MapObject* sprites, int numSprites, double zBuffer[])
{
    // Määritetään karttaobjektien kokonaismäärä.
    int totalObjects = numSprites;

    // Allokoidaan muistia kaikille karttaobjekteille.
    MapObject* allObjects = (MapObject*)malloc(totalObjects * sizeof(MapObject));

    // Alustetaan indeksi yhdistetylle objektitaulukolle.
    int index = 0;
   
    // Kopioidaan sprite-objektit yhdistettyyn taulukkoon.
    memcpy(allObjects + index, sprites, numSprites * sizeof(MapObject));

    // Päivitetään indeksi sprite-objektien määrällä.
    index += numSprites;

    int i, stripe;

    // Lasketaan etäisyydet pelaajasta kaikkiin objekteihin.
    for (i = 0; i < totalObjects; i++)
    {
        allObjects[i].distance = ((player.posX - allObjects[i].posX) *
        (player.posX - allObjects[i].posX) +
                                  (player.posY - allObjects[i].posY) *
        (player.posY - allObjects[i].posY));
    }

    // Järjestetään objektit etäisyyden mukaan laskevassa järjestyksessä.
    qsort(allObjects, totalObjects, sizeof(MapObject), Compare);

    // Käydään läpi kaikki objektit piirtämistä varten.
    for (i = 0; i < totalObjects; i++)
    {
        if (allObjects[i].active)
        {
            // Lasketaan objektin suhteellinen sijainti pelaajaan nähden.
            double objectX = allObjects[i].posX - player.posX;
            double objectY = allObjects[i].posY - player.posY;

            // Haetaan objektin nykyinen tekstuuri
            BITMAP* currentBitmap = allObjects[i].getBitmap(&allObjects[i]);

            // Lasketaan transformaation käänteismatriisin determinantti.
            double invDet = 1.0 / (planeX * player.dirY - player.dirX * planeY);

            // Suoritetaan perspektiivitransformaatio objektin sijainnille.
            double transformX = invDet * (player.dirY * objectX - player.dirX * objectY);
            double transformY = invDet * (-planeY * objectX + planeX * objectY);

            // Lasketaan objektin x-koordinaatti näytöllä.
            int objectScreenX = (int)((SCREEN_WIDTH / 2) * (1 + transformX / transformY));

            // Lasketaan objektin korkeus perspektiivin mukaan.
            int objectHeight = abs((int)(VIEW3D_HEIGHT / transformY));
            int drawStartY = -objectHeight / 2 + VIEW3D_HEIGHT / 2;
            int drawEndY = objectHeight / 2 + VIEW3D_HEIGHT / 2;
 
            // Määritetään tekstuurin alku- ja loppukohta y-akselilla.
            int texYStart = 0;
            int texYEnd = currentBitmap->h;

            // Varmistetaan, että objektin y-koordinaatit ovat näytön sisällä.
            if (drawStartY < 0)
            {
                texYStart = (0 - drawStartY) * currentBitmap->h / objectHeight;
                drawStartY = 0;
            }

            if (drawEndY >= VIEW3D_HEIGHT)
            {
                texYEnd = currentBitmap->h - ((drawEndY - VIEW3D_HEIGHT + 1)
                * currentBitmap->h / objectHeight);
 
                drawEndY = VIEW3D_HEIGHT;
            }

            // Lasketaan objektin leveyden suhde korkeuteen.
            double aspectRatio = (double)currentBitmap->w / (double)currentBitmap->h;

            // Lasketaan objektin leveys näytöllä
            int objectWidth = abs((int)(objectHeight * aspectRatio));
            int drawStartX = -objectWidth / 2 + objectScreenX;
            int drawEndX = objectWidth / 2 + objectScreenX;

            // Varmistetaan, että objektin x-koordinaatit ovat näytön sisällä.
            if (drawStartX <= 0)
                drawStartX = 0;

            if (drawEndX >= SCREEN_WIDTH)
                drawEndX = SCREEN_WIDTH;

            // Lasketaan korkeuskerroin suurille objekteille.
            double heightFactor = (objectHeight > VIEW3D_HEIGHT)
            ? (double)objectHeight / VIEW3D_HEIGHT : 1.0;

            // Määritetään viivan leveys objektin korkeuden perusteella.
            int stripeWidth;

            if (objectHeight >= VIEW3D_HEIGHT * heightFactor)
                stripeWidth = (int)(4 * heightFactor);
            else if (objectHeight >= 192)
                stripeWidth = 3;
            else if (objectHeight >= 128)
                stripeWidth = 2;
            else
                stripeWidth = 1;

            // Piirretään objektin pystysuorat viivat näytölle.
            for (stripe = drawStartX; stripe < drawEndX; stripe += stripeWidth)
            {
                if (transformY > 0 && stripe >= 0 && stripe < SCREEN_WIDTH && 
                transformY < zBuffer[stripe])
                {
                    // Lasketaan tekstuurin x-koordinaatti nykyistä viivaa varten.
                    int texX = (int)(256 * (stripe - (-objectWidth / 2 + objectScreenX))
                    * currentBitmap->w / objectWidth) / 256;

                    // Piirretään viiva nykyisellä tekstuurilla näytölle.
                    masked_stretch_blit(currentBitmap, buffer, texX, texYStart, 1,
                    texYEnd - texYStart, stripe, drawStartY, stripeWidth, drawEndY - drawStartY);
                }
            }
        }
    }

    // Vapautetaan muistivaraukset objektitaulukosta.
    free(allObjects);
}

Lisää vielä draw.c-lähdekooditiedostoon kutsu HandleMapObjects-funktiolle.

void DrawGame()
{
    double zBuffer[SCREEN_WIDTH];

    clear_to_color(buffer, makecol(47, 47, 47));

    HandleWalls(zBuffer);

    HandleMapObjects(sprites, numSprites, zBuffer);

    vsync();

    blit(buffer, screen, 0, 0, 0, 0, SCREEN_WIDTH, VIEW3D_HEIGHT);
}

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.

#ifndef UTILS_H
#define UTILS_H

BITMAP* GetObjectBitmap(void* object);

int Collided(double posX, double posY, double radius, MapObject* sprites,
int numSprites);

int Compare(const void* a, const void* b);

#endif

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:

  1. Iterointi sprite-objektien läpi:
    • Jokainen sprite-objekti käydään yksitellen läpi silmukan avulla.
  2. 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.
  3. 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.
  4. Etäisyyden laskeminen:
    • Pelaajan sijainnin ja lähimmän pisteen välinen etäisyys (x- ja y-komponentit) lasketaan.
  5. Törmäyksen tarkistaminen:
    1. Jos etäisyyden neliö (distX² + distY²) on pienempi kuin pelaajan säteen neliö (radius²), pelaaja on törmäysalueella.
    2. Lisäksi tarkistetaan, onko objektin block-ominaisuus aktiivinen, mikä tarkoittaa, että objekti voi estää pelaajan liikkumisen.
    3. Jos molemmat ehdot täyttyvät, funktio palauttaa arvon 1, joka tarkoittaa törmäystä.
  6. Törmäyksen puuttuminen:
    1. Jos yksikään sprite ei täytä törmäysehtoja, funktio jatkaa kaikkien objektien tarkistamista.
int Collided(double posX, double posY, double radius, MapObject* sprites,
int numSprites)
{
    int mapX = (int)posX;
    int mapY = (int)posY;

    int dx, dy, i, num;

    for (dx = -1; dx <= 1; dx++)
    {
        for (dy = -1; dy <= 1; dy++)
        {
            if (map[mapX + dx][mapY + dy] == 1)
            {
                double nearestX = (posX < mapX + dx) ? mapX + dx :
                ((posX > mapX + dx + 1) ? mapX + dx + 1 : posX);
 
                double nearestY = (posY < mapY + dy) ? mapY + dy :
                ((posY > mapY + dy + 1) ? mapY + dy + 1 : posY);

                double distX = posX - nearestX;
                double distY = posY - nearestY;

                if ((distX * distX + distY * distY) < (radius * radius))
                {
                    return 1;
                }
            }
        }
    }

    num = numSprites;

    for (i = 0; i < num; i++)
    {
        MapObject* obj;

        obj = &sprites[i];
 
        double objMinX = obj->posX - 0.5;
        double objMaxX = obj->posX + 0.5;
        double objMinY = obj->posY - 0.5;
        double objMaxY = obj->posY + 0.5;

        double nearestX = (posX < objMinX) ? objMinX : ((posX > objMaxX) ? objMaxX : posX);
        double nearestY = (posY < objMinY) ? objMinY : ((posY > objMaxY) ? objMaxY : posY);

        double distX = posX - nearestX;
        double distY = posY - nearestY;

        if ((distX * distX + distY * distY) < (radius * radius) && obj->block)
        {
            return 1;
        }
    }

    return 0;
}

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!

void MovePlayer()
{
    double newPosX, newPosY;

    if (key[KEY_W])
    {
        newPosY = player.posY + player.dirY * player.moveSpeed;
        newPosX = player.posX + player.dirX * player.moveSpeed;

        if (!Collided(player.posX, newPosY, player.radius, sprites, numSprites)
        ||  !Collided(newPosX, player.posY, player.radius, sprites, numSprites))
        {
            if (!Collided(player.posX, newPosY, player.radius, sprites, numSprites))
            {
                player.posY = newPosY;
            }

            if (!Collided(newPosX, player.posY, player.radius, sprites, numSprites))
            {
                player.posX = newPosX;
            }
        }

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