Lesson 01 - GUI Widgets And The Game Loop

 *//* 
This lesson will show you how to begin creating use graphical user interfaces and how to design and run a game loop -- the heartbeat of a game engine.

Procedural Overview -- Items in bold are additions/changes to the previous lesson.

Comments explaining previously covered material will be made more terse or deleted entirely in each successive lesson. If something is not explained well enough, it was probably already explained in previous lessons.

Code Diving!

 */
// This header MUST be included in every source/header file.
#include "xrb.hpp"

#include "xrb_button.hpp"     // For use of the Button widget class.
#include "xrb_event.hpp"      // For use of the Event classes.
#include "xrb_eventqueue.hpp" // For use of the EventQueue class.
#include "xrb_inputstate.hpp" // For use of the InputState class (via Singleton::).
#include "xrb_label.hpp"      // For use of the Label widget class.
#include "xrb_layout.hpp"     // For use of the Layout widget class.
#include "xrb_lineedit.hpp"   // For use of the LineEdit widget class.
#include "xrb_screen.hpp"     // For use of the necessary Screen widget class.
#include "xrb_sdlpal.hpp"     // For use of the SDLPal platform abstraction layer.

// Used so we don't need to qualify every library type/class/etc with Xrb::
using namespace Xrb;

// This is just a helper function to group all the shutdown code together.
void CleanUp ()
{
    fprintf(stderr, "CleanUp();\n");

    // Shutdown the platform abstraction layer.
    Singleton::Pal().Shutdown();
    // Shutdown the game engine singletons.  This is necessary for the
    // game engine to shutdown cleanly.
    Singleton::Shutdown();
}

int main (int argc, char **argv)
{
    fprintf(stderr, "main();\n");

    // Initialize the game engine singleton facilities.
    Singleton::Initialize(SDLPal::Create, "none");
    // Initialize the platform abstraction layer (Pal).
    if (Singleton::Pal().Initialize() != Pal::SUCCESS)
        return 1;
    // Set the window caption.
    Singleton::Pal().SetWindowCaption("XuqRijBuh Lesson 01");

    // This call creates the Screen object and initializes the given video mode.
    // The Screen object is the root widget of the GUI widget hierarchy, and
    // does a bunch of special handling to draw its child widgets properly.
    Screen *screen = Screen::Create(
        800,    // video mode/screen width
        600,    // video mode/screen height
        32,     // video mode pixel bitdepth
        false); // not fullscreen -- none for now.
    // If the Screen failed to initialize, print an error message and quit.
    if (screen == NULL)
    {
        fprintf(stderr, "failure during Screen creation\n");
        // this shuts down the Pal and singletons.
        CleanUp();
        // return with an error value.
        return 2;
    }

    /* 
At this point, the singletons and the Pal have been initialized, the video mode has been set, and the engine is ready to go. Here is where the application-specific code begins.

Everything that is visible in this game engine happens inside an instance of a Widget subclass. Therefore, we need to instantiate some widgets before anything interesting can happen.

There are custom subclasses of Widget (such as Label, LineEdit, Button, Layout, etc) to perform various functions. Widgets are organized in a hierarchy (i.e. parent/child relationship) which also dictates the spatial organization of widgets onscreen -- a child widget is completely contained within its parent, and cannot draw anything outside itself. This is the same widget paradigm as used by the FLTK, MFC, QT (and many other) toolkits. The GUI system in this game engine is primarily modeled after Trolltech's excellent QT GUI toolkit.

Instead of having to worry about the exact screen coordinates at which to place our widgets, there is a Widget subclass called Layout which automatically handles widget sizing and placement. Layouts can place widgets in horizontal/vertical lines or in grids, and can be nested within one another to form complicated formatting of widgets.

 */
    {
        /* 
We will now create a Layout widget as a child of our Screen (which, not coincidentally, is itself a Widget subclass). this Layout will be the parent to the widgets we place inside. The first parameter is the orientation of the Layout (a vertical column of widgets). The second parameter is the Widget to assign as its parent -- in this case, the Screen itself. The third parameter, which is present in almost all Widget subclasses after the parent widget parameter, is the "name" of the widget. This parameter is optional, but its use is recommended. It isn't used directly by the engine, but is invaluable in debugging GUI problems -- the ability to identify which widget you're dealing with in a mostly meaningless call stack within some event loop.
 */
        Layout *main_layout = new Layout(VERTICAL, screen, "main layout");
        /* 
This call causes main_layout to fill out the entire space of screen. SetMainWidget is a Widget method, so can be used on any parent widget for a child. If the parent widget's size or location change, its main widget will be resized and/or moved accordingly.
 */
        screen->SetMainWidget(main_layout);

        /* 
Now to create some directly useful widgets.

A Label is a simple, non-interactive widget which draws text or a picture (we'll get to picture labels later). the default justification for a Label's contents is centered both horizontally and vertically, but this, among other properties, can be changed.

 */
        new Label("I LIKE ZOMBIES.", main_layout, "awesome zombie text label");
        /* 
A Button can be clicked upon to signal some other piece of code to do something. This button isn't connected to anything, but we'll get to that later.
 */
        new Button("This button does nothing", main_layout, "do-nothing button");

        /* 
Layouts, just like any Widget subclass, can be contained within other Layouts. This layout will be used to place a text Label and a LineEdit side-by-side. The code block is only being used to indicate creation of the layout and its child widgets.

A LineEdit is a text-entry box. The first parameter in its constructor indicates the maximum number of characters that can be entered into it. It derives from the same baseclass as Label, so it shares many of the same properties as Label, such as alignment, text color, and so forth.

 */
        {
            Layout *sub_layout = new Layout(HORIZONTAL, main_layout, "label and line-edit layout");

            // Create a text Label to indicate what to do with the following LineEdit.
            new Label("You can enter up to 30 characters in this LineEdit ->", sub_layout, "indicator label");
            // Create the LineEdit after the Label, and it will be placed next
            // in the horizontal layout.
            new LineEdit(30, sub_layout, "30-char line edit");
        }

        /* 
Add another horizontal Layout so we can have a row of text Labels to demonstrate the word wrapping and alignment properties. Again, the code block is only being used to indicate creation of the layout and its child widgets. Make sure to read the text contained in each Label, as each contains useful information.
 */
        {
            Layout *sub_layout = new Layout(HORIZONTAL, main_layout, "note label layout");

            // Another text Label.  It notes that holding down a key will not cause
            // the character to repeat.  Doing this will be discussed later.  This
            // Label demonstrates word wrapping and justification.
            Label *note_label =
                new Label(
                    "Note that holding a key down while typing into the\n"
                    "LineEdit does not cause more than one character to\n"
                    "be entered (i.e. no keyboard repeating).  Doing so\n"
                    "complicates the event-polling loop, and it was\n"
                    "omitted to keep this example as simple as possible.\n"
                    "Notice how this text Label is wider than the other\n"
                    "three adjacent Labels.  This is because by default,\n"
                    "text Labels' minimum width is fixed to the text\n"
                    "width, so none of the text is cut off.  The other\n"
                    "three Labels can be resized horizontally because\n"
                    "they each have word wrapping enabled, so the text\n"
                    "formatting is dictated by the width of the Label.",
                    sub_layout,
                    "left-aligned note label");
            // All text will be aligned with the left edge of the Label.  By default,
            // a Label's alignment is CENTER.
            note_label->SetAlignment(Dim::X, LEFT);

            // Add another label, this time, with centered alignment.
            note_label =
                new Label(
                    "This Label widget is using word wrapping with center alignment.  "
                    "The width of the Label dictates where the words will be wrapped.",
                    sub_layout,
                    "center-aligned note label with word wrapping");
            // This call does exactly what it looks like it does.
            note_label->SetWordWrap(true);
            // The default alignment is already CENTER, so we don't need to do anything.

            // Add another label, this time, with right alignment.
            note_label =
                new Label(
                    "This Label widget is using word wrapping with right alignment. "
                    "Any Label (not just word wrapped Labels) can use the alignment "
                    "property, including aspect-ratio-preserving picture Labels.",
                    sub_layout,
                    "right-aligned note label with word wrapping");
            // This call does exactly what it looks like it does.
            note_label->SetWordWrap(true);
            // All text will be aligned with the right edge of the Label.
            note_label->SetAlignment(Dim::X, RIGHT);

            // Add another label, this time, with right alignment.
            note_label =
                new Label(
                    "This Label widget uses word wrapping with character spacing. "
                    "Spacing is a special type of alignment that only applies to word "
                    "wrapped Labels, and will attempt to space the characters out to "
                    "fill out the entire width of the Label.  The extra spacing between "
                    "characters seems to sometimes screw up the font's kerning (the "
                    "space between specific glyph pairs to make the text look more "
                    "natural).",
                    sub_layout,
                    "spaced note label with word wrapping");
            // This call does exactly what it looks like it does.
            note_label->SetWordWrap(true);
            // see note_label's text for a description.
            note_label->SetAlignment(Dim::X, SPACED);
        }

        /* 
Note the apparently dangling pointers that the above calls to "new" returned. We don't have to ever worry about deleting widgets manually (except for the Screen), because when a Widget is destroyed, it deletes all its children (pretty heartless, huh?). Thus, when the Screen is deleted, its entire widget hierarchy is massacred.

We're done creating widgets for this example. Next up is the game loop, where all the computation, screen-rendering, event-processing, user-input-processing, etc takes place.

The screen keeps track of if a quit request was detected (i.e. Alt+F4 or clicking the little X in the corner of the window pane). We want to loop until the user requests to quit the app.

 */
        while (!screen->IsQuitRequested())
        {
            /* 
Sleep for 33 milliseconds so we don't suck up too much CPU time. This will result in limiting the game loop framerate to about 30 frames per second (because there is one screen-rendering per game loop iteration).
 */
            Singleton::Pal().Sleep(33);
            /* 
Certain facilities of the game engine require the current time. This value should be represented as a Float (notice the capitalization), and should measure the number of seconds since the application started. Singleton::Pal().CurrentTime() returns the number of milliseconds since the app started, so take one thousandth of that.
 */
            Float time = 0.001f * Singleton::Pal().CurrentTime();
            /* 
This loop sucks up all pending events, produces Xrb::Event objects out of them, and craps them onto the top of the widget hierarchy (the Screen object). Singleton::Pal().PollEvent will return non-NULL while there are still events to process. We will loop until there are no more events left.
 */
            Event *event = NULL;
            while ((event = Singleton::Pal().PollEvent(screen, time)) != NULL)
            {
                /* 
If the event is of the keyboard or mouse, let the InputState singleton process it. This step is necessary so that the state of said user-input devices is updated.
 */
                if (event->IsKeyEvent() || event->IsMouseButtonEvent())
                    Singleton::InputState().ProcessEvent(event);
                /* 
All events are delegated to the proper widgets via the top of the widget hierarchy (the Screen object). Events must go through the widget hierarchy because certain events are handled based on locations of widgets (e.g. mousewheel events always go to the widget(s) directly under the mouse cursor).
 */
                screen->ProcessEvent(event);
                /* 
We don't want to rely on widgets deleting events themselves, as it would create a maintenance nightmare, so we insist that events must be deleted at whatever code scope they were created. This also allows events that were created on the stack to be passed in without fear that they will be illegally deleted.
 */
                Delete(event);
            }

            /* 
Events can be enqueued for asynchronous/delayed processing (which will be discussed later), and so processing them on a time-basis must be done each game loop iteration.
 */
            screen->OwnerEventQueue()->ProcessFrame(time);
            /* 
This call is where all the off-screen (strictly non-rendering) computation for the widget hierarchy takes place. Widget subclasses can override a particular method so that they can do per-frame computation. The widget hierarchy is traversed in an undefined order (not strictly prefix, infix or postfix), calling each widget's "think" method.
 */
            screen->ProcessFrame(time);
            /* 
Turn the crank on the event queue again, since there may have been new events enqueued during the previous call, and we want to handle these delayed events at the earliest possible time.
 */
            screen->OwnerEventQueue()->ProcessFrame(time);
            /* 
Here is where the visual magic happens. All the visible widgets in this hierarchy are drawn in this call. If a widget is invisible -- if it is hidden, of zero area, or it is not on-screen -- its Draw method is not called.
 */
            screen->Draw();
        }
    }

    // Delete the Screen object, and with it the entire GUI widget hierarchy.
    Delete(screen);
    // this shuts down the Pal and the singletons.
    CleanUp();
    // return with success value.
    return 0;
}
/* 

Exercises

Thus concludes lesson01. Do you feel smarter? Well, you aren't.


Hosted by SourceForge.net Logo -- Generated on Fri Aug 21 21:46:38 2009 for XuqRijBuh by doxygen 1.5.8