Raycaster-pelimoottori 2: Seinien piirtäminen ja pelaajan liikkuminen

Tässä osassa lisätään seinien laskeminen ja piirtäminen sekä pelaajan liikkuminen törmäystunnistuksineen.

Globals.h

Tee globals.h-otsikkotiedostoon seuraavat lisäykset:

  • Vakiot MAP_WIDTH ja MAP_HEIGHT määrittävät pelin kartan koon (24 × 24 ruutua).
  • Map[MAP_HEIGHT][MAP_WIDTH] on globaali 2D-taulukko, joka kuvaa pelin karttaa. Kartassa arvo 1 määrittää seinät ja 0 tyhjän tilan.
  • PlaneX ja planeY määrittelevät kameratason, joka vaikuttaa pelaajahahmon näkökenttään ja säteiden suuntaan.
#define MAP_WIDTH 24
#define MAP_HEIGHT 24
 
extern int map[MAP_HEIGHT][MAP_WIDTH];
 
extern double planeX, planeY;

Main.c

Tee main.c-lähdekooditiedostoon seuraavat lisäykset main-funktiomäärityksen yläpuolelle:

  • Map-muuttuja alustetaan kuvastamaan pelin kenttää. Kartta sisältää seiniä ja tyhjiä alueita, jotka määrittävät, miten pelaajahahmo ja säteet liikkuvat.
    • Tee kentästä millainen haluat, mutta anna kaikkien reuna-alkioiden arvoksi 1; emmehän halua, että pelaaja voi nähdä tai karata kentän ulkopuolelle.
  • PlaneX ja planeY asettavat kameratason, joka vaikuttaa näkökentän leveyteen ja perspektiiviin.
    • Huomaa planeY-muuttujalle annettu arvo 158.4 / VIEW3D_HEIGHT. Tällä arvolla 3D-näkymän muoto piirretään oikein.
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,0,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,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,0,0,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,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,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,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,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1},
    {1,0,0,0,0,0,0,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,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1},
    {1,0,0,0,1,0,0,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}
};
 
double planeX = 0, planeY = 158.4 / VIEW3D_HEIGHT;

Seinien laskeminen ja piirtäminen

Draw.h

Lisää draw.h-otsikkotiedostoon seuraavat funktioesittelyt parametreineen:

  • HandleWalls laskee 3D-näkymän seinät.
  • DrawWallsAsFlatColors piirtää seinät yksinkertaisilla väreillä.
#ifndef DRAW_H
#define DRAW_H
 
void HandleWalls(double zBuffer[]);
 
void DrawWallsAsFlatColors(int x, int mapX, int mapY, int side, int drawStart,
int
drawEnd);
 
void DrawGame();

#endif

Draw.c

Sisällytä draw.c-lähdekooditiedostoon matematiikkakirjasto math.h.

#include <allegro.h>
#include <math.h>
 
#include "globals.h"
#include "draw.h"

Lisää sitten HandleWalls-funktion määrittely, jossa toteutetaan raycasting-algoritmi. Tiivistetysti pelaajan sijainnista lähetetään yhtä monta sädettä kuin ruudun vaakasuunnassa on pikseleitä (eli 320), ja aina, kun säde kohtaa karttaruudun reunan, katsotaan, onko ruutu tyhjä (map-taulukon arvo 0) vai onko ruutu määritetty seinäksi (map-taulukon arvo 1). Jos ruutu on tyhjä, säde jatkaa matkaa seuraavan ruudun reunaan asti, ja sama tarkistus tehdään uudestaan. Tämä jatkuu niin kauan, kun tarkasteltava ruutu on tyhjä. Kun seinä sitten jossain vaiheessa tulee vastaan, lopetetaan säteen eteneminen ja piirretään ruutuun pystyviiva; mitä kauempana pelaajasta seinäosuma tulee, sitä lyhyempi viiva piirretään.

Kattavampi selitys ja alkuperäinen koodi löytyy osoitteesta https://lodev.org/cgtutor/raycasting.html#Untextured_Raycaster_.

void HandleWalls(double zBuffer[])
{
    int x;

    // Käydään läpi jokainen näytön pikseli vaakasuunnassa.
    for (x = 0; x < SCREEN_W; x++)
    {
        // Lasketaan kamerakoordinaatti (−1 ... 1), jossa x sijaitsee.
        double cameraX = 2 * x / (double)SCREEN_W - 1;

        // Säteiden suunta määritellään pelaajan suunnan ja kameratason perusteella.
        double rayDirX = player.dirX + planeX * cameraX;
        double rayDirY = player.dirY + planeY * cameraX;

        // Lasketaan kuinka paljon säde etenee x- tai y-suunnassa yhdessä askeleessa.
        double deltaDistX = (rayDirX == 0) ? 1e30 : fabs(1 / rayDirX);
        double deltaDistY = (rayDirY == 0) ? 1e30 : fabs(1 / rayDirY);

        // Seuraava askel x- ja y-suunnassa sekä niiden suunta (+1 tai -1).
        int stepX, stepY;

        // Etäisyydet seuraavaan x- ja y-sivuun karttaruudussa.
        double sideDistX, sideDistY;

        // Selvitetään, missä karttaruudussa pelaaja on tällä hetkellä.
        int mapX = (int)(player.posX);
        int mapY = (int)(player.posY);

        // Määritellään askel x-suunnassa ja ensimmäinen etäisyys x-sivuun.
        if (rayDirX < 0)
        {
            stepX = -1;
            sideDistX = (player.posX - mapX) * deltaDistX;
        }
        else
        {
            stepX = 1;
            sideDistX = (mapX + 1.0 - player.posX) * deltaDistX;
        }

        // Määritellään askel y-suunnassa ja ensimmäinen etäisyys y-sivuun.
        if (rayDirY < 0)
        {
            stepY = -1;
            sideDistY = (player.posY - mapY) * deltaDistY;
        }
        else
        {
            stepY = 1;
            sideDistY = (mapY + 1.0 - player.posY) * deltaDistY;
        }

        // Indikaattorit sille, onko säde osunut seinään ja kummalle sivulle seinää
        // osuma on tullut: x vai y.
        int hit = 0;
        int side;

        // Suoritetaan DDA-algoritmi (Digital Differential Analysis) löytääksemme osumat seinään.
        while (hit == 0)
        {
            // Siirrytään seuraavaan karttaruutuun x- tai y-suunnassa.
            if (sideDistX < sideDistY)
            {
                sideDistX += deltaDistX;
                mapX += stepX;
                // X-sivu. 
                side = 0;
            }
            else
            {
                sideDistY += deltaDistY;
                mapY += stepY;
                // Y-sivu. 
                side = 1;
            }

            // Tarkistetaan, osuiko säde seinään (map- taulukon arvo 1).
            if (map[mapX][mapY] == 1)
            {
                // Osuma! 
                hit = 1;
            }
        }

        // Lasketaan lopullinen etäisyys seinään.
        double perpWallDist;

        // Lasketaan seinän etäisyys pelaajasta (projektio kameran suuntaan).
        if (side == 0)
            perpWallDist = (sideDistX - deltaDistX);
        else
            perpWallDist = (sideDistY - deltaDistY);

        // Tallennetaan seinän etäisyys z-puskuriin säteen x-koordinaattia varten.
        // Tätä tarvitaan sitten, kun piirretään seiniin bittikarttatekstuurit. 
        zBuffer[x] = perpWallDist;

        // Lasketaan seinän korkeus näytöllä.
        int lineHeight = (int)(VIEW3D_HEIGHT / perpWallDist);

        // Seinän piirtämisen aloitus- ja lopetuskohdat (y-koordinaatit).
        int drawStart = -lineHeight / 2 + VIEW3D_HEIGHT / 2;
        int drawEnd = lineHeight / 2 + VIEW3D_HEIGHT / 2;

        // Säilytetään alkuperäiset piirtokohdat vertailua varten.
        // Tätä tarvitaan sitten, kun piirretään seiniin bittikarttatekstuurit. 
        int originalDrawStart = drawStart;
        int originalDrawEnd = drawEnd;

        // Varmistetaan, ettei piirtokohdat ylitä ruudun reunoja.
        if (drawStart < 0)
            drawStart = 0;

        if (drawEnd >= VIEW3D_HEIGHT)
            drawEnd = VIEW3D_HEIGHT - 1;

        // Piirretään seinä pystysuorana viivana.
        DrawWallsAsFlatColors(x, mapX, mapY, side, drawStart, drawEnd);
    }
}

Lisää sitten draw.c-lähdekooditiedostoon DrawWallsAsFlatColors-funktion määrittely, jossa määritellään seinien väri ja piirretään pystyviiva.

  • Seinät piirretään punaiksi rgbColor[3]-funktion arvolla {255, 0, 0}.
  • Jos seinä piirretään y-suunnassa (if (side == 1)), sen väri tummennetaan jakamalla alkuperäinen väri kahdella.
    • Tämä tekee 3D-näkymästä selkeämmän, sillä kun y-suunnan seinien väri on tummempi, ne ovat ikään kuin varjossa, mikä tuo niiden muodon esiin.
  •  Lopulta piirretään pystysuora viiva kutsumalla funktio vline.
void DrawWallsAsFlatColors(int x, int mapX, int mapY, int side, int drawStart,
int drawEnd)
{                      
    int rgbColor[3] = {255, 0, 0};

    if (side == 1)
    {
        rgbColor[0] = rgbColor[0]/2;
        rgbColor[1] = rgbColor[1]/2;
        rgbColor[2] = rgbColor[2]/2;
    }

    vline(buffer, x, drawStart, drawEnd, makecol(rgbColor[0], rgbColor[1],
    rgbColor[2]));
}

Tee seuraavat lisäykset draw.c-lähdekooditiedoston DrawGame-funktioon.

  • Luo muuttuja zBuffer[SCREEN_WIDTH].
  • Lisää kutsu HandleWalls-funktiolle.
  • Lisää kutsu vsync-funktiolle, jolla synkronoidaan ruudunpäivitys.
void DrawGame()
{
    double zBuffer[SCREEN_WIDTH];

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

    HandleWalls(zBuffer);

    vsync();

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

Seinien piirtämiseen liittyvät lisäykset on nyt tehty. Jotta seinät saadaan pelitilassa näkyviin, täytyy vielä määritellä pelaajahahmo ja sen ominaisuudet.

Alkuvalmistelut pelaajan liikuttamiseksi ja törmäyksien havaitsemiseksi

Pelaajahahmo laitetaan liikkumaan nykyaikaisella WASD-näppäinten ja hiiren yhdistelmällä. Jos haluaisimme toteuttaa ihan oikean vanhan koulukunnan buumeriammuskelun, liikkuminen eteen ja taakse sekä kääntyminen vasemmalle ja oikealle tapahtuisi nuolinäppäimillä. Entäs sitten liikkuminen sivulle eli streiffaaminen? Painettaessa nuolinäppäimiä vasemmalle ja oikealle pidettäisiin ALT-näppäintä pohjassa. Yksinkertaista!

Pelaajahahmon liikkumisen ja kääntymisen lisäksi toteutetaan myös törmäystunnistus, jotta seinistä ei päästä läpi.

Types.h

Määrittele types.h-otsikkotiedostoon Player-rakenne, joka sisältää tiedot pelaajan sijainnista, suunnasta, nopeudesta ja fyysisestä säteestä.

struct Player {
    double posX, posY;
    double dirX, dirY;
    double moveSpeed;
    double radius;
};

Globals.h

Sisällytä siten globals.h-otsikkotiedostoon types.h-otsikkotiedosto.

#include "types.h"
 
Lisää myös vakio P_MOVE_SPEED ja anna sille arvoksi esim. 0.1. Mitä suurempi arvo, sitä suurempi nopeus. Tällä vakiolla määritetään pelaajan liikkumisnopeus.
 
#define P_MOVE_SPEED 0.1

Tee myös seuraavat lisäykset:

  • Määrittele player-muuttuja globaaliksi, jotta sitä voidaan käyttää kaikissa tiedostoissa.
  • Määrittele globaalit muuttujat mouseX ja mouseY, joita käytetään hiiren sijainnin seuraamiseen.
extern struct Player player;
 
extern int mouseX, mouseY; 

Main.c

Alusta main.c-lähdekooditiedostoon player-rakenteen arvot, jotka määrittelevät pelaajan alkutilan:

  • posX ja posY ovat pelaajahahmon sijainti
  • dirX ja dirY ovat pelaajahahmon suunta
  • moveSpeed on pelaajahahmon liikkumisnopeus, jonka arvo määritettiin vakioon P_MOVE_SPEED
  • radius on pelaajahahmon säde.

Alusta myös muuttujat mouseX ja mouseY alkuarvoilla 0.

struct Player player = {
    0,            // posX
    0,            // posY
    0,            // dirX
    0             // dirY
    P_MOVE_SPEED, // moveSpeed
    0.2,          // radius
    };
 
int mouseX, mouseY = 0;

Setup.h

Esittele setup.h-otsikkotiedostossa SetupGame-funktio.

#ifndef SETUP_H
#define SETUP_H

void TimerHandler();
 
int SetupProgram();
 
void SetupGame();

#endif

Setup.c

Määrittele setup.c-lähdekooditiedostossa SetupGame-funktio:

  • asennetaan hiiren toiminnallisuus funktiokutsulla install_mouse
  • laitetaan pelaajahahmo tiettyyn paikkaan ja sen katse tiettyyn suuntaan antamalla arvot muuttujille player.posX, player.posY, player.dirX ja player.dirY.
void SetupGame()
{
    install_mouse();

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

Main.c

Lisää kutsu SetupGame-funktiolle main.c-lähdekooditiedoston main-funktiooon. Näin siis aina, kun peli alkaa, asennetaan hiiri ja laitetaan pelaaja aloituspaikkaan ja -suuntaan.
 
int main(void)
{
    if (SetupProgram() != 0)
        return 1;

    SetupGame();

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

            DrawGame();

            thCounter--;
        }
    }

    destroy_bitmap(buffer);

    remove_timer();

    allegro_exit();

    return 0;
}
END_OF_MAIN()

Nyt kun koodin kääntää ja GAME.EXEn suorittaa, piirretään seinät yksivärisinä pystyviivoina. Vielä ei voida kuitenkaan liikkua.

Pelaajan törmäystunnistus

Seuraavaksi tehdään kaikki pelaajahahmon törmäystunnistukseen liittyvät lisäykset.

Utils.h

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

#ifndef UTILS_H
#define UTILS_H

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

#endif

Utils.c

Tee sitten seuraavat lisäykset utils.c-lädekooditiedostoon:

  • sisällytetään Allegro-kirjasto.
  • sisällytetään globals.h, jotta päästään käsiksi globaaleihin muuttujiin.
  • sisällytetään tämän tiedoston otsikkotiedosto utils.h.
#include <allegro.h>

#include "globals.h"
#include "utils.h"

Lisää sitten utils.c-lähdekooditiedostoon Collided-funktion määrittely.

Collided-funktio tarkistaa pelaajahahmon sijainnin ja säteen perusteella, osuuko pelaajahahmo ympärillään oleviin seinäruutuihin. Se laskee seinäruudun lähimmän pisteen etäisyyden pelaajahahmon sijaintiin ja vertaa tätä säteeseen. Jos etäisyys on pienempi kuin säde, funktio havaitsee törmäyksen ja palauttaa arvon 1; muuten palautetaan 0.

int Collided(double posX, double posY, double radius)
{
    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;
                }
            }
        }
    }

    return 0;
}

Pelaajan liikkuminen ja kääntyminen

Seuraavaksi toteutetaan pelaajahahmon liikkuminen WASD-näppäimillä ja sen kääntyminen hiirellä.

Player.h

Lisää player.h-otsikkotiedostoon MovePlayer- ja RotatePlayer-funktioiden esittely.

#ifndef PLAYER_H
#define PLAYER_H

void MovePlayer();
 
void RotatePlayer();

#endif

Player.c

Tee sitten seuraavat kirjasto- ja otsikkotiedostosisällytykset player.c-lähdekooditiedostoon:

  • matematiikkakirjasto math.h, jota käytetään esim. kulmien laskemiseen
  • Allegro-kirjasto allegro.h, jonka avulla päästään käsiksi näppäimistön ja hiiren syötteisiin
  • globals.h, player.h sekä utils.h, joista saadaan käyttöön globaalit muuttujat, pelaajahahmon tietue sekä Collided-funktio.
#include <math.h>
#include <allegro.h>

#include "globals.h"
#include "player.h"
#include "utils.h"

Lisää sitten player.c-lähdekooditiedostoon Moveplayer-funktion määrittely.

Tämä funktio vastaa pelaajahahmon liikkumisesta eteen, taakse ja sivuille. Pelaajahahmon liike on riippuvainen seuraavista:

  • näppäinpainallukset W (eteen), S (taakse), A (vasemmalle), D (oikealle).
  • tarkistetaan törmäykset ja estetään pelaajahahmoa menemästä seinien läpi
  • jos pelaajahahmo törmää seinään, liukumaominaisuus sallii hänen jatkaa liikkumista toista akselia pitkin
  • estetään pelaajahahmon jumiutuminen seinien kulmiin.

Funktio käyttää törmäyksen havaitsemiseen Collided-funktiota ja muokkaa pelaajan sijaintia muuttujien posX ja posY avulla.

Funktiomäärittelystä tulee aika hirviömäinen, sillä kaikissa neljässä suunnassa, joihin pelaajahahmo voi liikkua, katsotaan, törmääkö pelaajahahmo seinään liikkuessaan diagonaalisesti kaikkiin neljään suuntaan: vasempaan ja oikeaan ylä- ja alaviistoon. Tämä sen takia, jotta pelaajahahmo ei jää jumiin seinien kulmiin. Toimiipahan ainakin.

void MovePlayer()
{
    double newPosX, newPosY;

    // Liikutaan eteenpäin W-näppäimellä.
    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)
        ||  !Collided(newPosX, player.posY, player.radius))
        {
            if (!Collided(player.posX, newPosY, player.radius))
            {
                // Liu'utaan Y-akselia pitkin.
                player.posY = newPosY;
            }
 
            if (!Collided(newPosX, player.posY, player.radius))
            {
                // Liu'utaan X-akselia pitkin.
                player.posX = newPosX;
            }
        }
        else
        {
            // Liikutaan eteenpäin kohti vasenta yläviistoa.
            if (player.dirX < 0 && player.dirY < 0)
            {
                if (map[(int)(player.posX) - 1][(int)(player.posY)] == 0 &&
                    map[(int)(player.posX)][(int)(player.posY) - 1] == 0)
                {
                    // Liikutetaan pelaajahahmoa sivusuunnassa, jotta estetään kulmiin
                    // jumiutuminen. 
                    player.posY += player.dirX * player.moveSpeed;
                    player.posX -= player.dirY * player.moveSpeed;
                }
            }  
            // Liikutaan eteenpäin kohti oikeaa yläviistoa.
            else if (player.dirX > 0 && player.dirY < 0)
            {
                if (map[(int)(player.posX) + 1][(int)(player.posY)] == 0 &&
                    map[(int)(player.posX)][(int)(player.posY) - 1] == 0)
                {
                    player.posY += player.dirX * player.moveSpeed;
                    player.posX -= player.dirY * player.moveSpeed;
                }
            }
            // Liikutaan eteenpäin kohti vasenta alaviistoa.
            else if (player.dirX < 0 && player.dirY > 0)
            {
                if (map[(int)(player.posX) - 1][(int)(player.posY)] == 0 &&
                    map[(int)(player.posX)][(int)(player.posY) + 1] == 0)  
                {
                    player.posY += player.dirX * player.moveSpeed;
                    player.posX -= player.dirY * player.moveSpeed;
                }
            }
            // Liikutaan eteenpäin kohti oikeaa alaviistoa.
            else if (player.dirX > 0 && player.dirY > 0)
            {
                if (map[(int)(player.posX) + 1][(int)(player.posY)] == 0 &&
                    map[(int)(player.posX)][(int)(player.posY) + 1] == 0)
                {
                    player.posY += player.dirX * player.moveSpeed;
                    player.posX -= player.dirY * player.moveSpeed;
                }
            }
        }
    }

    // Liikutaan taaksepäin S-näppäimellä.
    if (key[KEY_S])
    {
        newPosY = player.posY - player.dirY * player.moveSpeed;
        newPosX = player.posX - player.dirX * player.moveSpeed;

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

            if (!Collided(newPosX, player.posY, player.radius))
            {
                player.posX = newPosX;
            }
        }
        else
        {
            // Liikutaan taaksepäin kohti vasenta yläviistoa.
            if (player.dirX > 0 && player.dirY > 0)
            {
                if (map[(int)(player.posX) - 1][(int)(player.posY)] == 0 &&
                    map[(int)(player.posX)][(int)(player.posY) - 1] == 0)
                {
                    player.posY += player.dirX * player.moveSpeed;
                    player.posX -= player.dirY * player.moveSpeed;
                }
            }  
            // Liikutaan taaksepäin kohti oikeaa yläviistoa.
            else if (player.dirX < 0 && player.dirY > 0)
            {
                if (map[(int)(player.posX) + 1][(int)(player.posY)] == 0 &&
                    map[(int)(player.posX)][(int)(player.posY) - 1] == 0)
                {
                    player.posY += player.dirX * player.moveSpeed;
                    player.posX -= player.dirY * player.moveSpeed;
                }
            }
            // Liikutaan taaksepäin kohti vasenta alaviistoa.
            else if (player.dirX > 0 && player.dirY < 0)
            {
                if (map[(int)(player.posX) - 1][(int)(player.posY)] == 0 &&
                    map[(int)(player.posX)][(int)(player.posY) + 1] == 0)  
                {
                    player.posY += player.dirX * player.moveSpeed;
                    player.posX -= player.dirY * player.moveSpeed;
                }
            }
            // Liikutaan taaksepäin kohti oikeaa alaviistoa.
            else if (player.dirX < 0 && player.dirY < 0)
            {
                if (map[(int)(player.posX) + 1][(int)(player.posY)] == 0 &&
                    map[(int)(player.posX)][(int)(player.posY) + 1] == 0)
                {
                    player.posY += player.dirX * player.moveSpeed;
                    player.posX -= player.dirY * player.moveSpeed;
                }
            }
        }
    }

    // Liikutaan sivusuunnassa vasemmalle A-näppäimellä.
    if (key[KEY_A])
    {
        newPosY = player.posY + player.dirX * player.moveSpeed;
        newPosX = player.posX - player.dirY * player.moveSpeed;

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

            if (!Collided(newPosX, player.posY, player.radius))
            {
                player.posX = newPosX;
            }
        }
        else
        {
            double strafeDirX = player.dirY;
            double strafeDirY = -player.dirX;

            int diagonalX = (int)(player.posX + strafeDirX * player.moveSpeed);
            int diagonalY = (int)(player.posY + strafeDirY * player.moveSpeed);

            // Liikutaan sivusuunnassa kohti oikeaa alaviistoa.
            if (strafeDirX < 0 && strafeDirY < 0)
            {
                if (map[diagonalX + 1][diagonalY] == 0 &&
                    map[diagonalX][diagonalY + 1] == 0)
                {
                    // Liikutetaan pelaajahahmoa sivusuunnassa, jotta estetään kulmiin
                    // jumiutuminen. 
                    player.posX += player.dirX * player.moveSpeed;
                    player.posY += player.dirY * player.moveSpeed;                        
                }
            }

            // Liikutaan sivusuunnassa kohti oikeaa yläviistoa.
            if (strafeDirX > 0 && strafeDirY < 0)
            {
                if (map[diagonalX - 1][diagonalY] == 0 &&
                    map[diagonalX][diagonalY + 1] == 0)
                {
                    player.posX += player.dirX * player.moveSpeed;
                    player.posY += player.dirY * player.moveSpeed;
                }
            }

            // Liikutaan sivusuunnassa kohti vasenta alaviistoa.
            if (strafeDirX < 0 && strafeDirY > 0)
            {
                if (map[diagonalX + 1][diagonalY] == 0 &&
                    map[diagonalX][diagonalY - 1] == 0)
                {
                    player.posX += player.dirX * player.moveSpeed;
                    player.posY += player.dirY * player.moveSpeed;
                }
            }

            // Liikutaan sivusuunnassa kohti vasenta yläviistoa.
            if (strafeDirX > 0 && strafeDirY > 0)
            {
                if (map[diagonalX - 1][diagonalY] == 0 &&
                    map[diagonalX][diagonalY - 1] == 0)
                {
                    player.posX += player.dirX * player.moveSpeed;
                    player.posY += player.dirY * player.moveSpeed;
                }
            }
        }
    }

    // Liikutaan sivusuunnassa oikealle D-näppäimellä.
    if (key[KEY_D])
    {
        newPosY = player.posY - player.dirX * player.moveSpeed;
        newPosX = player.posX + player.dirY * player.moveSpeed;

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

            if (!Collided(newPosX, player.posY, player.radius))
            {
                player.posX = newPosX;
            }
        }
        else
        {
            double strafeDirX = -player.dirY;
            double strafeDirY = player.dirX;

            int diagonalX = (int)(player.posX + strafeDirX * player.moveSpeed);
            int diagonalY = (int)(player.posY + strafeDirY * player.moveSpeed);
 
            // Liikutaan sivusuunnassa kohti oikeaa alaviistoa.
            if (strafeDirX < 0 && strafeDirY < 0)
            {
                if (map[diagonalX + 1][diagonalY] == 0 &&
                    map[diagonalX][diagonalY + 1] == 0)
                {
                    // Liikutetaan pelaajahahmoa sivusuunnassa, jotta estetään kulmiin
                    // jumiutuminen.
                    player.posX += player.dirX * player.moveSpeed;
                    player.posY += player.dirY * player.moveSpeed;
                }
            }

            // Liikutaan sivusuunnassa kohti oikeaa yläviistoa.
            if (strafeDirX > 0 && strafeDirY < 0)
            {
                if (map[diagonalX - 1][diagonalY] == 0 &&
                    map[diagonalX][diagonalY + 1] == 0)
                {
                    player.posX += player.dirX * player.moveSpeed;
                    player.posY += player.dirY * player.moveSpeed;
                }
            }

            // Liikutaan sivusuunnassa kohti vasenta alaviistoa.
            if (strafeDirX < 0 && strafeDirY > 0)
            {
                if (map[diagonalX + 1][diagonalY] == 0 &&
                    map[diagonalX][diagonalY - 1] == 0)
                {
                    player.posX += player.dirX * player.moveSpeed;
                    player.posY += player.dirY * player.moveSpeed;
                }
            }

            // Liikutaan sivusuunnassa kohti vasenta yläviistoa.
            if (strafeDirX > 0 && strafeDirY > 0)
                if (map[diagonalX - 1][diagonalY] == 0 &&
                    map[diagonalX][diagonalY - 1] == 0)
                {
                    player.posX += player.dirX * player.moveSpeed;
                    player.posY += player.dirY * player.moveSpeed;
                }
            }                
        }
    }
}

Lisää sitten player.c-lähdekooditiedostoon RotatePlayer-funktion määrittely.

Tämä funktio vastaa pelaajan suunnan muuttamisesta hiiren liikkeen perusteella:

  • lasketaan hiiren liikkeen määrä x-akselilla ja muutetaan sitä herkkyyskertoimen avulla
  • päivitetään pelaajan suunta (dirX ja dirY) pyörimismatriisilla trigonometristen funktioiden kosini (cos) ja sini (sin) avulla.
  • päivitetään kamerataso (planeX ja planeY), joka vaikuttaa näkymään pelimaailmassa.

Lopuksi hiiren sijainti nollataan, jotta pelaajan ohjaaminen on sujuvaa.

void RotatePlayer()
{
    // Haetaan hiiren nykyinen sijainti.
    get_mouse_mickeys(&mouseX, &mouseY);

    // Annetaan arvo hiiren herkkyydelle.
    double mouseSensitivity = 0.001;

    // Lasketaan kääntyminen hiiren vaakasijainnin perusteella.
    double rotAmount = -mouseX * mouseSensitivity;

    // Käännetään pelaajahahmoa.
    if (rotAmount != 0)
    {
        double oldDirX = player.dirX;
        player.dirX = player.dirX * cos(rotAmount) - player.dirY * sin(rotAmount);
        player.dirY = oldDirX * sin(rotAmount) + player.dirY * cos(rotAmount);

        double oldPlaneX = planeX;
        planeX = planeX * cos(rotAmount) - planeY * sin(rotAmount);
        planeY = oldPlaneX * sin(rotAmount) + planeY * cos(rotAmount);
    }

    // Resetoidaan hiiren sijainti.
    position_mouse(SCREEN_W / 2, SCREEN_H / 2);
}

Main.c

Lisää lopuksi main.c-lähdekooditiedoston main-funktioon kutsut funktioille MovePlayer- ja RotatePlayer.

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

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

            thCounter--;
        }
    }

    destroy_bitmap(buffer);

    remove_timer();

    allegro_exit();

    return 0;
}
END_OF_MAIN()

Nyt kun pelin kääntää ja suorittaa, pelimaailmassa voi liikkua ja kääntyä, eikä seinistäkään pääse läpi. Täydestä menee, vaikka ruutuun piirretään vain eripituisia pystyviivoja. Pienestäpä on kolmiulotteinen illuusio kiinni!

Kommentit