The Allegro Wiki is migrating to github at https://github.com/liballeg/allegro_wiki/wiki

Allegro Vivace/Dissecting Code

From Allegro Wiki
Jump to: navigation, search

Dissecting the basic game structure

In this section we will go through the source code for a basic game structure. The focus is on explaining what is happening and some larger concepts that are important to understand. Some other concepts will be briefly mentioned but are discussed more in-depth in other sections of this tutorial.

In short, the code below initializes allegro, creates a black window, runs the game loop and waits for the user to press escape and then quits.

Let's begin!

#include <allegro5/allegro5.h>
#include <stdio.h>
#include <stdlib.h>

The first part includes the necessary header files we need:

  • allegro5/allegro5.h: necessary for all the allegro 5 functionality we are using in the source code
  • stdio.h: required for using the printf() function
  • stdlib.h: required for using the exit() function
bool done;
ALLEGRO_EVENT_QUEUE* event_queue;
ALLEGRO_TIMER* timer;
ALLEGRO_DISPLAY* display;

Here we declare some global variables that we need.

The done variable is used to keep track of when we should quit the game. The event_queue variable is an allegro event queue, it's vital for being able to gather user input among other things. The allegro event system is discussed more in-depth in the section that covers how to get user input from the keyboard. The timer variable is an allegro timer, used to control the speed of our logic updates. The reason why we need to do this is discussed below when we talk about the game loop. The display variable is an allegro display, which represents the computer screen or a window within the operating system you are using.

void abort_game(const char* message)
{
    printf("%s \n", message);
    exit(1);
}

This function is a helper function that is there to help us reduce code redundancy, i.e. we don't want to repeat the same code over and over if we can avoid it. It basically prints out a message to the console, and then exits the game. It's only used when something doesn't go right when we are initializing the game, which is very useful for debugging purposes.

Initialization

void init(void)
{
    if (!al_init())
        abort_game("Failed to initialize allegro");

The init function corresponds to the initialization phase of the game structure we discussed previously. The first thing we need to do is to initialize allegro itself. We accomplish this through calling al_init. If al_init fails (i.e. returns false), we call abort_game, and tells the reason why aborted the game.

The importance of checking return values

It's important to always check for return values whenever you can, especially when loading external resources such as graphics and sounds. This might seem tedious at first but will save you a lot of headache down the road as you can't figure out why your program crashes and the reason might be because you placed the graphics file in the wrong place. Checking for return values will often easily identify these fatal errors.

    if (!al_install_keyboard())
        abort_game("Failed to install keyboard");

Next, we call al_install_keyboard. The function installs a keyboard driver, and is necessary if we want to be able to capture input from the keyboard.

    timer = al_create_timer(1.0 / 60);
    if (!timer)
        abort_game("Failed to create timer");

We create the timer through calling al_create_timer. A timer is basically an object that increments a counter after a certain amount of time (one "tick") has passed. The parameter passed to the function is how many seconds there are between each "tick". We pass 1.0 / 60 to the function as we want the timer to tick 60 times every frame. Again, we will discuss this when it's time to dissect the game loop.

    al_set_new_display_flags(ALLEGRO_WINDOWED);
    display = al_create_display(640, 480);
    if (!display)
        abort_game("Failed to create display");

Before we create the display, there are certain flags we can set which control what kind of display we are creating and how it get initialized. Here, we are interested in creating a windowed display, so we pass ALLEGRO_WINDOWED to the al_set_new_display_flags function.

After we have set the flags we require, we create the display through calling al_create_display. The parameters passed to the function are the display height and width.

    event_queue = al_create_event_queue();
    if (!event_queue)
        abort_game("Failed to create event queue");

The event queue is what it sounds like. A queue of events, which is filled up on a first-in first-out basis whenever an event is triggered. An event is something that happens in your program which requires a response, such as a key being pressed, the mouse being pressed or a timer that has ticked. We'll go through the event system in more detail in another chapter.

    al_register_event_source(event_queue, al_get_keyboard_event_source());
    al_register_event_source(event_queue, al_get_timer_event_source(timer));
    al_register_event_source(event_queue, al_get_display_event_source(display));
    
    done = false;
}

The last things we do in the init function is register some event sources. Event sources are objects which generate events. We need to register them so that when an event happens, they are automatically passed to the queue. Here we register the keyboard so we can handle key presses, the timer so we know when it ticks (and can handle the speed of our logic updates) and the display. We don't really use the display events in this code, but it can be useful to be able to know when the user closes a window for example. The last thing is to set the done variable to false, as a true value would cause the game loop to terminate.

Shutting down

void shutdown()
{
    if (timer)
        al_destroy_timer(timer);
    
    if (display)
        al_destroy_display(display);
    
    if (event_queue)
        al_destroy_event_queue(event_queue);
}

The shutdown function is relatively short compared to the init function. When you initialize allegro with the al_init function (yes, there are other ways to initialize allegro that we won't cover in this tutorial), a lot of functions that clean up allegro when it's time to quit are automatically set up to be called when the program finishes its execution. For example, there is an equivalent al_uninstall_keyboard function that we won't have to call, because allegro does it for us. Look up atexit if you want to know the details.

There are some things that we do need to handle. Specifically, we need to destroy the timer, display and event queue as this is not taken care of automatically. As a rule of thumb, any object that is created must also be destroyed manually.

The Game Loop

void game_loop()
{
    bool redraw = true;
    al_start_timer(timer);

So, we finally arrive at the heart of the game. Before we enter the actual while loop, we need to start the timer so it begins ticking, and we set a redraw variable to true to let the game know when to redraw.

    while (!done) {
        ALLEGRO_EVENT event;
        al_wait_for_event(event_queue, &event);


Now we are in the actual loop. First we declare an event variable to hold a triggered event object. Then we call al_wait_for_event, which waits until an event happens.

        if (event.type == ALLEGRO_EVENT_TIMER) {
            redraw = true;
            update_logic();
        }

When the wait is over we need to handle the event. The type field of the event variable indicates the type of event. First, we check if the timer has ticked, indicated by the type equaling ALLEGRO_EVENT_TIMER. This means two things: a) we need to redraw the screen and thus set the redraw variable to true, and b) it's time to process the game logic. The update_logic function is neither defined nor declared in our code, it's up to you what to put in that function. The update_logic function corresponds directly to the processing phase of the game loop that we discussed earlier. That means that in that function we should handle things such as moving the player, collision detection, and game AI.

        else if (event.type == ALLEGRO_EVENT_KEY_DOWN) {
            if (event.keyboard.keycode == ALLEGRO_ESCAPE) {
                done = true;
            }
        }

Next we check if the event was a key press. This part corresponds to the input phase of the game loop. You could replace this with a function instead and pass the event variable to that function where you would then handle any input from the user. In this simple example we only care whether a key was pressed. If a key was pressed, we then check to see if a specific key was pressed, namely the escape key. If it was pressed, we want the game loop to end and thus set the done variable to true.

        if (redraw && al_is_event_queue_empty(event_queue)) {
            redraw = false;
            al_clear_to_color(al_map_rgb(0, 0, 0));
            draw();
            al_flip_display();
        }
    }
}

The last part of the game loop corresponds to the output phase. We first check if we need to redraw by checking if the redraw variable is set to true and that there are no unhandled events in the game loop. By doing this we avoid redrawing unless it's absolutely necessary, which will save both computer processing power and battery life.

If we need to redraw, we first set the redraw variable to false. We then clear the color of the display to black. Then it's time to draw. Again, the draw function is not defined/declared anywhere, and you will need to implement it in your own game. In that function you draw everything that should appear on screen. After everything has been drawn you flip the display.

Double buffering

What is this flipping of the display business you might ask? Well, it relates to a concept called double-buffering. It's a technique that is used to avoid flickering, tearing and other graphic artifacts that appear on the screen. Computer monitors are constantly being redrawn (at around 60 times a second) and if you draw directly to the screen it is very possible that you are drawing when the screen is being updated, resulting in a visible tear where the "new" image and the "old" image are both shown at the same time. As such, it is better to draw to a buffer, and then draw that buffer to the screen at once, thus avoiding this problem.

Luckily, allegro takes care of this for you. You only need to remember to clear the buffer, draw everything you need to draw to this buffer, and then call al_flip_display.

We'll talk about this more in-depth when we introduce graphics, but it is important to grasp this concept. It is also important to realize that animating things, for example moving a rectangle on the screen, is not just about updating the position of the object. Think of animation as frames that are displayed rapidly in sequence which fools the eye into believing that something is moving. In the same way, when we redraw the screen we are displaying one frame of the animation. To create the illusion of a rectangle moving across the screen, we need to both update the position of the rectangle during the logic update, but also clear the buffer, redraw the rectangle and flip the display every frame. This will result in the user seeing a rectangle moving across the screen. This is a basic principle that you need to understand, which has consequences for both how you need to think when you try to implement something in your game but also how you structure the code itself. You'll probably get the hang of it when we make more interesting things happen in the next chapters.

main()

int main(int argc, char* argv[])
{
    init();
    game_loop();
    shutdown();
}

Finally, here is the main function, where every program starts its execution.

As you can see, the code is very similar to the game structure we defined in the previous chapter. We start by initializing our game by calling init, then we call game_loop and when the game is done we call shutdown to deallocate any resources.