Nyt kun meillä on hieno raycaster-pelimoottori, koodataan sille pieni peli! Ei mitään ihmeellistä, vaan oikein perinteistä buumeriräiskintämeininkiä, jossa pelaaja pistää vihulaisia matalaksi ampumalla niitä tarpeeksi monta kertaa. Pelin pääsee läpi, kun kaikki kentän viholliset on tuhottu, ja pelaaja kuolee, jos viholliset osuvat pelaajaan tarpeeksi monta kertaa. Pelaaja saa terveyspisteensä täyteen keräämällä ensiapulaukun.
Eiköhän aloiteta!
Ihan aluksi varmista, että seuraavien tunnisteiden alla olevien postausten toimenpiteet on suoritettu:
Kun kehitysympäristösi ja raycaster-pelimoottorisi on pystyssä, tehdään ensin pienet alkuvalmistelut varsinaisen pelin koodaamiseksi. Sitten koodataan lentävien ammusten piirtäminen, luominen, liikuttaminen ja tuhoutuminen. Lopuksi koodataan itse ampuminen, joka tapahtuu hiiren vasemmalla painikkeella.
Alkuvalmistelut
Aluksi luodaan ammusten käsittelemiselle oma lähdekooditiedosto projs.c, joka lisätään build-komentoriviparametreihin. Lähdekooditiedostolle luodaan tietysti myös otsikkotiedosto projs.h.
Projs.c ja projs.h
Luo siis projs.c-lähdekooditiedosto sekä projs.h-otsikkotiedosto.
Projs.c-lähdekooditiedostoon lisätään kaikki ammusten lentämiseen liittyvät funktiot.
Lisää projs.c-lähdekooditiedostoon:
#include <allegro.h>
#include "globals.h"
// Kaikki lisäykset tähän.
...sekä projs.h-otsikkotiedostoon:
#ifndef PROJS_H
#define PROJS_H
// Kaikki lisäykset tähän.
#endif
Build.bat
Lisää sitten build.bat-batch-tiedostoon myös projs.c-lähdekooditiedosto, jotta sekin on mukana ohjelmaa koostettaessa.
gcc main.c utils.c draw.c player.c setup.c projs.c -o game.exe -lalleg
Ammusten piirtäminen
Seuraavaksi koodataan pelaajasta lentävien ammusten piirtäminen. Teknisesti homma tapahtuu samalla lailla kuin spritejäkin piirrettäessä, sillä ammuksetkin ovat ihan tavallisia spritejä. Tosin sillä erotuksella, että ne liikkuvat siihen suuntaan, mihin pelaaja on ne ampunut.
Globals.h
Määrittele globals.h-otsikkotiedostoon vakio MAX_PROJECTILES, joka asettaa pelissä kerralla käsiteltävien ammusten maksimimäärän 100:aan.
#define MAX_PROJECTILES 100
Lisää globals.h-otsikkotiedostoon määrittely muuttujalle *projectileBitmap, johon ladataan ammuksen bittikarttagrafiikka.
extern BITMAP *textureA;
extern BITMAP *textureB;
extern BITMAP *lampBitmap;
extern BITMAP *projectileBitmap;
Main.c
Lisää sitten main.c-lähdekooditiedostoon *projectileBitmap-muuttujan alustus arvoksi NULL.
BITMAP *textureA = NULL;
BITMAP *textureB = NULL;
BITMAP *lampBitmap = NULL;
BITMAP *projectileBitmap = NULL;
Lisää vielä main-funktion while-silmukan jälkeen destroy_bitmap-funktion kutsu parametrinaan projectileBitmap, 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);
Setup.c
Lisää setup.c-lähdekooditiedoston SetupProgram-funktioon projectileBitmap-muuttujalle arvo, joka on load_bitmap-funktiokutsu parametrinaan projecti.bmp-bittikarttakuva. Ladataan siis pelaajan ampuman ammuksen 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);
 |
| Projecti.bmp. |
Lisää sitten SetupGame-funktioon määrittely lokaalille kokonaislukumuuttujalle i sekä sille for-silmukka, jossa käydään läpi kaikki ammus-oliot (projectiles[i]) ja asetetaan niiden getBitmap-funktiopointteri osoittamaan GetProjectileBitmap-funktioon. Tämä mahdollistaa jokaisen ammuksen piirtämisen oikealla bittikarttagrafiikalla.
void SetupGame()
{
int i;
install_mouse();
InitMapObjects(sprites, &numSprites, 2, lampBitmap);
for (i = 0; i < MAX_PROJECTILES; i++)
{
projectiles[i].bitmap = projectileBitmap;
}
player.posX = 22, player.posY = 12; //x and y start position
player.dirX = -1, player.dirY = 0; //initial direction vector
}
Types.h
Lisää types.h-otsikkotiedostoon MapObject-tyyppinen taulukkomuuttuja projectiles ja anna sen kooksi esim. 100. Lentäviä ammuksia voi siis yhtä aikaa olla maksimissaan sata.
MapObject sprites[100];
MapObject projectiles[100];
Draw.h
Lisää draw.h-otsikkotiedoston HandleMapObjects-funktion esittelyn parametreihin MapObject-muuttuja projectiles sekä kokonaislukumuuttuja numProjectiles, jolloin myös ammukset käsitellään samanlaisina karttaobjekteina kuin staattiset spritetkin.
void HandleMapObjects(MapObject* sprites, int numSprites, MapObject* projectiles,
int numProjectiles, double zBuffer[]);
Draw.c
Lisää sitten draw.c-lähdekooditiedoston HandleMapObjects-funktion määrittelyyn samat parametrit kuin funktion esittelyssäkin. Tee myös funktion koodiin ammuksiin liittyvät lisäykset:
- Kokonaislukumuuttuja totalObjects kertoo kaikkien olioiden yhteismäärän; lasketaan siis yhteen spritet ja ammukset (numSprites + numProjectiles).
- Yhdistetään sitten sprite- että projectile-oliot yhdeksi taulukoksi allObjects, jotta ne voidaan käsitellä ja piirtää yhtenä listana. Varataan sitä varten tarvittava määrä muistia malloc-funktiokutsulla
. - Kopioidaan spritejen lisäksi myös ammukset allObjects-taulukkoon memcpy-funktiokutsulla.
- Lisätään index-kokonaislukumuuttujan arvoon spriten lisäksi myös ammusten määrä (index += numProjectiles).
void HandleMapObjects(MapObject* sprites, int numSprites, MapObject* projectiles,
int numProjectiles, double zBuffer[])
{
int totalObjects = numSprites + numProjectiles;
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;
int i, s, stripe;
Lisää vielä DrawGame-funktiomäärittelyssä olevaan HandleMapObjects-funktiokutsuun uudet parametrit arvoilla projectiles ja MAX_PROJECTILES.
void DrawGame()
{
double zBuffer[SCREEN_WIDTH];
clear_to_color(buffer, makecol(47, 47, 47));
HandleWalls(zBuffer);
HandleMapObjects(sprites, numSprites, projectiles, MAX_PROJECTILES, zBuffer);
vsync();
blit(buffer, screen, 0, 0, 0, 0, SCREEN_WIDTH, VIEW3D_HEIGHT);
}
Nyt on tehty kaikki tarvittava pelaajan ammusten piirtämiseksi. Ammuksia ei kuitenkaan piirretä vielä, sillä toistaiseksi mikään ei luo ammuksen karttaobjekteja kenttään. Seuraavaksi koodataankin pelaajahahmolle kyky luoda ja lennättää (eli ampua) näitä koodaamiamme ammuksia.
Ammusten luominen, liikuttaminen ja tuhoaminen
Globals.h
Määrittele globals.h-otsikkotiedostoon vakiot:
- PROJECTILE_SPEED, joka kertoo kuinka nopeasti ammukset liikkuvat ruudulla (tässä tapauksessa 0,25 ruutua per pelisilmukka); mitä suurempi arvo, sitä suurempi ammuksen nopeus.
- P_SHOOT_COOLDOWN, joka määrittää pelaajan tulinopeuden; mitä pienempi arvo, sitä suurempi tulinopeus.
#define PROJECTILE_SPEED 0.25
#define P_SHOOT_COOLDOWN 10
Types.h
Lisää Player-rakenteeseen kaksi uutta kokonaislukumuuttujaa:
- allowShooting kertoo, saako pelaaja ampua.
- coolDown on laskuri, jolla voidaan säätää pelaajan tulinopeutta.
struct Player {
double posX, posY;
double dirX, dirY;
double moveSpeed;
double radius;
int allowShooting;
int coolDown;
};
Koska ammukset ovat samanlaisia karttaobjekteja kuin spritetkin, lisää MapObject-rakenteeseen seuraavat liukulukumuuttujat:
- dirX ja dirY määrittävät ammuksen etenemissuunnan
- speed määrittää ammuksen liikkumisnopeuden.
typedef struct {
double posX, posY;
double distance;
double dirX, dirY;
double speed;
int active;
int block;
BITMAP* (*getBitmap)(void* object);
} MapObject;
Main.c
Alusta main.c-lähdekooditiedostossa uudet Player-rakenteen muuttujat arvoilla 0.
struct Player player = {
0, // posX
0, // posY
0, // dirX
0, // dirY
P_MOVE_SPEED, // moveSpeed
0.2, // radius
0, // allowShooting
0, // coolDown
};
Projs.h
Lisää projs.h-otsikkotiedostoon kahden funktion esittely:
-
SpawnProjectile luo uuden ammuksen ja sijoittaa sen kentälle annetulla sijainnilla ja suunnalla.
-
UpdateProjectiles päivittää kaikkien aktiivisten ammusten liikettä ja tarkistaa törmäykset seinien kanssa.
void SpawnProjectile(MapObject projectiles[], int numProjectiles, double startX,
double startY, double dirX, double dirY, BITMAP* spriteBitmap);
void UpdateProjectiles(MapObject projectiles[], int numProjectiles);
Projs.c
Lisää sitten projs.c-lähdekooditiedostoon SpawnProjectile-funktion määrittely, jossa spawnataan ammus;
- ammuksen sijainti (posX ja posY) ja suunta (dirX ja dirY) asetetaan parametrien mukaan (startX ja startY sekä dirX ja dirY)
- nopeudeksi (speed) asetetaan PROJECTILE_SPEED -makroon määritetty arvo.
- ammus merkitään aktiiviseksi (active = 1)
- ammuksen piirtämiseen käytettävään funktiopointteriin getBitmap asetetaan parametri getBitmapFunction
- silmukka katkaistaan, kun ammus on alustettu (break).
void SpawnProjectile(MapObject projectiles[], int numProjectiles, double startX,
double startY, double dirX, double dirY, 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->bitmap = spriteBitmap; projectile->getBitmap = GetObjectBitmap;
break;
}
}
}
Lisää projs.c-lähdekooditiedostoon myös määrittely funktiolle UpdateProjectiles, jossa päivitetään ammuksen sijaintia ja tarkastellaan, osuuko se seinään.
void UpdateProjectiles(MapObject projectiles[], int numProjectiles)
{
int i;
for (i = 0; i < numProjectiles; i++)
{
MapObject* projectile = &projectiles[i];
// Jos ammus on aktiivinen...
if (projectile->active)
{
// ...kasvatetaan sen sijaintia suuntavektorin ja nopeuden mukaisesti...
projectile->posX += projectile->dirX * projectile->speed;
projectile->posY += projectile->dirY * projectile->speed;
// ...ja muutetaan sijainti karttaruuduiksi.
int mapX = (int)(projectile->posX);
int mapY = (int)(projectile->posY);
// Jos sen karttaruudun, jossa ammus on, arvo on 1
// (eli jos ammus osuu seinään)... if (map[mapX][mapY] == 1)
{
// ...deaktivoidaan se (eli tuhotaan ammus).
projectile->active = 0;
continue;
}
}
}
}
Koska ammuksen osuessa seinään se vain deaktivoidaan (projectile->active = 0), jolloin ammus vain tylsästi häviää (tai näyttää uppoavan seinän sisään). Oikeastihan sen pitäisi esittää oma "kuolinanimaationsa" tyyliin räjähtää tai tussahtaa, mutta meille simppelin pikkupelimme tapauksessa riittää tylsistely.
Draw.c
Lisää lopuksi draw.c-lähdekooditiedoston DrawGame-funktioon kutsu UpdateProjectiles-funktiolle parametreillä projectiles ja MAX_PROJECTILES.
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, zBuffer);
vsync();
blit(buffer, screen, 0, 0, 0, 0, SCREEN_WIDTH, VIEW3D_HEIGHT);
}
Nyt on kunnossa ammusten luominen, liikuttaminen ja tuhoaminen. Kunpa joku vielä vain ampuisi niitä. Hoidetaanpa siis sekin puoli kuntoon antamalla pelaajalle kyky ampua.
Pelaajan ampumiskyky
Kuten sanottu, ampuminen tapahtuu hiiren vasemmalla näppäimellä. Eli, kun sitä painetaan, ammus luodaan pelaajan sijaintiin ja singotaan siihen suuntaan, mihin pelaaja katsoo.
Player.h
Lisää player.h-otsikkotiedostoon PlayerShooting-funktion esittely.
Player.c
Lisää player.c-lähdekooditiedostoon PlayerShooting-funktion määrittely:
void PlayerShooting()
{
// Jos hiiren vasen näppäin on painettuna ja coolDown kulunut loppuun...
if ((mouse_b & 1) && player.coolDown == 0)
// ...sallitaan ampuminen.
player.allowShooting = 1;
// Muussa tapauksessa...
else
// ...estetään se.
player.allowShooting = 0;
// Jos pelaajan sallitaan ampua... if (player.allowShooting)
{
// ...nostetaan siihen arvoon, joka määritettiin P_SHOOT_COOLDOWN-vakioon... player.coolDown = P_SHOOT_COOLDOWN;
// ...ja spawnataan ammus.
SpawnProjectile(projectiles, MAX_PROJECTILES, player.posX, player.posY,
player.dirX, player.dirY, projectileBitmap);
}
// Jos coolDown ei ole kulunut loppuun, vähennetään sitä yhdellä. if (player.coolDown > 0)
player.coolDown--;
}
Eli, jos ampumisnäppäin on painettuna, luodaan ammus 10 ruudun välein; tuo arvo määritettiin P_SHOOT_COOLDOWN-vakion yhteyteen.
Main.c
Lisää main.c-lähdekooditiedoston sisempään while-silmukkaan kutsu PlayerShooting-funktiolle, mikä on viimeinen silaus pelaajan kyvylle ampua.
while(!key[KEY_ESC])
{
while (thCounter > 0)
{
MovePlayer();
RotatePlayer();
PlayerShooting();
DrawGame();
thCounter--;
}
}
Nyt pelaajahahmomme pystyy ampumaan! Eipä sitten muuta kuin pew pew pew!
Kommentit
Lähetä kommentti