Buumeriräiskintä 2: Vihollisten piirtäminen ja tuhoaminen

Nyt, kun pelaajahahmomme pystyy ampumaan, pitäähän meidän antaa hänelle jotain mitä ampua! Lisätään siis vihollisten piirtäminen sekä niille kyky kuolla pelaajan ammuksen osuessa niihin.

Alkuvalmistelut

Aluksi luodaan ammusten käsittelemiselle oma lähdekooditiedosto enemies.c, joka lisätään build-komentoriviparametreihin. Lähdekooditiedostolle luodaan tietysti myös otsikkotiedosto enemies.h.

Enemies.c ja enemies.h

Luo siis enemies.c-lähdekooditiedosto sekä enemies.h-otsikkotiedosto.
Enemies.c-lähdekooditiedostoon lisätään kaikki vihollisten käsittelemiseen liittyvät funktiot.
 
Lisää enemies.c-lähdekooditiedostoon:
 
#include <allegro.h>
#include <math.h>
#include "globals.h"
#include "utils.h"
 
// Kaikki lisäykset tähän.
 
...sekä enemies.h-otsikkotiedostoon:
 
#ifndef ENEMIES_H
#define ENEMIES_H
 
// Kaikki lisäykset tähän.

#endif

Build.bat

Lisää sitten build.bat-batch-tiedostoon myös enemies.c-lähdekooditiedosto, jotta sekin on mukana ohjelmaa koostettaessa.
 
@echo off
gcc main.c utils.c draw.c player.c setup.c projs.c enemies.c -o game.exe -lalleg

Vihollisten piirtäminen

 Vihollisten piirtämisessä ei ole mitään ihmeellistä, sillä nekin ovat vain spritejä, eli ne piirtäminen koodataan samalla lailla kuin lamppu- ja ammusspritetkin.

Globals.h

Lisää globals.h-otsikkotiedostoon määrittely muuttujalle *enemyBitmap, johon ladataan ammuksen bittikarttagrafiikka.

extern BITMAP *textureA;
extern BITMAP *textureB;
extern BITMAP *lampBitmap;
extern BITMAP *projectileBitmap;
extern BITMAP *enemyBitmap;

Lisää myös kokonaislukumuuttuja numEnemies, jolla tarkastellaan vihollisten määrää kentässä.
 
extern int numSprites;
extern int numEnemies;

Main.c 

Lisää sitten main.c-lähdekooditiedostoon *enemyBitmap-muuttujan alustus arvoksi NULL.
 
BITMAP *textureA = NULL;
BITMAP *textureB = NULL;
BITMAP *lampBitmap = NULL;
BITMAP *projectileBitmap = NULL;
BITMAP *enemyBitmap = NULL;

Muuta osa map-taulukon eli kentän alkioarvoista 0 arvoiksi 3. Näihin kohtiin luodaan siis vihollinen.
 
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,3,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,3,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,3,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,3,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,3,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}
};

Alusta numEnemies-kokonaislukumuuttuja antamalla sille arvo 0.
 
int numSprites = 0;
int numEnemies = 0;
 
Lisää vielä main-funktion while-silmukan jälkeen destroy_bitmap-funktion kutsu parametrinaan enemyBitmap, jotta sen muistivaraus vapautetaan, kun hypätään while-silmukasta pois (eli kun ohjelma suljetaan).
 
destroy_bitmap(textureA);
destroy_bitmap(textureB);
destroy_bitmap(lampBitmap);
destroy_bitmap(projectileBitmap);
destroy_bitmap(enemyBitmap);

Types.h 

Lisää types.h-otsikkotiedostoon MapObject-tyyppinen taulukkomuuttuja enemies, ja anna sen kooksi esim. 100. Kentässä voi olla siis maksimissaan 100 vihollista.

MapObject sprites[100];
MapObject projectiles[100];
MapObject enemies[100];

Setup.c

Lisää setup.c-lähdekooditiedoston SetupProgram-funktioon enemyBitmap-muuttujalle arvo, joka on load_bitmap-funktiokutsu parametrinaan enemy.bmp-bittikarttakuva. Ladataan siis vihollisen grafiikka muistiin.

textureA = load_bitmap("walla.bmp", NULL);
textureB = load_bitmap("wallb.bmp", NULL);
lampBitmap = load_bitmap("lamp.bmp", NULL);
projectileBitmap = load_bitmap("projecti.bmp", NULL);
enemyBitmap = load_bitmap("enemy.bmp", NULL);

Kiitos muuten ChatGPT:lle hienosta leijutankkikuvasta, josta GIMPissä muokkasin lopullisen enemy.bmp-spriten. No mutta miksi juuri tällainen skifihtävä leijutankki? Vain ja ainoastaan siksi, että säästymme kaikenlaisilta animaatiokikkailuilta, sillä leijuva tankki ei teoriassa tarvitse mitään muita frameja kuin sen, jossa se "katsoo" pelaajaan päin.

Enemy.bmp.
 
Lisää sitten SetupGame-funktioon InitMapObjects-funktiokutsu parametreinaan enemies, &numEnemies, 3 (kenttätaulukon alkioarvo, jolla luodaan viholliset kenttään) sekä GetEnemyBitmap.
 
Huomaa ko. funktiokutsun alla oleva for-silmukka, jossa alustetaan vihollisen karttaobjektille sellaisia lisäominaisuuksia, joita tavallisilla spritellä ei ole:
  • seesPlayer tarkastelee, onko pelaaja vihollisen näkökentässä
  • shootCooldownilla määritetään vihollisen tulinopeus
  • health määrittää vihollisen terveyspisteet, joita tässä tapauksessa on 5
 
void SetupGame()
{
    int i;

    install_mouse();

    InitMapObjects(sprites, &numSprites, 2, lampBitmap);
 
    InitMapObjects(enemies, &numEnemies, 3, enemyBitmap);

    for (i = 0; i < MAX_PROJECTILES; i++)
    {
        projectiles[i].bitmap = projectileBitmap;
    }

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

Draw.h 

Lisää draw.h-otsikkotiedoston HandleMapObjects-funktioesittelyyn seuraavat parametrit:
  • MapObject* enemies
  • int numEnemies
void HandleMapObjects(MapObject* sprites, int numSprites, MapObject* projectiles,
int numProjectiles, MapObject* enemies, int numEnemies, double zBuffer[]);

Draw.c

Tee sitten draw.c-lähdekooditiedoston HandleMapObjects-funktiomäärittelyyn seuraavat lisäykset.
Yhdistetään siis myös vihollisten karttaobjektit samaan listaan lamppuspritejen ja ammusten kanssa.
 
void HandleMapObjects(MapObject* sprites, int numSprites, MapObject* projectiles,
int numProjectiles, MapObject* enemies, int numEnemies, double zBuffer[])
{
    int totalObjects = numSprites + numProjectiles + numEnemies;

    MapObject* allObjects = (MapObject*)malloc(totalObjects * sizeof(MapObject));
    
    int index = 0;
   
    memcpy(allObjects + index, sprites, numSprites * sizeof(MapObject));
    index += numSprites;

    memcpy(allObjects + index, projectiles, numProjectiles * sizeof(MapObject));
    index += numProjectiles;

    memcpy(allObjects + index, enemies, numEnemies * sizeof(MapObject));
    index += numEnemies;

Muokkaa lopuksi DrawGame-funktiomäärittelyn HandleMapObjects-funktiokutsua niin, että siinä on uudet parametrit arvoilla enemies ja numEnemies.
 
void DrawGame()
{
    double zBuffer[SCREEN_WIDTH];

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

    HandleWalls(zBuffer);

    UpdateProjectiles(projectiles, MAX_PROJECTILES);

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

    vsync();

    blit(buffer, screen, 0, 0, 0, 0, SCREEN_WIDTH, VIEW3D_HEIGHT);
}
 
No niin, nyt peliin piirretään lamppuspritejen ja ammusten lisäksi myös vihulaisia! Ne ovat kuitenkin toistaiseksi vain staattisia spritejä.
 

Vihollisiin törmääminen

Seuraavaksi toteutetaan törmäystunnistus pelaajan ja vihollisten välillä.

Utils.h

Lisää utils.h-otsikkotiedoston Collided-funktion esittelyyn seuraavat parametrit:
 
int Collided(double posX, double posY, double radius, MapObject* sprites,
int numSprites, MapObject* enemies, int numEnemies, int enemiesCollision,
int ignoreIndex);

Utils.c

Tee sitten utils.c-lähdekooditiedoston Collided-funktion määrittelyyn seuraavat lisäykset:

  • parametrit MapObject* enemies, int numEnemiesint enemiesCollision sekä int ignoreIndex
  • spritejen ja vihollisten läpikäynti; ks. varsinainen funktiomäärittely alla.
int Collided(double posX, double posY, double radius, MapObject* sprites,
int numSprites, MapObject* enemies, int numEnemies, int enemiesCollision,
int ignoreIndex)
{
    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;
                }
            }
        }
    }

    // Lasketaan spritet ja viholliset yhteen.
    num = numSprites + numEnemies;

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

        // Käydään ensin spritet läpi ja asetetaan obj-muuttujan arvoksi sprite, jolloin
        // tutkitaan, törmätäänkö spriteen. 
        if (i < numSprites)
        {
            obj = &sprites[i];
        }
        // Kun spritet on käyty läpi, käydään läpi viholliset.  
        else
        { 
            int enemyIndex = i - numSprites;

            // Varmistetaan, ettei vihollinen voi törmätä itseensä; tätä tarvitaan
            // myöhemmin, kun koodataan viholliset liikkumaan.
            if (enemiesCollision && enemyIndex == ignoreIndex)
            {
                continue;
            }
            // Asetetaan obj-muuttujan arvoksi vihollinen, jolloin tutkitaan, törmätäänkö
            // viholliseen. 
            obj = &enemies[enemyIndex];
        }
 
        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

Lisää kaikkiin MovePlayer-funktiomäärittelyn Collided-funktiokutsuihin kuudenneksi parametriksi enemies, seitsemänneksi numEnemies ja kahdeksanneksi enemiesCollision arvolla 0.
 
Vaikka alla on esimerkkinä parametrien lisäys vain kahteen Collided-funktiokutsuun, muista tehdä lisäys KAIKKIIN MovePlayer-funktiomäärittelyn Collided-funktiokutsuihin!
 
if (!Collided(player.posX, newPosY, player.radius, sprites, numSprites, enemies,
numEnemies, 0, 0) ||
!Collided(newPosX, player.posY, player.radius, sprites, numSprites, enemies,
numEnemies, 0, 0))
{
if (!Collided(player.posX, newPosY, player.radius, sprites, numSprites,
        enemies, numEnemies, 0, 0))
    {
        player.posY = newPosY;
    }

    if (!Collided(newPosX, player.posY, player.radius, sprites, numSprites,
        enemies, numEnemies, 0, 0))
    {
        player.posX = newPosX;
    }
}
 
Nyt pelaaja ei voi enää kävellä vihollisten läpi.

Vihollisten tuhoaminen

Koska pelaajalla on jo kyky puolustaa itseään, koodataan seuraavaksi mahdollisuus poistaa kaikki ulkoiset uhat pelaajan ammusten avulla. Eli kun pelaajan ammus osuu viholliseen, "tapetaan" tämä.

Enemies.h

Lisää enemies.h-otsikkotiedostoon HandleEnemyHit-funktion esittely. Kyseinen funktio käsittelee tilanteet, joissa vihollinen saa osuman pelaajan ammuksesta.
 
#ifndef ENEMIES_H
#define ENEMIES_H

void HandleEnemyHit(MapObject* enemy);

#endif

Enemies.c

Lisää sitten enemies.c-lähdekooditiedostoon HandleEnemyHit-funktion määrittely. 
 
Eli, kun vihollinen saa osuman, sen active-ja block-muuttujille annetaan arvo 0. Käytännössä vihollinen siis häviää näkyvistä, eikä sitä lasketa enää esteeksi.
 
#include <allegro.h>
#include <math.h>
#include "globals.h"
#include "utils.h"

void HandleEnemyHit(MapObject* enemy)
{
    enemy->active = 0;
    enemy->block = 0;
}

Projs.h

Lisää projs.h-otsikkotiedoston UpdateProjectiles-funktioesittelyyn parametrit MapObject enemies[] sekä int numEnemies, jotta kyseisen funktion määrittelyssä voidaan tarkastella, osuuko pelaajan ammus viholliseen.
 
void UpdateProjectiles(MapObject projectiles[], int numProjectiles, MapObject enemies[],
int numEnemies);

Projs.c

Tee sitten samat parametri- ja muutkin lisäykset projs.c-lähdekooditiedoston UpdateProjectiles-funktion määrittelyyn:
 
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;
            }
    
            // Käydään läpi kaikki viholliset.
            for (j = 0; j < numEnemies; j++)
            {
                MapObject* enemy = &enemies[j];
 
                // Lasketaan ammuksen etäisyys viholliseen.
                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 ja vihollinen on aktiivinen...
                if (distToEnemy < 0.3 && enemy->active)
                {
                    // ...tuhotaan ammus... 
                    projectile->active = 0;
                       
                    // ...käsitellään se vihollinen, johon ammus on osunut... 
                    HandleEnemyHit(enemy);
                    // ...ja keskeytetään silmukka.
                    break;
                }
            }
        }
    }
}

Draw.c

Lisää uudet parametrit vielä draw.c-lähdekooditiedoston ja DrawGame-funktiomäärittelyn UpdateProjectiles-funktiokutsuun.
 
void DrawGame()
{
    double zBuffer[SCREEN_WIDTH];

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

    HandleWalls(zBuffer);

    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 pelaaja pystyy tappamaan vihollisen yhdellä osumalla! Buhahaa!

Ekstra: Vihollisten terveyspisteet

Kerrasta kuoleva vihollinen on vähän turha helppo vastus. Lisätään siis viholliselle terveyspisteet, jolloin siitä voi tehdä vaikka ihan epäreilun kestävän jos siltä tuntuu.

Globals.h

Lisää globals.h-otsikkotiedostoon vakio E_HEALTH ja anna sille arvoksi vaikka 5. Tätä käytetään vihollisen terveyspisteiden määrittämiseen.
 
#define E_HEALTH 5

Types.h

Lisää types.h-otsikkotiedoston MapObject-rakenteeseen uusi kokonaislukumuuttuja health.
 
typedef struct {
    double posX, posY;
    double distance;
    double dirX, dirY;
    double speed;
    int active;
    int block;
    int health;
    BITMAP* bitmap;
    BITMAP* (*getBitmap)(void* object);
} MapObject;

Setup.c

Lisää sitten setup.c-lähdekooditiedoston SetupGame-funktion määrittelyyn for-silmukka, jossa kaikille vihollisille annetaan E_HEALTH-vakioon määritellyn arvon verran terveyspisteitä eli tässä tapauksessa 5 (enemies[i].health = E_HEALTH).
 
void SetupGame()
{
    int i;

    install_mouse();

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

    InitMapObjects(enemies, &numEnemies, 3, enemyBitmap);

    for (i = 0; i < numEnemies; i++)
    {
        enemies[i].health = E_HEALTH;        
    }

    for (i = 0; i < MAX_PROJECTILES; i++)
    {
        projectiles[i].bitmap = projectileBitmap;
    }

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

Enemies.c

Muuta enemies.c-lähdekooditiedoston HandleEnemyHit-funktiomäärittelyä, jossa nyt:

  1. vähennetään vihollisen health-arvoa yhdellä (enemy->health--)
  2.  tarkistetaan, onko vihollisen health tippunut nollaan tai alle ja onko vihollinen aktiivinen (if (enemy->health <= 0 && enemy->active));
    • tällöin se deaktivoidaan ja sen block-arvo asetetaan nollaksi kuten aiemminkin.
 
void HandleEnemyHit(MapObject* enemy)
{
    enemy->health--;

    if (enemy->health <= 0 && enemy->active)
    {
        enemy->active = 0;
        enemy->block = 0;
    }
}

Tämän ansiosta vihollinen ei kuole enää yhdestä osumasta, vaan vaatii viisi osumaa ennen kuin kupsahtaa.

Kommentit