TUTORIAL 32 - Popup Menus I
by HypoThermia
This tutorial is in two parts, describing a dynamic popup game menu system that
can be used while the the player is in the middle of a game. This kind of menu can be
used to issue commands to team mates, buy items, or set in-game options where
where the Escape menu is just too cumbersome.
The menu responds to the position of the cursor, automatically displaying or hiding
a sub-menu. This makes it very easy to structure the menu and group commands together.
Its also just about the quickest way to get a command out without resorting to dedicated
key binds or complicated key sequencing.
This first part covers the core menu framework system. Once implemented you'll
be able to start writing your own hierarchical menus. I've tried to make the setup
of these menus as simple as possible. you just need to initialize a menu, add the items,
and then complete the init once all the items are in place. Then you have to write the
payload for the command that'll be issued.
The second part of the tutorial
covers a detailed implementation of the bot
command list. Even if you don't want to add this to your mod, there's a lot of useful
information about how the User Interface (and client code) can get access to info that
is stored on the server. This includes things like a list of all players, identifying bots,
and items present on the map currently being played.
One of the most important things to understand about code like this is the role
of sanity checking values before using them. Any errors are going to appear at run-time, and
will be that much more difficult to debug. It's very easy to access elements that are
outside an array, or haven't been initialized yet. The second part of the tutorial
shows how to do this by example.
If you want to see the menu system in action, then go grab my Q3 User Interface mod
UI Enhanced (shameless plug!).
Those of you that have played Unreal Tournament will recognize the menu format straight away.
1. Setting up data structures
The items in the menu and sub-menus are all stored sequentially in an array. Boundaries are
maintained between each menu, and there are no gaps. The array is filled from the bottom up, with
dead sub-menus being removed and overwritten by their replacements.
All the code changes, unless otherwise stated, go into ui_ingame.c, the current
in-game menu source code file. You can start appending this code after the last function in
that file.
The order in which functions are placed in the file means that
function declaration isn't needed. Make sure that you place the function before its first use,
otherwise you'll get redeclaration warnings/errors.
We'll start with essential data structures and constants.
/*
===========================
INGAME DYNAMIC COMMAND MENU
===========================
*/
#define MAX_DYNAMICDEPTH 6
#define MAX_MENUSTRING 16
#define MENUSPACE_X 4
#define MENUSPACE_Y 1
typedef void (*createHandler)(void);
typedef void (*eventHandler)(int index);
typedef struct {
char text[MAX_MENUSTRING];
int index;
int id;
createHandler createSubMenu;
eventHandler runEvent;
} dynamicitem_t;
typedef struct {
menuframework_s menu;
menutext_s item[MAX_MENUITEMS];
dynamicitem_t data[MAX_MENUITEMS];
int start[MAX_DYNAMICDEPTH];
int end[MAX_DYNAMICDEPTH]; // indicates to (last item + 1)
int active[MAX_DYNAMICDEPTH];
int gametype;
int depth;
} dynamicmenu_t;
static dynamicmenu_t s_dynamic;
MAX_DYNAMICDEPTH controls how many sub-menus are supported. With the maximum number
of chars in each menu item set to MAX_MENUSTRING, in practice we can only get 4
menus on screen anyway. Make sure your items/commands are meaningful in 16 chars!
MENUSPACE_X and MENUSPACE_Y control spacing when the menu is drawn on-screen.
The typedefs createHandler and eventHandler are the generic functions used to create sub-menus
and issue commands respectively. Any menu item can have either one or the other associated with it,
and the functions called must be in this format.
The dynamicitem_t structure carries all additional information needed to draw a menu item. index
gives array position in dynamicmenu_t, while id is an identifying value for
your use. createSubMenu and runEvent control how the item behaves when the mouse hovers
over it, or clicks on it, respectively.
Finally, the dynamicmenu_t contains all the info needed to draw and control the menu.
MAX_MENUITEMS is set in ui_local.h, and and you might find the default value of 32
a little restrictive. It can be increased without harming any other code.
The arrays dynamicmenu_t->start[] and dynamicmenu_t->end[] control which items are grouped with
each menu. Take care to remember that dynamicmenu_t->end[] actually points to the next available free
item slot, and so isn't actually initialized yet!
dynamicmenu_t->active[] allows you to track back through the sub-menus and see which items have been
activated, very useful for building up commands based on earlier menu selections. Its also used
to draw a highlight under the activated menu items.
The dynamicmenu_t->gametype is set very early on, and allows menu code to check the
gametype without having to grab the g_gametype Cvar.
Care needs to be used with dynamicmenu_t->depth, it actually counts the
number of open menus (so the very first menu created has a depth of 1). In many parts of the
code you'll see (depth-1) to convert this back to a zero based array. This convention must
be understood, otherwise you'll access un-initialized data!
2. Dynamic menu creation and initialization
With these data structures in place we now need to add the means to set them up for use.
There are three steps:
- Initialize for a sub-menu
- Add each of the menu items
- Prepare the menu for drawing on screen
Starting with initialization, we only need to call this once for a new menu. It
returns qfalse if the init fails for any reason.
The very first menu starts at the beginning of the array, while sub-menus follow on
at the first free slot pointed to by s_dynamic.end[]. No menu item is yet active, so
we set a safe value, and init the range of items in this menu.
/*
=================
DynamicMenu_InitSubMenu
=================
*/
static qboolean DynamicMenu_SubMenuInit( void)
{
int pos;
if (s_dynamic.depth == MAX_DYNAMICDEPTH)
return qfalse;
if (s_dynamic.depth == 0)
pos = 0;
else
pos = s_dynamic.end[s_dynamic.depth - 1];
if (pos == MAX_MENUITEMS)
return qfalse;
s_dynamic.depth++;
s_dynamic.active[s_dynamic.depth - 1] = -1;
s_dynamic.start[s_dynamic.depth - 1] = pos;
s_dynamic.end[s_dynamic.depth - 1] = pos;
return qtrue;
}
For each item in the menu we need to give the string that'll be drawn, and a unique id for the
create or event handler to use (if needed). This id is stored in the dynamicitem_t->id, as
the generic.id value in the menutext_s items are already in use (see below).
The action that the item will take is also set here, by supplying the required handler function.
These functions must be in the format of the typdefs that define them (createHandler
and eventHandler).
If we try and overflow the arrays reserved for us, the extra items are quietly dropped. This
prevents a possible crash in the framework code by an overly large menu, at the expense of not
drawing all the menu items. Increasing MAX_MENUITEMS will fix this.
An id value only needs to be unique for any single createHandler or
eventHandler function. There is no requirement to make the id unique across all your
menu handler functions.
/*
=================
DynamicMenu_AddItem
=================
*/
static qboolean DynamicMenu_AddItem( const char* string,
int id, createHandler crh, eventHandler evh)
{
int index, depth;
depth = s_dynamic.depth - 1;
index = s_dynamic.end[depth];
if (index == MAX_MENUITEMS)
return qfalse;
// can't have submenu and event attached to menu item
if (crh && evh)
return qfalse;
if (!string || !string[0])
string = "[no text]";
s_dynamic.data[index].index = index;
s_dynamic.data[index].id = id;
s_dynamic.data[index].createSubMenu = crh;
s_dynamic.data[index].runEvent = evh;
Q_strncpyz(s_dynamic.data[index].text, string, MAX_MENUSTRING);
s_dynamic.end[depth]++;
return qtrue;
}
Finally, we need to pay attention to all the details we couldn't handle while
creating the menu.
The width of the menu is set, and space is created (if needed)
to draw a marker that indicates a sub-menu will pop-up. This marker is a special character
in the Q3 font, a right pointing arrow head. Its referenced by the character '\r', I picked it
up from the menu code for radio buttons.
We then need to set the position of each menu item on the screen. The very first menu is easy:
centered halfway up the screen on the left edge. After that it becomes a little more complicated.
Ideally we'd like the first menu item to be level with the item the cursor is hovering over, so we
try and set the height to that value. Unfortunately this might push the menu off the bottom of the
screen, so we bump it up if it might do this. No error checking for the top of the screen...
just don't make your menus too large!
Finally we set the screen position and cursor hit area, and allow the control to be drawn
on screen. Note that this code doesn't wipe out the QMF_GRAYED flag. This allows it to be
set by your own code during the AddItem phase. See an example for this in part II of this tutorial.
/*
=================
DynamicMenu_FinishInitSubMenu
=================
*/
static void DynamicMenu_FinishSubMenuInit( void )
{
int depth;
int width, maxwidth;
int height, lineheight;
int posx, posy;
int i, count, start, active;
float scale;
menutext_s* ptr;
qboolean submenu;
depth = s_dynamic.depth - 1;
// find the widest item
submenu = qfalse;
maxwidth = 0;
start = s_dynamic.start[depth];
count = s_dynamic.end[depth] - start;
for ( i = 0; i < count; i++)
{
width = UI_ProportionalStringWidth(s_dynamic.data[i + start].text);
if (width > maxwidth)
maxwidth = width;
if (s_dynamic.data[i + start].createSubMenu)
submenu = qtrue;
}
scale = UI_ProportionalSizeScale(UI_SMALLFONT);
if (submenu)
{
// space and submenu pointer
maxwidth += UI_ProportionalStringWidth(" \r");
}
maxwidth *= scale;
// determine the position of the menu
lineheight = PROP_HEIGHT * scale + 2*MENUSPACE_Y;
height = count * lineheight;
if (depth == 0)
{
posy = 240 - height/2;
posx = 0;
}
else
{
active = s_dynamic.active[depth - 1];
posx = s_dynamic.item[active].generic.right;
posy = s_dynamic.item[active].generic.top;
if (posy + height > 480 - 64)
posy = 480 - 64 - height;
}
for (i = 0; i < count; i++)
{
ptr = &s_dynamic.item[start + i];
ptr->generic.x = posx + MENUSPACE_X;
ptr->generic.y = posy + i*lineheight + MENUSPACE_Y;
ptr->generic.left = posx;
ptr->generic.right = posx + maxwidth + 2*MENUSPACE_X;
ptr->generic.top = posy + i*lineheight;
ptr->generic.bottom = posy + (i+1)*lineheight - 1;
ptr->generic.flags &= ~(QMF_HIDDEN|QMF_INACTIVE);
}
}
3. Drawing the menu item
These are custom drawn menu items, consisting of red text on a translucent white
box background. Each box leaves a slight gap with the box above and below.
A little helper function detects whether an item is on the active list, leading to
a slightly brighter background being drawn instead. The right arrow head is also drawn
to indicate the presence of a sub-menu.
I've also provided a custom draw function for the entire menu. It has some useful
debugging info commented out, you can add to this or use it as needed.
/*
=================
DynamicMenu_OnActiveList
=================
*/
static qboolean DynamicMenu_OnActiveList( int index )
{
int depth;
int i;
depth = s_dynamic.depth;
for ( i = 0; i < depth ; i++)
if (s_dynamic.active[i] == index)
return qtrue;
return qfalse;
}
/*
=================
DynamicMenu_MenuItemDraw
=================
*/
static void DynamicMenu_MenuItemDraw( void* self )
{
int x;
int y;
int w,h;
float * color;
int style;
menutext_s* t;
vec4_t back_color;
t = (menutext_s*)self;
// draw the background;
x = t->generic.left;
y = t->generic.top;
w = t->generic.right - x;
h = t->generic.bottom - y;
back_color[0] = 1.0;
back_color[1] = 1.0;
back_color[2] = 1.0;
if (DynamicMenu_OnActiveList(t->generic.id))
{
back_color[3] = 0.33;
}
else
{
back_color[3] = 0.1;
}
UI_FillRect(x, y, w, h, back_color);
// draw the text
x = t->generic.x;
y = t->generic.y;
if (t->generic.flags & QMF_GRAYED)
color = text_color_disabled;
else
color = t->color;
style = t->style;
if( t->generic.flags & QMF_PULSEIFFOCUS ) {
if( Menu_ItemAtCursor( t->generic.parent ) == t ) {
style |= UI_PULSE;
}
else {
style |= UI_INVERSE;
}
}
UI_DrawProportionalString( x, y, t->string, style, color );
// draw the cursor for submenu if needed
x = t->generic.left + w;
if (s_dynamic.data[ t->generic.id ].createSubMenu)
{
UI_DrawChar( x, y, 13, style|UI_RIGHT, color);
}
}
/*
=================
DynamicMenu_MenuDraw
=================
*/
static void DynamicMenu_MenuDraw( void )
{
// UI_DrawString(0, 0, va("depth:%i", s_dynamic.depth),
// UI_SMALLFONT, color_white);
// UI_DrawString(0, 32, va("active: %i %i %i",
// s_dynamic.active[0], s_dynamic.active[1], s_dynamic.active[2] ),
// UI_SMALLFONT, color_white);
Menu_Draw(&s_dynamic.menu);
}
4. Menu event handling
We'll now start to bring the menu to life, with functions that handle mouse events created
by movement of the cursor.
There are three possible events that a control can receive:
- QM_GOTFOCUS
- When the cursor moves over the hit area for a control
- QM_LOSTFOCUS
- When the cursor leaves teh hit area
- QM_ACTIVATED
- when the mouse button is pressed
The QM_LOSTFOCUS message isn't used, but a placeholder function
DynamicMenu_ClearFocus() is provided in case you need to use it.
When the focus is set on a menu item, DynamicMenu_SetFocus() closes all submenus at a
greater depth. The QMF_GRAYED flag is stripped at this point, so any future menu
items don't inherit it. If the item opens a sub-menu then that sub-menu is prepared and created.
When activated, DynamicMenu_ActivateControl() calls the event handler that issues the
command associated with the menu item.
Every menu event in the QM_* family is handled through DynamicMenu_MenuEvent().
It's expected that once the command has been issued, the menu will be closed. This should be
done by calling DynamicMenu_Close(), and every command should do this. Why do it this way? Well
in UI Enhanced I've implemented this function slightly
differently, so that a UI Cvar decides whether UI_PopMenu() is called. This allows multiple
commands to be issued without closing the menu. The user can decide whether they want to do this
or have the menu close each time a command is completed. (an trivial exercise for the reader!)
The last remaining function is DynamicMenu_IndexDepth(), it identifies
the depth of the item just selected (this could be at any depth in
the menu structure). If it returns a value of zero then something has gone wrong somewhere,
and we don't have a valid menu item.
/*
=================
DynamicMenu_IndexDepth
=================
*/
static int DynamicMenu_IndexDepth( int pos )
{
int i;
int maxdepth, depth;
maxdepth = s_dynamic.depth;
depth = 0;
for (i = 0; i < maxdepth; i++)
{
if (pos < s_dynamic.end[i])
{
depth = i + 1;
break;
}
}
return depth;
}
/*
=================
DynamicMenu_SetFocus
=================
*/
static void DynamicMenu_SetFocus( int pos )
{
int i;
int depth, newdepth;
depth = s_dynamic.depth;
newdepth = DynamicMenu_IndexDepth(pos);
if (newdepth == 0)
{
Com_Printf("SetFocus: index %i outside menu\n", pos);
return;
}
s_dynamic.active[ newdepth - 1 ] = pos;
s_dynamic.depth = newdepth;
// hide any previous submenus
if (newdepth < depth)
{
for (i = s_dynamic.start[ newdepth ];
i < s_dynamic.end[depth - 1]; i++)
{
s_dynamic.item[i].generic.flags |= (QMF_HIDDEN|QMF_INACTIVE);
s_dynamic.item[i].generic.flags &= ~QMF_GRAYED;
}
}
s_dynamic.active[newdepth - 1] = pos;
// show this sub-menu (if needed)
if (s_dynamic.data[pos].createSubMenu)
s_dynamic.data[pos].createSubMenu();
}
/*
=================
DynamicMenu_ClearFocus
=================
*/
static void DynamicMenu_ClearFocus( int pos )
{
}
/*
=================
DynamicMenu_ActivateControl
=================
*/
static void DynamicMenu_ActivateControl( int pos )
{
int i;
int depth;
depth = DynamicMenu_IndexDepth(pos);
if (depth == 0)
{
Com_Printf("ActivateControl: index %i outside menu\n", pos);
return;
}
// not at the deepest level, can't be a command
if (depth < s_dynamic.depth)
return;
if (s_dynamic.data[pos].runEvent)
s_dynamic.data[pos].runEvent(pos);
else
Com_Printf("ActivateControl: index %i has no event\n", pos);
}
/*
=================
DynamicMenu_MenuEvent
=================
*/
static void DynamicMenu_MenuEvent( void* self, int event )
{
menutext_s* t;
t = (menutext_s*)self;
switch (event)
{
case QM_GOTFOCUS:
DynamicMenu_SetFocus(t->generic.id);
break;
case QM_LOSTFOCUS:
DynamicMenu_ClearFocus(t->generic.id);
break;
case QM_ACTIVATED:
DynamicMenu_ActivateControl(t->generic.id);
break;
}
}
/*
=================
DynamicMenu_Close
=================
*/
static void DynamicMenu_Close( void )
{
UI_PopMenu();
}
5. Initializing the menu controls
With all of the dynamic initialization out of the way, we still need to prepare the menu
for use. Fortunately we only need to do this once, just like the standard static menu code
used in the rest of the User Interface.
There are several sections of code that are commented out. These comments need to be removed
if you're adding the second part of this tutorial as well.
The DynamicMenu_MenuInit() function does most of the work here. Each displayed control
needs to be connected to the owner draw and event handler function. Although the text displayed for
each control might changed, the pointer to the text buffer won't, so this is also set. The use
of QMF_NODEFAULTINIT is essential, as we set up and provide these values ourselves.
When it comes to drawing the menu on screen, using menu.fullscreen = qfalse prevents a
black background being drawn and the rest of the game being paused. Action will continue around
the player, even in the single player game against the bots.
Although no graphics are used that need caching, a placeholder UI_DynamicMenuCache()
function is provided.
UI_DynamicMenu() is the entry point into the dynamic menu, and kicks the whole process off.
This is the best place to check for, and reject, the creation of the menu. Two useful examples are
provided: checks for the player as spectator, and game type.
Finally, we have UI_DynamicCommandMenu_f(), which is called by the key bind code
that we also need to add.
/*
=================
DynamicMenu_MenuInit
=================
*/
static void DynamicMenu_MenuInit( void )
{
int i;
s_dynamic.menu.draw = DynamicMenu_MenuDraw;
s_dynamic.menu.fullscreen = qfalse;
s_dynamic.menu.wrapAround = qfalse;
for (i = 0; i < MAX_MENUITEMS; i++)
{
s_dynamic.item[i].generic.type = MTYPE_PTEXT;
s_dynamic.item[i].generic.flags = QMF_INACTIVE
|QMF_HIDDEN|QMF_NODEFAULTINIT|QMF_PULSEIFFOCUS;
s_dynamic.item[i].generic.ownerdraw = DynamicMenu_MenuItemDraw ;
s_dynamic.item[i].generic.callback = DynamicMenu_MenuEvent ;
s_dynamic.item[i].generic.id = i;
s_dynamic.item[i].string = s_dynamic.data[i].text;
s_dynamic.item[i].style = UI_SMALLFONT|UI_DROPSHADOW;
s_dynamic.item[i].color = color_red;
Menu_AddItem(&s_dynamic.menu, &s_dynamic.item[i]);
}
// start up the menu system
s_dynamic.depth = 0;
// Uncomment the next line if adding part II as well
// DynamicMenu_InitMapItems();
DynamicMenu_InitPrimaryMenu();
}
/*
=================
UI_DynamicMenuCache
=================
*/
void UI_DynamicMenuCache( void )
{
}
/*
=================
UI_DynamicMenu
=================
*/
void UI_DynamicMenu( void )
{
uiClientState_t cs;
char info[MAX_INFO_STRING];
int playerTeam;
trap_GetClientState( &cs );
trap_GetConfigString( CS_PLAYERS
+ cs.clientNum, info, MAX_INFO_STRING );
playerTeam = atoi(Info_ValueForKey(info, "t"));
// Uncomment the next two code lines if adding part II
// as well, or specs can't use the menu either
// if (playerTeam == TEAM_SPECTATOR)
// return;
memset(&s_dynamic.menu, 0, sizeof(dynamicmenu_t));
s_dynamic.gametype = (int)trap_Cvar_VariableValue("g_gametype");
// Uncomment the next three lines if adding part II as well
// if (s_dynamic.gametype != GT_TEAM &&
// s_dynamic.gametype != GT_CTF)
// return;
UI_DynamicMenuCache();
// force as top level menu
uis.menusp = 0;
// set menu cursor to a nice location
uis.cursorx = 50;
uis.cursory = 240;
DynamicMenu_MenuInit();
UI_PushMenu( &s_dynamic.menu );
}
/*
=================
UI_DynamicCommandMenu_f
=================
*/
void UI_DynamicCommandMenu_f( void )
{
UI_DynamicMenu();
}
With this code done, this almost completes the framework for the menu system. The remaining
changes are in two different source files:
In ui_local.h, around line 307, we need to add a declaration for several
functions so they can be seen by the rest of the code:
//
// ui_ingame.c
//
extern void InGame_Cache( void );
extern void UI_InGameMenu(void);
extern void UI_DynamicMenuCache(void);
extern void UI_DynamicMenu( void );
extern void UI_BotCommandMenu_f( void );
The other change is in ui_atoms.c, in UI_ConsoleCommand(), where we
add a new command to display our menu. You can choose any appropriate command name,
and this will become the key bind used to create the menu.
You might also want to modify the code in ui_controls.c so that the key bind
can be set from within the game, without using the console.
if ( Q_stricmp (cmd, "ui_cinematics") == 0 ) {
UI_CinematicsMenu_f();
return qtrue;
}
if ( Q_stricmp (cmd, "ui_dynamicmenu") == 0 ) {
UI_DynamicCommandMenu_f();
return qtrue;
}
6. A trivial example to get you started
That's done everything needed for the framework, although if you've compiled the code you'll
find that there's still an error. This is for the function DynamicMenu_InitPrimaryMenu(),
which starts by creating the top level menu.
This little example includes that function (you'll want to provide your own of course!) All
it does is add an item that closes the menu just opened, but you can see how the dynamic menu
is initialized, item(s) are added, and the menu completed, ready for drawing on screen.
This one example doesn't use DynamicMenu_Close() for the simple reason that
we want to guarantee closure of the menu. If you've made the changes I described earlier
then the command might not close the menu.
You should place this code just before the DynamicMenu_MenuInit(void) function.
/*
=================
DM_Close_Event
=================
*/
static void DM_Close_Event( int index )
{
UI_PopMenu();
}
/*
=================
DynamicMenu_InitPrimaryMenu
=================
*/
static void DynamicMenu_InitPrimaryMenu( void )
{
DynamicMenu_SubMenuInit();
DynamicMenu_AddItem("Close!", 0, NULL, DM_Close_Event);
DynamicMenu_FinishSubMenuInit();
}
7. Framework completed!
With all the code above in place, you're now in a position to go a write your own
dynamic menus.
I'd suggest that you go and read the second part of this tutorial,
even if you don't implement it. It contains a very detailed implementation of all the bot commands,
and shows how all of this framework is used to full effect. There are also a large number of
helper functions that get access to data from the User Interface, data that's normally associated
with the server.
|