ARTICLE 6 - UI Menu Primer I
by HypoThermia
We're going to take a look at the menu system that Quake 3 uses. If you've already
written programs for a windows based Operating System (Windows, MacOS etc.) then you'll find the
way that Id have written their menu system to be very familiar. Previous experience
of "GUI" (graphical user interface) programming isn't required however.
These two primer articles should act as both introduction and reference on
creating menus, hopefully allowing you to dive in and make the menu tweaks that you need.
The first primer (you're reading it!) describes how the menu system works, some
of the do's and don'ts of programming using the menu system, and most of the details on how
to setup a menu from scratch. After reading this first primer you should be able to
look at the menu source code with understanding.
The second part provides a reference for all the
controls you can use in a menu or page of controls, helping
you understand how to get the most from them. In
the third part of this primer
we'll cover some of the more advanced things you can do with menus,
and talk about good menu design as well.
1. Overview
The menu code is built into the user interface ("ui") source code, and can't be accessed
directly from the client game code ("cgame"), or the server game code ("game"). This isn't
really surprising as the server code can be running on a remote machine, and the client code
is concerned with the playing of the game itself.
With the game running on a virtual machine on either Windows, Mac, or Linux,
then we can't have OS specific menu code. Id wrote their own menu system, the source
code for which is in ui_qmenu.c, with the associated data structures and defined
constants in ui_local.h. You can have a look there if something isn't covered
in this article, or if you just want to see the "nuts and bolts".
The menu systems actually rolls together two "types" of menu. The first is a "traditional"
vertical list of options where you choose an action or get a new menu.
You also have a page of controls that can be twiddled with in any order until
it's set up as you want. Both types are supported, and are set up in the same way.
Fortunately there are many examples throughout the ui code of how to use
individual controls, so if you want to copy something neat that's already in a menu
then you'll find most of the hard work already done for you.
2. The basics
All menus behave in the same way: consisting of a list of passive and active controls
that are drawn on the screen. The "passive" controls are things like menu art or pictures,
while "active" controls can be selected, highlighted, or changed in value using the keyboard
or mouse.
The menu is redrawn again and again, as quickly as your graphics drivers
will allow. Any changes made to a control will be seen immediately (without having
to ask for a screen redraw each time): so you can't draw something once and
expect it to remain on screen.
To avoid nasty screen sizing problems all screen co-ordinates are based on a 640x480
resolution, with the origin (0,0) at the top left of the screen. The resizing is
done behind the scenes: fonts will scale automatically, and controls will
retain their relative positions.
When something happens to a control an "event" is generated. This is an
opportunity for you to handle how a control behaves. The event is passed back to you
in the form of a message using a "callback" function. A callback function is forced
to use a specific combination of arguments: otherwise the code will crash. Don't worry if
you're not sure, examples will follow.
When you're creating a menu from scratch there are several steps you need to take.
We'll look at each of these in more detail in the following sections:
- Create static data for each control
- Cache all graphical and sound data
- Initialize menu controls
- Display the menu
- Process events on individual controls
- Tie up loose ends
If you're starting from scratch then you'll need to include the
ui_local.h header file to get access to the constants and data structures
used in a menu.
3. Create static data for each control
Each control needs to have some data asociated with it: information on
its current position, its value, and how it will be drawn. As there's no malloc
system available we have to allocate space for this statically.
There are 7 types of control provided for you, listed below. More details
on each control are given in the second part of this primer.
- menufield_s
- menuslider_s
- menulist_s
- menuaction_s
- menuradiobutton_s
- menubitmap_s
- menutext_s
The best way to organize this static data is to create a struct that
contains all the controls a menu uses. You get the benefit of putting
all the information in one place, making it easier to initialize.
This data structure is then created as a static variable, limiting direct
access to the data to that particular source code file.
You can also put data into this structure that is used by the menu. Sound effects,
useful intermediate values, and graphics can be organized in this way.
Each menu also needs a menuframework_s data structure. This acts as a unique
identifier and there MUST be one for each menu. Only one menuframework_s can be
active at any time. We'll see how it's used when we come to initialize the menu controls.
This example is taken from ui_spskill.c:
typedef struct {
// a menuframework_s is required for each menu
menuframework_s menu;
menubitmap_s art_frame;
menutext_s art_banner;
menutext_s item_baby;
menutext_s item_easy;
menutext_s item_medium;
menutext_s item_hard;
menutext_s item_nightmare;
menubitmap_s art_skillPic;
menubitmap_s item_back;
menubitmap_s item_fight;
const char *arenaInfo;
qhandle_t skillpics[5];
sfxHandle_t nightmareSound;
sfxHandle_t silenceSound;
} skillMenuInfo_t;
static skillMenuInfo_t skillMenuInfo;
4. Caching all graphical and sound data
If you use any graphics or sounds in your menu (background graphics, map pictures, audio fx etc.)
then you'll need to cache them before initializing the menu. This ensures
they're in memory for usage, and minimizes disk access while the player is using
the menu. It's especially important if you create graphics for your menu rather than
using those provided by Id.
It is better to put all the caching into it's own function. When Quake3 starts up
it trys to cache all graphics into memory by calling these caching functions. We'll
see later when we're tying up some loose ends
why we're doing this.
This example shows how graphics and a sound effect are cached, again taken from
ui_spskill.c:
#define ART_FRAME "menu/art/cut_frame"
#define ART_MAP_COMPLETE1 "menu/art/level_complete1"
/*
=================
UI_SPSkillMenu_Cache
=================
*/
void UI_SPSkillMenu_Cache( void ) {
trap_R_RegisterShaderNoMip( ART_FRAME );
// code snipped...
skillMenuInfo.skillpics[0] =
trap_R_RegisterShaderNoMip( ART_MAP_COMPLETE1 );
// code snipped...
skillMenuInfo.nightmareSound =
trap_S_RegisterSound( "sound/misc/nightmare.wav" );
// code snipped...
}
The function trap_R_RegisterShaderNoMip() registers the graphic, and also
returns a unique handle/identifier called a shader. The graphics controls can draw either
a named graphic (as in ART_FRAME, used when the graphic is fixed) or a shader (when the
graphics can change). More on this when the bitmap_s control data structure
is explained later.
By default the graphics files are JPEGs and a ".jpg" extension is added
(so ART_FRAME is actually cut_frame.jpg),
but you can also use TARGA files so long as you add the ".tga" extension. These
graphics and sounds are stored in the pk3 files in baseq3. They have an internal
directory structure that must be used, otherwise the files won't be found.
Sounds are cached in a similar fashion. They also return a unique identifying handle,
stored as a sfxHandle_t. This sound can be played by a call into
trap_S_StartLocalSound().
5. Initalizing menu controls
Initialization needs to be done each time the menu is prepared for display.
All of this can (and preferably should) be done in one function.
It breaks up into three steps:
- setting the menuframework_s according to the type of menu
- setting the initial values of each control in the menu
- registering each control in the menuframework_s
IMPORTANT: The static data for the controls should be set to zero with a memset()
so that reasonable default values are setup. You will get unexpected behaviour
if you don't do this each time you initialize the controls.
Using the earlier example struct it's as easy as:
memset(&skillMenuInfo, 0, sizeof(skillMenuInfo_t));
Take a look at UI_SPSkillMenu_Init() in ui_spskill.c to see this
in action.
|
5.1. Initializing menu controls: menuframework_s
The menuframework_s menu structure is initialized by setting
the three values menu.fullscreen, menu.wrapAround, and menu.draw.
menu.fullscreen (you must set this value)
Set this to qtrue if you want the menu to take full control of the screen. Any game
being played will be paused in the client. You should also use this for a menu like the
main intro where no game is being played at all.
When set to qfalse the background action will continue. Use this if the menu
will be displayed while playing a multiplayer game. The player will stand still and the
action will continue in the background while the menu is used.
If your menu needs a dual role (pauses action in single player and continues in multiplayer)
then the following code fragment will be useful:
uiClientState_t cstate;
trap_GetClientState( &cstate );
if ( cstate.connState >= CA_CONNECTED ) {
s_confirm.menu.fullscreen = qfalse;
}
else {
s_confirm.menu.fullscreen = qtrue;
}
Change s_confirm to match your struct variable name.
menu.wrapAround (you must set this value)
If set to qtrue the menu will not appear to have a "first" or "last" item
when using keyboard navigation. Use this value for menus that are like
"traditional" lists, usually when up/down is a sensible way to move between items.
It is important that the controls are registered in the order in which the
cursor will move between them.
When set to qfalse the keyboard navigation will not move beyond the first or last item
on the list. Use this when the "menu" is more like a page of controls than a list.
menu.draw (set only if required)
A pointer to a function that allows you to draw additional items without
adding (and initializing) controls. Any graphics that you draw here should also be cached.
This is covered in more detail in the second part of this primer.
5.2. Initializing menu controls: individual controls
Individual controls are setup according to their type. Each control has
a core set of data that must be initialized, refered to as the "generic" data.
In some cases the data has a behaviour specific to that control.
Each control structure includes the generic data as the first item, so
we are guaranteed that a pointer to the control can always be re-cast as a
pointer to the generic data. This example is typical of all the controls:
typedef struct
{
menucommon_s generic; // always first
char* string;
int style;
float* color;
} menutext_s;
The generic data structure looks like this:
typedef struct
{
int type;
const char *name;
int id;
int x, y;
int left;
int top;
int right;
int bottom;
menuframework_s *parent;
int menuPosition;
unsigned flags;
void (*callback)( void *self, int event );
void (*statusbar)( void *self );
void (*ownerdraw)( void *self );
} menucommon_s;
At the very least you will have to initialize the following data:
- type of control (generic.type)
- position on screen (generic.x, generic.y)
- behaviour flags, QMF_* (generic.flags)
- some content, specific to the control
If it can be interacted with, or changed in value, then the following also
need to be set:
- pointer to a callback function that handles behaviour (generic.callback)
- an identifier for the control (generic.id)
For each control to be uniquely identified then the generic.callback
and generic.id combination needs to be unique. You can have a separate
callback function for each control, or use one callback function with unique values of
id for each control. Or a combination of the two, but never the same id for two controls using the
same callback function. This opens the possibility of the id value being
used for something else (like an array index).
The generic.callback function must take the following form:
// choose an appropriate function name
// "ptr" is a pointer to the control, re-cast to
// (menucommon_s*) for access to the generic data
static void FunctionName_Event( void *ptr, int event )
Finally, the following can be set to enhance or change the behaviour
of the control:
- a callback function that draws the control (generic.ownerdraw)
- a callback function for modifying a statusbar (generic.statusbar)
Use of these values are explained in more detail in
the second part of this primer.
The following is an example of how a data structure is initialized, taken
from ui_spskill.c. The control type being filled is a
menutext_s, and there's a function UI_SPSkillMenu_SkillEvent
that handles what happens when the control is selected.
#define ID_BABY 10
skillMenuInfo.item_baby.generic.type = MTYPE_PTEXT;
skillMenuInfo.item_baby.generic.flags =
QMF_CENTER_JUSTIFY|QMF_PULSEIFFOCUS;
skillMenuInfo.item_baby.generic.x = 320;
skillMenuInfo.item_baby.generic.y = 170;
skillMenuInfo.item_baby.generic.callback =
UI_SPSkillMenu_SkillEvent;
skillMenuInfo.item_baby.generic.id = ID_BABY;
skillMenuInfo.item_baby.string = "I Can Win";
skillMenuInfo.item_baby.color = color_red;
skillMenuInfo.item_baby.style = UI_CENTER;
5.3. Initializing menu controls, registering controls
Registering controls is the easiest part of the coding. You need to make a
one line function call to Menu_AddItem() for each control, refering to the unique
menuframework_s variable for your menu as well.
If keyboard navigation is important (as in a "traditional" menu list) then
you have to register menu items in the order of navigation.
This example shows item_baby being registered into the skill menu (again
taken from ui_spskill.c):
Menu_AddItem( &skillMenuInfo.menu, ( void * )&skillMenuInfo.item_baby );
6. Displaying the menu
Surprisingly there is very little you have to do to get the menu displayed.
Once the menu is fully initialized you just need to call UI_PushMenu()
using a pointer to your menuframework_s. The menu system then takes care of
setting up the screen, drawing the menu, and making sure that events are passed
back through the callback functions for you to act upon. You can push menus to
a maximum depth of MAX_MENUDEPTH, which by default is 8.
When you're closing the menu down all you need to do is call
UI_PopMenu() and the previous menu will be displayed and given control.
This action is usually associated with a control that you put in the menu
(like a back button). Pressing the ESC key also has the same effect.
If you need to close all menus (usually because you're in a sub-menu and "popping"
the menu isn't appropriate) then call UI_ForceMenuOff() instead. Use carefully because
you'll not have an active menu system anymore. Most useful when you're overlaying a menu
on a game in progress and need to close from a sub-menu.
This code shows how straightforward it is to get the menu displayed.
Initialization by UI_SPSkillMenu_Init() encompasses all the graphics caching
and control init that has been previously discussed.
If the menu is created through actions in another ui source file
then we have to add a function prototype to ui_local.h, and so we don't
use static functions.
Taken from ui_spskill.c (I've added comments):
void UI_SPSkillMenu( const char *arenaInfo ) {
// initialization
UI_SPSkillMenu_Init();
skillMenuInfo.arenaInfo = arenaInfo;
// menu registered for displayed
UI_PushMenu( &skillMenuInfo.menu );
// force this menu item to be selected
// and the default action
Menu_SetCursorToItem( &skillMenuInfo.menu,
&skillMenuInfo.item_fight );
}
7. Event processing
With all the hard work of getting your menu system set up you now have to
breathe life into it, make it behave as the user might expect; in other words "connecting
all the dots". This is done by handling the message events generated by
each control.
For each control the events are passed through the function assigned to
generic.callback. This function has two arguments: a void* pointing
to the control generating the event, and an int describing what the event is.
We can obtain the generic.id of the control by recasting the void pointer as a pointer to
the "generic" menucommon_s data struct that all controls use. We'll see this
in the example below.
There are three possible events that can be generated: QM_ACTIVATED,
QM_GOTFOCUS, and QM_LOSTFOCUS. The first occurs every time there
is a change in the value of a control. The last two occur when a control gets or
loses "focus" (usually when it starts or stops "pulsing" as the current control).
For the most part you'll only use the QM_ACTIVATED event message.
You can have as many event callback functions for your menu as you want. If you use
the generic.id of a control then it must be unique for that callback function. This
opens up the possibility of using the generic.id for another purpose (like an index
to an array of data).
The example code comes from ui_demo2.c, a menu showing all the
recorded demos that can be played back (additional comments are my own).
/*
===============
Demos_MenuEvent
===============
*/
static void Demos_MenuEvent( void *ptr, int event ) {
// ignore QM_GOTFOCUS and QM_LOSTFOCUS
if( event != QM_ACTIVATED ) {
return;
}
switch( ((menucommon_s*)ptr)->id ) // get the id for the control
{
case ID_GO:
UI_ForceMenuOff (); // closes all open menus
trap_Cmd_ExecuteText( EXEC_APPEND, va( "demo %s.dm3\n",
s_demos.list.itemnames[s_demos.list.curvalue] ) );
break;
case ID_BACK:
UI_PopMenu(); // returns to previous menu
break;
case ID_LEFT:
ScrollList_Key( &s_demos.list, K_LEFTARROW );
break;
case ID_RIGHT:
ScrollList_Key( &s_demos.list, K_RIGHTARROW );
break;
}
}
8. Tying up loose ends
There are surprisingly few loose ends to tie up. The lions share of the coding
effort is in initializing the menu and implementing the functionality. The few things
that you do need to look at are all outside the single source file you're editing.
When creating a menu you have to cache the graphics. Quake3 tries to load all of these
graphics into memory during startup, so it's best if you put the caching into
a separate function (and not give it static scope). Find the function
UI_Cache_f() in ui_atoms.c and add a call to your caching function
here. You should also add your function prototype to ui_local.h as in
the following example:
//
// ui_video.c
//
extern void UI_GraphicsOptionsMenu( void );
extern void GraphicsOptions_Cache( void );
extern void DriverInfo_Cache( void );
Notice that the function that gets the ball rolling by setting up the menu
(initializing it and making the call to UI_PushMenu()) is not static either.
The prototype should also be placed in ui_local.h so that other
source code files can "see" the function and use it to create the menu.
9. To be continued...
This is the first part of the menu primer completed. Congratulations on
reaching this far! You should now be able to look at any piece of menu code
in the ui, understand how the menu is being setup, and how its behaviour
is controlled.
The second part of this primer provides a reference
to all of the controls that are available for you to use. Most of the constant
values that you might need to use are also covered.
The third part moves onto some more advanced topics such as ownerdrawn
controls and statusbars. There are some design tips for good menus as well.
|