Alkuvalikko ja pelitila

Nyt päästään kunnon hommiin, nimittäin pelimme varsinaiseen koodaukseen! Ensin toteutetaan alkuvalikko ja pelitila, joiden pohjalta voidaan koodata peli kuin peli.

Suunnitelma on seuraava:

  • Kun ohjelma käynnistetään, näytetään alkuvalikko.
  • Alkuvalikossa on kohteet "New Game", "Options" sekä "Quit to DOS", jotka voi valita nuolinäppäimillä ylös ja alas;
    • kun on valittu New game ja painetaan Enter, siirrytään pelitilaan
    • kun on valittu Quit to DOS ja painetaan Enter, suljetaan ohjelma
    • jos valinta on Optionsissa ja painetaan Enter, mitään ei tapahdu (koodataan kolme alkuvalikon kohdetta ihan vain, jotta voimme nähdä valikon selaamisen toimivan oikein)
    • jos valinta Quit to DOSissa ja painetaan nuolinäppäintä alas, siirretään valinta New Gameen ja päinvastoin.
  • Kun pelitilassa painetaan Esc, siirrytään alkuvalikkoon.

Luodaan lähdekooditiedostot ja sisällytetään kirjastot

Ihan ensimmäiseksi tehdään muutama tiedosto-operaatio;

  1. Luo kansioon C:\DOSDEV kansio GAME.
  2. Luo GAME-kansioon tiedostot main.c ja main.h.
    • main.c on lähdekooditiedosto, jossa määritetään ja kutsutaan funktiot sekä määritetään niiden paikalliset muuttujat
    • main.h on otsikkotiedosto, jossa esitellään funktiot sekä määritetään vakiot ja globaalit muuttujat.

Seuraavaksi avaa kumpikin kooditiedosto valitsemassasi tekstieditorissa, sillä lisäämme koodia kumpaankin aina kun tarve vaatii.

Lisää tiedostoon main.c kaikkein ylimmäksi seuraavat rivit, jotta päästään käsiksi kirjastojen stdio.h ja allegro.h funktioihin kuten myös otsikkotiedostomme main.h sisältöön.

#include <stdio.h>
#include <allegro.h>
#include "main.h"

Seuraavaksi avaa main.h-otsikkotiedosto ja lisää siihen alla olevat rivit, jotta kaikki otsikkotiedostoon määritellyt vakiot ja muuttujat sekä esitellyt funktiot sisällytetään lähdekooditiedostoon vain kerran. Huomaa, että kaikki lisäykset main.h-otsikkotiedostoon on tultava rivien #define MAIN_H sekä #endif väliin.

#ifndef MAIN_H
#define MAIN_H
 
// Kaikki sisältö tähän.
  
#endif

Jostain syystä kääntäjä haluaa yhden tyhjän rivin viimeisen koodirivin jälkeen sekä main.c-lähdekooditiedostoon että main.h-otsikkotiedostoon, sillä kääntäjä antaa varoituksen, jos tyhjää koodiriviä ei lopusta löydy. Lisää siis vielä se kumpaankin tiedostoon.

Koodataan alkuvalikko

Avaa main.h-otsikkotiedosto ja lisää sinne seuraavat rivit:

  • määritellään lueteltu tyyppi GameState, sille arvot MENUSTATE ja GAME ja muuttuja activeGameState 
  • määritellään muuttuja exitProgram 
  • esitellään funktiot SetupProgram(), DrawMenu(), DrawGame(), FadeAndBlit(), MenuKeyInput() ja Terminate().

Muista tehdä lisäys rivien #define MAIN_H sekä #endif väliin!

enum GameState
{
    MENUSTATE,
    GAME
} activeGameState;

unsigned short int exitProgram;
 
int SetupProgram();
void DrawMenu();
void DrawGame();
void FadeAndBlit();
void MenuKeyInput();
void Terminate();

Lisää main.c-tiedostoon alla oleva main-funktion määrittely #include-rivien jälkeen. Tämä on siis ohjelmamme varsinainen, suoritettava osuus, jonka while-silmukkaa toistetaan niin kauan kunnes ohjelma päätetään sulkea.

int main()
{
    // Alustetaan ohjelma ja asetetaan muutama alkuarvo. Jos SetupProgram-
    // funktion palautusarvo on jotain muuta kuin 0, keskeytetään main-funktion
    // suorittaminen palautusarvolla 1.
    if (SetupProgram() != 0)
        return 1;
    
    // Niin kauan kuin muuttujan exitProgram arvo on epätosi...
    while (!exitProgram)
    {
        // ...valitaan aktiiviseksi tilaksi joko...
        switch (activeGameState)
        {
            // ...alkuvalikko... 
            case MENUSTATE:
 
                // ...jolloin piirretään alkuvalikko buffer-näyttömuistiin...
                DrawMenu();
               
                break;

            // ...tai pelitila...
            case GAME:
 
                // ...jolloin piirretään pelitila buffer-näyttömuistiin. 
                DrawGame();
               
                break;
        }
 
        // Sitten siirretään buffer-näyttömuistin sisältö ruudulle...
        FadeAndBlit();

        // ...toteutetaan näppäinkomennot ja palataan while-silmukan alkuun.
        MenuKeyInput();
    }
 
    // Kun muuttujan exitProgram arvo on tosi, eli kun ohjelma päätetään sulkea,
    // hypätään silmukasta pois ja valmistellaan ohjelman sulkeminen.
    Terminate();
    
    // Suljetaan ohjelma onnistuneesti palautusarvolla 0.
    return 0;
}
END_OF_MAIN()

Seuraavaksi määritellään esitellyt ja kutsutut funktiot. Funktioiden määrittelyt lisätään main-funktion ja END_OF_MAIN()-makron alapuolelle.

SetupProgram() – alustetaan ohjelma

Määritä main.h-otsikkotiedostoon vakiot SCREEN_WIDTH ja SCREEN_HEIGHT sekä niiden arvoiksi 320 ja 240; näillä arvoilla määritetään ruudun pikselien määrä leveys- ja pystysuunnassa.

#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240

"Mutta hei!" joku DOS-pelejä tunteva ajattelee. "Eikös klassinen VGA-resoluutio ole Mode 13h, jossa pystypikselien määrä on 200 pikseliä? Miksi meidän pelimme on resoluutiossa Mode X, eli pystypikseleitä on 240?" Lyhyt vastaus: kuvasuhde. DOS-pelien valtakaudella tietokonenäyttöjen kuvasuhde oli 4:3 – tiedäthän, se melkein neliön muotoinen. Resoluution 320 × 200 kuvasuhde on kuitenkin 16:10, ja kun se esitetään 4:3-kuvasuhteen näytössä, kaikkea näytön esittämää sisältöä yksittäisiä pikseleitä myöten venytetään pystysuunnassa. Tämä saa aikaan sen, että esim. koodiin kirjoitettu täydellisen ympyrän piirtäminen saa ruudulla aikaan soikion. "Mutta mikä ongelma tässä on? Eihän nykynäyttöjen kuvasuhde ole 4:3 vaan yleensä joko 16:10 tai 16:9, joihin resoluutio 320 × 200 sopii hyvin." Totta, mutta minulle DOS-pelien ainoa oikea kuvasuhde on 4:3, näytöstä riippumatta. Ja resoluution 320 × 240 kuvasuhde on aina 4:3, pikselit neliöitä ja ympyrät ympyröitä.

Alla olevista kuvista ylempi esittää väärässä, 16:10-kuvasuhteessa esitetyn kuvakaappauksen Commander Keen -pelistä. Alempi kuva on oikeassa kuvasuhteessa eli 4:3. Huomaa punaiset tikkarit, jotka ylemmässä kuvassa ovat väärän kuvasuhteen takia soikeita; tikkari-spritejä piirrettäessä on siis pitänyt ottaa tämä kuvasuhdekikkailu huomioon ja piirtää ne soikeiksi, jotta ne esitetään 4:3-näytössä täydellisinä ympyröinä, kuten alempi kuva osoittaa.

 


Määrittele myös lueteltu tyyppi MenuItem, sille arvot NEW_GAME, SETTINGS, EXIT ja NUM_MENU_ITEMS sekä lopuksi muuttuja selectedMenuItem. Nämä ovat alkuvalikon kohteet sekä niiden määrä.

Määrittele myös BITMAP-osoitinmuuttuja *buffer, jonka muistiin kaikki grafiikka piirretään ennen sen siirtämistä varsinaiseen näyttömuistiin eli monitorin ruudulle.

enum MenuItem
{
    NEW_GAME,
    SETTINGS,
    EXIT,
    NUM_MENU_ITEMS
} selectedMenuItem;
 
BITMAP *buffer;

Lisää sitten main.c-lähdekooditiedostoon SetupProgram-funktion määrittely, jossa tehdään ohjelman toiminnan kannalta tarpeelliset alustukset:

int SetupProgram()
{
    // Alustetaan Allegro-kirjasto.
    if (allegro_init() != 0)
        return 1;
    
    // Asetetaan grafiikkatila.
    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;
        }
    }

    // Alustetaan näppäinpainallusten lukeminen.
    install_keyboard();
    
    // Luodaan buffer-osoitinmuuttujalle näyttömuisti
    buffer = create_bitmap(SCREEN_WIDTH, SCREEN_HEIGHT);

    // Asetetaan enum-muuttujille alkuarvot, jotta käynnistettäessä ohjelma valitaan
    // menutila ja laitetaan alkuvalikon valituksi kohteeksi New Game.
    activeGameState = MENUSTATE;
    selectedMenuItem = NEW_GAME;

    return 0;
}

DrawMenu() – piirretään alkuvalikko

Lisää main.h-otsikkotiedostoon alla olevat vakiot, joiden arvoiksi annetaan alkuvalikon tekstit ja värit.

#define TEXT_MAINMENU_TITLE "The Game"
#define TEXT_MAINMENU_NEWGAME "New Game"
#define TEXT_MAINMENU_OPTIONS "Options"
#define TEXT_MAINMENU_QUIT "Quit to DOS"

#define COLOR_MAINMENU_BG makecol(31,31,31)
#define COLOR_MAINMENU_POINTER makecol(255,0,0)
#define COLOR_MAINMENU_TEXT makecol(255,255,255)

Lisää sitten DrawMenu-funktion määrittely main.c-tiedostoon SetupProgram-funktiomäärityksen alapuolelle.

void DrawMenu()
{
    int i;
 
    // Puhdistetaan buffer-näyttömuisti ja täytetään se harmaalla värillä.
    clear_to_color(buffer, COLOR_MAINMENU_BG);

    // Piirretään otsikkoteksti.
    textout_centre_ex(buffer, font, TEXT_MAINMENU_TITLE, SCREEN_WIDTH / 2,
    SCREEN_HEIGHT / 4, COLOR_MAINMENU_TEXT, -1);
 
    // Piirretään valitun alkuvalikkokohteen osoitin sekä alkuvalikon tekstit. 
    for (i = 0; i < NUM_MENU_ITEMS; i++)
    {
        if (i == selectedMenuItem)
        {
            // Piirretään osoitin ">" valitun alkuvalikkokohteen kohdalle.
            textout_centre_ex(buffer, font, ">", SCREEN_WIDTH / 2 - 50,
            SCREEN_HEIGHT / 2 + i * 25, COLOR_MAINMENU_POINTER, -1);
        }
        
        // Piirretään alkuvalikkotekstit. 
        textout_centre_ex(buffer, font, (i == NEW_GAME) ? TEXT_MAINMENU_NEWGAME : 
        (i == SETTINGS) ? TEXT_MAINMENU_OPTIONS : TEXT_MAINMENU_QUIT, SCREEN_WIDTH / 2,
        SCREEN_HEIGHT / 2 + i * 25, COLOR_MAINMENU_TEXT, -1);
    }
}

DrawGame() – piirretään pelitila

Toistaiseksi pelitilassa ei tehdä muuta kuin tyhjennetään buffer-näyttömuisti ja täytetään se harmaalla värillä, ihan vain jotta voimme siirtyä tilasta toiseen ja nähdä sen sujuvan niin kuin pitää.

Lisää siis main.h-otsikkotiedostoon vakio pelitilan taustavärille ja määritä se harmaaksi.

#define COLOR_GAME_BG makecol(85,85,85)

Lisää sitten main.c-lähdekooditiedostoon funktion DrawGame määrittely, jossa on vain clear_to_color-funktiokutsu buffer-näyttömuistin tyhjentämiseksi ja täyttämiseksi COLOR_GAME_BG-vakioon määritetyllä värillä.

void DrawGame()
{  
    clear_to_color(buffer, COLOR_GAME_BG);
}

FadeAndBlit() – siirretään buffer-näyttömuistin sisältö ruudulle

FadeAndBlit-funktion määrittelyssä varmistetaan grafiikan päivitys synkronissa monitorin virkistystaajuuden kanssa (vsync()) sekä suoritetaan ns. blit- eli block image transfer -operaatio, jossa buffer-näyttömuistin sisältö siirretään screen-näyttömuistiin, mikä saa piirtämämme grafiikan näkymään ruudulla.

void FadeAndBlit()
{
    vsync();

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

MenuKeyInput() – näppäinkomennot

Määritellään näppäinkomennot näppäimille Enter (key[KEY_ENTER]) ja Esc (key[KEY_ESC]) sekä nuolille ylös ja alas (key[KEY_UP] ja key[KEY_DOWN]).

void MenuKeyInput()
{
    // Kun alkuvalikossa painetaan nuolinäppäintä ylös...
    if (key[KEY_UP] && keypressed() && activeGameState == MENUSTATE)
    {
        // ...selataan alkuvalikkoa ylös. 
        selectedMenuItem = (selectedMenuItem - 1 + NUM_MENU_ITEMS) % NUM_MENU_ITEMS;
    }
    // Kun alkuvalikossa painetaan nuolinäppäintä alas... 
    else if (key[KEY_DOWN] && keypressed() && activeGameState == MENUSTATE)
    {
        // ...selataan alkuvalikkoa alas. 
        selectedMenuItem = (selectedMenuItem + 1) % NUM_MENU_ITEMS;
    }
    // Kun alkuvalikossa painetaan Enteriä... 
    else if (key[KEY_ENTER] && keypressed())
    {
        // ...aktiivisen tilan ollessa alkuvalikko...
        if (activeGameState == MENUSTATE)
        {
            // ...ja jos alkuvalikon valittu kohde on New Game... 
            if (selectedMenuItem == NEW_GAME)
            {
                // ...asetetaan aktiiviseksi tilaksi pelitila, eli siirrytään
                // alkuvalikosta pelitilaan.  
                activeGameState = GAME;
            }
            // Jos taas alkuvalikon valittu kohde on Quit to DOS...   
            else if (selectedMenuItem == EXIT)
            {
                // ...hypätään main-funktion while-silmukasta ulos ja suljetaan
                // ohjelma.   
                exitProgram = 1;
            }
        }
    }
    // Kun painetaan Esciä...
    else if (key[KEY_ESC] && keypressed())
    {
        // ...ja aktiivinen tila on pelitila... 
        if (activeGameState == GAME)
        {
            // ...asetetaan aktiiviseksi tilaksi alkuvalikko, eli siirrytään
            // pelitilasta alkuvalikkoon.
            activeGameState = MENUSTATE;
        }
    }
 
    // Lopuksi tyhjennetään näppäinpuskuriin kertyneet näppäinpainallukset. 
    clear_keybuf();
}

Terminate() – suljetaan Allegro

Terminate-funktiossa suljetaan Allegro.

void Terminate()
{
    allegro_exit();
}

Käännä ja suorita!

No niin, olemme lisänneet kaiken tarvittavan alkuvalikon toteuttamiseksi sekä alkuvalikosta pelitilaan ja takaisin siirtymiseksi! Seuraavaksi tallenna lähdekoodi- ja otsikkotiedosto ja käännä ne DOSBoxissa käskyllä gcc main.c -o -game.exe -lalleg. Jos kaikki meni hyvin, ei virheilmoituksia eikä varoituksia tule, ja sinulla on suoritettava ohjelma GAME.EXE. Meidän pelimme!


Ekstrakikkailua!

Pienellä lisävaivalla saamme ohjelmaamme vähän visuaalista lisämaustetta: häivytyksen mustaan ja takaisin tilojen välillä siirryttäessä sekä ns. loppuruudun. Nämä eivät ole ohjelman toiminnan kannalta välttämättömiä kikkoja mutta ovat osaltaan antamassa pelistä viimeistellyn vaikutelman.

Häivytys mustaan ja takaisin

Nyt, kun ohjelmassa siirrytään eri tilojen välillä, siirtyminen tehdään salamana. Onneksi siirtymiin saadaan kuitenkin pieni mutta merkittävä Allegron tarjoama erikoistehoste, nimittäin häivytys mustaan ja takaisin.

Lisää main.h-otsikkotiedostoon alla olevat rivit. Määritellään siis vakio häivytyksen kestolle (#define FADE_DURATION) ja valitaan sen arvo väliltä 1–64; tässä tapauksessa arvo on 8. Määritellään myös muuttujat häivytyksille sisään (allowFadeIn ja allowStartupFadeIn) ja ulos (allowFadeOut) sekä sille, onko häivytys parhaillaan käynnissä (fadeInProgress).

#define FADE_DURATION 8
 
unsigned short int allowFadeIn, allowFadeOut, allowStartupFadeIn, fadeInProgress;

Lisää main.c-lähdekooditiedoston SetupProgram-funktiomääritykseen seuraava rivi, jotta sallitaan alkuvalikon häivytys sisään kun pelin käynnistää:

activeGameState = MENUSTATE;
selectedMenuItem = NEW_GAME;
 
allowStartupFadeIn = 1;
 
return 0;

Lisää SetupProgram-funktioon myös fade_out-funktiokutsu, jolloin peliä käynnistettäessä jopa DOS häivytetään hienosti pois.

    if (allegro_init() != 0)
        return 1;

    fade_out(FADE_DURATION);

    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;
        }
    }

Tee FadeAndBlit-funktioon seuraavat lisäykset, jotta häivytys tapahtuu siirryttäessä alkuvalikosta pelitilaan ja takaisin sekä kun pelin käynnistää.

Kun peli käynnistetään, suoritetaan siis allowStartupFadeIn-ehtolausekkessa oleva fade_interpolate-häivytys, paletin asettaminen ja blit-operaatio, mutta tilojen välillä siirryttäessä toteutetaan sitten allowFadeOut- ja allowFadeIn-ehtolausekkeissa olevat fade_out- ja fade_in-häivytykset. Pelin käynnistämiselle on oma häivytyksensä siksi, koska fade_in-häivytystä käytettäessä grafiikkatilan asettamisessa välähtää ikävää väri- ja symbolitauhkaa, mitä ei fade_interpolate-häivytystä käytettäessä tapahdu.

void FadeAndBlit()
{
    if (allowFadeOut)
    {
        fade_out(FADE_DURATION);
       
        allowFadeOut = 0;
    }

    if (allowStartupFadeIn)
    {
        int pos;
 
        PALETTE currentPal;
 
        for (pos = 0; pos <= 63; pos += 4)
        {
            fade_interpolate(black_palette, default_palette, currentPal, pos, 0, 255);

            set_palette(currentPal);

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

        set_palette(default_palette);

        allowStartupFadeIn = 0;
    }

    vsync();

    blit(buffer, screen, 0, 0, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

    if (allowFadeIn)
    {
        fade_in(default_palette, FADE_DURATION);

        allowFadeIn = 0;

        fadeInProgress = 0;
    }
}

Tee MenuKeyInput-funktioon seuraavat lisäykset; sallitaan siis häivytykset, kun siirrytään tilasta toiseen.

void MenuKeyInput()
{
    if (key[KEY_UP] && keypressed() && activeGameState == MENUSTATE)
    {
        selectedMenuItem = (selectedMenuItem - 1 + NUM_MENU_ITEMS) % NUM_MENU_ITEMS;
    }
    else if (key[KEY_DOWN] && keypressed() && activeGameState == MENUSTATE)
    {
        selectedMenuItem = (selectedMenuItem + 1) % NUM_MENU_ITEMS;
    }
    else if (key[KEY_ENTER] && keypressed())
    {
        if (activeGameState == MENUSTATE)
        {
            if (selectedMenuItem == NEW_GAME)
            {
                allowFadeOut = 1;
                allowFadeIn = 1;

                activeGameState = GAME;
            }
            else if (selectedMenuItem == EXIT)
            {
                exitProgram = 1;
            }
        }
    }
    else if (key[KEY_ESC] && keypressed())
    {
        if (activeGameState == GAME)
        {
            allowFadeOut = 1;
            allowFadeIn = 1;
               
            activeGameState = MENUSTATE;
        }
    }

    clear_keybuf();
}

Lisää vielä fade_out-funktiokutsu Terminate-funktion määrittelyyn, jotta alkuvalikko häivytetään pois kun ohjelma suljetaan.

void Terminate()
{
    fade_out(FADE_DURATION);

    allegro_exit();
}

Nyt, kun peli käynnistetään, DOS häivytetään pois ja alkuvalikko häivytetään sisään. Lisäksi häivytykset sisään ja ulos tehdään myös pelitilan ja alkuvalikon välillä siirryttäessä sekä vielä häivytys ulos, kun ohjelma suljetaan. Peli tuntuu heti paljon ammattimaisemmalta, ja ensiaskel kotikutoisesta indiekötöstyksestä kohti kolmen A:n taideteosta on otettu!

Loppuruutu

Yksi erityinen piirre vanhoissa hyvän ajan DOS-peleissä oli loppuruutu, eli kun peli suljettiin ja siirryttiin takaisin DOSiin, tulostettiin jopa kokonainen ruudullinen tekstiä ja ASCII-grafiikkaa. Esim. alla olevassa kuvassa on Rise of the Triad -pelin loppuruutu, jossa soimautetaan warettajien omatuntoa ja mainostetaan muita Apogee Softwaren pelejä.


Meidän loppuruutumme on toistaiseksi vain yksi nöyrä rivi, joka lisätään main.h-otsikkotiedostoon vakion TEXT_ENDMESSAGE arvoksi. Laita lainausmerkkien väliin oma viestisi; viimeiset merkit \n tekevät tyhjän rivin loppuruudun ja DOSin komentorivin väliin.

#define TEXT_ENDMESSAGE "Thanks for playing! :)\n"

Lisää alla olevat rivit Terminate-funktioon, jolloin kun peli suljetaan, grafiikkatila asetetaan tekstimuotoon ja tulostetaan vakiossa TEXT_ENDMESSAGE määritetty teksti ruudulle.

void Terminate()
{
    fade_out(FADE_DURATION);

    set_gfx_mode(GFX_TEXT, 0, 0, 0, 0);
   
    allegro_message(TEXT_ENDMESSAGE);

    allegro_exit();
}

Näin on koodattu alkuvalikko ja pelitila muutamin lisämaustein. Tästä on hyvä jatkaa!

Kommentit