Code3Arena

PlanetQuake | Code3Arena | Articles | << Prev | Article 6 | Next >>

menu

  • Home/News
  • ModSource
  • Compiling
  • Help!!!
  • Submission
  • Contributors
  • Staff
  • Downloads

    Tutorials
    < Index >
    1. Mod making 101
    2. Up 'n running
    3. Hello, QWorld!
    4. Infinite Haste
    5. Armor Piercing Rails
    6. Bouncing Rockets
    7. Cloaking
    8. Ladders
    9. Favourite Server
    10. Flame Thrower
    11. Vortex Grenades
    12. Grapple
    13. Lightning Discharge
    14. Locational Damage
    15. Leg Shots
    16. Weapon Switching
    17. Scoreboard frag-rate
    18. Vortex Grenades II
    19. Vulnerable Missiles
    20. Creating Classes
    21. Scrolling Credits
    22. Weapon Dropping
    23. Anti-Gravity Boots
    24. HUD scoreboard
    25. Flashlight and laser
    26. Weapon Positioning
    27. Weapon Reloading
    28. Progressive Zooming
    29. Rotating Doors
    30. Beheading (headshot!)
    31. Alt Weapon Fire
    32. Popup Menus I
    33. Popup Menus II
    34. Cluster Grenades
    35. Homing Rockets
    36. Spreadfire Powerup
    37. Instagib gameplay
    38. Accelerating rockets
    39. Server only Instagib
    40. Advanced Grapple Hook
    41. Unlagging your mod


    Articles
    < Index >
    1. Entities
    2. Vectors
    3. Good Coding
    4. Compilers I
    5. Compilers II
    6. UI Menu Primer I
    7. UI Menu Primer II
    8. UI Menu Primer III
    9. QVM Communication, Cvars, commands
    10. Metrowerks CodeWarrior
    11. 1.27g code, bugs, batch


    Links

  • Quake3 Files
  • Quake3 Forums
  • Q3A Editing Message Board
  • Quake3 Editing


    Feedback

  • SumFuka
  • Calrathan
  • HypoThermia
  • WarZone





    Site Design by:
    ICEmosis Design


  •  
    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:

    1. setting the menuframework_s according to the type of menu
    2. setting the initial values of each control in the menu
    3. 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.

    PlanetQuake | Code3Arena | Articles | << Prev | Article 6 | Next >>