Video Engine GUI

From Hero of Allacrost Wiki
Revision as of 08:22, 6 December 2012 by Roots (talk | contribs) (Copying page from old wiki.)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Overview[edit]

This section provides a quick introduction to the video engine's Graphical User Interface (GUI). There are four interface classes in the GUI API: GUISupervisor, MenuWindow, TextBox, and OptionBox. GUISupervisor is a class which contains various shared data and methods that effect or are accessed by all other GUI classes. MenuWindow is represented as a rectangle composed of a border image and a filled interior, which may be either a color or a texture. TextBoxes are defined areas on the screen where text should be drawn, and allows for effects such as scrolling and word-wrapping. OptionBoxes present a series of images and text organized in a grid of rows and columns that allow the user to choose and select an option. MenuWindow, TextBox, and OptionBox classes are derived from an abstract class called GUIElement. GUIElement declares Update() and Draw() methods that each of the inheriting classes implement. GUIElement also provides methods for setting the position of the GUI object, the object's alignment options, etcetera. The TextBox and OptionBox class are additionally defined as GUI controls, and inherit from a GUIControl class. A GUI control is nothing more than a GUI class which may be defined to exist on a MenuWindow object, which we call ownership. Ownership will be explained further in a future section.


The general usage process for GUI classes is:

  1. Initialize once by calling various Set() methods
  2. Update every frame by invoking the Update() method
  3. Draw every frame by invoking the Draw() method


GUI Coordinates[edit]

One special property of the GUI system is that it is largely independent from many of the active draw properties of the video engine, such as the active coordinate system or the draw cursor. This is because every GUI object stores its own position coordinates and alignment flags independently of the engine. The GUI only operates in the coordinate system of (0, 1024, 768, 0), so you must keep this fact in mind at all times when dealing with GUI coordinates and positioning. A GUI object's position is set with the SetPosition(float x, float y) method. You can use the SetAlignment(int32 xalign, int32 yalign) method to set the alignment of the GUI object. The default alignment for all GUI objects is (VIDEO_X_LEFT, VIDEO_Y_TOP).


Remember, always keep in mind that the following states and properties do not have any effect on GUI objects.

  • The position of the draw cursor
  • The active coordinate system
  • Any draw flags that are set


Control Ownership[edit]

You can attach a control to a MenuWindow by calling the control's SetOwner(MenuWindow *window) function and passing in a pointer to the menu window which should own the control. Most of the game's controls are attached to a menu window. Ownership is extremely useful as it provides a number of useful features listed below.

  • Relative positioning

The positions of the control becomes relative to the MenuWindow which it is owned by, rather than in absolute coordinates. One distinct advantage of this is if the window is repositioned, all the controls will automatically move along with that MenuWindow.

  • Scissoring

Suppose we accidentally make a control which is too big for the MenuWindow that is its owner, or that one part of the control is hanging off the edge of the MenuWindow. With scissoring, we can "cut away" part of the control so that it doesn't cover up parts of the screen outside of the MenuWindow.

Not all controls need to be attached to a menu window. An example of a control which would not need to be attached to a menu is the opening scene from the original Final Fantasy.

Ff1 opening screen.png


MenuWindow[edit]

The MenuWindow object lets you draw a menu window with borders, which looks something like this:

Gui menu window.png

Initialization[edit]

The most simple example of creating a menu would be:

menu_window.Create(320.0f, 240.0f);
menu_window.SetPosition(512.0f, 384.0f);
menu_window.SetAlignment(VIDEO_X_CENTER, VIDEO_Y_CENTER);

This creates a 320x240 pixel menu and positions in the center of the screen. Note that the default alignment is top left, so you only need to call SetAlignment() if you want to do use another type of alignment. Note that since the alignment is stored in the menu window itself, the video engine's alignment draw flags do not have any effect when you draw the menu window. Besides the position and size, the two other parameters you can set are the display mode and the edge flags. The display mode tells how the menu should animate when its shown or hidden. By default, VIDEO_MENU_INSTANT is used, which means no animation. However, if you want a menu to "expand" when it's shown and "shrink" when it's hidden, you can use the following function.

main_window.SetDisplayMode(VIDEO_MENU_EXPAND_FROM_CENTER);

Edge flags are useful in the case of "sub-menus", which are menus that share a common border. For example, if you want to put 2 menus side by side, you could just draw them both normally, but then you'd end up with a "double border", as shown in the picture below:

Gui menu double border.png

Instead, the goal is to have something like below:

Gui menu single border.png

This is a bit complicated, but basically you need to draw one of the menus with one of its borders missing, and the other menu with all of its borders. Furthermore, on the menu which has all of its borders, the corner borders need to be replaced with 3-way or 4-way connector images instead of just the usual "2-way" connectors. The way to do this is to pass a 3rd and 4th parameter to the Create() function. These are optional parameters to this function and omitting them will use the default behavior. The 3rd parameter is used for the case where you want to remove borders from a menu. It is an integer which contains some bit flags for each edge telling whether it's visible for not. So, for example, if you wanted to remove the left edge, then you could do:

menu_window.Create(320.0f, 240.0f, VIDEO_MENU_EDGE_TOP | VIDEO_MENU_EDGE_RIGHT | VIDEO_MENU_EDGE_BOTTOM);

A more concise way to do this is by simply using the bitwise complement of the left edge flag:

menu_window.Create(320.0f, 240.0f, ~VIDEO_MENU_EDGE_LEFT);

Note that if you want to draw all edges, you can also use VIDEO_MENU_EDGE_ALL, which is the default value for this parameter. The 4th parameter of the Create() function allows you to replace the corner images with 3-way or 4-way connectors. To do this, basically you need to tell the menu object what edges are shared with other menus. Again, the actual flags used to do this are the same. So to finally bring everything together into one clear example, here is how you would create the two "sub-menus" which are shown above, where they are sharing the same border.

MenuWindow left, right;
left.Create(320.0f, 240.0f, VIDEO_MENU_EDGE_ALL, VIDEO_MENU_EDGE_RIGHT);
left.SetPosition(0, 0);
right.Create(320.0f, 240.0f, VIDEO_MENU_EDGE_LEFT);
right.SetPosition(320, 0);

So what we're doing is we're telling the menu on the left, "go ahead and draw all of its borders, but by the way, there's a menu on your right, so you better use the correct connector images", and we're telling the menu on the right, "strip off your left border". The decision of which menu keeps its borders and which one strips it off is somewhat arbitrary so you can decide for yourself how you want to split it. To keep things consistent as a rule of thumb, have menus to the top or left retain their borders, while ones to the right or bottom strip their borders. Also, note that when you strip a border off of a menu window, it doesn't become smaller. It's the same size, it's just that the part that used to be the border gets filled up with whatever the interior color/texture is.


Showing and Hiding[edit]

Menus are not always visible. You can show and hide them with the Show() and Hide() functions. Note that the default state for menus is hidden so you need to call Show() before it will be visible on the screen! Calling Draw() on a menu window that is hidden will result in no operation. Note that if you're using an animated display mode (anything other than VIDEO_MENU_INSTANT), then the menu is not immediately shown when you call Show(), rather it starts out hidden and then gradually animates into being shown. Likewise when the menu is being hidden. In total, there are four states a menu window can be in at any time:

VIDEO_MENU_STATE_SHOWN       // completely shown
VIDEO_MENU_STATE_SHOWING     // in the process of being shown
VIDEO_MENU_STATE_HIDING      // in the process of being hidden
VIDEO_MENU_STATE_HIDDEN      // completely hidden

You can use the GetState() function to find out the menu's current state. This is useful for example, if you want to draw some text or images on top of a window, but you don't want to show it until the window is fully visible. If you have any TextBox or OptionBox objects drawing contents in the window and the window is the owner of those GUI controls, then the text/options will gradually be shown or disappear along with the menu window, so long as scissoring is enabled for those controls.

Update and Draw[edit]

For every frame where the window is active, call Update() and Draw(). The Update() function processes the show/hide animation and the Draw() function renders the state of the menu window to the screen. Remember that if the menu window is in the VIDEO_MENU_STATE_HIDDEN state, nothing will be drawn to the screen and no warning messages will be printed.

menu_window.Update(time_elapsed);
menu_window.Draw();

Destruction[edit]

For MenuWindow objects, you must remember to call Destroy() on them when they are to be discarded. This allows the video engine to free up the image that was created to represent that window. When you're done with a menu window, you have to destroy it. This is extremely important, because if you forget to do this, then DeleteImage() doesn't get called, which can result in a huge texture memory leak. Simply remember that for every MenuWindow that you Create() you must also Destroy().

menu_window.Destroy();


TextBox[edit]

The TextBox class provides a standard RPG text display. It works by specifying an invisible rectangle on the screen where text should be drawn to. It can scroll the text into view one character at a time, and it also provides automatic word wrapping. The example below shows how to initialize a text box.

TextBox box;
box.SetPosition(512.0f, 0.0f);                    // Place at (x,y) coordinates: (512, 0)
box.SetDimensions(400.0f, 100.0f);                // Make the text box 400 units wide and 100 units high
box.SetDisplaySpeed(30);                          // Display the text at a rate of 30 characters per second
box.SetFont("default");                           // Use the font named "default"
box.SetDisplayMode(VIDEO_TEXT_REVEAL);            // Reveal the text gradually
box.SetTextAlignment(VIDEO_X_LEFT, VIDEO_Y_TOP);  // Align the text to the left top corner of the text box
box.SetDisplayText("For great justice!");         // Display the following string in the text box

There are equivalent Get methods to match each of these Set methods. After you've initialized a text box, invoke the Update() and Draw() methods on the text box every frame. You can reset all text box properties by calling the TextBox::Clear() method. Additionally, may also check if there is no text currently set to the text box by testing the return value of bool TextBox::IsEmpty().


Display modes[edit]

There are several ways to scroll text on to the text box. The flash video below shows how each of these display modes appear in the game.

<flash>file=gui_text_display_modes.swf|width=388|height=165</flash>


Except for the VIDEO_TEXT_INSTANT mode, all display modes require explicit calls to the Update() method each frame in order to draw the next section of text. If you forget to do this, the text will not be rendered and thus not seen if you attempt to draw the text box to the screen. Sometimes you may wish for the gradual display of the text to finish immediately. To do this, you may call the ForceFinish() method to instantly end the gradual display of text. You may also check if the entire text has been rendered by checking the return value of the bool IsFinished() method.


Note: Don't forget to call Update()

Remember that even if you create a TextBox and call the Draw() method, nothing will be drawn unless you call Update() each frame to render the text into view! If you are using the VIDEO_TEXT_INSTANT display mode however, it is not necessary to call Update() at all.

OptionBox[edit]

The OptionBox class is for standard RPG-like option menus where the player has several options which they are allowed to browse and select. You can use this class to construct a simple "Yes/No" menu, an inventory menu (like choosing a spell to cast or an item to use), or a menu to display items for sale in a shop. Below is an example of a relatively complex option box from Chrono Trigger:

Chrono trigger shop.png


Initialization[edit]

The OptionBox class is fairly complex because it has so many options that can change its behavior. We'll look at proper initialization of an option box one part at a time. There are a lot of settings for option boxes so lets look at this one step at a time. First lets cover the layout and data structure. All the options are stored in a mutable 2D data structure. It is mutable because we can dynamically change the number of rows and columns of the data structure, but typically this is set during initialization and never changed. Each visible option in an option box is stored in a "cell", and the programmer can determine the layout and size of cells by specifying the number of rows and columns of cells that should be visible in the option box.

OptionBox box;
box.SetPosition(512.0f, 384.0f);
box.SetDimensions(300.0f, 50.0f, 3, 1, 3, 1);
box.SetOptionAlignment(VIDEO_X_CENTER, VIDEO_Y_CENTER);

SetPosition() determines the screen coordinates where the option box will be drawn. Note that the position of the option box is affected by the video engine's draw flags. For example, if we want the option box to be centered in the middle of the screen and the coordinate system in which the box is drawn is 1024x768, we would use (512.0f, 384.0f) for the position and set the VIDEO_X_CENTER and VIDEO_Y_CENTER draw flags prior to drawing the box.

SetDimensions() has six parameters. The first two are the width and height of the option box area that is visible on the screen. The next two parameters represent the columns and rows of the internal 2D data structure that holds the options added to the class. In this case we have 3 columns and 1 row. The final two parameters are the number of columns and rows of cells that are visible in the option boxes dimensions. This directly determines the size of each cell in an option box (total width/height divided by number of cell columns/rows). The number of cell columns/rows can not exceed the number of data columns/rows. This is because, for example, it doesn't make sense to have 4 rows of cells and only 3 rows of data (there would always be a blank row of cells).

SetOptionAlignment() specifies the horizontal and vertical alignment of all options within their cell. The default option alignment for option boxes is VIDEO_X_LEFT and VIDEO_Y_CENTER, so if that works for your code then you don't need to call this function.


Now I've covered layout which is the hardest part... Here's the rest of the initialization code.

box.SetFont("default");
box.SetSelectMode(VIDEO_SELECT_DOUBLE);
box.SetCursorOffset(-35.0f, -4.0f);
box.EnableSwitching(true);
box.SetHorizontalWrapMode(VIDEO_WRAP_MODE_STRAIGHT);

The selection mode can either be single-confirm (VIDEO_SELECT_SINGLE) or double-confirm (VIDEO_SELECT_DOUBLE). In single-confirm mode, you just press confirm on an option once to confirm it. In double-confirm mode, you have to confirm the same selection twice to actually confirm it. Note: the default for an option box is single-confirm, so you don't have to explicitly call SetSelectMode() in this case.

The cursor offset tells where to draw the cursor relative to the left edge of each option. In the example above, this puts the cursor to the left of the text, basically.

Switching is when you select one item, then select another item to switch their places in the option list. (This is used a lot in inventory screens for example). Note that for switching to work, the selection mode MUST be double-confirm. Note: by default, switching is off.

Wrapping is where you go past the end of the option list and the cursor wraps around to the other side. For an OptionBox, you can set the horizontal and/or vertical wrap mode to one of the following values:

  • VIDEO_WRAP_MODE_NONE
  • VIDEO_WRAP_MODE_STRAIGHT
  • VIDEO_WRAP_MODE_SHIFTED

By default, option boxes use NONE, so if you try going too far to one side, the cursor just stops. SHIFTED and STRAIGHT are similar... Suppose you move the cursor all the way to the rightmost column, and then you press the right arrow key again. In either case, the cursor will wrap around to the leftmost column. However, in STRAIGHT, it will remain on the same row when it wraps around, whereas in SHIFTED, it will move down one column. (Just like text- you go from left to right, and when you reach the end you go down one row).

Setting/Adding Options[edit]

Now you have an initialized option box, but it's not much of an option menu yet since there aren't any options in it! To add options, you use the SetOptions() function:

bool SetOptions(const std::vector<hoa_utils::ustring> &formatText);

Note that although the OptionBox is a 2D array (rows and columns), this is just a 1D array. The mapping of elements in this array to cells goes left to right, top to bottom (the same order you read text). You can also pass in more options than can be displayed at one time, in which case a scrollbar appears and the player can scroll up and down (like an inventory in old FF games for example).

Also, note that you can call this function at any time. For example, if one of the labels in the option box changes while it's still on screen, just call SetOptions() again with the updated vector of strings.

The text for each option can be something simple like "Yes" and "No", or it can include formatting tags, which I'll talk about in the next section...


Formatting tags

Say you have a shop screen, like the one from Chrono Trigger above. In this option box, each option isn't just a text. It actually has 3 parts. For example:

  1. An icon of a sword
  2. The name of the weapon ("Iron Blade")
  3. The price of the weapon (350)

So, with an OptionBox, you can create this kind of layout using a format string like this: <img/icons/sword.png><30>Iron Blade<R>350

First, we have an Image Tag, which is simply the filename of an image enclosed in brackets. This allows you to embed an image into option, just as easily as if it were text.

Then we have a Position Tag, which is just a number in brackets. This tells OptionBox to move the draw cursor to a point 30 pixels right of the left edge of the option cell. Note that image tags do not automatically move the draw position, so if you put an image tag and then some text without a position tag in between the image and the text, then the text will show up in the same position as the image (bad). Note: position tags only apply for left alignment.

Finally, we have an Alignment Tag, which can be <L>, <C>, or <R> for left, center, or right alignment.

Note that alignment (whether by SetOptionAlignment() or by alignment tags) is applied on a per-element basis. (An element is either a piece of text, or an image). If you have an option that contains some text and an image, and it uses center alignment, then the text will appear in the center, and so will the image (i.e. they will be on top of each other). Thus, whenever your options consist of more than just a simple text string, you should use left alignment exclusively, with position tags separating each element, and then the last element can be right aligned.


Setting the currently selected option

box.SetSelection(0);

After creating most option boxes, you'll want to set the current selection to the first item (0), unless you're implementing "cursor memory". You should always call this for each textbox you create, otherwise nothing will be selected initially, and plus, you should always call this AFTER calling SetOptions(), otherwise it will be an error. (If there are no options in the box, you can't select the first option in the list, because there is no first option!)


Updating / Drawing

As with the TextBox object, you must call Update() and Draw() on the option box every frame.

box.Update(time_elapsed);
box.Draw();

Sending Input to the option box

The OptionBox wouldn't be much fun if it didn't actually do anything!

Firstly, you need to inform the option box when certain keys are pressed. When you detect an up key is pressed, call HandleUpKey(), etc. Here are all the functions:

   box.HandleUpKey();
   box.HandleDownKey();
   box.HandleLeftKey();
   box.HandleRightKey();
   box.HandleConfirmKey();
   box.HandleCancelKey();

Receiving events from the option box

Every frame, you need to also check if the option box has generated any events. The events are:

VIDEO_OPTION_SELECTION_CHANGE  // user changed the selection
VIDEO_OPTION_CONFIRM           // user confirmed a selection
VIDEO_OPTION_CANCEL            // user canceled out of the selection
VIDEO_OPTION_SWITCH            // user switched two options

To check for events, call the GetEvent() function, which returns one of the above event codes, or zero if there is no event. Note: Calling GetEvent() clears the event flag, so only call it ONCE per frame.

Here's a sample of event checking:

int32 event = box.GetEvent();

if(event)
{
   if(event == VIDEO_OPTION_SELECTION_CHANGE) ...
   else if(event == VIDEO_OPTION_CONFIRM)     ...   
   else if(event == VIDEO_OPTION_CANCEL)      ...
   else if(event == VIDEO_OPTION_SWITCH)      ...
}

Now let's go a bit more in depth into each of these options...

Selection Changed event (VIDEO_OPTION_SELECTION_CHANGE)

This event is useful mainly when you want to update some part of the interface every time the selection changes. For example, if you're in an item shop, then every time the selection changes to a different item, you want to update the price display.

When this event happens, you can call the GetSelection() function which returns the current selection index, from 0 to numOptions - 1.

Option Confirmed event (VIDEO_OPTION_CONFIRM)

This event happens when the player confirms once on an option in single-confirm mode, or twice on the same option in double-confirm mode. To find out which option was confirmed on, you can call GetSelection().

Option Cancel event (VIDEO_OPTION_CANCEL)

This event happens when the player presses cancel in single-confirm mode, or if the player presses cancel in double-confirm mode and the player has not already made a partial confirmation.

Option Switch event (VIDEO_OPTION_SWITCH)

This event happens when the player switches two items. This is a somewhat complex event to handle, because the problem with it is that, it switches the order of the items. For example, if you implemented a Yes/No menu, then generally "Yes" would be index 0 and "No" would be index 1. If the user switched these items, then GetSelection()==0 means "No" instead of "Yes"!

So, for any options where you want to allow switching, you must make sure that the indices are not hardcoded. Instead, you need to use an array. For example in my silly Yes/No example, you could have an array like this:

int32 YES_ID = 0;
int32 NO_ID = 1;
int32 yesNoArray[2] = { YES_ID, NO_ID };

Then, when you receive a switch event, you must call GetSwitchSelection() to find out which item was the 1st one the player clicked on and GetSelection() to find out the 2nd one they clicked. Then, you can swap the values in the array. For example:

if(event == VIDEO_OPTION_SWITCH)
{
    int32 a, b;
    a = box.GetSwitchSelection();
    b = box.GetSelection();
    Swap(yesNoArray[a], yesNoArray[b]);
}

This way, if some idiot player decides to switch Yes and No, then your array looks like { NO_ID, YES_ID }, and thus your confirm code still looks the same:

if(event == VIDEO_OPTION_CONFIRM)
{
    int32 option = box.GetSelection();
    if(yesNoArray[option] == YES_ID)   // as opposed to if(option == YES_ID)
        // YES
    else if(yesNoArray[option] == NO_ID)
        // NO
}

Other things to know about the OptionBox control

Here are some final miscellaneous notes and features you may want to know about.

  • To prevent 2 events from being generated at the same time, the option box stops processing input if it already has an event that occurred. Thus, it's absolutely necessary to call GetEvent() every frame because it clears the event flag.
  • Option boxes also ignore input while they are scrolling
  • You can disable an option by calling EnableOption(index, false), where index is the number of the option to disable.
  • SetCursorState() can be used to change the cursor to visible, hidden, or blinking.
  • For now, the number of options you create with SetOptions() MUST be a multiple of the number of columns. To support sizes which aren't a multiple of columns, the calculations would become a bit more twisted, but if this turns out to be a major limitation, then I can fix it in the future.