C: Hur man gör själva spelet

De här instruktionerna är oberoende av vilket operativsystem man använder. De förutsätter att Allegro-biblioteket redan är installerat på datorn, och att läsaren vet hur man skapar Allegro-program i den aktuella programmeringsmiljön.

1. Ett tråkigt och trasigt exempelprogram

Vi börjar med samma exempelprogram som vi använt tidigare:
// A very simple Allegro program

#include <stdlib.h>
#include <stdio.h>
#define USE_CONSOLE
#include <allegro5/allegro.h>
#include <allegro5/allegro_primitives.h>

int main(int argc, char *argv[]) {
    ALLEGRO_DISPLAY *display = NULL;

    // Initializing allegro
    if (!al_init()) {
        fprintf(stderr, "Couldn't initialize allegro. Sorry.\n");
        return EXIT_FAILURE;
    }

    if (!al_init_primitives_addon()) {
        fprintf(stderr, "Couldn't initialize allegro addons. Sorry.\n");
        return EXIT_FAILURE;
    }

    // Creating a window
    display = al_create_display(400, 300);
    if (display == NULL) {
        fprintf(stderr, "Couldn't create the window.\n");
        return EXIT_FAILURE;
    }

    // Painting it green
    al_clear_to_color(al_map_rgb(0, 255, 0)); 
    al_draw_filled_rectangle(200, 100, 250, 150, al_map_rgb(255, 0, 0));
    al_flip_display();

    printf("Now you should see a green window with a red square.\n");
    printf("Press ENTER to exit.\n");
    getchar();

    // Exiting the program
    al_destroy_display(display);

    return EXIT_SUCCESS;
}
Namnen på funktionsanropen, al_init, al_create_display och så vidare, borde tillsammans med kommentarerna göra att man förstår ungefär hur programmet fungerar.

Prefixet al i al_init med flera betyder förstås allegro. Prefixet behövs ifall man vill använda flera olika bibliotek. Om till exempel två olika bibliotek har en startfunktion som fått namnet init, skulle man få en namnkonflikt, och då skulle det inte gå att använda båda biblioteken i samma program. (Många andra språk, som C++ och Java, har andra och bättre lösningar på problemet med namnkonflikter.)

Programmet öppnar ett fönster som innehåller en liten röd ruta på en grön bakgrund. Det är inget särskilt spännande spel, och dessutom har programmet felet att om man minimerar det fönstret, och sen öppnar det igen, så blir det svart.

Repetition från tidigare:

I grafiska fönstersystem, som Microsoft Windows eller Unix-världens X, brukar själva systemet inte lagra och komma ihåg innehållet i de fönster som visas på skärmen. I stället måste programmet som kör i fönstret rita upp innehållet på nytt varje gång fönstret visas, till exempel om det varit minimerat eller dolt av ett annat fönster. Men det här testprogrammet är upptaget med att stå och vänta på inmatning (med getchar), så det kan inte rita något!

Om fönstret ska ritas om, måste vi skriva om programmet så att det gör det. Mer om det senare. Och nej, det fungerar inte att bara ta bort getchar-anropet, för då kommer programmet att köra klart main-funktionen och därmed avslutas, och då försvinner det gröna fönstret, kanske innan vi ens hinner se det!

2. Ett roligare program

Nu ska vi titta på ett längre, mindre trasigt och (kanske) aningen roligare program.

Spelet

Den röda fyrkanten rör sig över skärmen, och den lilla svarta fyrkanten fungerar som muspekare.

Kopiera och provkör den här programkoden:

#include <stdlib.h>
#include <stdio.h>
#define USE_CONSOLE
#include <allegro5/allegro.h>
#include <allegro5/allegro_primitives.h>

const double FPS = 60;
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const int BALL_SIZE = 100;

void fatal_error(char* message) {
    fprintf(stderr, "%s\n", message);
    fprintf(stderr, "Sorry.\n");

#ifdef WIN32
    fprintf(stderr, "Press ENTER to exit the program.\n");
    getchar();
#endif

    exit(EXIT_FAILURE);
}

int main(int argc, char *argv[]) {
    ALLEGRO_DISPLAY *display = NULL;
    ALLEGRO_EVENT_QUEUE *event_queue = NULL;
    ALLEGRO_TIMER *timer = NULL;
    double x_pos = SCREEN_WIDTH / 2;
    double y_pos = SCREEN_HEIGHT / 2;
    double delta_x = 2.5;
    double delta_y = 0.5;
    double mouse_x = -1;
    double mouse_y = -1;
    int please_redraw = 1;
 
    if (!al_init()) {
        fatal_error("Couldn't initialize allegro.");
    }

    if (!al_init_primitives_addon()) {
        fatal_error("Couldn't initialize allegro addons.");
    }

    if (!al_install_keyboard()) {
        fatal_error("Couldn't initialize keyboard.");
    }

    if (!al_install_mouse()) {
        fatal_error("Couldn't initialize mouse.");
    }
 
    timer = al_create_timer(1.0 / FPS);
    if (timer == NULL) {
        fatal_error("Couldn't create timer.");
    }
 
    // Attempts full screen
    al_set_new_display_flags(ALLEGRO_FULLSCREEN_WINDOW);
    display = al_create_display(SCREEN_WIDTH, SCREEN_HEIGHT);
    if (display == NULL) {
        // Attempts a window instead
        fprintf(stderr, "Fullscreen failed, trying a window...\n");
        al_set_new_display_flags(ALLEGRO_WINDOWED);
        display = al_create_display(SCREEN_WIDTH, SCREEN_HEIGHT);
        if (display == NULL) {
            fatal_error("Couldn't create display.");
        }
    }

    event_queue = al_create_event_queue();
    if (event_queue == NULL) {
        al_destroy_display(display);
        al_destroy_timer(timer);
        fatal_error("Couldn't create event_queue.");
    }
 
    al_register_event_source(event_queue, al_get_display_event_source(display));
    al_register_event_source(event_queue, al_get_keyboard_event_source());
    al_register_event_source(event_queue, al_get_mouse_event_source());
    al_register_event_source(event_queue, al_get_timer_event_source(timer));
 
    al_start_timer(timer);
 
    // The big game loop

    while (1) {
        ALLEGRO_EVENT ev;
        al_wait_for_event(event_queue, &ev);
 
        if (ev.type == ALLEGRO_EVENT_TIMER) {
            x_pos += delta_x;
            y_pos += delta_y;
            please_redraw = 1;
        }
        else if (ev.type == ALLEGRO_EVENT_MOUSE_AXES ||
                 ev.type == ALLEGRO_EVENT_MOUSE_ENTER_DISPLAY) {
            mouse_x = ev.mouse.x;
            mouse_y = ev.mouse.y;
            please_redraw = 1;
        }
        else if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {
            break;
        }
        else if (ev.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN) {
            break;
        }
        else if (ev.type == ALLEGRO_EVENT_MOUSE_BUTTON_UP) {
            break;
        }
        else if (ev.type == ALLEGRO_EVENT_KEY_DOWN) {
            break;
        }
        else if (ev.type == ALLEGRO_EVENT_KEY_UP) {
            break;
        }

        if (please_redraw && al_is_event_queue_empty(event_queue)) {
            please_redraw = 0;
 
            al_set_target_bitmap(al_get_backbuffer(display));

            al_clear_to_color(al_map_rgb(0, 255, 0));
            al_draw_filled_rectangle(x_pos, y_pos, x_pos + BALL_SIZE, y_pos + BALL_SIZE, al_map_rgb(255, 0, 0));
            al_draw_filled_rectangle(mouse_x, mouse_y, mouse_x + 10, mouse_y + 10, al_map_rgb(0, 0, 0));

            al_flip_display();
        }
    }
 
    al_destroy_timer(timer);
    al_destroy_display(display);
    al_destroy_event_queue(event_queue);
 
    printf("Goodbye.\n");
#ifdef WIN32
    printf("Press ENTER to exit the program.\n");
    getchar();
#endif

    return EXIT_SUCCESS;
}

3. Genomgång av det roligare programmet

Vi analyserar programkoden lite närmare:
#define USE_CONSOLE
#include <allegro5/allegro.h>
#include <allegro5/allegro_primitives.h>
allegro.h är förstås Allegro-bibliotekets include-fil. allegro_primitives.h innehåller funktioner för att rita rektanglar och figurer. Raden med USE_CONSOLE behövs eftersom operativsystemet Windows startar C-program på olika sätt beroende på om det är ett konsolprogram eller ett fönsterprogram, och vi måste tala om för systemet att det här är ett konsolprogram.

(Men det kan hända att USE_CONSOLE inte alls behövs. Jag hade med det i Allegro 4, men det kanske inte behövs nu i Allegro 5.)

const double FPS = 60;
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const int BALL_SIZE = 100;
Här skapar vi några konstanter. Eftersom de inte ska användas som arraydimensioner kan vi använda const-deklarerade variabler i stället för #define-konstanter. FPS står för "frames per second", och används i programmet för att välja tidsintervall för timern (se nedan). BALL_SIZE är sidlängden på den kvadratiska "bollen".
void fatal_error(char* message) {
    fprintf(stderr, "%s\n", message);
    fprintf(stderr, "Sorry.\n");

#ifdef WIN32
    fprintf(stderr, "Press ENTER to exit the program.\n");
    getchar();
#endif

    exit(EXIT_FAILURE);
}
fatal_error är en funktion som vi kan anropa när något programmet misslyckats med att göra något. Den skriver ut ett felmeddelande och avslutar programmet. På det viset slipper vi upprepa all den där koden på flera ställen i programmet.

#ifdef WIN32 betyder att den koden bara ska med om man kompilerar programmet i Windows och inte, till exempel, om man kör i Linux.

int main(int argc, char *argv[]) {
    ALLEGRO_DISPLAY *display = NULL;
    ALLEGRO_EVENT_QUEUE *event_queue = NULL;
    ALLEGRO_TIMER *timer = NULL;
    double x_pos = SCREEN_WIDTH / 2;
    double y_pos = SCREEN_HEIGHT / 2;
    double delta_x = 2.5;
    double delta_y = 0.5;
    double mouse_x = -1;
    double mouse_y = -1;
    int please_redraw = 1;
Här börjar main-funktionen, och vi definierar en hel rad variabler. Variablerna förklaras senare.
    if (!al_init()) {
        fatal_error("Couldn't initialize allegro.");
    }

    if (!al_init_primitives_addon()) {
        fatal_error("Couldn't initialize allegro addons.");
    }
Allegro-biblioteket behöver "startas upp", och om det misslyckas så anropas funktionen fatal_error.
    if (!al_install_keyboard()) {
        fatal_error("Couldn't initialize keyboard.");
    }

    if (!al_install_mouse()) {
        fatal_error("Couldn't initialize mouse.");
    }
Programmet ska känna av om användaren trycker på tangenterna, så därför "installerar" vi tangentbordet i Allegro. Man kunde också sagt att man "startar", "aktiverar" eller "börjar lyssna" på tangentbordet.

På samma sätt "installerar" vi musen.

    timer = al_create_timer(1.0 / FPS);
    if (timer == NULL) {
        fatal_error("Couldn't create timer.");
    }
Vi skapar en timer. Skärmen ska ritas upp FPS ("frames per second") gånger per sekund, så vi skickar med tidsintervallet mellan två skärmuppdateringar. Timern kommer att "lösa ut" med det intervallet.
    // Attempts full screen
    al_set_new_display_flags(ALLEGRO_FULLSCREEN_WINDOW);
    display = al_create_display(SCREEN_WIDTH, SCREEN_HEIGHT);
    if (display == NULL) {
        // Attempts a window instead
        fprintf(stderr, "Fullscreen failed, trying a window...\n");
        al_set_new_display_flags(ALLEGRO_WINDOWED);
        display = al_create_display(SCREEN_WIDTH, SCREEN_HEIGHT);
        if (display == NULL) {
            fatal_error("Couldn't create display.");
        }
    }
Vi försöker få programmet att ta över hela skärmen. Går inte det, öppnar vi ett fönster. Går inte det heller, anropar vi fatal_error.
    event_queue = al_create_event_queue();
    if (event_queue == NULL) {
        al_destroy_display(display);
        al_destroy_timer(timer);
        fatal_error("Couldn't create event_queue.");
    }
Vi använder oss av så kallade händelsestyrd programmering, och det här är en kö av händelser som inträffat. En händelse kan vara till exempel att användaren flyttat på musen, eller att det gått en viss tid. Programmet ska hämta information om händelserna, och hantera dem en efter en.
    al_register_event_source(event_queue, al_get_display_event_source(display));
    al_register_event_source(event_queue, al_get_keyboard_event_source());
    al_register_event_source(event_queue, al_get_mouse_event_source());
    al_register_event_source(event_queue, al_get_timer_event_source(timer));
Vi talar om vilka händelser vi är intresserade av att hantera: såna som är relaterade till fönstret, tangentbordet, musen och timers.
    al_start_timer(timer);
Starta timern så den börjar ticka. Den kommer att generera "händelser" med det angivna tidsintervallet.
    // The big game loop

    while (1) {
Den stora loopen. I varje varv i loopen kommer vi först stå och vänta på händelser, och sen (under vissa omständigheter) rita om fönstret.
        ALLEGRO_EVENT ev;
        al_wait_for_event(event_queue, &ev);
 
Här står vi och väntar på händelser.
        if (ev.type == ALLEGRO_EVENT_TIMER) {
            x_pos += delta_x;
            y_pos += delta_y;
            please_redraw = 1;
        }
Om det var timern som löst ut, så räknar vi ut den fyrkantiga "bollens" nya position. x_pos och y_pos är bollens aktuella position, och delta_x och delta_y är bollens hastighet, dvs hur mycket bollen flyttar sig i varje uppdatering.

please_redraw sätts till 1 för att ange att något på skärmen har ändrats. Lite längre ner i programmet använder vi den för att avgöra om skärmen måste ritas om.

        else if (ev.type == ALLEGRO_EVENT_MOUSE_AXES ||
                 ev.type == ALLEGRO_EVENT_MOUSE_ENTER_DISPLAY) {
            mouse_x = ev.mouse.x;
            mouse_y = ev.mouse.y;
            please_redraw = 1;
        }
Om händelsen var att musen flyttat på sig i fönstret, eller kommit in i fönstret, hämtar vi muspositionen (som skickas med i informationen om händelsen) och lägger i variablerna mouse_x och mouse_y, som anger muspositionen. När vi sen ritar upp skärmen använder vi dem för att placera muspekaren.
        else if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {
            break;
        }
        else if (ev.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN) {
            break;
        }
        else if (ev.type == ALLEGRO_EVENT_MOUSE_BUTTON_UP) {
            break;
        }
        else if (ev.type == ALLEGRO_EVENT_KEY_DOWN) {
            break;
        }
        else if (ev.type == ALLEGRO_EVENT_KEY_UP) {
            break;
        }
Alla andra typer av händelser (nämligen musklick, tangenttryck och att fönstret stängs) gör att programmet ska avslutas. break avslutar inte programmet, men gör att vi hoppar ur loopen.
        if (please_redraw && al_is_event_queue_empty(event_queue)) {
            please_redraw = 0;
 
            al_set_target_bitmap(al_get_backbuffer(display));

            al_clear_to_color(al_map_rgb(0, 255, 0));
            al_draw_filled_rectangle(x_pos, y_pos, x_pos + BALL_SIZE, y_pos + BALL_SIZE, al_map_rgb(255, 0, 0));
            al_draw_filled_rectangle(mouse_x, mouse_y, mouse_x + 10, mouse_y + 10, al_map_rgb(0, 0, 0));

            al_flip_display();
        }
    }
Om vi ska om skärmen, så gör vi det. Men vi väntar tills kön av händelser är tom, och alla händelser är hanterade, för om det finns fler händelser som gör att skärmen ska ändras är det dumt att rita upp skärmen nu, och sen rita om den direkt efteråt, utan vi väntar tills alla skärmändrande händelser är processade.

Vi ritar med al_clear_to_color och al_draw_filled_rectangle. Det finns två "bitmappar", som är minnesutrymmen där en representation av fönstret lagras, och det är på en sådan bitmap som man ritar. Allegro använder sig av så kallad dubbelbuffring, vilket betyder att man använder sig av två minnesutrymmen. Det ena visas, och programmet ritar i det andra.

Om vi bara hade en bitmap, och ritade (med al_clear_to_color och al_draw_filled_rectangle) direkt i det minnesutrymme som samtidigt visas på skärmen, kan det hända att skärmen uppdateras asynkront, dvs oberoende av vad vi håller på med i programmet. Det betyder att ibland när skärmen ska visas, så håller vi just då på och ritar om innehållet, och det blir en halvfärdig bild som visas. Det orsakar flimmer.

Lösningen är att man ritar i en separat minnesarea, och lämnar över den färdigritade bilden för visning först när den är klar, alltså dubbelbuffring. Anropet al_flip_display byter plats på de två buffertarna.

Därmed är också huvudloopen avslutad.

    al_destroy_timer(timer);
    al_destroy_display(display);
    al_destroy_event_queue(event_queue);
Hit kommer vi när vi hoppat ur huvudloopen, vilket bara sker när programmet ska avslutas. Vi städar upp efter oss genom att ta bort de skapade objekten. Det behöver egentligen inte göras, eftersom programmet ändå ska avslutas nu, och då kommer de ändå att försvinna. (Men det kan finnas operativsystem där det fungerar annorlunda.)
    printf("Goodbye.\n");
#ifdef WIN32
    printf("Press ENTER to exit the program.\n");
    getchar();
#endif

    return EXIT_SUCCESS;
}
Och så är programmet slut.

6. Uppgiften!

Gör så att fyrkanten stannar om man pekar på den med musen, och att den inte försvinner utanför kanten.

  1. Gör så att fyrkanten stannar om man pekar på den med musen. Det är inte så svårt. Allt man behöver göra är att se om musens x- och y-koordinater befinner sig inuti den stora röda rektangeln, och i så fall låta bli att ändra den rektangelns position. Tips: Funktionen inuti från inlämningsuppgift 3.
  2. Nu försvinner fyrkanten när den åker iväg utanför skärmen. Låt den i stället byta riktning och "studsa tillbaka" när den når kanten på skärmen. (Alternativt kan man låta den "teleportera" till andra sidan, och fortsätta åt samma håll.) Det är inte heller så svårt. När x_pos eller y_pos har uppdaterats, kontrollera att de fortfarande är kvar innanför fönstret.

7. Frivilliga utökningar av spelet

Det är bara obligatoriskt att göra ändringen ovan, men här är några förslag på ytterligare förbättringar av programmet som man kan göra om man vill:
  1. Låt fyrkanten påverkas av gravitationen. Det betyder att y-komponenten i hastigheten ökar av sig själv hela tiden. (Tips: Det behövs bara en enda rad till i programmet!)
  2. Låt den röda fyrkanten byta riktning i stället för att bara stanna när den krockar med musen.
  3. Nu arbetar spelet med skärmupplösningen 640 gånger 480, även i fullskärmsläge. Ändra så det använder skärmens riktiga upplösning. (Tips: funktionen al_get_monitor_info.)
  4. Fyrkanten borde gå fortare ju hårdare man slår till den.
  5. Byt till en boll, dvs en röd cirkel i stället för en rektangel. Då blir "inuti"-kollen lite mer komplicerad.
  6. Ha mer än en boll, och gör så att bollarna kan studsa mot varandra.
  7. Inför någon sorts poängräkning, till exempel genom att man får en minuspoäng för varje gång en boll krockar med kanten eller en annan boll. Spelarens uppgift är alltså att hindra kollisioner.
  8. Gör det möjligt att på något sätt välja antalet bollar.

Thomas Padron-McCarthy (Thomas.Padron-McCarthy@oru.se), 21 juni 2011