Buumeriräiskintä 3: Vihollisten käyttäytyminen

Tässä osassa koodataan vihollisille kyky havaita pelaaja, ampua sitä ja liikkua sitä kohti.

Alkuvalmistelut

Types.h

Lisää types.h-otsikkotiedoston MapObject-rakenteeseen seuraavat uudet muuttujat:
  • seesPlayer tarkastelee, onko pelaaja vihollisen näköpiirissä

  • shootCooldown määrittää vihollisen tulinopeuden

  • isEnemyProjectile erottaa toisistaan vihollisten ja pelaajan ammukset, jotta peli osaa päättää keneen ne voivat osua.

 
typedef struct {
    double posX, posY;
    double distance;
    double dirX, dirY;
    double speed;
    int active;
    int block;
    int health;
    int seesPlayer;
    int shootCooldown;
    int isEnemyProjectile;
    BITMAP* bitmap;
    BITMAP* (*getBitmap)(void* object);
} MapObject;

Setup.c

Lisää setup.c-lähdekooditiedoston SetupGame-funktiomäärittelyyn seesPlayer- ja shootCooldown-muuttujien alustus 0:ksi.
 
for (i = 0; i < numEnemies; i++)
{
    enemies[i].seesPlayer = 0;
    enemies[i].shootCooldown = 0;
    enemies[i].health = E_HEALTH;        
}

Projs.h

Lisää sitten projs.h-otsikkotiedoston SpawnProjectile-funktion esittelyyn uusi parametri: kokonaislukumuuttuja isEnemyProjectile.

Samaa SpawnProjectile-funktiota käytetään siis sekä pelaajan että vihollisen ammusten luomiseen, ja tällä parametrilla tarkastellaan, onko luotu ammus pelaajan vai vihollisen.

 
void SpawnProjectile(MapObject projectiles[], int numProjectiles, double startX,
double startY, double dirX, double dirY, int isEnemyProjectile, BITMAP* spriteBitmap);

Projs.c

Lisää myös projs.c-lähdekooditiedoston SpawnProjectile-funktion määrittelyyn sama parametri, jonka arvo välitetään projectile-MapObject-rakenteen isEnemyProjectile-muuttujalle (projectile->isEnemyProjectile = isEnemyProjectile).
 
void SpawnProjectile(MapObject projectiles[], int numProjectiles, double startX,
double startY, double dirX, double dirY, int isEnemyProjectile,
BITMAP* spriteBitmap)
{
    int i;

    for (i = 0; i < numProjectiles; i++)
    {
        MapObject* projectile = &projectiles[i];

        if (!projectile->active)
        {
            projectile->posX = startX;
            projectile->posY = startY;
            projectile->dirX = dirX;
            projectile->dirY = dirY;
            projectile->speed = PROJECTILE_SPEED;
            projectile->active = 1;
            projectile->isEnemyProjectile = isEnemyProjectile;
            projectile->bitmap = spriteBitmap;
            projectile->getBitmap = GetObjectBitmap;
            break;
        }
    }
}
 
Tee sitten UpdateProjectiles-funktiomäärittelyyn seuraavat lisäykset:
 
void UpdateProjectiles(MapObject projectiles[], int numProjectiles, MapObject enemies[],
int numEnemies)
{
    int i, j;

    for (i = 0; i < numProjectiles; i++)
    {
        MapObject* projectile = &projectiles[i];
       
        if (projectile->active)
        {
            projectile->posX += projectile->dirX * projectile->speed;
            projectile->posY += projectile->dirY * projectile->speed;

            int mapX = (int)(projectile->posX);
            int mapY = (int)(projectile->posY);

            if (map[mapX][mapY] == 1)
            {
                projectile->active = 0;

                continue;
            }

            // Lasketaan ammuksen etäisyys pelaajaan.
            double distToPlayer = sqrt((player.posX - projectile->posX) * 
                                       (player.posX - projectile->posX) +
                                       (player.posY - projectile->posY) * 
                                       (player.posY - projectile->posY));
 
            // Jos etäisyys on pienempi kuin 0.3 ja ammus on lähtöisin vihollisesta...
            if (distToPlayer < 0.3 && projectile->isEnemyProjectile)
            {
                // ...tuhotaan ammus. 
                projectile->active = 0;

                continue;
            }

            for (j = 0; j < numEnemies; j++)
            {
                MapObject* enemy = &enemies[j];

                double distToEnemy = sqrt((enemy->posX - projectile->posX) * 
                                          (enemy->posX - projectile->posX) +
                                          (enemy->posY - projectile->posY) * 
                                          (enemy->posY - projectile->posY));

                // Jos etäisyys on pienempi kuin 0.3, ammus ei ole vihollisen ampuma
                // ja vihollinen on aktiivinen...
                if (distToEnemy < 0.3 && !projectile->isEnemyProjectile && enemy->active)
                {
                    projectile->active = 0;

                    HandleEnemyHit(enemy);

                    break;
                }
            }
        }
    }
}

Player.c

Muuta vielä player.c-lähdekooditiedoston PlayerShooting-funktiomäärittelyssä kutsuttua SpawnProjectiles-funktiota niin, että toiseksi viimeiseksi sen parametreihin on lisätty isEnemyProjectile-muuttuja arvolla 0. Eli, pelaajan ampuma ammus ei ole vihollisen ampuma (duh).
 
void PlayerShooting()
{
    if ((mouse_b & 1) && player.coolDown == 0)
        player.allowShooting = 1;
    else
        player.allowShooting = 0;

    if (player.allowShooting)
    {
        player.coolDown = P_SHOOT_COOLDOWN;

        SpawnProjectile(projectiles, MAX_PROJECTILES, player.posX, player.posY,
        player.dirX, player.dirY, 0, projectileBitmap);
    }

    if (player.coolDown > 0)
        player.coolDown--;
}

Vihollisten näkö- ja ampumiskyky

Jotta vihollisista olisi oikeasti jotain uhkaa pelaajalle, niiden pitää pystyä näkemään pelaaja sekä tietysti ampumaan tätä. 

Globals.h

Lisää globals.h-otsikkotiedostoon vakio E_SHOOT_COOLDOWN eli vihollisen tulinopeus. Anna sen arvoksi esim. 10, joka on tässä tapauksessa kuin pelaajan tulinopeus.
 
#define E_SHOOT_COOLDOWN 10

Enemies.h  

Lisää enemies.h-otsikkotiedostoon esittely funktiolle HandleEnemies, joka käy läpi kaikki viholliset ja huolehtii niiden tekoälystä: näkevätkö ne pelaajan ja mitä ne tekevät kun ne näkevät ja eivät näe pelaajaa.
 
#ifndef ENEMIES_H
#define ENEMIES_H

void HandleEnemies(MapObject enemies[], int numEnemies);

void HandleEnemyHit(MapObject* enemy);

#endif

Enemies.c

Lisää sitten enemies.c-lähdekooditiedostoon HandleEnemies-funktion määrittely. 
 
void HandleEnemies(MapObject enemies[], int numEnemies)
{
    int i;

    // Käydään läpi kaikki viholliset.
    for (i = 0; i < numEnemies; i++)
    {
        MapObject* enemy = &enemies[i];

        // Oletuksena vihollinen ei näe pelaajaa.
        enemy->seesPlayer = 0;
 
        // Lasketaan säteen suunta kohti pelaajaa.
        double rayDirX = player.posX - enemy->posX;
        double rayDirY = player.posY - enemy->posY;

        // Jos vihollinen on aktiivinen eikä ole nähnyt pelaajaa...
        if (enemy->active && !enemy->seesPlayer)
        {
            double stepSize = 0.01;
            double currentX = enemy->posX;
            double currentY = enemy->posY;

            // ...kuljetaan askel kerrallaan kohti pelaajaa ja tarkistetaan esteet tai osuma.
            while (sqrt((currentX - player.posX) * (currentX - player.posX) +
                        (currentY - player.posY) * (currentY - player.posY)) > stepSize)
            {
                currentX += rayDirX * stepSize;
                currentY += rayDirY * stepSize;

                // Jos törmätään seinään, keskeytetään tarkistus.
                if (map[(int)currentX][(int)currentY] == 1)
                {
                    break;
                }

                // Jos saavutetaan pelaaja, merkitään osuma.
                if (fabs(currentX - player.posX) < 0.05 &&
                    fabs(currentY - player.posY) < 0.05)
                {
                    enemy->seesPlayer = 1;
                    break;
                }
            }
        }

        // Jos vihollinen näkee pelaajan...
        if (enemy->active && enemy->seesPlayer)
        {
            // ...odotetaan, että laukaisuaika nollaantuu...
            if (enemy->shootCooldown > 0)
            {
                enemy->shootCooldown--;
            }
            else
            {
                double length = sqrt(rayDirX * rayDirX + rayDirY * rayDirY);
                rayDirX /= length;
                rayDirY /= length;

                // ...ammutaan pelaajaa kohti...
                SpawnProjectile(projectiles, MAX_PROJECTILES, enemy->posX, enemy->posY,
                rayDirX, rayDirY, 1, projectileBitmap);

                // ...ja asetetaan uusi laukaisuviive.
                enemy->shootCooldown = E_SHOOT_COOLDOWN;
            }
        }
    }
}

Draw.c

Lisää sitten draw.c-lähdekooditiedoston DrawGame-funktiomäärittelyyn kutsu HandleEnemies-funktiolle parametrinaan enemies ja numEnemies. 
 
void DrawGame()
{
    double zBuffer[SCREEN_WIDTH];

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

    HandleWalls(zBuffer);
   
    HandleEnemies(enemies, numEnemies);

    UpdateProjectiles(projectiles, MAX_PROJECTILES, enemies, numEnemies);

    HandleMapObjects(sprites, numSprites, projectiles, MAX_PROJECTILES, enemies, numEnemies, zBuffer);

    vsync();

    blit(buffer, screen, 0, 0, 0, 0, SCREEN_WIDTH, VIEW3D_HEIGHT);
}
 
Nyt viholliset alkavat ampua pelaajaa aina tämän nähdessään. Hui!
 

Vihollisten liikkuminen kohti pelaajaa

Nythän vihulaiset seisovat paikallaan kuin tatit, mutta aika tylsäähän sekin on. Toki jos pelin vihollisiksi riittää tykkitornit tai muut liikkumattomat uhat, voisimme lopettaa tähän. Mutta tietysti viholliset pitää saada myös liikkeelle! Koodataanpa siis vihollisten käyttäytymistä niin, että aina, kun ne eivät näe pelaajaa, ne liikkuvat tätä kohti. Tuplahui!

Globals.h

Lisää globals.h-otsikkotiedostoon vakio E_MOVE_SPEED ja anna sille arvoksi esim. 0.1. Mitä suurempi arvo, sitä suurempi nopeus.
 
#define E_MOVE_SPEED 0.1

Types.h

Lisää types.h-otsikkotiedoston MapObject-rakenteeseen kokonaislukumuuttuja index. Annetaan siis kullekin viholliselle oma numeronsa, jotta voidaan paremmin käsitellä yksittäistä vihollista.
 
typedef struct {
    double posX, posY;
    double distance;
    double dirX, dirY;
    double speed;
    int active;
    int block;
    int health;
    int seesPlayer;
    int shootCooldown;
    int isEnemyProjectile;
    int index;              
    BITMAP* (*getBitmap)(void* object);
} MapObject;

Setup.c

Tee setup.c-lähdekooditiedoston InitMapObjects-funktiomäärittelyyn lisäys, jossa index-muuttujalle annetaan *numObjects-muuttujan arvo. Näin kukin vihollinen saa oman index-numeronsa. 
 
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->index = *numObjects;
                obj->bitmap = spriteBitmap;
                obj->getBitmap = GetObjectBitmap;
                (*numObjects)++;
            }
        }
    }
}

Enemies.c

Tee sitten enemies.c-lähdekooditiedoston HandleEnemies-funktiomäärittelyn pohjalle lisäys, jossa aina, kun vihollinen on aktiivinen eikä se ole havainnut pelaajaa, lasketaan sille uusi sijainti liukulukumuuttujiin newPosX ja newPosY. Lisäksi tarkistetaan, törmääkö vihollinen muihin objekteihin (!Collided-funktiokutsu). Jos ei, siirretään se uuteen sijaintiin.
 
if (enemy->active && enemy->seesPlayer)
{

        if (enemy->shootCooldown > 0)
    {
        enemy->shootCooldown--;
    }
    else
    {
        double length = sqrt(rayDirX * rayDirX + rayDirY * rayDirY);
        rayDirX /= length;
        rayDirY /= length;

        SpawnProjectile(projectiles, MAX_PROJECTILES, enemy->posX, enemy->posY,
        rayDirX, rayDirY, 1, projectileBitmap);
 
        enemy->shootCooldown = E_SHOOT_COOLDOWN;
    }
}
else
{
    double newPosX = enemy->posX + rayDirX * E_MOVE_SPEED;
    double newPosY = enemy->posY + rayDirY * E_MOVE_SPEED;

    if (!Collided(enemy->posX, newPosY, 0.45, sprites, numSprites, enemies, numEnemies,
        1, enemy->index) && enemy->active)
        enemy->posY = newPosY;

    if (!Collided(newPosX, enemy->posY, 0.45, sprites, numSprites, enemies, numEnemies,
        1, enemy->index) && enemy->active)
        enemy->posX = newPosX;
}
 
Nyt viholliset liikkuvat pelaajaa kohti aina, kun ne eivät näe sitä. Huomaa, että vihollinen yrittää päästä suoraan pelaajan luo aina, kun näköyhteyttä ei ole; silloinkin, kun välissä on seinä. Vihollinen saattaa siis jäädä paikoilleen puskemaan nurkkia ja seiniä. Pöljä mikä pöljä. Mutta ehkä jotain on tehtävissä...

Ekstra: vihollisten "järkevämpi" liikkuminen

Edellinen ratkaisu on kyllä teknisesti helppo, mutta myös "halpa". Sen mukaan toimivat viholliset eivät siis aiheuta sellaista uhkaa pelaajalle kuin niiden kuuluisi, koska ne jäävät niin helposti puskemaan seiniä. Säädetäänpä siis vihollisten käyttäytymistä hieman uhkaavammaksi.

Suunnitelma on seuraavanlainen:

  • Jos vihollinen ei ole vielä nähnyt pelaajaa, se pysyy paikallaan.
  • Kuten aiemminkin, aina, kun vihollinen saa näköyhteyden pelaajaan, se alkaa ampua tätä. Mutta nyt samalla tallennetaan pelaajan sijainti "menneisyydestä" eli siitä, missä pelaaja on hetki sitten ollut.
  • Kun vihollinen kadottaa näköyhteyden pelaajaan, se siirtyy tallennettua sijaintia kohti ja jos pääsee perille, pysähtyy siihen.

Eli, kun vihollinen siirtyy näköyhteyden kadotettuaan siihen, missä pelaaja on hetki sitten ollut, se saattaa pysyä kauempana seinistä ja päästä todennäköisemmin uuteen näköyhteyteen pelaajan kanssa. Ei tämäkään ratkaisu kuitenkaan aukoton ole, sillä viholliset saattavat edelleen tietyissä tilanteissa jäädä puskemaan seiniä. Mutta harvemmin!

Globals.h

Lisää globals.h-otsikkotiedostoon vakio PLAYER_HISTORY ja anna sille arvoksi esim. 30; tarkastellaan pelaajan historiaa siis 30:n framen ajalta.
 
#define PLAYER_HISTORY 30

Types.h

Lisää types.h-otsikkotiedoston Player-rakenteeseen liukulukumuuttujat historyX[] ja historyY[] sekä kokonaislukumuuttuja historyIndex.

Anna historyX- ja historyY-muuttujien kooksi sama arvo, jonka annoit PLAYER_HISTORY-vakiolle; tässä tapauksessa 30.
 
struct Player {
    double posX, posY;
    double dirX, dirY;
    double moveSpeed;
    double radius;
    int allowShooting;
    int coolDown;
    int isDead, won;
    int health;
    double historyX[30], historyY[30];
    int historyIndex;
};
 
Lisää sitten MapObject-rakenteeseen liukulukumuuttujat lastSeenX ja lastSeenY sekä kokonaislukumuuttuja hasLastSeen.
 
typedef struct {
    double posX, posY;
    double distance;
    double dirX, dirY;
    double speed;
    int active;
    int block;
    int health;
    int seesPlayer;
    int shootCooldown;
    int isEnemyProjectile;
    int index;
    BITMAP* bitmap;
    BITMAP* (*getBitmap)(void* object);
    double lastSeenX, lastSeenY;
    int hasLastSeen;
} MapObject;

Player.c

Tee sitten player.c-lähdekooditiedostoon aivan MovePlayer-funktiomäärittelyn pohjalle, ennen ko. funktiomäärittelyn sulkevaa aaltosuljetta, seuraava lisäys, jossa tallennetaan pelaajan sijainti jonkin matkaa menneisyydestä.
 
player.historyX[player.historyIndex] = player.posX;
player.historyY[player.historyIndex] = player.posY;
player.historyIndex = (player.historyIndex + 1) % PLAYER_HISTORY;

Enemies.c

Tee sitten enemies.c-lähdekooditiedoston HandleEnemies-funktiomäärittelyyn seuraavat muutokset siihen kohtaan, jossa tarkastellaan, onko vihollinen aktiivinen ja onko sillä näköyhteys pelaajaan. Huomaa, että ko. ehtolausekkeessa ei enää ole else-tilannetta, kuten aiemmassa ratkaisussa. Poista siis se.
 
if (enemy->active && enemy->seesPlayer)
{
    // Otetaan pelaajan sijainti vähän menneisyydestä esim. 10 framea taaksepäin
    int delay = 10;
    int index = (player.historyIndex - delay + PLAYER_HISTORY) % PLAYER_HISTORY;

    enemy->lastSeenX = player.historyX[index];
    enemy->lastSeenY = player.historyY[index];
    enemy->hasLastSeen = 1;

    if (enemy->shootCooldown > 0)
    {
        enemy->shootCooldown--;
    }
    else
    {
        double length = sqrt(rayDirX * rayDirX + rayDirY * rayDirY);
        rayDirX /= length;
        rayDirY /= length;

        SpawnProjectile(projectiles, MAX_PROJECTILES,
                        enemy->posX, enemy->posY,
                        rayDirX, rayDirY, 1, projectileBitmap);

        enemy->shootCooldown = E_SHOOT_COOLDOWN;
    }
}
// Jos vihollinen on aktiivinen ja nähnyt pelaajan... 
else if (enemy->active && enemy->hasLastSeen)
{
    // ...määritellään kohde johon liikutaan (pelaajan aiempi sijainti)
    // ja lasketaan etäisyys siihen.
    double targetX = enemy->lastSeenX - enemy->posX;
    double targetY = enemy->lastSeenY - enemy->posY;
    double dist = sqrt(targetX * targetX + targetY * targetY);
    
    // Jos etäisyys kohteesta on enemmän kuin 0.05...
    if (dist > 0.05)
    {
        // ...liikutaan sitä kohti. 
        targetX /= dist;
        targetY /= dist;

        double newPosX = enemy->posX + targetX * E_MOVE_SPEED;
        double newPosY = enemy->posY + targetY * E_MOVE_SPEED;

        if (!Collided(enemy->posX, newPosY, 0.45, sprites, numSprites, enemies, numEnemies,
        1, enemy->index))
            enemy->posY = newPosY;

        if (!Collided(newPosX, enemy->posY, 0.45, sprites, numSprites, enemies, numEnemies,
        1, enemy->index))
            enemy->posX = newPosX;
    }
    // Jos taas etäisyys kohteesta on vähemmän kuin 0.05... 
    else
    {
        // ...ollaan perillä, ja pysäytetään vihollinen.
        enemy->hasLastSeen = 0;
    }
}
 
Nyt vihollinen pääsee helpommin uudelleen näköyhteyteen pelaajan kanssa tämän siirryttyä sen katseen alta pois. Triplahui! 

Kommentit